Введение в Java NIO Selector

Введение в Java NIO Selector

1. обзор

В этой статье мы рассмотрим вводные части компонентаSelector Java NIO.

Селектор предоставляет механизм для мониторинга одного или нескольких каналов NIO и определения, когда один или несколько каналов становятся доступными для передачи данных.

Таким образом,a single thread can be used for managing multiple channels и, следовательно, несколько сетевых подключений.

2. Зачем использовать селектор?

С помощью селектора мы можем использовать один поток вместо нескольких для управления несколькими каналами. Context-switching between threads is expensive for the operating system, и дополнительноeach thread takes up memory.

Поэтому чем меньше ниток мы используем, тем лучше. Однако важно помнить, чтоmodern operating systems and CPU’s keep getting better at multitasking, чтобы накладные расходы на многопоточность со временем уменьшались.

Мы рассмотрим, как можно обрабатывать несколько каналов с помощью одного потока с помощью селектора.

Также обратите внимание, что селекторы не просто помогают читать данные; они также могут прослушивать входящие сетевые соединения и записывать данные по медленным каналам.

3. Настроить

Чтобы использовать селектор, нам не нужно никаких специальных настроек. Все классы, которые нам нужны, - это основной пакетjava.nio, и нам просто нужно импортировать то, что нам нужно.

После этого мы можем зарегистрировать несколько каналов с помощью селектора объекта. Когда активность ввода-вывода происходит на любом из каналов, селектор уведомляет нас. Вот как мы можем читать из большого количества источников данных из одного потока.

Любой канал, который мы регистрируем с помощью селектора, должен быть подклассомSelectableChannel. Это специальный тип каналов, которые можно перевести в неблокирующий режим.

4. Создание селектора

Селектор может быть создан путем вызова статического методаopen классаSelector, который будет использовать системный поставщик селектора по умолчанию для создания нового селектора:

Selector selector = Selector.open();

5. Регистрация выбираемых каналов

Чтобы селектор мог контролировать любые каналы, мы должны зарегистрировать эти каналы в селекторе. Мы делаем это, вызывая методregister выбираемого канала.

Но прежде чем канал будет зарегистрирован с помощью селектора, он должен быть в неблокирующем режиме:

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

Это означает, что мы не можем использоватьFileChannels с селектором, поскольку они не могут быть переключены в неблокирующий режим, как мы это делаем с каналами сокетов.

Первый параметр - это объектSelector, который мы создали ранее, второй параметр определяет интересующий набор,, означающий, какие события мы заинтересованы в прослушивании в отслеживаемом канале через селектор.

Мы можем прослушивать четыре различных события, каждое из которых представлено константой в классеSelectionKey:

  • Connect, когда клиент пытается подключиться к серверу. ПредставленоSelectionKey.OP_CONNECT

  • Accept, когда сервер принимает соединение от клиента. ПредставленоSelectionKey.OP_ACCEPT

  • Read, когда сервер готов к чтению из канала. ПредставленоSelectionKey.OP_READ

  • Write, когда сервер готов к записи в канал. ПредставленоSelectionKey.OP_WRITE

Возвращенный объектSelectionKey представляет регистрацию выбираемого канала с помощью селектора. Мы рассмотрим это далее в следующем разделе.

6. ОбъектSelectionKey

Как мы видели в предыдущем разделе, когда мы регистрируем канал с помощью селектора, мы получаем объектSelectionKey. Этот объект содержит данные, представляющие регистрацию канала.

Он содержит некоторые важные свойства, которые мы должны хорошо понимать, чтобы иметь возможность использовать селектор на канале. Мы рассмотрим эти свойства в следующих подразделах.

6.1. Набор процентов

Набор интересов определяет набор событий, которые мы хотим, чтобы селектор наблюдал на этом канале. Это целочисленное значение; мы можем получить эту информацию следующим образом.

Во-первых, у нас есть набор процентов, возвращенный методомSelectionKey‘sinterestOps. Затем у нас есть константа события вSelectionKey, которую мы рассмотрели ранее.

Когда мы И эти два значения, мы получаем логическое значение, которое сообщает нам, отслеживается ли событие или нет:

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. Готовый набор

Готовый набор определяет набор событий, к которым канал готов. Это также целочисленное значение; мы можем получить эту информацию следующим образом.

У нас есть готовый набор, возвращенный методомSelectionKey ’sreadyOps. Когда мы И это значение с константами событий, как мы это сделали в случае набора интересов, мы получаем логическое значение, представляющее, готов ли канал к определенному значению или нет.

Другой альтернативный и более короткий способ сделать это - использовать для этой же цели удобные методыSelectionKey's:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWriteable();

6.3. Канал

Получить доступ к просматриваемому каналу из объектаSelectionKey очень просто. Мы просто вызываем методchannel:

Channel channel = key.channel();

6.4. Селектор

Так же, как получение канала, очень легко получить объектSelector из объектаSelectionKey:

Selector selector = key.selector();

6.5. Присоединение объектов

Мы можем прикрепить объект кSelectionKey.. Иногда нам может потребоваться присвоить каналу собственный идентификатор или присоединить любой тип Java-объекта, который мы можем отслеживать.

Присоединение объектов - удобный способ сделать это. Вот как вы прикрепляете и получаете объекты изSelectionKey:

key.attach(Object);

Object object = key.attachment();

