Java NIO fornece uma maneira diferente de trabalhar com IO do que com IO padrão:
Canais e Buffers: O IO padrão opera com base em fluxos de bytes e fluxos de caracteres, enquanto o NIO opera com base em canais (Canal) e buffers (Buffer Os dados são sempre lidos do canal para a área do buffer ou gravados do buffer para o buffer). canal.
IO assíncrono: Java NIO permite que você use IO de forma assíncrona. Por exemplo, quando um thread lê dados de um canal em um buffer, o thread ainda pode fazer outras coisas. Quando os dados são gravados no buffer, o thread pode continuar processando-os. Escrever em um canal a partir de um buffer é semelhante.
Seletores: Java NIO introduz o conceito de seletores, que são usados para escutar eventos em múltiplos canais (por exemplo: abertura de conexão, chegada de dados). Portanto, um único thread pode escutar vários canais de dados.
Vamos apresentar detalhadamente o conhecimento relevante de Java NIO.
Visão geral do JavaNIO
Java NIO consiste nas seguintes partes principais:
Canais
Buffers
Seletores
Embora existam muitas outras classes e componentes em Java NIO, na minha opinião, Channel, Buffer e Selector constituem a API principal. Outros componentes, como Pipe e FileLock, são apenas classes utilitárias usadas com os três componentes principais. Portanto, nesta visão geral vou me concentrar nesses três componentes. Outros componentes são abordados em capítulos separados.
Canal e Buffer
Basicamente, todo IO no NIO começa em um canal. Os canais são um pouco como streams. Os dados podem ser lidos do Canal para o Buffer ou gravados do Buffer para o Canal. Aqui está uma ilustração:
Existem vários tipos de canais e buffers. A seguir estão as implementações de alguns canais principais em JAVA NIO:
Canal de arquivo
DatagramaCanal
SocketChannel
ServerSocketChannel
Como você pode ver, esses canais cobrem IO de rede UDP e TCP, bem como IO de arquivo.
Junto com essas classes existem algumas interfaces interessantes, mas por uma questão de simplicidade tentei não mencioná-las na visão geral. Vou explicá-los em outros capítulos deste tutorial onde forem relevantes.
A seguir está a principal implementação do Buffer em Java NIO:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Esses buffers cobrem os tipos de dados básicos que você pode enviar via IO: byte, short, int, long, float, double e char.
Java NIO também possui um Mappyteuffer, que é usado para representar arquivos mapeados na memória. Não vou explicar isso na visão geral.
Seletor
O seletor permite que um único thread lide com vários canais. Se o seu aplicativo abre múltiplas conexões (canais), mas o tráfego de cada conexão é muito baixo, usar o Selector pode ser conveniente. Por exemplo, em um servidor de chat.
Esta é uma ilustração do uso de um seletor para processar 3 canais em um único thread:
Para usar um Seletor, você deve registrar um Canal com o Seletor e então chamar seu método select(). Este método será bloqueado até que um canal registrado tenha um evento pronto. Assim que esse método retornar, o thread poderá lidar com esses eventos. Exemplos de eventos são entrada de novas conexões, recebimento de dados, etc.
Java NIO x IO
(Endereço original desta parte, autor: Jakob Jenkov, tradutor: Guo Lei, revisor: Fang Tengfei)
Depois de aprender sobre Java NIO e IO API, uma pergunta veio imediatamente à mente:
Citar
Quando devo usar IO e quando devo usar NIO? Neste artigo, tentarei explicar claramente as diferenças entre Java NIO e IO, seus cenários de uso e como eles afetam o design do seu código.
Principais diferenças entre Java NIO e IO
A tabela a seguir resume as principais diferenças entre Java NIO e IO. Descreverei as diferenças em cada parte da tabela com mais detalhes.
IO NIO
Orientado a fluxo Orientado a buffer
IO bloqueador IO não bloqueador
Seletores
Orientado a fluxo e orientado a buffer
A primeira maior diferença entre Java NIO e IO é que IO é orientado a fluxo e NIO é orientado a buffer. Java IO é orientado a fluxo, o que significa que um ou mais bytes são lidos do fluxo por vez e, até que todos os bytes sejam lidos, eles não são armazenados em cache em nenhum lugar. Além disso, ele não pode mover dados no fluxo para frente ou para trás. Se você precisar mover os dados lidos do fluxo para frente e para trás, primeiro será necessário armazená-los em cache em um buffer. A abordagem orientada a buffer do Java NIO é um pouco diferente. Os dados são lidos em um buffer que é processado posteriormente, movendo-se para frente e para trás no buffer conforme necessário. Isso aumenta a flexibilidade no processamento. No entanto, você também precisa verificar se o buffer contém todos os dados que você precisa processar. Além disso, certifique-se de que, à medida que mais dados são lidos no buffer, os dados não processados no buffer não sejam substituídos.
IO bloqueador e não bloqueador
Vários fluxos de Java IO estão bloqueando. Isso significa que quando um thread chama read() ou write(), o thread é bloqueado até que alguns dados sejam lidos ou que os dados sejam completamente gravados. O thread não pode fazer mais nada durante este período. O modo sem bloqueio do Java NIO permite que um thread envie uma solicitação para ler dados de um determinado canal, mas só pode obter os dados atualmente disponíveis. Se nenhum dado estiver disponível no momento, nada será obtido. Em vez de manter o thread bloqueado, o thread pode continuar a fazer outras coisas até que os dados se tornem legíveis. O mesmo vale para gravações sem bloqueio. Um thread solicita a gravação de alguns dados em um canal, mas não precisa esperar que eles sejam completamente gravados. Enquanto isso, o thread pode fazer outras coisas. Threads normalmente usam tempo ocioso em IO sem bloqueio para executar operações de IO em outros canais, portanto, um único thread agora pode gerenciar vários canais de entrada e saída.
Seletores
Os seletores do Java NIO permitem que um único thread monitore vários canais de entrada. Você pode registrar vários canais usando um seletor e, em seguida, usar um thread separado para "selecionar" canais: esses canais já possuem entrada que pode ser processada. pronto para escrever. Esse mecanismo de seleção facilita que um único thread gerencie vários canais.
Como NIO e IO impactam o design de aplicativos
Quer você escolha uma caixa de ferramentas IO ou NIO, há vários aspectos que podem afetar o design do seu aplicativo:
Chamadas de API para classes NIO ou IO.
Processamento de dados.
O número de threads usados para processar dados.
Chamada de API
É claro que as chamadas de API ao usar o NIO parecem diferentes das do IO, mas isso não é inesperado porque, em vez de apenas ler um InputStream byte por byte, os dados devem primeiro ser lidos em um buffer e depois processados.
Processamento de dados
Usando o design NIO puro em comparação com o design IO, o processamento de dados também é afetado.
No design de IO, lemos dados byte por byte de InputStream ou Reader. Suponha que você esteja processando um fluxo de dados de texto baseado em linha, por exemplo:
Copie o código do código da seguinte forma:
Nome: Ana
Idade: 25
E-mail: [email protected]
Telefone: 1234567890
O fluxo de linhas de texto pode ser tratado assim:
Copie o código do código da seguinte forma:
InputStream input = … ; // obtém o InputStream do soquete do cliente
Leitor BufferedReader = novo BufferedReader(new InputStreamReader(entrada));
String nomeLinha = leitor.readLine();
String idadeLine = leitor.readLine();
String emailLine= leitor.readLine();
String phoneLine= leitor.readLine();
Observe que o status do processamento é determinado pelo tempo de execução do programa. Em outras palavras, uma vez que o método reader.readLine() retornar, você terá certeza de que a linha de texto foi lida. É por isso que readline() bloqueia até que toda a linha tenha sido lida. Você também sabe que esta linha contém nomes; da mesma forma, quando a segunda chamada readline() retorna, você sabe que esta linha contém idades, etc. Como você pode ver, esse manipulador só é executado quando novos dados são lidos e sabe quais são os dados em cada etapa. Depois que um thread em execução tiver processado alguns dos dados lidos, ele não reverterá os dados (na maior parte). A figura a seguir também ilustra esse princípio:
Lendo dados de um fluxo bloqueado
Embora uma implementação NIO seja diferente, aqui está um exemplo simples:
Copie o código do código da seguinte forma:
Buffer ByteBuffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
Observe a segunda linha, lendo bytes do canal em um ByteBuffer. Quando essa chamada de método retorna, você não sabe se todos os dados necessários estão no buffer. Tudo o que você sabe é que o buffer contém alguns bytes, o que dificulta um pouco o processamento.
Suponha que após a primeira chamada read(buffer), os dados lidos no buffer sejam apenas meia linha, por exemplo, "Nome: An", você pode processar os dados? Obviamente não, você precisa esperar até que toda a linha de dados seja lida no cache. Antes disso, qualquer processamento dos dados não terá sentido.
Então, como saber se o buffer contém dados suficientes para processar? Bem, você não sabe. Os métodos descobertos só podem visualizar dados no buffer. O resultado é que você terá que verificar os dados do buffer diversas vezes antes de saber que todos os dados estão no buffer. Isso não é apenas ineficiente, mas também pode sobrecarregar a solução de programação. Por exemplo:
Copie o código do código da seguinte forma:
Buffer ByteBuffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(!bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
O método bufferFull() deve monitorar quantos dados foram lidos no buffer e retornar verdadeiro ou falso, dependendo se o buffer está cheio. Em outras palavras, se o buffer estiver pronto para ser processado, ele estará cheio.
O método bufferFull() verifica o buffer, mas deve permanecer no mesmo estado de antes do método bufferFull() ser chamado. Caso contrário, os próximos dados lidos no buffer poderão não ser lidos no local correto. Isso é impossível, mas é mais uma questão a ter em conta.
Se o buffer estiver cheio, ele poderá ser processado. Se não funcionar e fizer sentido no seu caso real, você poderá lidar com parte disso. Mas em muitos casos este não é o caso. A figura a seguir mostra "ciclo de dados do buffer pronto":
Ler dados de um canal até que todos os dados sejam lidos no buffer
Resumir
O NIO permite gerenciar vários canais (conexões de rede ou arquivos) usando apenas um único thread (ou alguns), mas a desvantagem é que a análise dos dados pode ser mais complexa do que lê-los de um fluxo de bloqueio.
Se você precisar gerenciar milhares de conexões abertas simultaneamente que enviam apenas pequenas quantidades de dados por vez, como um servidor de chat, um servidor que implemente NIO pode ser uma vantagem. Da mesma forma, se você precisar manter muitas conexões abertas com outros computadores, como em uma rede P2P, pode ser uma vantagem usar um thread separado para gerenciar todas as suas conexões de saída. O esquema de projeto de múltiplas conexões em um thread é mostrado na figura abaixo:
Thread único gerencia múltiplas conexões
Se você tiver um pequeno número de conexões usando largura de banda muito alta, enviando grandes quantidades de dados de uma só vez, talvez uma implementação típica de servidor IO possa ser uma boa opção. A figura a seguir ilustra um design típico de servidor IO:
Um design típico de servidor IO:
Uma conexão é tratada por um thread
Canal
Os canais Java NIO são semelhantes aos streams, mas um pouco diferentes:
Os dados podem ser lidos do canal e os dados podem ser gravados no canal. Mas os fluxos de leitura e escrita geralmente são unilaterais.
Os canais podem ser lidos e gravados de forma assíncrona.
Os dados no canal devem primeiro ser lidos de um Buffer, ou sempre escritos a partir de um Buffer.
Conforme mencionado acima, os dados são lidos do canal para o buffer e os dados são gravados do buffer para o canal. Conforme mostrado abaixo:
Implementação de canal
Estas são as implementações dos canais mais importantes em Java NIO:
FileChannel: lê e grava dados de arquivos.
DatagramChannel: pode ler e gravar dados na rede através de UDP.
SocketChannel: Pode ler e gravar dados na rede através de TCP.
ServerSocketChannel: pode monitorar conexões TCP de entrada, como um servidor web. Um SocketChannel é criado para cada nova conexão de entrada.
Exemplo de canal básico
A seguir está um exemplo de uso do FileChannel para ler dados em um Buffer:
Copie o código do código da seguinte forma:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesLeitura! = -1) {
System.out.println("Ler " + bytesLeitura);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Observe que a chamada para buf.flip() primeiro lê os dados no Buffer, depois reverte o Buffer e depois lê os dados do Buffer. A próxima seção entrará em mais detalhes sobre o Buffer.
Tampão
Buffer em Java NIO é usado para interagir com canais NIO. Como você sabe, os dados são lidos do canal para o buffer e gravados do buffer para o canal.
Um buffer é essencialmente um bloco de memória no qual os dados podem ser gravados e a partir do qual os dados podem ser lidos. Essa memória é empacotada como um objeto NIO Buffer e fornece um conjunto de métodos para acessar essa memória de maneira conveniente.
Uso básico do Buffer
Usar o Buffer para ler e gravar dados geralmente segue as quatro etapas a seguir:
Gravar dados no Buffer
Chame o método flip()
Ler dados do Buffer
Chame o método clear() ou o método compact()
Quando os dados são gravados no buffer, o buffer registra a quantidade de dados gravados. Quando quiser ler os dados, você precisa mudar o Buffer do modo de gravação para o modo de leitura por meio do método flip(). No modo de leitura, todos os dados previamente gravados no buffer podem ser lidos.
Depois que todos os dados forem lidos, o buffer precisa ser limpo para que possa ser gravado novamente. Existem duas maneiras de limpar o buffer: chamando o método clear() ou compact(). O método clear() limpa todo o buffer. O método compact() limpará apenas os dados que foram lidos. Quaisquer dados não lidos são movidos para o início do buffer e os dados recém-gravados são colocados após os dados não lidos no buffer.
Aqui está um exemplo de uso do Buffer:
Copie o código do código da seguinte forma:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//cria buffer com capacidade de 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //lê no buffer.
while (bytesLeitura! = -1) {
buf.flip(); // deixa o buffer pronto para leitura
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // lê 1 byte por vez
}
buf.clear(); //prepara o buffer para gravação
bytesRead = inChannel.read(buf);
}
aFile.close();
Capacidade, posição e limite do buffer
Um buffer é essencialmente um bloco de memória no qual os dados podem ser gravados e a partir do qual os dados podem ser lidos. Essa memória é empacotada como um objeto NIO Buffer e fornece um conjunto de métodos para acessar essa memória de maneira conveniente.
Para entender como o Buffer funciona, você precisa estar familiarizado com suas três propriedades:
capacidade
posição
limite
O significado da posição e do limite depende se o Buffer está em modo de leitura ou modo de gravação. Não importa em que modo o Buffer esteja, o significado de capacidade é sempre o mesmo.
Aqui está uma explicação sobre capacidade, posição e limite no modo de leitura e gravação, com explicações detalhadas seguindo a ilustração.
capacidade
Como um bloco de memória, o Buffer tem um valor de tamanho fixo, também chamado de "capacidade". Você só pode gravar nele capacidade de byte, long, char e outros tipos. Quando o buffer estiver cheio, ele precisará ser esvaziado (lendo ou limpando dados) antes que a gravação dos dados possa continuar.
posição
Quando você grava dados no Buffer, a posição representa a posição atual. O valor da posição inicial é 0. Quando um dado de byte, longo, etc. é gravado no Buffer, a posição avançará para a próxima unidade de Buffer onde os dados podem ser inseridos. A posição máxima pode ser a capacidade 1.
Quando os dados são lidos, eles também são lidos de um local específico. Ao mudar o Buffer do modo de gravação para o modo de leitura, a posição será redefinida para 0. Quando os dados são lidos da posição do Buffer, a posição avança para a próxima posição legível.
limite
No modo de gravação, o limite do Buffer indica a quantidade máxima de dados que você pode gravar no Buffer. No modo de gravação, o limite é igual à capacidade do Buffer.
Ao mudar o Buffer para o modo de leitura, o limite indica a quantidade máxima de dados que você pode ler. Portanto, ao mudar o Buffer para modo de leitura, o limite será definido para o valor da posição no modo de escrita. Em outras palavras, você pode ler todos os dados gravados antes (o limite é definido para o número de dados gravados, este valor é a posição no modo de gravação)
Tipo de buffer
Java NIO possui os seguintes tipos de buffer:
ByteBuffer
MapeadoByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Como você pode ver, esses tipos de buffer representam diferentes tipos de dados. Em outras palavras, os bytes no buffer podem ser manipulados através dos tipos char, short, int, long, float ou double.
MappedByteBuffer é um pouco especial e será discutido em seu próprio capítulo.
Alocação de buffer
Para obter um objeto Buffer, você deve primeiro alocá-lo. Cada classe Buffer possui um método de alocação. Abaixo está um exemplo de alocação de um ByteBuffer com 48 bytes de capacidade.
Copie o código do código da seguinte forma:
ByteBuffer buf = ByteBuffer.allocate(48);
Isso aloca um CharBuffer que pode armazenar 1.024 caracteres:
Copie o código do código da seguinte forma:
CharBuffer buf = CharBuffer.allocate(1024);
Gravar dados no Buffer
Existem duas maneiras de gravar dados no Buffer:
Escreva do canal para o buffer.
Escreva no Buffer por meio do método put() do Buffer.
Exemplo de gravação de canal para buffer
Copie o código do código da seguinte forma:
int bytesRead = inChannel.read(buf); //lê no buffer.
Exemplo de escrita de Buffer através do método put:
Copie o código do código da seguinte forma:
buf.put(127);
Existem muitas versões do método put, permitindo gravar dados no Buffer de diferentes maneiras. Por exemplo, gravar em um local especificado ou gravar uma matriz de bytes em um Buffer. Para obter mais detalhes sobre a implementação do Buffer, consulte JavaDoc.
método flip()
O método flip muda o Buffer do modo de gravação para o modo de leitura. Chamar o método flip() definirá a posição de volta para 0 e definirá o limite para o valor da posição anterior.
Em outras palavras, position agora é usado para marcar a posição de leitura, e limit representa quantos bytes, caracteres, etc. foram escritos antes - quantos bytes, caracteres, etc. podem ser lidos agora.
Ler dados do Buffer
Existem duas maneiras de ler dados do Buffer:
Leia dados do Buffer para o Canal.
Use o método get() para ler dados do Buffer.
Exemplo de leitura de dados do Buffer para Canal:
Copie o código do código da seguinte forma:
//lê do buffer para o canal.
int bytesWritten = inChannel.write(buf);
Exemplo de uso do método get() para ler dados do Buffer
Copie o código do código da seguinte forma:
byte aByte = buf.get();
Existem muitas versões do método get, permitindo ler dados do Buffer de diferentes maneiras. Por exemplo, leia de uma posição especificada ou leia dados de um Buffer em uma matriz de bytes. Para obter mais detalhes sobre a implementação do Buffer, consulte JavaDoc.
método retroceder()
Buffer.rewind() define a posição de volta para 0, para que você possa reler todos os dados no Buffer. O limite permanece inalterado e ainda indica quantos elementos (byte, char, etc.) podem ser lidos do Buffer.
Métodos clear() e compact()
Depois que os dados no Buffer forem lidos, o Buffer precisa estar pronto para ser gravado novamente. Isso pode ser feito através dos métodos clear() ou compact().
Se o método clear() for chamado, a posição será redefinida para 0 e o limite será definido para o valor da capacidade. Em outras palavras, o buffer é limpo. Os dados no Buffer não são apagados, mas essas marcas nos dizem onde começar a gravar os dados no Buffer.
Se houver alguns dados não lidos no Buffer e você chamar o método clear(), os dados serão "esquecidos", o que significa que não haverá mais marcadores para informar quais dados foram lidos e quais não foram.
Se ainda houver dados não lidos no Buffer e os dados forem necessários posteriormente, mas você quiser gravar alguns dados primeiro, use o método compact().
O método compact() copia todos os dados não lidos para o início do Buffer. Em seguida, defina a posição logo atrás do último elemento não lido. O atributo limit ainda está definido como capacidade como o método clear(). O Buffer agora está pronto para gravar dados, mas os dados não lidos não serão substituídos.
métodos mark() e reset()
Chamando o método Buffer.mark(), você pode marcar uma posição específica no Buffer. Posteriormente, você pode restaurar esta posição chamando o método Buffer.reset(). Por exemplo:
Copie o código do código da seguinte forma:
buffer.marca();
//chama buffer.get() algumas vezes, por exemplo, durante a análise.
buffer.reset(); //define a posição de volta para a marca.
métodos equals() e compareTo()
Você pode usar os métodos equals() e compareTo() para dois Buffers.
é igual()
Quando as seguintes condições forem atendidas, significa que os dois Buffers são iguais:
Têm o mesmo tipo (byte, char, int, etc.).
O número de bytes, caracteres, etc. restantes no Buffer é igual.
Todos os bytes, caracteres, etc. restantes no Buffer são iguais.
Como você pode ver, equals compara apenas parte do Buffer, não todos os elementos nele. Na verdade, ele compara apenas os elementos restantes do Buffer.
método compareTo()
O método compareTo() compara os elementos restantes (byte, char, etc.) de dois Buffers Se as seguintes condições forem atendidas, um Buffer é considerado "menor que" o outro Buffer:
O primeiro elemento desigual é menor que o elemento correspondente no outro Buffer.
Todos os elementos são iguais, mas o primeiro Buffer se esgota antes do outro (o primeiro Buffer possui menos elementos que o outro).
(Anotação: Os elementos restantes são os elementos da posição ao limite)
Dispersão/Reunião
(Endereço original desta parte, autor: Jakob Jenkov, tradutor: Guo Lei)
Java NIO começa a suportar dispersão/reunião. Dispersão/reunião é usado para descrever a operação de leitura ou gravação em Canal (Nota do tradutor: Canal é frequentemente traduzido como canal em chinês).
A leitura dispersa do canal significa gravar os dados lidos em vários buffers durante a operação de leitura. Portanto, o Canal “espalha” os dados lidos do Canal em vários Buffers.
Coletar e gravar em um Canal significa gravar dados de vários buffers no mesmo Canal durante uma operação de gravação. Portanto, o Canal "reúne" os dados em vários Buffers e os envia para o Canal.
A dispersão/reunião é frequentemente usada em situações em que os dados transmitidos precisam ser processados separadamente. Por exemplo, ao transmitir uma mensagem que consiste em um cabeçalho e um corpo da mensagem, você pode espalhar o corpo e o cabeçalho da mensagem em buffers diferentes. você pode processar convenientemente cabeçalhos e corpos de mensagens.
Leituras dispersas
Leituras de dispersão referem-se à leitura de dados de um canal em vários buffers. Conforme descrito na figura abaixo:
O exemplo de código é o seguinte:
Copie o código do código da seguinte forma:
Cabeçalho ByteBuffer = ByteBuffer.allocate(128);
Corpo ByteBuffer = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {cabeçalho, corpo};
canal.leitura(bufferArray);
Observe que o buffer é primeiro inserido no array e, em seguida, o array é usado como parâmetro de entrada para channel.read(). O método read() grava os dados lidos do canal no buffer na ordem do buffer na matriz. Quando um buffer é preenchido, o canal grava em outro buffer.
As leituras dispersas devem preencher o buffer atual antes de passar para o próximo buffer, o que também significa que não é adequado para mensagens dinâmicas (Nota do tradutor: o tamanho da mensagem não é fixo). Em outras palavras, se houver um cabeçalho e um corpo de mensagem, o cabeçalho da mensagem deverá ser completamente preenchido (por exemplo, 128 bytes) para que as leituras dispersas funcionem corretamente.
Reunindo escritas
Coletar gravações significa que os dados são gravados de vários buffers no mesmo canal. Conforme descrito na figura abaixo:
O exemplo de código é o seguinte:
Copie o código do código da seguinte forma:
Cabeçalho ByteBuffer = ByteBuffer.allocate(128);
Corpo ByteBuffer = ByteBuffer.allocate(1024);
//grava dados em buffers
ByteBuffer[] bufferArray = {cabeçalho, corpo};
canal.write(bufferArray);
O array buffers é o parâmetro de entrada do método write(). O método write() gravará os dados no canal na ordem dos buffers no array. Portanto, se um buffer tiver capacidade de 128 bytes, mas contiver apenas 58 bytes de dados, então os 58 bytes de dados serão gravados no canal. Portanto, ao contrário das leituras de dispersão, as gravações de coleta podem lidar melhor com mensagens dinâmicas.
Transferência de dados entre canais
(Endereço original desta parte, autor: Jakob Jenkov, tradutor: Guo Lei, revisor: Zhou Tai)
No Java NIO, se um dos dois canais for um FileChannel, você poderá transferir dados diretamente de um canal (Nota do tradutor: canal geralmente é traduzido como canal em chinês) para outro canal.
transferirDe()
O método transferFrom() de FileChannel pode transferir dados do canal de origem para o FileChannel (Nota do tradutor: este método é explicado na documentação do JDK como a transferência de bytes de um determinado canal de bytes legíveis para o arquivo deste canal). Aqui está um exemplo simples:
Copie o código do código da seguinte forma:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
posição comprada = 0;
contagem longa = fromChannel.size();
toChannel.transferFrom(posição, contagem, fromChannel);
O parâmetro de entrada position do método indica o início da posição para gravar dados no arquivo de destino e count indica o número máximo de bytes transferidos. Se o canal de origem tiver menos que a contagem de bytes de espaço restante, o número de bytes transferidos será menor que o número de bytes solicitados.
Além disso, deve-se observar que na implementação do SoketChannel, o SocketChannel transmitirá apenas os dados preparados neste momento (que podem ser menores que a contagem de bytes). Portanto, o SocketChannel pode não transferir todos os dados solicitados (contagem de bytes) para o FileChannel.
transferirPara()
O método transferTo() transfere dados do FileChannel para outros canais. Aqui está um exemplo simples:
Copie o código do código da seguinte forma:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
posição comprada = 0;
contagem longa = fromChannel.size();
fromChannel.transferTo(posição, contagem, toChannel);
Você achou que este exemplo é particularmente semelhante ao anterior? Exceto que o objeto FileChannel que chama o método é diferente, todo o resto é igual.
Os problemas mencionados acima sobre SocketChannel também existem no método transferTo(). SocketChannel continuará a transmitir dados até que o buffer de destino seja preenchido.
Seletor
(Link para o texto original desta seção, autor: Jakob Jenkov, tradutor: Langjiv, revisor: Ding Yi)
Seletor é um componente do Java NIO que pode detectar um ou mais canais NIO e saber se o canal está pronto para eventos como leitura e gravação. Desta forma, um único thread pode gerenciar múltiplos canais e, portanto, múltiplas conexões de rede.
(1) Por que usar o Seletor?
A vantagem de usar apenas um único thread para lidar com vários canais é que menos threads são necessários para lidar com os canais. Na verdade, é possível usar apenas um thread para lidar com todos os canais. Para o sistema operacional, a alternância de contexto entre threads é muito cara e cada thread ocupa alguns recursos do sistema (como memória). Portanto, quanto menos threads forem usados, melhor.
No entanto, lembre-se de que os sistemas operacionais e CPUs modernos estão cada vez melhores em multitarefa, de modo que a sobrecarga do multithreading se torna cada vez menor com o tempo. Na verdade, se uma CPU tiver vários núcleos, não usar multitarefa pode ser um desperdício de energia da CPU. De qualquer forma, a discussão desse design deveria estar em um artigo diferente. Aqui basta saber que você pode manipular vários canais usando o Selector.
A seguir está um diagrama de exemplo de um único thread usando um seletor para processar três canais:
(2)Criação do Seletor
Crie um Seletor chamando o método Selector.open(), como segue:
Copie o código do código da seguinte forma:
Seletor seletor = Selector.open();
(3) Registre o canal com o Seletor
Para usar Canal e Seletor juntos, o canal deve estar registrado no seletor. Isto é conseguido através do método SelectableChannel.register(), como segue:
Copie o código do código da seguinte forma:
canal.configureBlocking(falso);
Chave SelectionKey = canal.register(seletor,
Chave de seleção.OP_READ);
Quando usado com um Seletor, o Canal deve estar no modo sem bloqueio. Isso significa que você não pode usar o FileChannel com um seletor porque o FileChannel não pode ser alternado para o modo sem bloqueio. Canais de soquete estão bem.
Observe o segundo parâmetro do método Register(). Esta é uma “coleção de interesses”, que significa quais eventos você tem interesse ao ouvir o Canal através do Seletor. Existem quatro tipos diferentes de eventos que podem ser ouvidos:
Conectar
Aceitar
Ler
Escrever
Um canal que aciona um evento significa que o evento está pronto. Portanto, um canal que se conecta com sucesso a outro servidor é chamado de “pronto para conexão”. Diz-se que um canal de soquete de servidor está "pronto para receber" quando está pronto para receber conexões de entrada. Um canal que possui dados para leitura é considerado "pronto para leitura". Pode-se dizer que um canal aguardando para gravar dados está “pronto para gravação”.
Esses quatro eventos são representados pelas quatro constantes de SelectionKey:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
Se estiver interessado em mais de um evento, você pode usar o operador OR bit a bit para conectar as constantes, como segue:
Copie o código do código da seguinte forma:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
As coleções de juros serão mencionadas abaixo.
(4) Chave de seleção
Na seção anterior, ao registrar um Canal com o Seletor, o método register() retorna um objeto SelectionKey. Este objeto contém algumas propriedades que podem ser do seu interesse:
cobrança de juros
coleção pronta
Canal
Seletor
Objetos adicionais (opcional)
Abaixo descrevo essas propriedades.
cobrança de juros
Conforme descrito na seção Registrando um Canal com um Seletor, a coleção de interesse é uma coleção de eventos interessantes que você seleciona. Você pode ler e escrever a coleção de juros através do SelectionKey, assim:
Copie o código do código da seguinte forma:
int interestSet = seleçãoKey.interess();
booleano isInterestedInAccept= (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
booleano isInterestedInConnect = interesseSet & SelectionKey.OP_CONNECT;
booleano isInterestedInRead = interestSet & SelectionKey.OP_READ;
booleano isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
Pode-se observar que usando "bit AND" para operar a coleção de juros e a constante SelectionKey fornecida, você pode determinar se um determinado evento está na coleção de juros.
coleção pronta
O conjunto pronto é o conjunto de operações para as quais o canal está pronto. Após uma seleção (Selection), você acessará primeiro o conjunto pronto. A seleção será explicada na próxima seção. A coleção pronta pode ser acessada assim:
int readySet = seleçãoKey.readyOps();
Você pode usar o mesmo método de detecção da coleta de juros para detectar quais eventos ou operações estão prontas no canal. No entanto, os quatro métodos a seguir também estão disponíveis, e todos retornam um tipo booleano:
Copie o código do código da seguinte forma:
seleçãoKey.isAcceptable();
seleçãoKey.isConnectable();
seleçãoKey.isReadable();
seleçãoKey.isWritable();
Canal+Seletor
Acessar o canal e o seletor do SelectionKey é simples. do seguinte modo:
Copie o código do código da seguinte forma:
Canalcanal= seleçãoKey.canal();
Seletor seletor = seleçãoKey.selector();
objetos adicionais
Um objeto ou mais informações podem ser anexados ao SelectionKey para identificar facilmente um determinado canal. Por exemplo, você pode anexar um Buffer para uso com um canal ou um objeto que contenha dados agregados. Como usar:
Copie o código do código da seguinte forma:
seleçãoKey.attach(oObjeto);
Objeto anexadoObj = seleçãoKey.attachment();
Você também pode anexar objetos ao registrar o Canal com o Seletor usando o método Register(). como:
Copie o código do código da seguinte forma:
Chave SelectionKey = canal.register (seletor, SelectionKey.OP_READ, theObject);
(5) Selecione o canal através do Seletor
Depois que um ou mais canais são registrados com um Seletor, vários métodos select() sobrecarregados podem ser chamados. Esses métodos retornam os canais que estão prontos para o evento de seu interesse (como conectar, aceitar, ler ou gravar). Em outras palavras, se você estiver interessado em canais "prontos para leitura", o método select() retornará os canais para os quais os eventos de leitura estão prontos.
Aqui está o método select():
seleção interna()
int select (tempo limite longo)
int selectNow()
select() bloqueia até que pelo menos um canal esteja pronto para o evento que você registrou.
select(long timeout) é o mesmo que select(), exceto que irá bloquear por até milissegundos de tempo limite (parâmetro).
selectNow() não bloqueia e retorna imediatamente, não importa qual canal esteja pronto (Nota do tradutor: Este método executa uma operação de seleção sem bloqueio. Se nenhum canal se tornar selecionável desde a operação de seleção anterior, este método retorna zero diretamente.).
O valor int retornado pelo método select() indica quantos canais estão prontos. Ou seja, quantos canais ficaram prontos desde a última chamada ao método select(). Se o método select() for chamado, 1 será retornado porque um canal ficará pronto. Se o método select() for chamado novamente, se outro canal estiver pronto, ele retornará 1 novamente. Se nenhuma operação for feita no primeiro canal pronto, agora existem dois canais prontos, mas entre cada chamada do método select(), apenas um canal está pronto.
Chaves selecionadas()
Uma vez que o método select() é chamado e o valor de retorno indica que um ou mais canais estão prontos, os canais prontos no "conjunto de chaves selecionadas" podem então ser acessados chamando o método selectedKeys() do seletor. Conforme mostrado abaixo:
Copie o código do código da seguinte forma:
Definir teclas selecionadas = selector.selectedKeys();
Ao registrar um Canal como um Seletor, o método Channel.register() retorna um objeto SelectionKey. Este objeto representa o canal cadastrado no Seletor. Esses objetos podem ser acessados através do método selectedKeySet() de SelectionKey.
Os canais prontos podem ser acessados percorrendo este conjunto selecionado de teclas. do seguinte modo:
Copie o código do código da seguinte forma:
Definir teclas selecionadas = selector.selectedKeys();
Iterador keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
Chave SelectionKey = keyIterator.next();
if(key.isAcceptable()) {
// uma conexão foi aceita por um ServerSocketChannel.
} else if (key.isConnectable()) {
// uma conexão foi estabelecida com um servidor remoto.
} senão if (key.isReadable()) {
// um canal está pronto para leitura
} senão if (key.isWritable()) {
// um canal está pronto para escrita
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">remover</a </tuihighlight>();
}
Este loop itera através de cada chave no conjunto de chaves selecionado e detecta o evento pronto para o canal correspondente a cada chave.
Observe a chamada keyIterator.remove() no final de cada iteração. O Seletor não remove instâncias de SelectionKey do próprio conjunto de chaves selecionado. Deve ser removido quando o canal for processado. Na próxima vez que o canal estiver pronto, o Seletor irá colocá-lo novamente no conjunto de teclas selecionado.
O canal retornado pelo método SelectionKey.channel() precisa ser convertido no tipo que você deseja processar, como ServerSocketChannel ou SocketChannel, etc.
(6) despertar()
Um thread é bloqueado após chamar o método select(). Mesmo que nenhum canal esteja pronto, existe uma maneira de retorná-lo a partir do método select(). Apenas deixe outros threads chamarem o método Selector.wakeup() no objeto onde o primeiro thread chamou o método select(). O thread bloqueado no método select() retornará imediatamente.
Se outro thread chamar o método wakeup(), mas nenhum thread estiver bloqueado no método select(), o próximo thread que chamar o método select() "acordará" imediatamente.
(7)fechar()
Chamar seu método close() após usar o Seletor fechará o Seletor e invalidará todas as instâncias de SelectionKey registradas no Seletor. O canal em si não fecha.
(8) Exemplo completo
Aqui está um exemplo completo: abra um seletor, registre um canal no seletor (o processo de inicialização do canal é omitido) e monitore continuamente se os quatro eventos do seletor (aceitar, conectar, ler, escrever) estão prontos.
Copie o código do código da seguinte forma:
Seletor seletor = Selector.open();
canal.configureBlocking(falso);
Chave SelectionKey = canal.register(seletor, SelectionKey.OP_READ);
enquanto(verdadeiro) {
int readyChannels = selector.select();
if(readyChannels == 0) continuar;
Definir teclas selecionadas = selector.selectedKeys();
Iterador keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
Chave SelectionKey = keyIterator.next();
if(key.isAcceptable()) {
// uma conexão foi aceita por um ServerSocketChannel.
} else if (key.isConnectable()) {
// uma conexão foi estabelecida com um servidor remoto.
} senão if (key.isReadable()) {
// um canal está pronto para leitura
} senão if (key.isWritable()) {
// um canal está pronto para escrita
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">remover</a </tuihighlight>();
}
}
canal de arquivo
(Link para o texto original desta seção, autor: Jakob Jenkov, tradutor: Zhou Tai, revisor: Ding Yi)
FileChannel em Java NIO é um canal conectado a um arquivo. Os arquivos podem ser lidos e gravados através de canais de arquivos.
FileChannel não pode ser definido para modo sem bloqueio, ele sempre é executado em modo de bloqueio.
OpenFileChannel
Antes de usar o FileChannel, ele deve ser aberto. No entanto, não podemos abrir um FileChannel diretamente. Precisamos obter uma instância de FileChannel usando um InputStream, OutputStream ou RandomAccessFile. Aqui está um exemplo de abertura de um FileChannel via RandomAccessFile:
Copie o código do código da seguinte forma:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
Ler dados do FileChannel
Chame um dos vários métodos read() para ler dados do FileChannel. como:
Copie o código do código da seguinte forma:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
Primeiro, aloque um buffer. Os dados lidos do FileChannel serão lidos no Buffer.
Em seguida, chame o método FileChannel.read(). Este método lê dados do FileChannel no Buffer. O valor int retornado pelo método read() indica quantos bytes foram lidos no Buffer. Se retornar -1, significa que o final do arquivo foi alcançado.
Gravar dados no FileChannel
Use o método FileChannel.write() para gravar dados em FileChannel. O parâmetro deste método é um Buffer. como:
Copie o código do código da seguinte forma:
String newData = "Nova String para gravar no arquivo..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
canal.write(buf);
}
Observe que FileChannel.write() é chamado em um loop while. Como não há garantia de quantos bytes o método write() pode gravar no FileChannel de uma vez, o método write() precisa ser chamado repetidamente até que não haja bytes no Buffer que não tenham sido gravados no canal.
CloseFileChannel
O FileChannel deve ser fechado quando você terminar. como:
Copie o código do código da seguinte forma:
canal.fechar();
Método de posição FileChannel
Às vezes pode ser necessário ler/gravar dados em um local específico no FileChannel. Você pode obter a posição atual do FileChannel chamando o método position().
Você também pode definir a posição atual do FileChannel chamando o método position(long pos).
Aqui estão dois exemplos:
Copie o código do código da seguinte forma:
posição longa = canal.posição();
canal.posição(pos +123);
Se você definir a posição após o final do arquivo e então tentar ler os dados do canal do arquivo, o método read retornará -1 - o sinalizador de fim do arquivo.
Se você definir a posição após o final do arquivo e depois gravar os dados no canal, o arquivo será expandido para a posição atual e os dados serão gravados. Isso pode levar a “furos de arquivo”, lacunas entre os dados gravados nos arquivos físicos do disco.
Método de tamanho do FileChannel
O método size() de uma instância FileChannel retornará o tamanho do arquivo associado à instância. como:
Copie o código do código da seguinte forma:
tamanho de arquivo longo = canal.size();
Método truncado do FileChannel
Você pode usar o método FileChannel.truncate() para interceptar um arquivo. Ao interceptar um arquivo, a parte após o comprimento especificado do arquivo será excluída. como:
Copie o código do código da seguinte forma:
canal.truncar(1024);
Este exemplo intercepta os primeiros 1.024 bytes do arquivo.
Método de força do FileChannel
O método FileChannel.force() força os dados do canal que ainda não foram gravados no disco para disco. Por motivos de desempenho, o sistema operacional armazena dados em cache na memória, portanto não há garantia de que os dados gravados no FileChannel serão gravados no disco imediatamente. Para garantir isso, o método force() precisa ser chamado.
O método force() possui um parâmetro booleano que indica se os metadados do arquivo (informações de permissão, etc.) devem ser gravados no disco ao mesmo tempo.
O exemplo a seguir força os dados do arquivo e os metadados para o disco:
Copie o código do código da seguinte forma:
canal.force(verdadeiro);
Canal de soquete
(Link para o texto original desta seção, autor: Jakob Jenkov, tradutor: Zheng Yuting, revisor: Ding Yi)
SocketChannel em Java NIO é um canal conectado a um soquete de rede TCP. SocketChannel pode ser criado das 2 maneiras a seguir:
Abra um SocketChannel e conecte-se a um servidor na Internet.
Quando uma nova conexão chega ao ServerSocketChannel, um SocketChannel é criado.
Abrir SocketChannel
Veja a seguir como abrir o SocketChannel:
Copie o código do código da seguinte forma:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
Fechar SocketChannel
Quando terminar de usar o SocketChannel, chame SocketChannel.close() para fechar o SocketChannel:
Copie o código do código da seguinte forma:
socketChannel.close();
Ler dados do SocketChannel
Para ler dados de um SocketChannel, chame um dos métodos read(). Aqui estão alguns exemplos:
Copie o código do código da seguinte forma:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead=socketChannel.read(buf);
Primeiro, aloque um buffer. Os dados lidos do SocketChannel serão colocados neste Buffer.
Em seguida, chame SocketChannel.read(). Este método lê dados do SocketChannel no Buffer. O valor int retornado pelo método read() indica quantos bytes foram lidos no Buffer. Se -1 for retornado, significa que o final do fluxo foi lido (a conexão foi fechada).
Escreva para SocketChannel
Escrever dados em SocketChannel usa o método SocketChannel.write(), que usa um Buffer como parâmetro. Os exemplos são os seguintes:
Copie o código do código da seguinte forma:
String newData = "Nova String para gravar no arquivo..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
canal.write(buf);
}
Observe que o método SocketChannel.write() é chamado em um loop while. O método Write() não pode garantir quantos bytes podem ser gravados no SocketChannel. Então, chamamos write() repetidamente até que o Buffer não tenha mais bytes para escrever.
modo sem bloqueio
Você pode definir SocketChannel para o modo sem bloqueio. Após a configuração, você pode chamar connect(), read() e write() no modo assíncrono.
conectar()
Se SocketChannel estiver no modo sem bloqueio e connect() for chamado neste momento, o método poderá retornar antes que a conexão seja estabelecida. Para determinar se a conexão foi estabelecida, você pode chamar o método finishConnect(). Assim:
Copie o código do código da seguinte forma:
socketChannel.configureBlocking(falso);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
while(!socketChannel.finishConnect() ){
// espere ou faça outra coisa...
}
escrever()
No modo sem bloqueio, o método write() pode retornar antes de escrever qualquer coisa. Então write() precisa ser chamado no loop. Já houve exemplos antes, então não vou entrar em detalhes aqui.
ler()
No modo sem bloqueio, o método read() pode retornar antes de qualquer dado ser lido. Então você precisa prestar atenção ao seu valor de retorno int, que lhe dirá quantos bytes foram lidos.
Modo sem bloqueio e seletores
O modo sem bloqueio funciona melhor com seletores. Ao registrar um ou mais SocketChannels com o Seletor, você pode perguntar ao seletor qual canal está pronto para leitura, escrita, etc. A combinação de Selector e SocketChannel será discutida em detalhes posteriormente.
Canal ServerSocket
(Link para o texto original desta seção, autor: Jakob Jenkov, tradutor: Zheng Yuting, revisor: Ding Yi)
ServerSocketChannel em Java NIO é um canal que pode escutar novas conexões TCP de entrada, assim como ServerSocket em IO padrão. A classe ServerSocketChannel está no pacote java.nio.channels.
Aqui está um exemplo:
Copie o código do código da seguinte forma:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
enquanto(verdadeiro){
SocketChannel soqueteChannel =
serverSocketChannel.accept();
//faça algo com socketChannel...
}
Abra ServerSocketChannel
Abra o ServerSocketChannel chamando o método ServerSocketChannel.open().
Copie o código do código da seguinte forma:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Fechar ServerSocketChannel
Feche ServerSocketChannel chamando o método ServerSocketChannel.close().
Copie o código do código da seguinte forma:
serverSocketChannel.close();
Ouça novas conexões de entrada
Ouça novas conexões de entrada por meio do método ServerSocketChannel.accept(). Quando o método accept() retorna, ele retorna um SocketChannel contendo a nova conexão de entrada. Portanto, o método accept() será bloqueado até que uma nova conexão chegue.
Normalmente, em vez de apenas ouvir uma conexão, o método accept() é chamado no loop while, como no exemplo a seguir:
Copie o código do código da seguinte forma:
enquanto(verdadeiro){
SocketChannel soqueteChannel =
serverSocketChannel.accept();
//faça algo com socketChannel...
}
Claro, você também pode usar outros critérios de saída além de true no loop while.
modo sem bloqueio
ServerSocketChannel pode ser definido para o modo sem bloqueio. No modo sem bloqueio, o método accept() retornará imediatamente. Se não houver nenhuma nova conexão de entrada, o valor de retorno será nulo. Portanto, você precisa verificar se o SocketChannel retornado é nulo. como:
Copie o código do código da seguinte forma:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(falso);
enquanto(verdadeiro){
SocketChannel soqueteChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//faça algo com socketChannel...
}
}
Canal de datagrama
(Link para o texto original desta seção, autor: Jakob Jenkov, tradutor: Zheng Yuting, revisor: Ding Yi)
DatagramChannel em Java NIO é um canal que pode enviar e receber pacotes UDP. Como o UDP é um protocolo de rede sem conexão, ele não pode ser lido e gravado como outros canais. Ele envia e recebe pacotes de dados.
OpenDatagramCanal
Veja como o DatagramChannel é aberto:
Copie o código do código da seguinte forma:
canal DatagramChannel = DatagramChannel.open();
canal.socket().bind(new InetSocketAddress(9999));
O DatagramChannel aberto por este exemplo pode receber pacotes na porta UDP 9999.
receber dados
Receba dados do DatagramChannel através do método receiver(), como:
Copie o código do código da seguinte forma:
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
canal.receber(buf);
O método receiver() copiará o conteúdo do pacote de dados recebido para o Buffer especificado. Se o Buffer não puder acomodar os dados recebidos, os dados em excesso serão descartados.
Enviar dados
Envie dados do DatagramChannel através do método send(), como:
Copie o código do código da seguinte forma:
String newData = "Nova String para gravar no arquivo..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = canal.send(buf, new InetSocketAddress("jenkov.com", 80));
Este exemplo envia uma sequência de caracteres para a porta UDP 80 do servidor "jenkov.com". Como o servidor não está monitorando esta porta, nada acontecerá. Também não irá notificá-lo se o pacote de saída foi recebido, porque o UDP não tem nenhuma garantia em termos de entrega de dados.
Conecte-se a um endereço específico
Um DatagramChannel pode ser “conectado” a um endereço específico na rede. Como o UDP não tem conexão, conectar-se a um endereço específico não cria uma conexão real como um canal TCP. Em vez disso, o DatagramChannel é bloqueado para que só possa enviar e receber dados de um endereço específico.
Aqui está um exemplo:
Copie o código do código da seguinte forma:
canal.connect(new InetSocketAddress("jenkov.com", 80));
Uma vez conectado, você também pode usar os métodos read() e write() como faria com um canal tradicional. Simplesmente não há garantias em relação à transferência de dados. Aqui estão alguns exemplos:
Copie o código do código da seguinte forma:
int bytesRead = canal.read(buf);
int bytesWritten = canal.write(mas);
Cano
(Link para o texto original desta seção, autor: Jakob Jenkov, tradutor: Huang Zhong, revisor: Ding Yi)
Um canal Java NIO é uma conexão de dados unidirecional entre 2 threads. Pipe tem um canal de origem e um canal de destino. Os dados serão gravados no canal coletor e lidos no canal de origem.
Aqui está uma ilustração do princípio Pipe:
Criar pipeline
Abra o pipe através do método Pipe.open(). Por exemplo:
Copie o código do código da seguinte forma:
Tubo tubo = Pipe.open();
Gravar dados no pipe
Para gravar dados no pipe, você precisa acessar o canal coletor. Assim:
Copie o código do código da seguinte forma:
Pipe.SinkChannel sinkChannel = pipe.sink();
Grave dados em SinkChannel chamando o método write() de SinkChannel, assim:
Copie o código do código da seguinte forma:
String newData = "Nova String para gravar no arquivo..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
<b>sinkChannel.write(buf);</b>
}
[código]
Ler dados do pipe
Para ler dados de um pipe, você precisa acessar o canal de origem, assim:
[código]
Pipe.SourceChannel sourceChannel = pipe.source();
Chame o método read() do canal de origem para ler os dados, assim:
Copie o código do código da seguinte forma:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
O valor int retornado pelo método read() nos dirá quantos bytes foram lidos no buffer.