Introdução ao Seletor Java NIO

Neste artigo, exploraremos as partes introdutórias do componente Seletor do Java NIO .Um seletor fornece um mecanismo para monitorar um ou mais canais NIO e reconhecer quando um ou mais se tornam disponíveis para transferência de dados.Dessa forma, um único thread pode ser usado para gerenciar vários canais e, portanto, várias conexões de rede.

2. Por que usar um seletor?

Com um seletor, podemos usar um thread em vez de vários para gerenciar vários canais. A troca de contexto entre threads é cara para o sistema operacional e, além disso, cada thread ocupa memória.

Portanto, quanto menos segmentos usarmos, melhor. No entanto, é importante lembrar que os sistemas operacionais modernos e as CPUs estão cada vez melhores em multitarefa , de modo que as despesas gerais do multi-threading continuam diminuindo ao longo do tempo.

Nós vamos lidar aqui é como podemos lidar com vários canais com um único segmento usando um seletor.

Note também que os seletores não apenas ajudam você a ler dados; eles também podem ouvir conexões de rede de entrada e gravar dados em canais lentos.

3. Configuração

Para usar o seletor, não precisamos de nenhuma configuração especial. Todas as classes que precisamos são o pacote core java.nio e nós apenas temos que importar o que precisamos.

Depois disso, podemos registrar vários canais com um objeto seletor. Quando a atividade de E / S acontece em qualquer um dos canais, o seletor nos notifica. É assim que podemos ler de um grande número de fontes de dados de um único thread.

Qualquer canal que registramos com um seletor deve ser uma subclasse de SelectableChannel . Estes são um tipo especial de canais que podem ser colocados no modo sem bloqueio.

4. Criando um Seletor

Um seletor pode ser criado chamando o método aberto estático da classe Selector , que usará o provedor de seletor padrão do sistema para criar um novo seletor:

1
Selector selector = Selector.open();

5. Registrando Canais Selecionáveis

Para que um seletor monitore quaisquer canais, devemos registrar esses canais com o seletor. Fazemos isso invocando o método de registro do canal selecionável.

Mas antes de um canal ser registrado com um seletor, ele deve estar no modo sem bloqueio:

1
2
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Isso significa que não podemos usar FileChannel s com um seletor, pois eles não podem ser alternados para o modo não-bloqueante da maneira como fazemos com os canais de soquete.

O primeiro parâmetro é o objeto Seletor que criamos anteriormente, o segundo parâmetro define um conjunto de interesse , significando quais eventos estamos interessados ​​em escutar no canal monitorado, através do seletor.

Existem quatro eventos diferentes que podemos ouvir, cada um é representado por uma constante na classe SelectionKey :

  • Conectar  quando um cliente tenta se conectar ao servidor. Representado por SelectionKey.OP_CONNECT
  • Aceitar  quando o servidor aceita uma conexão de um cliente. Representado por SelectionKey.OP_ACCEPT
  • Ler  quando o servidor estiver pronto para ler do canal. Representado por SelectionKey.OP_READ
  • Write  quando o servidor estiver pronto para gravar no canal. Representado por SelectionKey.OP_WRITE

O objeto retornado SelectionKey representa o registro do canal selecionável com o seletor. Vamos ver mais na próxima seção.

6. O objeto SelectionKey

Como vimos na seção anterior, quando registramos um canal com um seletor, obtemos um objeto SelectionKey . Este objeto contém dados que representam o registro do canal.

Ele contém algumas propriedades importantes que devemos entender bem para poder usar o seletor no canal. Veremos essas propriedades nas subseções a seguir.

6.1. O conjunto de interesse

Um conjunto de interesses define o conjunto de eventos que queremos que o seletor atente neste canal. É um valor inteiro; Podemos obter essa informação da seguinte maneira.

Primeiro, temos o conjunto de interesse retornado pelo SelectionKey ‘s interestOps método. Então nós temos o evento constante na SelectionKey que analisamos anteriormente.

Quando nós E esses dois valores, obtemos um valor booleano que nos informa se o evento está sendo observado ou não:

1
2
3
4
5
6
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

6.2. O conjunto pronto

O conjunto pronto define o conjunto de eventos para o qual o canal está pronto. É um valor inteiro também; Podemos obter essa informação da seguinte maneira.

