Java sockets de e/S: o bloqueio, o bloqueio não e assíncrona

Foto por Evi Radauscher no Unsplash

Ao descrever a e/S, os termos não-bloqueio e assíncrona são frequentemente usados de forma intercambiável, mas há uma diferença significativa entre eles. Neste artigo são descritas as diferenças teóricas e práticas entre as operações de E/S de soquetes não bloqueadores e assíncronos em Java.

Sockets são endpoints para realizar comunicação bidirecional por protocolos TCP e UDP. Java sockets APIs são adaptadores para a funcionalidade correspondente dos sistemas operacionais. Comunicação de Sockets em sistemas operacionais compatíveis com POSIX (Unix, Linux, Mac OS X, BSD, Solaris, AIX, etc.) é realizado pela Berkeley sockets. A comunicação de soquetes no Windows é realizada pelo Winsock, que também é baseada em soquetes Berkeley com funcionalidade adicional para cumprir o modelo de programação do Windows.

as definições POSIX

neste artigo são usadas definições simplificadas da especificação POSIX.

thread bloqueado – um thread que está aguardando alguma condição antes de poder continuar a execução.

Bloqueio-uma propriedade de um soquete que faz com que as chamadas para ele para esperar que a ação solicitada a ser realizada antes de retornar.

Non-blocking-uma propriedade de um soquete que faz com que as chamadas retornem sem demora, quando é detectado que a ação solicitada não pode ser concluída sem um atraso desconhecido.

operação síncrona de E/S — uma operação de E/S que faz com que o thread solicitante seja bloqueado até que a operação de E / S seja concluída.

Operação de E/S assíncrona – uma operação de E/S que por si só não faz com que o thread solicitante seja bloqueado; isso implica que o thread e a operação de E / S podem estar sendo executados simultaneamente.

portanto, de acordo com a especificação POSIX, a diferença entre os Termos não-bloqueio e assíncrono é óbvia:

  • não-bloqueio — uma propriedade de um soquete que faz com que as chamadas para ele retornar sem demora
  • assíncrona e/S — uma propriedade em uma operação de e/S (leitura ou escrita), que é executado em simultâneo com o segmento solicitante

modelos de e/S

Os seguintes modelos de e/S são as mais comuns para o POSIX compatível com sistemas operacionais:

  • bloqueio modelo de e/S
  • non-blocking I/O modelo
  • e/S multiplexing modelo
  • sinal-driven I/O modelo
  • e/S assíncrona modelo

Bloqueio modelo de e/S

No bloqueio modelo de e/S, o aplicativo faz um sistema de bloqueio de chamada até que os dados são recebidos no kernel e são copiados a partir do espaço de kernel para o espaço do utilizador.

bloqueio modelo de e/S

bloqueio modelo de e/S

Prós:

  • O mais simples modelo de e/S para implementar

Contras:

  • O aplicativo é bloqueado

Non-blocking I/O modelo

Na seção non-blocking I/O modelo a aplicação faz uma chamada de sistema que imediatamente retorna uma das duas respostas:

  • se a operação de e/S pode ser concluída imediatamente, os dados são retornados
  • se a operação de e/S não pode ser concluída imediatamente, um código de erro é retornado, indicando que a operação de e/S iria bloquear ou o dispositivo está temporariamente indisponível

Para concluir a operação de e/S, o aplicativo deve busy-wait (fazer a repetição de chamadas de sistema) até a conclusão.

non-blocking I/O modelo

non-blocking I/O modelo

Prós:

  • o aplicativo não está bloqueado

contras:

  • A aplicação deve ocupado, aguarde até a conclusão, que faria com que muitos usuário do kernel alternâncias de contexto
  • Este modelo podem introduzir latência de e/S, pois pode haver uma lacuna entre a disponibilidade de dados no kernel e a leitura de dados pelo aplicativo

e/S multiplexing modelo

Na e/S multiplexing modelo (também conhecido como non-blocking I/O modelo com bloqueio de notificações), o aplicativo faz um bloqueio de selecionar o sistema de chamada para começar a monitorar a atividade em muitos descritores. Para cada descritor, é possível solicitar notificação de sua prontidão para certas operações de E/S (conexão, leitura ou escrita, ocorrência de erro, etc.). Quando a chamada select system retorna que pelo menos um descritor está pronto, o aplicativo faz uma chamada sem bloqueio e copia os dados do espaço do kernel para o espaço do Usuário.

e/S multiplexing modelo

e/S multiplexing modelo

Prós:

  • É possível executar operações de e/S em vários descritores em uma thread

Contras:

  • O aplicativo ainda está bloqueado na escolha do sistema de chamada de
  • Nem todos os sistemas operacionais oferecem suporte a este modelo de forma eficiente

Sinal-driven I/O modelo

