Um guia para soquetes Java

Um guia para soquetes Java

1. Visão geral

O termosocketprogramming refere-se a programas de gravação que são executados em vários computadores nos quais os dispositivos estão todos conectados entre si por meio de uma rede.

Existem dois protocolos de comunicação que podem ser usados ​​para a programação de soquetes:User Datagram Protocol (UDP) and Transfer Control Protocol (TCP).

A principal diferença entre os dois é que o UDP não possui conexão, o que significa que não há sessão entre o cliente e o servidor enquanto o TCP é orientado à conexão, o que significa que uma conexão exclusiva deve primeiro ser estabelecida entre o cliente e o servidor para que a comunicação ocorra.

Este tutorial apresenta redesan introduction to sockets programming over TCP/IP e demonstra como escrever aplicativos cliente / servidor em Java. O UDP não é um protocolo convencional e, como tal, pode não ser encontrado com frequência.

2. Configuração do Projeto

Java fornece uma coleção de classes e interfaces que cuidam dos detalhes de comunicação de baixo nível entre o cliente e o servidor.

Eles estão principalmente contidos no pacotejava.net, então precisamos fazer a seguinte importação:

import java.net.*;

Também precisamos do pacotejava.io, que nos fornece fluxos de entrada e saída para escrever e ler durante a comunicação:

import java.io.*;

Para simplificar, executaremos nossos programas cliente e servidor no mesmo computador. Se fôssemos executá-los em diferentes computadores em rede, a única coisa que mudaria é o endereço IP, neste caso, usaremoslocalhost em127.0.0.1.

3. Exemplo Simples

Vamos sujar as mãos com maisbasic of examples involving a client and a server. Vai ser um aplicativo de comunicação bidirecional, onde o cliente cumprimenta o servidor e o servidor responde.

Vamos criar o aplicativo de servidor em uma classe chamadaGreetServer.java com o código a seguir.

Incluímos o métodomain e as variáveis ​​globais para chamar a atenção sobre como estaremos executando todos os servidores neste artigo. No restante dos exemplos dos artigos, omitiremos esse tipo de código mais repetitivo:

public class GreetServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String greeting = in.readLine();
            if ("hello server".equals(greeting)) {
                out.println("hello client");
            }
            else {
                out.println("unrecognised greeting");
            }
    }

    public void stop() {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        GreetServer server=new GreetServer();
        server.start(6666);
    }
}

Vamos também criar um cliente chamadoGreetClient.java com este código:

public class GreetClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) {
        out.println(msg);
        String resp = in.readLine();
        return resp;
    }

    public void stopConnection() {
        in.close();
        out.close();
        clientSocket.close();
    }
}

Let’s start the server; em seu IDE, você faz isso simplesmente executando-o como um aplicativo Java.

E agora vamos enviar uma saudação ao servidor usando um teste de unidade, que confirma que o servidor realmente envia uma saudação em resposta:

@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
    GreetClient client = new GreetClient();
    client.startConnection("127.0.0.1", 6666);
    String response = client.sendMessage("hello server");
    assertEquals("hello client", response);
}

Não se preocupe se você não entender inteiramente o que está acontecendo aqui, pois este exemplo tem o objetivo de nos dar uma ideia do que esperar mais adiante neste artigo.

Nas seções a seguir, dissecaremossocket communication usando este exemplo simples e nos aprofundaremos nos detalhes com mais exemplos.

4. Como funcionam os soquetes

Usaremos o exemplo acima para percorrer as diferentes partes desta seção.

Por definição, asocket é um ponto final de um link de comunicação bidirecional entre dois programas em execução em computadores diferentes em uma rede. Um soquete é vinculado a um número de porta para que a camada de transporte possa identificar o aplicativo ao qual os dados estão destinados a serem enviados.

4.1. O servidor

Normalmente, um servidor é executado em um computador específico na rede e possui um soquete que está vinculado a um número de porta específico. No nosso caso, usamos o mesmo computador do cliente e iniciamos o servidor na porta6666:

ServerSocket serverSocket = new ServerSocket(6666);