Nós temos o conjunto pronto retornado por SelectionKey ‘s readyOps método. Quando nós AND este valor com as constantes de eventos como fizemos no caso do conjunto de interesse, obtemos um booleano representando se o canal está pronto para um valor particular ou não.

Outra maneira alternativa e mais curta de fazer isso é usar os métodos de conveniência da SelectionKeypara esse mesmo propósito:

1
2
3
4
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWriteable();

6.3. O canal

Acessar o canal sendo assistido do objeto SelectionKey é muito simples. Acabamos de chamar o método do canal :

1
Channel channel = key.channel();

6.4. O seletor

Assim como obter um canal, é muito fácil obter o objeto Selector do objeto SelectionKey :

1
Selector selector = key.selector();

6.5. Anexando Objetos

Podemos anexar um objeto a um SelectionKey. Às vezes, podemos dar a um canal um código personalizado ou anexar qualquer tipo de objeto Java que possamos acompanhar.

Anexar objetos é uma maneira prática de fazer isso. Aqui está como você anexa e obtém objetos de um SelectionKey :

1
2
3
key.attach(Object);
Object object = key.attachment();

Como alternativa, podemos optar por anexar um objeto durante o registro do canal. Nós o adicionamos como um terceiro parâmetro ao método de registro do canal , da seguinte forma:

1
2
SelectionKey key = channel.register(
  selector, SelectionKey.OP_ACCEPT, object);

7. Seleção da Chave do Canal

Até agora, vimos como criar um seletor, registrar canais para ele e inspecionar as propriedades do objeto SelectionKey que representa o registro de um canal para um seletor.

Esta é apenas a metade do processo, agora temos que realizar um processo contínuo de seleção do conjunto pronto que analisamos anteriormente. Fazemos a seleção usando o método select do seletor , assim:

1
int channels = selector.select();

Este método bloqueia até que pelo menos um canal esteja pronto para uma operação. O inteiro retornado representa o número de chaves cujos canais estão prontos para uma operação.

Em seguida, geralmente recuperamos o conjunto de chaves selecionadas para processamento:

1
Set<SelectionKey> selectedKeys = selector.selectedKeys();

O conjunto que obtivemos é de objetos SelectionKey , cada chave representa um canal registrado que está pronto para uma operação.

Depois disso, costumamos fazer uma iteração sobre esse conjunto e, para cada chave, obtemos o canal e executamos qualquer uma das operações que aparecem em nosso interesse.

Durante a vida útil de um canal, ele pode ser selecionado várias vezes conforme sua chave aparece no conjunto pronto para diferentes eventos. É por isso que devemos ter um loop contínuo para capturar e processar eventos do canal quando e quando ocorrerem.

8. Exemplo Completo

Para consolidar o conhecimento que adquirimos nas seções anteriores, vamos construir um exemplo completo de cliente-servidor.

Para facilitar o teste do nosso código, criaremos um servidor de eco e um cliente de eco. Nesse tipo de configuração, o cliente se conecta ao servidor e começa a enviar mensagens para ele. O servidor repete as mensagens enviadas por cada cliente.

Quando o servidor encontra uma mensagem específica, como end , ele a interpreta como o fim da comunicação e fecha a conexão com o cliente.

8.1. O servidor

Aqui está nosso código para o EchoServer.java :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class EchoServer {
    private static final String POISON_PILL = "POISON_PILL";
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("localhost", 5454));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(256);
        while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                if (key.isAcceptable()) {
                    register(selector, serverSocket);
                }
                if (key.isReadable()) {
                    answerWithEcho(buffer, key);
                }
                iter.remove();
            }
        }
    }
    private static void answerWithEcho(ByteBuffer buffer, SelectionKey key)
      throws IOException {
 
        SocketChannel client = (SocketChannel) key.channel();
        client.read(buffer);
        if (new String(buffer.array()).trim().equals(POISON_PILL)) {
            client.close();
            System.out.println("Not accepting client messages anymore");
        }
        buffer.flip();
        client.write(buffer);
        buffer.clear();
    }
    private static void register(Selector selector, ServerSocketChannel serverSocket)
      throws IOException {
 
        SocketChannel client = serverSocket.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
    }
    public static Process start() throws IOException, InterruptedException {
        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
        String classpath = System.getProperty("java.class.path");
        String className = EchoServer.class.getCanonicalName();
        ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classpath, className);
        return builder.start();
    }
}