No sinal-driven I/O modelo a aplicação de uma não-bloqueio de chamadas e registra um manipulador de sinais. Quando um descritor está pronto para uma operação de E/S, um sinal é gerado para o aplicativo. Em seguida, o manipulador de sinal copia os dados do espaço do kernel para o espaço do Usuário.

sinal-driven I/O modelo

sinal-driven I/O modelo

Prós:

  • O aplicativo não está bloqueado
  • Sinais podem oferecer um bom desempenho

Contras:

  • Nem todos os sistemas operacionais oferecem suporte a sinais de

e/S Assíncrona modelo

Na e/S assíncrona modelo (também conhecida como a e/S sobreposta modelo), o aplicativo faz com que o não-bloqueio de chamada e inicia uma operação de plano de fundo no kernel. Quando a operação é concluída (os dados são recebidos no kernel e são copiados do espaço do kernel para o espaço do usuário), um retorno de chamada de conclusão é gerado para concluir a operação de E/S.

uma diferença entre o modelo de E/S assíncrono e o modelo de E/S orientado por sinal é que, com E/S orientada por sinal, o kernel informa ao aplicativo quando uma operação de E/S pode ser iniciada, mas com o modelo de E/S assíncrono, o kernel informa ao aplicativo quando uma operação de E/S é concluída.

a e/S assíncrona modelo

e/S assíncrona modelo

Prós:

  • O aplicativo não está bloqueado
  • Este modelo pode fornecer o melhor desempenho

Contras:

  • O mais complicado modelo de e/S para implementar
  • Nem todos os sistemas operacionais oferecem suporte a este modelo de forma eficiente

Java APIs de e/S

Java IO API é baseada em fluxos (InputStream, OutputStream) que representam o bloqueio, um direcional de fluxo de dados.

Java NIO API

Java NIO API é baseado no canal, Buffer, classes seletoras, que são adaptadores para operações de E/S de baixo nível de sistemas operacionais.

a classe de canal representa uma conexão com uma entidade (dispositivo de hardware, arquivo, Soquete, componente de software, etc) que é capaz de realizar operações de E/S (leitura ou escrita).

em comparação com fluxos unidirecionais, os canais são bidirecionais.

a classe Buffer é um contêiner de dados de tamanho fixo com métodos adicionais para ler e gravar dados. Todos os dados do canal são tratados por meio de Buffer, mas nunca diretamente: todos os dados enviados para um canal são gravados em um Buffer, todos os dados recebidos de um canal são lidos em um Buffer.

em comparação com fluxos, que são orientados por Bytes, os canais são orientados por blocos. A E/S orientada a Bytes é mais simples, mas para algumas entidades de E/S pode ser bastante lenta. A E / S orientada a blocos pode ser muito mais rápida, mas é mais complicada.

a classe Selector permite subscrever eventos de muitos objetos selectablechannel registados numa única chamada. Quando os eventos chegam, um objeto seletor os despacha para os manipuladores de eventos correspondentes.

java NIO2 API

java NIO2 API é baseado em canais assíncronos (Assíncronoserversocketchannel, Assíncronosocketchannel, etc) que suportam operações de E/S assíncronas (conexão, leitura ou escrita, manipulação de erros).

os canais assíncronos fornecem dois mecanismos para controlar operações de E/S assíncronas. O primeiro mecanismo é retornar um java.util.concorrente.Objeto futuro, que modela uma operação pendente e pode ser usado para consultar o estado e obter o resultado. O segundo mecanismo é passando para a operação um java.nio.satelite.CompletionHandler object, que define métodos de manipulador que são executados após a conclusão ou falha da operação. A API fornecida para ambos os mecanismos é equivalente.

os canais assíncronos fornecem uma maneira padrão de executar a plataforma de operações assíncronas-independentemente. No entanto, a quantidade que a API Java sockets pode explorar os recursos assíncronos nativos de um sistema operacional dependerá do suporte para essa plataforma.

Socket echo server

a maioria dos modelos de E / S mencionados acima são implementados aqui em servidores echo e clientes com APIs Java sockets. Os servidores e clientes echo funcionam pelo seguinte algoritmo:

  1. um servidor escuta para um soquete em um registrada a porta TCP 7000
  2. um cliente se conecta a partir de um socket em uma porta TCP dinâmica para o servidor de socket
  3. o cliente lê uma seqüência de caracteres de entrada do console e envia os bytes a partir do seu socket para o servidor de socket
  4. o servidor recebe os bytes do seu soquete e envia-los de volta para o cliente socket
  5. o cliente recebe os bytes do seu soquete e escreve o ecoou cadeia de caracteres no console
  6. quando o cliente recebe o mesmo número de bytes que ele tem enviado, ele se desconecta do servidor
  7. quando o servidor recebe uma string especial, ele para de ouvir

