Java NIO: Detailed explanation of Selector and its application in network programming

Overview

Selector <---> Channel <---> Buffer, all three support two-way data transfer

In NIO network programming, the relationship between Selector&Channel&Buffer is very close. Buffer reads and writes from Channel, and Channel is registered in Selector. In the past network programming, a thread is usually created to maintain a socket communication. When the business volume is small, the work can be completed very well, but once the number of clients increases, the number of threads created will also increase. The hardware overhead is very large. At this time, NIO's Selector reflects its value:

Selector can detect one or more Channels in Java NIO , and can know whether the channel is ready for components such as read and write events . In this way, a single thread can manage multiple Channels , thereby managing multiple network connections. Such a single thread management and management of multiple channels can greatly reduce the overhead of switching between threads .

Example

package com.leolee.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Set;

/**
 * @ClassName SelectorTest
 * @Description: NIO socket编程demo,用于理解Selector
 * @Author LeoLee
 * @Date 2020/9/22
 * @Version V1.0
 **/
public class SelectorTest {

    //端口数组,用于和多个客户端建立连接后分配端口
    int[] ports = null;

    //起始端口
    int tempPort = 5000;

    //构造器初始化 端口数组ports,并从起始端口tempPort开始分配[size]个端口号
    public SelectorTest (int size) {
        this.ports = new int[size];
        for (int i = 0; i < size; i++) {
            this.ports[i] = tempPort + i;
        }
    }


    public void selectorTest () throws IOException {

        Selector selector = Selector.open();

        //windows系统下是sun.nio.ch.WindowsSelectorProvider,如果是linux系统,则是KQueueSelectorProvider
        //由于Selector.open()的源码涉及 sun 包下的代码,是非开源代码,具体实现不得而知
//        System.out.println(SelectorProvider.provider().getClass());//sun.nio.ch.WindowsSelectorProvider
//        System.out.println(sun.nio.ch.DefaultSelectorProvider.create().getClass());//sun.nio.ch.WindowsSelectorProvider

        for (int i = 0; i < ports.length; i++) {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);//非阻塞模式
            ServerSocket serverSocket = serverSocketChannel.socket();
            //绑定端口
            InetSocketAddress address = new InetSocketAddress("127.0.0.1", ports[i]);
            serverSocket.bind(address);

            //注册selector
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("[step1]监听端口:" + ports[i]);
        }

        //阻塞代码,始终监听来自客户端的连接请求
        while (true) {
            //获取我们“感兴趣的时间”已经准备好的通道,上面代码感兴趣的是SelectionKey.OP_ACCEPT,这里获取的就是SelectionKey.OP_ACCEPT事情类型准备好的通道
            //number为该“感兴趣的事件“的通道数量
            int number = selector.select();
            System.out.println("number:" + number);
            if (number > 0) {
                //由于selector中会有多个通道同时准备好,所以这里selector.selectedKeys()返回的是一个set集合
                Set<SelectionKey> selectionKeys =  selector.selectedKeys();
                System.out.println("[step2]selectionKeys:" + selectionKeys);
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //由于我们”感兴趣“的是SelectionKey.OP_ACCEPT,所以如下判断
                    if (selectionKey.isAcceptable()) {
                        //selectionKey.channel()返回是ServerSocketChannel的爷爷类SelectableChannel,所以做强制类型转换
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);//非阻塞模式

                        //重点重点重点重点重点重点重点重点重点重点
                        //将接收到的channel同样也注册到Selector上,Selector<--->channel<--->buffer,三者是双向的
                        socketChannel.register(selector, SelectionKey.OP_READ);//这时候”感兴趣的事件“是读操作,因为要接收客户端的数据了
                        //重点重点重点重点重点重点重点重点重点重点
                        //当以上代码执行完毕后,已经建立了服务端与客户端的socket连接,这时候就要移除Set集合中的selectionKey,以免之后重复创建该selectionKey对应的通道
                        iterator.remove();

                        System.out.println("[step3]成功获取客户端的连接:" + socketChannel);
                    } else if (selectionKey.isReadable()) {//判断selectionKey可读状态
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        int byteRead = 0;
                        while (true) {
                            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
                            byteBuffer.clear();
                            int read = socketChannel.read(byteBuffer);
                            //判断数据是否读完
                            if (read <= 0) {
                                socketChannel.register(selector, SelectionKey.OP_READ);
                                break;
                            }

                            //写回数据,这里为了简单:读取什么数据,就写回什么数据
                            byteBuffer.flip();
                            socketChannel.write(byteBuffer);
                            byteRead += read;
                        }
                        System.out.println("[step4]读取:" + byteRead + ",来自与:" + socketChannel);

                        //重点重点重点重点重点重点重点重点重点重点
                        //当以上代码执行完毕后,已经完成了对某一个已经“读准备好”通道的读写操作,这时候就要移除Set集合中的selectionKey,以免之后重复读写该selectionKey对应的通道
                        iterator.remove();
                    }
                }
            }
        }
    }


    /*
     * 功能描述: <br> 使用nc命令连接服务端:nc 127.0.0.1 5000
     * 〈〉
     * @Param: [args]
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/9/23 12:59
     */
    public static void main(String[] args) throws IOException {

        SelectorTest selectorTest = new SelectorTest(5);
        selectorTest.selectorTest();
    }
}

The basic idea:

  1. Define 5 listening ports through the construction method
  2. Create a Selector, and register the initialized ServerSocketChannel on the Selector, and the Selector starts to monitor the Channel
  3. Construct a while loop (blocking code), always listen to requests from the client, and handle different operations (connection establishment, reading, writing) by judging the status of each SelectionKey in the SelectionKey collection returned after the Selector registers the channel

"Events of interest" is a concept that requires special attention:

Mainly divided into four types, defined as four constants in the SelectionKey class

  1. Connect
  2. Accept
  3. Read
  4. Write

When the Channel registers with the Selector, it must be given an int type parameter [ ops ], which represents the type of channel that is "interested" in the monitoring, and the status of the SelectionKey returned later is the human word. Can be a composite state:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

This is in the form of an event. When certain events are completed, the state of the Channel is identified. Therefore, the state of the Channel represented by the SelectionKey has also changed. By distinguishing and judging different states, we know that we should treat these Channel does the corresponding operation (connection establishment, reading, writing).

run

Run the server-side demo, the server-side monitors 5 ports

[step1]监听端口:5000
[step1]监听端口:5001
[step1]监听端口:5002
[step1]监听端口:5003
[step1]监听端口:5004

Use the nc command to connect to the server

The server's 5000 port listens to the client's request to establish a connection and establishes the connection:

[step2]selectionKeys:[sun.nio.ch.SelectionKeyImpl@179d3b25]
[step3]成功获取客户端的连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]

The client sends a message to the server. When the server receives the message, it returns the message content to the client intact

[step2]selectionKeys:[sun.nio.ch.SelectionKeyImpl@20ad9418]
[step4]读取:13,来自与:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]

PS.

You can try to create several more clients and connect to different ports to get a feel for the code idea

 

Those who need code come here to get it: demo project address

Guess you like

Origin blog.csdn.net/qq_25805331/article/details/108757585