Isso é o que está acontecendo; nós criamos um objeto Selector chamando o método aberto estático . Em seguida, criamos um canal também chamando seu método aberto estático , especificamente uma instância ServerSocketChannel .

Isso ocorre porque ServerSocketChannel é selecionável e é bom para um soquete de escuta orientado a fluxo .

Nós então ligamos isso a uma porta de nossa escolha. Lembre-se que dissemos anteriormente que antes de registrar um canal selecionável em um seletor, devemos primeiro defini-lo para o modo sem bloqueio. Então, em seguida, fazemos isso e depois registramos o canal no seletor.

Não precisamos da instância SelectionKey deste canal neste estágio, por isso não nos lembraremos disso.

O Java NIO usa um modelo orientado a buffer diferente de um modelo orientado por fluxo. Portanto, a comunicação de soquete geralmente ocorre gravando e lendo de um buffer.

Nós, portanto, criamos um novo ByteBuffer para o qual o servidor estará escrevendo e lendo. Nós o inicializamos em 256 bytes, é apenas um valor arbitrário, dependendo da quantidade de dados que planejamos transferir para lá e para cá.

Por fim, realizamos o processo de seleção. Selecionamos os canais prontos, recuperamos as teclas de seleção, iteramos as teclas e executamos as operações para as quais cada canal está pronto.

Fazemos isso em um loop infinito, já que os servidores geralmente precisam continuar funcionando, quer haja uma atividade ou não.

A única operação que um ServerSocketChannel pode manipular é uma operação ACCEPT . Quando aceitamos a conexão de um cliente, obtemos um objeto SocketChannel no qual podemos ler e gravar . Nós o configuramos para o modo sem bloqueio e o registramos para uma operação de LEITURA no seletor.

Durante uma das seleções subsequentes, esse novo canal se tornará pronto para leitura. Nós recuperamos e lemos o conteúdo para o buffer. Fiel a ele como um servidor de eco, devemos escrever este conteúdo de volta para o cliente.

Quando desejamos gravar em um buffer a partir do qual estamos lendo, devemos chamar o método flip () .

Finalmente, definimos o buffer para o modo de gravação chamando o método flip e simplesmente escrevemos nele.

O método start () é definido para que o echo server possa ser iniciado como um processo separado durante o teste de unidade.

8.2. O cliente

Aqui está o nosso código para o EchoClient.java :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class EchoClient {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClient instance;
    public static EchoClient start() {
        if (instance == null)
            instance = new EchoClient();
        return instance;
    }
    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }
    private EchoClient() {
        try {
            client = SocketChannel.open(new InetSocketAddress("localhost", 5454));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println("response=" + response);
            buffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
    }
}

O cliente é mais simples que o servidor.

Usamos um padrão singleton para instanciá-lo dentro do método estático de início . Nós chamamos o construtor privado deste método.

No construtor privado, abrimos uma conexão na mesma porta na qual o canal do servidor estava ligado e ainda no mesmo host.

Em seguida, criamos um buffer para o qual podemos escrever e a partir do qual podemos ler.

Finalmente, temos um método sendMessage que lê encapsula qualquer string que passamos para ele em um buffer de bytes que é transmitido através do canal para o servidor.

Em seguida, lemos o canal do cliente para receber a mensagem enviada pelo servidor. Nós retornamos isso como o eco da nossa mensagem.

8.3. Testando

Dentro de uma classe chamada EchoTest.java , vamos criar um caso de teste que inicia o servidor, envia mensagens para o servidor e só passa quando as mesmas mensagens são recebidas de volta do servidor. Como etapa final, o caso de teste interrompe o servidor antes da conclusão.

Agora podemos executar o teste:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class EchoTest {
    Process server;
    EchoClient client;
    @Before
    public void setup() throws IOException, InterruptedException {
        server = EchoServer.start();
        client = EchoClient.start();
    }
    @Test
    public void givenServerClient_whenServerEchosMessage_thenCorrect() {
        String resp1 = client.sendMessage("hello");
        String resp2 = client.sendMessage("world");
        assertEquals("hello", resp1);
        assertEquals("world", resp2);
    }
    @After
    public void teardown() throws IOException {
        server.destroy();
        EchoClient.stop();
    }
}

9. Conclusão

Neste artigo, abordamos o uso básico do componente Seletor do Java NIO.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *