Um guia para o canal de soquete assíncrono NIO2

Um guia para o canal de soquete assíncrono NIO2

1. Visão geral

Neste artigo, demonstraremos como criar um servidor simples e seu cliente usando as APIs do canal Java 7 NIO.2.

Veremos as classesAsynchronousServerSocketChannel eAsynchronousSocketChannel, que são as principais classes usadas na implementação do servidor e do cliente, respectivamente.

Se você é novo nas APIs do canal NIO.2, temos um artigo introdutório neste site. Você pode lê-lo seguindo estelink.

Todas as classes necessárias para usar APIs de canal NIO.2 são agrupadas no pacotejava.nio.channels:

import java.nio.channels.*;

2. O servidor comFuture

Uma instância deAsynchronousServerSocketChannel é criada chamando a API estática aberta em sua classe:

AsynchronousServerSocketChannel server
  = AsynchronousServerSocketChannel.open();

Um canal de soquete do servidor assíncrono recém-criado está aberto, mas ainda não está vinculado, portanto, devemos vinculá-lo a um endereço local e, opcionalmente, escolher uma porta:

server.bind(new InetSocketAddress("127.0.0.1", 4555));

Também poderíamos ter passado nulo para que ele use um endereço local e se ligue a uma porta arbitrária:

server.bind(null);

Uma vez vinculado, a APIaccept é usada para iniciar a aceitação de conexões ao soquete do canal:

Future acceptFuture = server.accept();

Como nas operações de canal assíncronas, a chamada acima retorna imediatamente e a execução continua.

A seguir, podemos usar a APIget para consultar uma resposta do objetoFuture:

AsynchronousSocketChannel worker = future.get();

Essa chamada será bloqueada, se necessário, para aguardar uma solicitação de conexão de um cliente. Opcionalmente, podemos especificar um tempo limite se não quisermos esperar para sempre:

AsynchronousSocketChannel worker = acceptFuture.get(10, TimeUnit.SECONDS);

Após o retorno da chamada acima e a operação ter sido bem-sucedida, podemos criar um loop no qual ouvimos as mensagens recebidas e as ecoamos de volta ao cliente.

Vamos criar um método chamadorunServer dentro do qual faremos a espera e processaremos todas as mensagens recebidas:

public void runServer() {
    clientChannel = acceptResult.get();
    if ((clientChannel != null) && (clientChannel.isOpen())) {
        while (true) {
            ByteBuffer buffer = ByteBuffer.allocate(32);
            Future readResult  = clientChannel.read(buffer);

            // perform other computations

            readResult.get();

            buffer.flip();
            Future writeResult = clientChannel.write(buffer);

            // perform other computations

            writeResult.get();
            buffer.clear();
        }
        clientChannel.close();
        serverChannel.close();
    }
}

Dentro do loop, tudo o que fazemos é criar um buffer para leitura e gravação, dependendo da operação.

Então, toda vez que fizermos uma leitura ou uma gravação, podemos continuar executando qualquer outro código e quando estivermos prontos para processar o resultado, chamamos a APIget() no objetoFuture.

Para iniciar o servidor, chamamos seu construtor e, em seguida, o métodorunServer dentro demain:

public static void main(String[] args) {
    AsyncEchoServer server = new AsyncEchoServer();
    server.runServer();
}

3. O servidor comCompletionHandler

Nesta seção, veremos como implementar o mesmo servidor usando a abordagemCompletionHandler em vez de uma abordagemFuture.

Dentro do construtor, criamos umAsynchronousServerSocketChannel e o vinculamos a um endereço local da mesma maneira que fizemos antes:

serverChannel = AsynchronousServerSocketChannel.open();
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 4999);
serverChannel.bind(hostAddress);

Em seguida, ainda dentro do construtor, criamos um loop while no qual aceitamos qualquer conexão de entrada de um cliente. Este loop while é usado estritamente paraprevent the server from exiting before establishing a connection with a client.

Paraprevent the loop from running endlessly, chamamosSystem.in.read() em seu final para bloquear a execução até que uma conexão de entrada seja lida do fluxo de entrada padrão:

while (true) {
    serverChannel.accept(
      null, new CompletionHandler() {

        @Override
        public void completed(
          AsynchronousSocketChannel result, Object attachment) {
            if (serverChannel.isOpen()){
                serverChannel.accept(null, this);
            }

            clientChannel = result;
            if ((clientChannel != null) && (clientChannel.isOpen())) {
                ReadWriteHandler handler = new ReadWriteHandler();
                ByteBuffer buffer = ByteBuffer.allocate(32);

                Map readInfo = new HashMap<>();
                readInfo.put("action", "read");
                readInfo.put("buffer", buffer);

                clientChannel.read(buffer, readInfo, handler);
             }
         }
         @Override
         public void failed(Throwable exc, Object attachment) {
             // process error
         }
    });
    System.in.read();
}

Quando uma conexão é estabelecida, o método de retorno de chamadacompleted emCompletionHandler da operação de aceitação é chamado.

Seu tipo de retorno é uma instância deAsynchronousSocketChannel. Se o canal de soquete do servidor ainda estiver aberto, chamamos a APIaccept novamente para nos prepararmos para outra conexão de entrada enquanto reutilizamos o mesmo manipulador.

Em seguida, atribuímos o canal de soquete retornado a uma instância global. Em seguida, verificamos se não é nulo e se está aberto antes de executar operações nele.

O ponto em que podemos iniciar as operações de leitura e gravação é dentro da API de retorno de chamadacompleted do manipulador de operaçãoaccept. Esta etapa substitui a abordagem anterior em que pesquisamos o canal com a APIget.

Observe quethe server will no longer exit after a connection has been established, a menos que o fechemos explicitamente.

Observe também que criamos uma classe interna separada para lidar com operações de leitura e gravação; ReadWriteHandler. Veremos como o objeto de anexo é útil neste momento.

Primeiro, vamos dar uma olhada na classeReadWriteHandler:

class ReadWriteHandler implements
  CompletionHandler> {

    @Override
    public void completed(
      Integer result, Map attachment) {
        Map actionInfo = attachment;
        String action = (String) actionInfo.get("action");

        if ("read".equals(action)) {
            ByteBuffer buffer = (ByteBuffer) actionInfo.get("buffer");
            buffer.flip();
            actionInfo.put("action", "write");

            clientChannel.write(buffer, actionInfo, this);
            buffer.clear();

        } else if ("write".equals(action)) {
            ByteBuffer buffer = ByteBuffer.allocate(32);

            actionInfo.put("action", "read");
            actionInfo.put("buffer", buffer);

            clientChannel.read(buffer, actionInfo, this);
        }
    }

    @Override
    public void failed(Throwable exc, Map attachment) {
        //
    }
}

O tipo genérico de nosso anexo na classeReadWriteHandler é um mapa. Precisamos especificamente passar dois parâmetros importantes por ele - o tipo de operação (ação) e o buffer.

A seguir, veremos como esses parâmetros são usados.

A primeira operação que realizamos é umread, pois este é um servidor de eco que só reage às mensagens do cliente. Dentro do método de retorno de chamadaReadWriteHandler'scompleted, recuperamos os dados anexados e decidimos o que fazer de acordo.

Se for uma operaçãoread que foi concluída, recuperamos o buffer, alteramos o parâmetro de ação do anexo e executamos uma operaçãowrite imediatamente para ecoar a mensagem para o cliente.

Se for uma operaçãowrite que acabou de ser concluída, chamamos a APIread novamente para preparar o servidor para receber outra mensagem.

4. O cliente

Depois de configurar o servidor, podemos agora configurar o cliente chamando a APIopen na classeAsyncronousSocketChannel. Essa chamada cria uma nova instância do canal de soquete do cliente que usamos para fazer uma conexão com o servidor:

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 4999)
Future future = client.connect(hostAddress);

A operaçãoconnect não retorna nada em caso de sucesso. No entanto, ainda podemos usar o objetoFuture para monitorar o estado da operação assíncrona.

Vamos chamar a APIget para aguardar a conexão:

future.get()

Após esta etapa, podemos começar a enviar mensagens para o servidor e receber ecos para o mesmo. O métodosendMessage se parece com isto:

public String sendMessage(String message) {
    byte[] byteMsg = new String(message).getBytes();
    ByteBuffer buffer = ByteBuffer.wrap(byteMsg);
    Future writeResult = client.write(buffer);

    // do some computation

    writeResult.get();
    buffer.flip();
    Future readResult = client.read(buffer);

    // do some computation

    readResult.get();
    String echo = new String(buffer.array()).trim();
    buffer.clear();
    return echo;
}

5. O teste

Para confirmar que nossos aplicativos de servidor e cliente estão executando de acordo com as expectativas, podemos usar um teste:

@Test
public void givenServerClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");

    assertEquals("hello", resp1);
    assertEquals("world", resp2);
}

6. Conclusão

Neste artigo, exploramos as APIs de canal de soquete assíncrono Java NIO.2. Conseguimos avançar no processo de construção de um servidor e cliente com essas novas APIs.

Você pode acessar o código-fonte completo deste artigo noGithub project.