- Overview
- Key Features
- 2.1. IO – java.io
- 2.2. NIO – java.nio
- Configure Our Test Server
- Blocking IO – java.io
- 4.1. Envie um pedido
- 4.2. Espere pela Resposta
- Non-Blocking IO – java.nio
- 5.1. Envie um Pedido
- 5.2. Leia a resposta
- 5.3. Armazenando Dados do Nosso Buffer
- Conclusion
- Comece com Spring 5 e Spring Boot 2, através do curso Learn Spring:
Overview
A manipulação de entrada e saída são tarefas comuns para programadores Java. Neste tutorial, vamos olhar para as bibliotecas java.io (IO) originais e as bibliotecas java.nio (NIO) mais recentes e como elas diferem quando se comunicam através de uma rede.
Key Features
Vejamos as principais características de ambos os pacotes.
2.1. IO – java.io
O pacote java.io foi introduzido no Java 1.0, com o Reader introduzido no Java 1.1. Ele fornece:
- InputStream e OutputStream – que fornecem dados um byte de cada vez
- Reader and Writer – invólucros de conveniência para os streams
- modo de bloqueio – para esperar por uma mensagem completa
2.2. NIO – java.nio
O pacote java.nio foi introduzido no Java 1.4 e actualizado no Java 1.7 (NIO.2) com operações de ficheiros melhoradas e um ASynchronousSocketChannel. Ele fornece:
- Buffer – para ler pedaços de dados de cada vez
- CharsetDecoder – para mapear bytes brutos para/de caracteres legíveis
- Canal – para comunicação com o mundo exterior
- Selector – para habilitar multiplexação em um Canal Selecionável e fornecer acesso a quaisquer Canais que estejam prontos para E/S
- não-modo bloqueio – para ler o que estiver pronto
Agora vamos ver como usamos cada um destes pacotes quando enviamos dados para um servidor ou lemos a sua resposta.
Configure Our Test Server
Aqui estaremos usando WireMock para simular outro servidor para que possamos executar nossos testes independentemente.
Configuraremos para ouvir nossas solicitações e nos enviar respostas como um servidor web real faria. Também usaremos uma porta dinâmica para que não entremos em conflito com nenhum serviço em nossa máquina local.
Vamos adicionar a dependência Maven para WireMock com escopo de teste:
<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <version>2.26.3</version> <scope>test</scope></dependency>
Em uma classe de teste, vamos definir uma JUnit @Rule para iniciar o WireMock em uma porta livre. Vamos então configurá-lo para nos retornar uma resposta HTTP 200 quando pedirmos um recurso predefinido, com o corpo da mensagem como algum texto no formato JSON:
@Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());private String REQUESTED_RESOURCE = "/test.json";@Beforepublic void setup() { stubFor(get(urlEqualTo(REQUESTED_RESOURCE)) .willReturn(aResponse() .withStatus(200) .withBody("{ \"response\" : \"It worked!\" }")));}
Agora que tenhamos nosso servidor mock configurado, estamos prontos para executar alguns testes.
Blocking IO – java.io
Vejamos como o modelo original de bloqueio IO funciona lendo alguns dados de um site. Vamos usar um java.net.Socket para obter acesso a uma das portas do sistema operacional.
4.1. Envie um pedido
Neste exemplo, criaremos um pedido de GET para recuperar nossos recursos. Primeiro, vamos criar um Socket para acessar a porta que nosso servidor WireMock está escutando:
Socket socket = new Socket("localhost", wireMockRule.port())
Para comunicação HTTP ou HTTPS normal, a porta seria 80 ou 443. No entanto, neste caso, usamos wireMockRule.port() para acessar a porta dinâmica que configuramos anteriormente.
Agora vamos abrir um OutputStream no socket, envolto em um OutputStreamWriter e passá-lo para um PrintWriter para escrever nossa mensagem. E vamos ter a certeza de que vamos limpar o buffer para que o nosso pedido seja enviado:
OutputStream clientOutput = socket.getOutputStream();PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput));writer.print("GET " + TEST_JSON + " HTTP/1.0\r\n\r\n");writer.flush();
4.2. Espere pela Resposta
Passemos abrir um InputStream no socket para acessar a resposta, ler o stream com um BufferedReader, e armazená-lo em um StringBuilder:
InputStream serverInput = socket.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));StringBuilder ourStore = new StringBuilder();
Passemos usar o reader.readLine() para bloquear, esperando por uma linha completa, e então anexar a linha à nossa loja. Vamos continuar lendo até obter um zero, o que indica o fim do fluxo:
for (String line; (line = reader.readLine()) != null;) { ourStore.append(line); ourStore.append(System.lineSeparator());}
Non-Blocking IO – java.nio
Agora, vamos ver como o modelo nio do pacote non-blocking IO funciona com o mesmo exemplo.
Desta vez, vamos criar um java.nio.channel.SocketChannel para aceder à porta no nosso servidor em vez de um java.net.Socket, e passar-lhe um InetSocketAddress.
5.1. Envie um Pedido
Primeiro, vamos abrir o nosso SocketChannel:
InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());SocketChannel socketChannel = SocketChannel.open(address);
E agora, vamos obter um Charset UTF-8 padrão para codificar e escrever a nossa mensagem:
Charset charset = StandardCharsets.UTF_8;socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));
5.2. Leia a resposta
Depois de enviarmos o pedido, podemos ler a resposta em modo non-blocking, usando buffers.
Desde que vamos processar texto, vamos precisar de um ByteBuffer para os bytes em bruto e um CharBuffer para os caracteres convertidos (auxiliado por um CharsetDecoder):
ByteBuffer byteBuffer = ByteBuffer.allocate(8192);CharsetDecoder charsetDecoder = charset.newDecoder();CharBuffer charBuffer = CharBuffer.allocate(8192);
O nosso CharBuffer terá espaço sobrando se os dados forem enviados em um conjunto de caracteres multi-byte.
Note que se precisarmos de um desempenho especialmente rápido, podemos criar um MappedByteBuffer na memória nativa usando ByteBuffer.allocDirect(). No entanto, no nosso caso, usando allocate() do heap padrão é rápido o suficiente.
Quando lidamos com buffers, precisamos saber o tamanho do buffer (a capacidade), onde estamos no buffer (a posição atual), e até onde podemos ir (o limite).
Então, vamos ler do nosso SocketChannel, passando nosso ByteBuffer para armazenar nossos dados. A nossa leitura do SocketChannel terminará com a posição actual do nosso ByteBuffer definida para o próximo byte a escrever (logo após o último byte escrito), mas com o seu limite inalterado:
socketChannel.read(byteBuffer)
O nosso SocketChannel.read() retorna o número de bytes lidos que poderiam ser escritos no nosso buffer. Isto será -1 se o socket foi desconectado.
Quando nosso buffer não tem mais espaço porque ainda não processamos todos os seus dados, então SocketChannel.read() retornará zero bytes lidos mas nosso buffer.position() ainda será maior que zero.
Para ter certeza que começamos a ler do lugar certo no buffer, usaremos Buffer.flip() para definir a posição atual do nosso ByteBuffer para zero e seu limite para o último byte que foi escrito pelo SocketChannel. Nós então salvaremos o conteúdo do buffer usando nosso método storeBufferContents, que veremos mais tarde. Finalmente, vamos usar buffer.compact() para compactar o buffer e definir a posição actual pronta para a nossa próxima leitura a partir do SocketChannel.
Desde que nossos dados possam chegar em partes, vamos embrulhar nosso código de leitura de buffer em um loop com condições de terminação para verificar se nosso socket ainda está conectado ou se fomos desconectados mas ainda temos dados deixados em nosso buffer:
while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) { byteBuffer.flip(); storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore); byteBuffer.compact();}
E não vamos esquecer de fechar() nosso socket (a menos que o abramos em um bloco try-with-resources):
socketChannel.close();
5.3. Armazenando Dados do Nosso Buffer
A resposta do servidor irá conter cabeçalhos, o que pode fazer com que a quantidade de dados exceda o tamanho do nosso buffer. Então, usaremos um StringBuilder para construir nossa mensagem completa quando ela chegar.
Para armazenar nossa mensagem, nós primeiro decodificamos os bytes brutos em caracteres em nosso CharBuffer. Depois viramos os ponteiros para que possamos ler os dados do nosso personagem, e anexamos ao nosso StringBuilder expansível. Finalmente, vamos limpar o CharBuffer pronto para o próximo ciclo de escrita/leitura.
Então agora, vamos implementar nosso método completo storeBufferContents() passando em nossos buffers, CharsetDecoder, e StringBuilder:
void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder charsetDecoder, StringBuilder ourStore) { charsetDecoder.decode(byteBuffer, charBuffer, true); charBuffer.flip(); ourStore.append(charBuffer); charBuffer.clear();}
Conclusion
Neste artigo, vimos como o java original.O modelo io bloqueia, espera por um pedido e usa Streams para manipular os dados que recebe.
Em contraste, as bibliotecas java.nio permitem a comunicação sem bloqueio usando Buffers e Canais e podem fornecer acesso direto à memória para um desempenho mais rápido. Entretanto, com esta velocidade vem a complexidade adicional de lidar com buffers.
Como de costume, o código para este artigo está disponível no GitHub.
Comece com Spring 5 e Spring Boot 2, através do curso Learn Spring:
> > VERIFIQUE O CURSO