a conversão entre strings e bytes aqui é realizada explicitamente na codificação UTF-8.

além disso, apenas códigos simplificados para servidores echo são fornecidos. O link para os códigos completos para servidores e clientes echo é fornecido na conclusão.

Blocking IO echo server

no exemplo a seguir, o modelo de E/S de bloqueio é implementado em um servidor echo com API Java IO.

O ServerSocket.aceite blocos de método até que uma conexão seja aceita. O InputStream.leia blocos de método até que os dados de entrada estejam disponíveis ou um cliente seja desconectado. O OutputStream.escreva blocos de método até que todos os dados de saída sejam gravados.

public class IoEchoServer { public static void main(String args) throws IOException {
ServerSocket serverSocket = new ServerSocket(7000); while (active) {
Socket socket = serverSocket.accept(); // blocking InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream(); int read;
byte bytes = new byte;
while ((read = is.read(bytes)) != -1) { // blocking
os.write(bytes, 0, read); // blocking
} socket.close();
} serverSocket.close();
}
}

bloqueando o servidor nio echo

no exemplo a seguir, o modelo de E/S de bloqueio é implementado em um servidor echo com API Java NIO.

os objetos ServerSocketChannel e SocketChannel são configurados por padrão no modo de bloqueio. O ServerSocketChannel.aceitar blocos de método e retorna um objeto SocketChannel quando uma conexão é aceita. O ServerSocket.leia blocos de método até que os dados de entrada estejam disponíveis ou um cliente seja desconectado. O ServerSocket.escreva blocos de método até que todos os dados de saída sejam gravados.

public class NioBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // blocking
} socketChannel.close();
} serverSocketChannel.close();
}
}

servidor de eco nio sem bloqueio

no exemplo a seguir, o modelo de E/S sem bloqueio é implementado em um servidor echo com API Java NIO.

os objetos ServerSocketChannel e SocketChannel são explicitamente configurados no modo sem bloqueio. O ServerSocketChannel.o método accept não bloqueia e retorna null se nenhuma conexão for aceita ainda ou um objeto SocketChannel de outra forma. O ServerSocket.read não bloqueia e retorna 0 Se nenhum dado estiver disponível ou um número positivo de bytes lidos de outra forma. O ServerSocket.o método de gravação não bloqueia se houver espaço livre no buffer de saída do soquete.

public class NioNonBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // non-blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // can be non-blocking
} socketChannel.close();
}
} serverSocketChannel.close();
}
}

multiplexação nio echo server

no exemplo a seguir, o modelo de E/S de multiplexação é implementado em um servidor echo Java NIO API.

durante a inicialização, vários objetos ServerSocketChannel, configurados no modo sem bloqueio, são registrados no mesmo objeto seletor com a chave de seleção.Argumento OP_ACCEPT para especificar que um evento de aceitação de conexão é interessante.

no loop principal, o seletor.selecione blocos de método até que ocorra pelo menos um dos eventos registrados. Em seguida, o seletor.o método selectedKeys retorna um conjunto de objetos SelectionKey para os quais os eventos ocorreram. Iterando através dos objetos SelectionKey, é possível determinar qual evento de E / S (conectar, Aceitar, Ler, Escrever) aconteceu e quais objetos sockets (ServerSocketChannel, SocketChannel) foram associados a esse evento.

indicação de uma tecla de seleção de que um canal está pronto para alguma operação é uma dica, Não uma garantia.

public class NioMultiplexingEchoServer { public static void main(String args) throws IOException {
final int ports = 8;
ServerSocketChannel serverSocketChannels = new ServerSocketChannel; Selector selector = Selector.open(); for (int p = 0; p < ports; p++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannels = serverSocketChannel;
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000 + p)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} while (active) {
selector.select(); // blocking Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next(); if (key.isAcceptable()) {
accept(selector, key);
}
if (key.isReadable()) {
keysIterator.remove();
read(selector, key);
}
if (key.isWritable()) {
keysIterator.remove();
write(key);
}
}
} for (ServerSocketChannel serverSocketChannel : serverSocketChannels) {
serverSocketChannel.close();
}
}
}

quando um objeto SelectionKey indica que um evento de aceitação de conexão aconteceu, ele fez o ServerSocketChannel.aceitar chamada (que pode ser um não-bloqueio) para aceitar a conexão. Depois disso, um novo objeto SocketChannel é configurado no modo sem bloqueio e é registrado no mesmo objeto seletor com a chave de seleção.Argumento OP_READ para especificar que agora um evento de leitura é interessante.

