Conhecimento de fundo
Síncrono, assíncrono, bloqueador, não bloqueando
Primeiro de tudo, esses conceitos são muito fáceis de confundir, mas estão envolvidos no NIO, então vamos resumir.
Sincronização: Quando a chamada da API retorna, o chamador sabe como a operação é resultante (quantos bytes são realmente leitura/gravação).
Assíncrono: Comparado com a sincronização, o chamador não sabe o resultado da operação quando a chamada da API retornar e o retorno de chamada notificará o resultado posteriormente.
Bloqueio: quando não houver dados a serem lidos ou todos os dados não podem ser gravados, suspenda o thread atual esperando.
Não bloqueando: ao ler, você pode ler o máximo que os dados que você pode ler e retornar. Ao escrever, você pode escrever o máximo que os dados que você pode escrever e retornar.
Para as operações de E/S, de acordo com os documentos do site oficial da Oracle, o padrão de sincronização e divisão assíncrona é "se o chamador precisa aguardar a conclusão da operação de E/S". Essa "aguardando a operação de E/S ser concluída" não significa que os dados devem ser lidos ou todos os dados devem ser escritos, mas se o chamador precisa esperar quando a operação de E/S é realmente realizada, como o tempo em que os dados são transmitidos entre o buffer de pilha de protocol TCP/IP e o buffer JVM.
Portanto, nossos métodos comumente usados read () e write () são E/S síncronos. A E/S síncrona é dividida em dois modos: bloqueando e não bloqueando. Se for um modo não bloqueador, ele será retornado diretamente quando detectar que não há dados a serem lidos e a operação de E/S não é realmente executada.
Em resumo, em Java, na verdade existem apenas três mecanismos: E/S de bloqueio síncrono, E/S síncrona sem bloqueio e E/S assíncrona. O que estamos falando abaixo é os dois primeiros. O JDK1.7 começou a introduzir a E/S assíncrona, que é chamada NIO.2.
IO tradicional
Sabemos que o surgimento de uma nova tecnologia é sempre acompanhado de melhorias e melhorias, assim como o surgimento de Javanio.
A E/S tradicional está bloqueando a E/S, e o principal problema é o desperdício de recursos do sistema. Por exemplo, para ler os dados de uma conexão TCP, chamamos o método read () do InputStream, que fará com que o encadeamento atual seja suspenso e não será despertado até que os dados cheguem. O thread ocupa recursos de memória (pilha de threads de armazenamento) durante o período de chegada dos dados, mas não faz nada. É isso que diz o ditado, ocupando o poço e não o cocô. Para ler os dados de outras conexões, precisamos iniciar outro thread. Isso pode ser bom quando não houver muitas conexões simultâneas, mas quando o número de conexões atingir uma certa escala, os recursos de memória serão consumidos por um grande número de threads. Por outro lado, a troca de roscas requer alterar o status do processador, como os valores dos contadores e registros do programa, portanto, alternar entre um grande número de encadeamentos também é um desperdício de recursos.
Com o desenvolvimento da tecnologia, os sistemas operacionais modernos fornecem novos mecanismos de E/S que podem evitar esse desperdício de recursos. Com base nisso, Javanio nasceu e a característica representativa do NIO é a E/S não bloqueadora. Imediatamente depois, descobrimos que simplesmente o uso de E/S não bloqueador não pode resolver o problema, porque no modo não bloqueador, o método read () retornará imediatamente quando os dados não forem lidos. Não sabemos quando os dados chegarão, para que possamos continuar chamando o método read () para tentar novamente. Isso é obviamente um desperdício de recursos da CPU. A partir do seguinte, podemos saber que o componente seletor nasceu para resolver esse problema.
Componentes principais de Javanio
1.Channel
conceito
Todas as operações de E/S em Javanio são baseadas em objetos de canal, assim como as operações de fluxo são baseadas em objetos de fluxo, por isso é necessário entender primeiro o que é o canal. O conteúdo a seguir é extraído da documentação do JDK1.8
Achannel representa a conexão de anexo com a anexidade Suchasahardwardevice, Afile, Annetworksocket, componente Oroprograma que pode ser executado em uma operação distintiva de uma operação distintiva, ou a escrita ou escrita ou a escrita.
A partir do conteúdo acima, podemos ver que um canal representa uma conexão com uma certa entidade, que pode ser um arquivo, um soquete de rede etc. Em outras palavras, o canal é uma ponte fornecida pela Javanio para que nossos programas interajam com os serviços de E/S subjacentes do sistema operacional.
Os canais são uma descrição muito básica e abstrata, interagem com diferentes serviços de E/S, realizam operações de E/S diferentes e implementam implementações diferentes. Portanto, os específicos incluem Filechannel, Socketchannel, etc.
O canal é semelhante ao fluxo quando usado. Ele pode ler dados em um buffer ou gravar dados no buffer no canal.
Obviamente, também existem diferenças, que são refletidas principalmente nos dois pontos a seguir:
Um canal pode ser lido e escrito, enquanto um fluxo é unidirecional (tão dividido em InputStream e OutputStream)
O canal tem modo de E/S sem bloqueio
concluir
As implementações de canal mais usadas em Javanio são as seguintes, e pode -se ver que elas correspondem às classes de operação de E/S tradicionais uma a uma.
FileChannel: Leia e grava arquivos
DataGramChannel: Comunicação de rede de protocolo UDP
Socketchannel: Comunicação de Rede de Protocolo TCP
Serversocketchannel: ouça as conexões TCP
2.Buffer
O buffer usado no NIO não é uma matriz de bytes simples, mas uma classe buffer encapsulada. Através da API que ele fornece, podemos manipular de maneira flexível dados. Vamos dar uma olhada mais de perto.
Correspondente aos tipos básicos de Java, o NIO fornece uma variedade de tipos de buffer, como bytebuffer, charbuffer, intbuffer, etc. A diferença é que o comprimento da unidade do buffer é diferente ao ler e escrever (leitura e escrita nas unidades das variáveis de tipo correspondentes).
Existem 3 variáveis muito importantes no buffer. Eles são a chave para entender o mecanismo de trabalho do buffer, ou seja,
capacidade (capacidade total)
posição (a posição atual do ponteiro)
Limite (LEIA/WRITE LIMPARY POSIÇÃO)
O método de trabalho de buffer é muito semelhante às matrizes de caracteres em C. Na analogia, a capacidade é o comprimento total da matriz, a posição é a variável subscrita para lermos/gravar caracteres e o limite é a posição do caractere final. A situação das três variáveis no início do buffer é a seguinte
Durante o processo de leitura/escrita do buffer, a posição se moverá para trás e o limite é o limite do movimento da posição. Não é difícil imaginar que, ao escrever em um buffer, o limite deve ser definido para o tamanho da capacidade e, ao ler um buffer, o limite deve ser definido para a posição final real dos dados. (Nota: escrever dados de buffer para o canal é uma operação de leitura de buffer, e a leitura de dados do canal para o buffer é uma operação de gravação de buffer)
Antes de ler/escrever operações em um buffer, podemos chamar alguns métodos auxiliares fornecidos pela classe buffer para definir corretamente os valores de posição e limite, principalmente da seguinte maneira
flip (): defina limite para o valor da posição e, em seguida, defina a posição para 0. Chamadas antes de ler o buffer.
Rewind (): Basta definir a posição 0. Geralmente é chamado antes de reler os dados do buffer, por exemplo, será usado ao ler os dados do mesmo buffer e gravar -os em vários canais.
Clear (): retornar ao estado inicial, ou seja, o limite é igual à capacidade, posição definida como 0. Ligue para o buffer antes de escrever.
compact (): mova os dados não lidos (dados entre posição e limite) para o início do buffer e defina a posição para a próxima posição no final desses dados. De fato, é equivalente a escrever uma parte de dados para o buffer novamente.
Em seguida, consulte um exemplo, use o FileChannel para ler e gravar arquivos de texto e use este exemplo para verificar as características legíveis e graváveis do canal e o uso básico de buffer (observe que o FileChannel não pode ser definido como modo não bloqueador).
Canal filechannel = new RandomAccessFile ("test.txt", "rw"). mundo!/n ".getBytes (standardcharsets.utf_8)); // buffer -> canalbytebuffer.flip (); while (bytebuffer.hasReNaining ()) {canal.write (bytebuffer);} canal.position (0); CharSetDecoder decodificador = StandardCharSets.Utf_8.NewDecoder (); // Leia todos os dados bytebuffer.clear (); while (canal.read (bytebuffer)! = -1 || bytebuffer.position ()> 0) {bytebuffer.flip (); // decode charbuffer.clear (); decoder.decode (bytebuffer, charbuffer, falsel); Pode haver dados restantes} canal.close ();Neste exemplo, dois buffers são usados, onde o ByTeBuffer é o buffer de dados para leitura e escrita de canal, e o CharBuffer é usado para armazenar caracteres decodificados. O uso de Clear () e Flip () é como mencionado acima. Deve -se notar que o método Último Compact () é, mesmo que o tamanho do Charbuffer seja completamente suficiente para acomodar o bytebuffer de dados decodificado, esse compact () também é essencial. Isso ocorre porque a codificação UTF-8 de caracteres chineses comumente usados é responsável por 3 bytes; portanto, há uma alta probabilidade de que ela ocorra no truncamento médio. Por favor, veja a figura abaixo:
Quando o decodificador lê 0xe4 no final do buffer, ele não pode ser mapeado para um unicode. O terceiro parâmetro do método decode (), falso, é usado para fazer com que o decodificador trate os bytes não aplicáveis e os dados subsequentes como dados adicionais. Portanto, o método decode () parará por aqui e a posição voltará à posição 0xe4. Dessa forma, o primeiro byte codificado pela palavra "médio" é deixado no buffer e deve ser compactado na frente e emendado junto com os dados de sequência corretos e subsequentes. Em relação à codificação de personagens, você pode se referir a " Explicação de ANSI, Unicode, BMP, UTF e outros conceitos de codificação "
BTW, o CharSetDecoder no exemplo também é um novo recurso do Javanio, então você deve ter descoberto um pouco. As operações de NiO são orientadas a buffer (a E/S tradicional é orientada para o fluxo).
Nesse ponto, aprendemos sobre o uso básico de canal e buffer. Em seguida, falaremos sobre componentes importantes para deixar um thread gerenciar vários canais.
3.setor
O que é seletor
O seletor é um componente especial usado para coletar o estado (ou eventos) de cada canal. Primeiro, registramos o canal no seletor e definimos o evento com o qual nos preocupamos, e depois podemos esperar silenciosamente o evento que ocorre chamando o método select ().
O canal tem os 4 eventos a seguir para ouvirmos:
Aceitar: há uma conexão aceitável
Conecte: Conecte -se com sucesso
Leia: há dados para ler
Escreva: você pode escrever dados
Por que usar seletor
Como mencionado acima, se você usar a E/S de bloqueio, precisará multi-thread (um desperdício de memória) e, se você usar a E/S não bloqueadora, precisará tentar constantemente novamente (um consumo de CPU). O surgimento do seletor resolve esse problema embaraçoso. No modo não bloqueador, através do seletor, nossos threads funcionam apenas para canais prontos, e não há necessidade de tentar cegamente. Por exemplo, quando nenhum dado é atingido em todos os canais, nenhum evento de leitura ocorre e nosso thread será suspenso no método select (), desistindo de recursos da CPU.
Como usar
Como mostrado abaixo, crie um seletor e registre um canal.
NOTA: Para registrar o canal para seletor, você deve primeiro definir o canal para o modo não bloqueador, caso contrário, uma exceção será lançada.
Seletor seletor = selettor.open (); canal.configureblocking (false); selectionKey key = canal.register (seletor, seleçãokey.op_read);
O segundo parâmetro do método Register () é chamado de "conjunto de juros", que é o conjunto de eventos com o qual você está preocupado. Se você se preocupa com vários eventos, separe -os com um "bital ou operador", por exemplo
SelectionKey.op_read | SelectionKey.op_write
Este método de escrita não está familiarizado com ele. É reproduzido em linguagens de programação que suportam operações de bits. O uso de uma variável inteira pode identificar vários estados. Como é feito? Na verdade, é muito simples. Por exemplo, primeiro predefinido algumas constantes, e seus valores (binários) são os seguintes
Pode -se descobrir que os bits com seu valor 1 são todos escalonados; portanto, os valores obtidos após o execução bit neles ou cálculos neles não têm ambiguidade e podem ser deduzidos inversamente de quais variáveis são calculadas. Como julgar, sim, é a operação "bits e". Por exemplo, agora existe um valor variável de conjunto de estados de 0011. Precisamos determinar apenas se o valor de "0011 & op_read" é 1 ou 0 para determinar se o conjunto contém o estado OP_read.
Em seguida, observe que o método Register () retorna um objeto SelectionKey, que contém as informações para este registro, e também podemos modificar as informações de registro através dele. A partir do exemplo completo abaixo, podemos ver que, depois de selecionar (), também preparamos os canais com o estado, obtendo uma coleção de teclas de seleção.
Um exemplo completo
Os conceitos e coisas teóricas foram explicadas (na verdade, depois de escrevê -las aqui, descobri que não escrevi muito, o que é tão embaraçoso (⊙ˍ⊙)). Vamos dar uma olhada em um exemplo completo.
Este exemplo usa o Javanio para implementar um servidor de thread único. A função é muito simples. Ele ouve a conexão do cliente. Quando a conexão é estabelecida, ela lê a mensagem do cliente e responde a uma mensagem ao cliente.
Deve -se notar que eu uso o caractere '/0' (um byte com um valor 0) para identificar o final da mensagem.
Servidor rosqueado único
classe pública nioserver {public static void main (string [] args) lança ioexception {// crie um seletorSelector Selector = Selector.open (); // Inicialize o TCP Conexão de canal de audição servidorsocketchannel listEnchannel = serverSocketchAnnel.open (); listEnchannel.bind (new inetSocketAddress (9999)); listEnchannel.configureblocking (false); // Registre -se no seletor (ouça seu evento aceite) listenchannel.register (selettor, seleçãokey.op_accept); // crie um buffer buffer buffer = bytebuffer.allocate (100); while (true) {Selector.Select (); // bloqueia até que um evento seja ouvido ocorra iterator <sectionKey> keyiter = Selector.SelectedKeys (). Iterator (); // Acesse o evento de canal selecionado através de um iterador por sua vez (Keyiter.hasnext ()) {selectionKey Key = keyTer.next (); Canal de socketchannel = ((serverSocketchannel) key.Channel ()). Aceitou (); canal.configureblocking (false); canal.register (seletor, selectionKey.op_read); system.out.println ("conexão estabelecida com [" + canal.GeRRemoTEdDress () + "]! buffer.clear (); // Leia no final do fluxo, indicando que a conexão TCP foi desconectada, //, portanto, é necessário fechar o canal ou cancelar ouvindo o evento de leitura // caso contrário, ele será infinitamente se ((socketchAnnel) key.channel ()). leia (buffer) == -1) {{KeynEn.chann.chann.channel (). buffer.flip (); while (buffer.hasReNaining ()) {byte b = buffer.get (); if (b == 0) {// /0system.out.println () no final da mensagem do cliente; // resposta buffer.clear (); buffer.put ("hello!/0" .getBytes (); {((Socketchannel) key.channel ()). Write (buffer);}} else {System.out.print ((char) b);}}} // Para eventos que foram processados, você deve remover manualmente keyiter.remove ();}}}}Cliente
Este cliente é usado puramente para teste. Para tornar isso menos difícil, ele usa métodos de escrita tradicionais e o código é muito curto.
Se você precisar ser mais rigoroso nos testes, execute um grande número de clientes simultaneamente para contar o tempo de resposta do servidor e não enviar dados imediatamente após a criação da conexão, de modo a dar um jogo completo às vantagens da E/S não bloqueadora no servidor.
public class Client {public static void main (string [] args) lança exceção {soquete soquete = new Socket ("localhost", 9999); inputStream IS = Socket.getInputStream (); outputStream OS = Socket.getoutStream (); // Dados do servidor OS.write ("Hello,,,", ". b; while ((b = is.read ())! = 0) {System.out.print ((char) b);} system.out.println (); soket.close ();}}Resumir
O exposto acima é sobre um rápido entendimento dos componentes do NIO no Java. Espero que seja útil para todos. Os amigos interessados podem continuar se referindo a outros conteúdos relevantes deste site. Se houver alguma falha, deixe uma mensagem para apontá -la. Obrigado amigos pelo seu apoio para este site!