O servidor apenas espera, ouvindo o soquete para um cliente fazer uma solicitação de conexão. Isso acontece na próxima etapa:

Socket clientSocket = serverSocket.accept();

Quando o código do servidor encontra o métodoaccept, ele bloqueia até que um cliente faça uma solicitação de conexão a ele.

Se tudo correr bem, o servidoraccepts a conexão. Após a aceitação, o servidor obtém um novo soquete,clientSocket, vinculado à mesma porta local,6666, e também tem seu ponto de extremidade remoto definido para o endereço e a porta do cliente.

Neste ponto, o novo objetoSocket coloca o servidor em conexão direta com o cliente, podemos então acessar os fluxos de saída e entrada para escrever e receber mensagens de e para o cliente, respectivamente:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

A partir daqui, o servidor é capaz de trocar mensagens com o cliente sem parar até que o soquete seja fechado com seus fluxos.

No entanto, em nosso exemplo, o servidor pode enviar apenas uma resposta de saudação antes de fechar a conexão, isso significa que, se executarmos nosso teste novamente, a conexão será recusada.

Para permitir a continuidade na comunicação, teremos que ler o fluxo de entrada dentro de um loopwhilee sair apenas quando o cliente enviar uma solicitação de encerramento, veremos isso em ação na seção a seguir.

Para cada novo cliente, o servidor precisa de um novo soquete retornado pela chamadaaccept. OserverSocket é usado para continuar a escutar as solicitações de conexão enquanto atende às necessidades dos clientes conectados. Ainda não permitimos isso em nosso primeiro exemplo.

4.2. O cliente

O cliente deve saber o nome do host ou o IP da máquina na qual o servidor está executando e o número da porta na qual o servidor está atendendo.

Para fazer uma solicitação de conexão, o cliente tenta se encontrar com o servidor na máquina e na porta do servidor:

Socket clientSocket = new Socket("127.0.0.1", 6666);

O cliente também precisa se identificar com o servidor para que ele se ligue a um número de porta local, atribuído pelo sistema, que será usado durante esta conexão. Nós não lidamos com isso nós mesmos.

O construtor acima só cria um novo socket quando o servidor temaccepted a conexão, caso contrário, teremos uma exceção de conexão recusada. Quando criados com sucesso, podemos obter fluxos de entrada e saída a partir dele para se comunicar com o servidor:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

O fluxo de entrada do cliente está conectado ao fluxo de saída do servidor, assim como o fluxo de entrada do servidor está conectado ao fluxo de saída do cliente.

5. Comunicação Contínua

Nosso servidor atual bloqueia até que um cliente se conecte a ele e depois bloqueia novamente para ouvir uma mensagem do cliente, após a única mensagem, fecha a conexão porque não lidamos com a continuidade.

Portanto, é útil apenas em solicitações de ping, mas imagine que gostaríamos de implementar um servidor de bate-papo; seria necessária uma comunicação contínua entre o servidor e o cliente.

Teremos que criar um loop while para observar continuamente o fluxo de entrada do servidor para receber mensagens.

Vamos criar um novo servidor chamadoEchoServer.java, cujo único propósito é retornar todas as mensagens que receber dos clientes:

public class EchoServer {
    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

        String inputLine;
        while ((inputLine = in.readLine()) != null) {
        if (".".equals(inputLine)) {
            out.println("good bye");
            break;
         }
         out.println(inputLine);
    }
}

Observe que adicionamos uma condição de terminação onde o loop while termina quando recebemos um caractere de ponto.

ComeçaremosEchoServer usando o método principal da mesma forma que fizemos paraGreetServer. Desta vez, nós o iniciamos em outra porta como4444 para evitar confusão.

OEchoClient é semelhante aGreetClient, então podemos duplicar o código. Estamos separando-os para maior clareza.

Em uma classe de teste diferente, devemos criar um teste para mostrar que várias solicitações paraEchoServer serão atendidas sem que o servidor feche o soquete. Isso é verdade desde que estamos enviando solicitações do mesmo cliente.

Lidar com vários clientes é um caso diferente, que veremos em uma seção subsequente.

Vamos criar um métodosetup para iniciar uma conexão com o servidor:

@Before
public void setup() {
    client = new EchoClient();
    client.startConnection("127.0.0.1", 4444);
}

Iremos igualmente criar um métodotearDown para liberar todos os nossos recursos, esta é a melhor prática para todos os casos em que usamos recursos de rede:

@After
public void tearDown() {
    client.stopConnection();
}

Vamos então testar nosso servidor de eco com algumas solicitações:

@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");
    String resp3 = client.sendMessage("!");
    String resp4 = client.sendMessage(".");

    assertEquals("hello", resp1);
    assertEquals("world", resp2);
    assertEquals("!", resp3);
    assertEquals("good bye", resp4);
}

Esta é uma melhoria em relação ao exemplo inicial, onde apenas nos comunicaríamos uma vez antes de o servidor fechar nossa conexão; now we send a termination signal to tell the server when we’re done with the session.

6. Servidor com vários clientes

Por mais que o exemplo anterior tenha sido uma melhoria em relação ao primeiro, ainda não é uma solução ótima. Um servidor deve ter capacidade para atender muitos clientes e muitas solicitações simultaneamente.

Lidar com vários clientes é o que iremos abordar nesta seção.

Outro recurso que veremos aqui é que o mesmo cliente pode desconectar e reconectar novamente, sem obter uma exceção de conexão recusada ou uma redefinição de conexão no servidor. Anteriormente, não era possível fazer isso.

Isso significa que nosso servidor será mais robusto e resiliente em várias solicitações de vários clientes.

Como faremos isso é criar um novo soquete para cada novo cliente e atender às solicitações desse cliente em um thread diferente O número de clientes que estão sendo atendidos simultaneamente será igual ao número de threads em execução.

O thread principal estará executando um loop while enquanto escuta novas conexões.

Chega de conversa, vamos criar outro servidor chamadoEchoMultiServer.java.. Dentro dele, criaremos uma classe de thread de tratamento para gerenciar as comunicações de cada cliente em seu soquete:

public class EchoMultiServer {
    private ServerSocket serverSocket;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        while (true)
            new EchoClientHandler(serverSocket.accept()).start();
    }

    public void stop() {
        serverSocket.close();
    }

    private static class EchoClientHandler extends Thread {
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        public void run() {
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (".".equals(inputLine)) {
                    out.println("bye");
                    break;
                }
                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
    }
}

Observe que agora chamamosaccept dentro de um loopwhile. Cada vez que o loopwhile é executado, ele bloqueia na chamadaaccept até que um novo cliente se conecte e, em seguida, o thread de tratamento,EchoClientHandler, é criado para este cliente.

O que acontece dentro do thread é o que fizemos anteriormente emEchoServer, onde lidamos com apenas um único cliente. Portanto,EchoMultiServer delega esse trabalho aEchoClientHandler para que possa continuar ouvindo mais clientes no loopwhile.

Ainda usaremosEchoClient para testar o servidor, desta vez criaremos vários clientes, cada um enviando e recebendo várias mensagens do servidor.

Vamos começar nosso servidor usando seu método principal na porta5555.

Para maior clareza, ainda colocaremos testes em um novo conjunto:

@Test
public void givenClient1_whenServerResponds_thenCorrect() {
    EchoClient client1 = new EchoClient();
    client1.startConnection("127.0.0.1", 5555);
    String msg1 = client1.sendMessage("hello");
    String msg2 = client1.sendMessage("world");
    String terminate = client1.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

@Test
public void givenClient2_whenServerResponds_thenCorrect() {
    EchoClient client2 = new EchoClient();
    client2.startConnection("127.0.0.1", 5555);
    String msg1 = client2.sendMessage("hello");
    String msg2 = client2.sendMessage("world");
    String terminate = client2.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

Poderíamos criar quantos casos de teste quisermos, cada um gerando um novo cliente e o servidor servirá a todos eles.

7. Conclusão

Neste tutorial, nos concentramos eman introduction to sockets programming over TCP/IPe escrevemos um aplicativo cliente / servidor simples em Java.

O código-fonte completo do artigo pode ser encontrado - como de costume - no projetoGitHub.