private static void accept(Selector selector, SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept(); // can be non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}

quando um objeto SelectionKey indica que um evento de leitura aconteceu, ele é feito um SocketChannel.chamada de leitura (que pode ser um não bloqueio) para ler dados do objeto SocketChannel em um novo objeto bytebyffer. Depois disso, o objeto SocketChannel é registrado no mesmo objeto seletor com a chave de seleção.Op_write argumento para especificar que agora um evento de gravação é interessante. Além disso, este objeto ByteBuffer é usado durante o registro como um anexo.

private static void read(Selector selector, SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer); // can be non-blocking buffer.flip();
socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
}

quando um objeto SelectionKeys indica que um evento de escrita aconteceu, ele fez o SocketChannel.chamada de gravação (que pode ser um não bloqueio) para gravar dados no objeto SocketChannel do objeto ByteByffer, extraído da chave de seleção.método de fixação. Depois disso, o SocketChannel.a chamada cloase fecha a conexão.

private static void write(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); socketChannel.write(buffer); // can be non-blocking
socketChannel.close();
}

após cada leitura ou gravação, o objeto SelectionKey é removido do conjunto dos objetos SelectionKey para evitar sua reutilização. Mas o objeto SelectionKey para aceitação de conexão não é removido para ter a capacidade de fazer a próxima operação semelhante.

assíncrono NIO2 echo server

no exemplo a seguir, o modelo de E/S assíncrono é implementado em um servidor echo com API Java NIO2. As classes Assíncronosserversocketchannel, Assíncronossocketchannel aqui são usadas com o mecanismo de manipuladores de conclusão.

O Assíncronoserversocketchannel.o método accept inicia uma operação de aceitação de conexão assíncrona.

public class Nio2CompletionHandlerEchoServer { public static void main(String args) throws IOException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(7000)); AcceptCompletionHandler acceptCompletionHandler = new AcceptCompletionHandler(serverSocketChannel);
serverSocketChannel.accept(null, acceptCompletionHandler); System.in.read();
}
}

quando uma conexão é Aceita (ou a operação falha), a classe AcceptCompletionHandler é chamada, que pelo Assíncronossocketchannel.leia (destino ByteBuffer, um anexo, CompletionHandler<inteiro,? o método super a> handler) inicia uma operação de leitura assíncrona do objeto Assíncronossocketchannel para um novo objeto ByteBuffer.

class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Void> { private final AsynchronousServerSocketChannel serverSocketChannel; AcceptCompletionHandler(AsynchronousServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
} @Override
public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
serverSocketChannel.accept(null, this); // non-blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
ReadCompletionHandler readCompletionHandler = new ReadCompletionHandler(socketChannel, buffer);
socketChannel.read(buffer, null, readCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

quando a operação de leitura é concluída (ou falha), a classe ReadCompletionHandler é chamada, que pelo Assíncronosocketchannel.escrever (fonte ByteBuffer, um anexo, CompletionHandler <inteiro,? super a> handler) método inicia uma operação de gravação assíncrona para o objeto Assíncronossocketchannel do objeto ByteBuffer.

class ReadCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel;
private final ByteBuffer buffer; ReadCompletionHandler(AsynchronousSocketChannel socketChannel, ByteBuffer buffer) {
this.socketChannel = socketChannel;
this.buffer = buffer;
} @Override
public void completed(Integer bytesRead, Void attachment) {
WriteCompletionHandler writeCompletionHandler = new WriteCompletionHandler(socketChannel);
buffer.flip();
socketChannel.write(buffer, null, writeCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

quando a operação de gravação é concluída (ou falha), A classe WriteCompletionHandler é chamada, que pelo Assíncronossocketchannel.fechar método fecha a conexão.

class WriteCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel; WriteCompletionHandler(AsynchronousSocketChannel socketChannel) {
this.socketChannel = socketChannel;
} @Override
public void completed(Integer bytesWritten, Void attachment) {
try {
socketChannel.close();
} catch (IOException e) {
// exception handling
}
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

neste exemplo, operações de E/S assíncronas são realizadas sem anexo, porque todos os objetos necessários (Assíncronosocketchannel, ByteBuffer) são passados como argumentos de construtor para os manipuladores de conclusão apropriados.

conclusão

a escolha do modelo de E/S para comunicação de soquetes depende dos parâmetros do tráfego. Se as solicitações de E/S forem longas e infrequentes, A E / S assíncrona geralmente é uma boa escolha. No entanto, se as solicitações de E/S forem curtas e rápidas, a sobrecarga de processamento de chamadas do kernel pode tornar a E/S síncrona muito melhor.

apesar de o Java fornecer uma maneira padrão de executar e/s de soquetes nos diferentes sistemas operacionais, o desempenho real pode variar significativamente dependendo de sua implementação. É possível começar a estudar essas diferenças com o conhecido artigo de Dan Kegel the c10k problem.

exemplos completos de código estão disponíveis no repositório GitHub.

Deixe uma resposta

O seu endereço de email não será publicado.