В качестве альтернативы, мы можем прикрепить объект во время регистрации канала. Мы добавляем его в качестве третьего параметра в метод каналаregister, например:

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

7. Выбор ключа канала

До сих пор мы рассмотрели, как создать селектор, зарегистрировать для него каналы и проверить свойства объектаSelectionKey, который представляет регистрацию канала в селекторе.

Это только половина процесса, теперь мы должны выполнить непрерывный процесс выбора готового набора, который мы рассматривали ранее. Мы делаем выбор, используя метод селектораselect, например:

int channels = selector.select();

Этот метод блокируется, пока хотя бы один канал не будет готов к работе. Возвращаемое целое число представляет количество ключей, каналы которых готовы к операции.

Далее мы обычно получаем набор выбранных ключей для обработки:

Set selectedKeys = selector.selectedKeys();

Полученный набор состоит из объектовSelectionKey, каждый ключ представляет зарегистрированный канал, готовый к работе.

После этого мы обычно перебираем этот набор и для каждого ключа получаем канал и выполняем любые операции, которые появляются в нашем наборе интересов.

В течение срока жизни канала его можно выбрать несколько раз, поскольку его ключ появляется в готовом наборе для различных событий. Вот почему у нас должен быть непрерывный цикл для захвата и обработки событий канала, как и когда они происходят.

8. Полный пример

Чтобы закрепить знания, полученные в предыдущих разделах, мы собираемся создать полный пример клиент-сервер.

Чтобы упростить тестирование нашего кода, мы создадим эхо-сервер и эхо-клиент. При такой настройке клиент подключается к серверу и начинает отправлять на него сообщения. Сервер возвращает сообщения, отправленные каждым клиентом.

Когда сервер встречает определенное сообщение, такое какend, он интерпретирует его как окончание связи и закрывает соединение с клиентом.

8.1. Сервер

Вот наш код дляEchoServer.java:

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 selectedKeys = selector.selectedKeys();
            Iterator 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();
    }
}

Вот что происходит; мы создаем объектSelector, вызывая статический методopen. Затем мы создаем канал, также вызывая его статический методopen, в частности экземплярServerSocketChannel.

Это потому, чтоServerSocketChannel is selectable and good for a stream-oriented listening socket.

Затем мы связываем его с портом по нашему выбору. Помните, мы говорили ранее, что прежде чем регистрировать выбираемый канал для селектора, мы должны сначала установить его в неблокирующий режим. Итак, затем мы делаем это, а затем регистрируем канал для селектора.

На данном этапе нам не нужен экземплярSelectionKey этого канала, поэтому мы его не запомним.

Java NIO использует модель, ориентированную на буфер, отличную от модели, ориентированной на поток. Поэтому общение с сокетами обычно происходит путем записи в буфер и чтения из него.

Поэтому мы создаем новыйByteBuffer, в который сервер будет записывать и читать. Мы инициализируем его 256 байтами, это просто произвольное значение, в зависимости от того, сколько данных мы планируем передавать туда и обратно.

Наконец, мы выполняем процесс отбора. Мы выбираем готовые каналы, извлекаем их ключи выбора, перебираем ключи и выполняем операции, к которым готов каждый канал.

Мы делаем это в бесконечном цикле, поскольку серверы обычно должны продолжать работать независимо от того, есть активность или нет.

Единственная операция, которую может обработатьServerSocketChannel, - это операцияACCEPT. Когда мы принимаем соединение от клиента, мы получаем объектSocketChannel, для которого мы можем выполнять чтение и запись. Мы устанавливаем его в неблокирующий режим и регистрируем его для операции чтения для селектора.

Во время одного из следующих выборов этот новый канал станет готовым для чтения. Мы извлекаем его и читаем его содержимое в буфер. Верно, что это как эхо-сервер, мы должны записывать это содержимое обратно клиенту.

When we desire to write to a buffer from which we have been reading, we must call the flip() method.

Наконец, мы устанавливаем буфер в режим записи, вызывая методflip, и просто записываем в него.

Методstart() определен таким образом, что эхо-сервер может быть запущен как отдельный процесс во время модульного тестирования.

8.2. Клиент

Вот наш код дляEchoClient.java:

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;

    }
}

Клиент проще сервера.

Мы используем одноэлементный шаблон для его создания внутри статического методаstart. Мы вызываем приватный конструктор из этого метода.

В приватном конструкторе мы открываем соединение на том же порту, к которому был привязан канал сервера, и на том же хосте.

Затем мы создаем буфер, в который мы можем писать и из которого мы можем читать.

Наконец, у нас есть методsendMessage, который считывает любую строку, которую мы передаем ему, в байтовый буфер, который передается по каналу на сервер.

Затем мы читаем с клиентского канала, чтобы получить сообщение, отправленное сервером. Мы возвращаем это как эхо нашего сообщения.

8.3. тестирование

Внутри класса под названиемEchoTest.java мы собираемся создать тестовый пример, который запускает сервер, отправляет сообщения на сервер и проходит только тогда, когда те же сообщения возвращаются с сервера. В качестве последнего шага, тестовый пример останавливает сервер до завершения.

Теперь мы можем запустить тест:

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. Заключение

В этой статье мы рассмотрели базовое использование компонента Java NIO Selector.

Полный исходный код и все фрагменты кода для этой статьи доступны в моемGitHub project.