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.