NIO-Selector使用

123 阅读3分钟

基本使用

网络框架中服务端最基本的功能是需要监听客户端的连接事件、读写请求。下面代码片段模拟了一个最简单的服务端监听程序。

ServerSocketChannel ssc = ServerSocketChannel.open();  
ssc.bind(new InetSocketAddress(8080));  
while (true){  
    SocketChannel sc = ssc.accept();  // 等待客户端连接请求 
    ByteBuffer buffer = ByteBuffer.allocate(1024);  
    int read = sc.read(buffer);  // 等待客户端发送数据;
    if (read !=-1){  
        buffer.flip();  
        while (buffer.hasRemaining()){  
            System.out.print((char)buffer.get());  
        }  
        buffer.clear();  
    }  
}

上述代码开启了一个阻塞式的监听程序,不难看出其中的几个问题:

  1. ssc.accept()方法会阻塞当前线程,在等待连接时,无法读取之前已连接的客户端发送的数据
  2. ssc.read(buffer) 方法也会阻塞当前线程,在等待读取时,无法受理其他客户端的连接请求。

既然这样,那我们将两个channel都设置为非阻塞式的试一试:

ServerSocketChannel ssc = ServerSocketChannel.open();  
ssc.configureBlocking(false);  
ssc.bind(new InetSocketAddress(8080));  
List<SocketChannel> scs = new ArrayList<>();  
while (true) {  
    SocketChannel sc = ssc.accept();  
    if (sc != null) {  
        sc.configureBlocking(false);  
        log.info("成功建立连接");  
        scs.add(sc);  
    }
    for (SocketChannel socketChannel : scs) {  
        ... 
    }  
}

通过设置channel为非阻塞式的,能解决无法处理多个客户端的请求问题,但是,cpu差点给我干冒烟了。从代码能够看出来,程序在不断循环的判断是否有新的连接请求,如果长时间没有客户端的请求,那么cpu一直在空转,极大浪费cpu资源,那么,怎么样解决这个问题,就要用到Selector了。

Selector

selector在nio模型中充当的一个管理者,统一管理事件,并在事件产生时通知channel进行处理,一个简单的Selector示例:

Selector selector = Selector.open();  
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
serverSocketChannel.configureBlocking(false);  
serverSocketChannel.bind(new InetSocketAddress(8080));  
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, null);  
while (true){  
    Selector sct = selectionKey.selector();  
    Iterator<SelectionKey> iterator = sct.selectedKeys().iterator();  
    while (iterator.hasNext()) {  
        SelectionKey next = iterator.next();  
        iterator.remove();  
        if (next.isAcceptable()){  
            ServerSocketChannel channel = (ServerSocketChannel)next.channel();  
            channel.register(selector,SelectionKey.OP_READ,null);  
        }  
        if (next.isReadable()){  
            SocketChannel channel = (SocketChannel)next.channel();  
            ByteBuffer buffer = ByteBuffer.allocate(1024);  
            channel.read(buffer);  
            buffer.flip();  
            String message = new String(buffer.array(),0,buffer.limit());  
            System.out.println(message);  
        }  
    }  
}

在示例中我们将ServerSocketChannel与SocketChannel注册至一个Selector中进行管理,并指定所关注的事件为accept还是read,通过调用select()来监听事件,当事件产生时判断属于哪一类事件并进行相应的处理。 但是,仍然采用的单线程处理方式,没有充分利用多核CPU的优势。

多线程使用

为了充分利用多核CPU资源,又引入了多线程进行优化:

定义一个Manager,专门处理客户端的连接请求
Selector selector = Selector.open();  
ServerSocketChannel ssc = ServerSocketChannel.open();  
ssc.configureBlocking(false);  
int processors = Runtime.getRuntime().availableProcessors();  // 获取CPU线程数
// 构造一个Worker组,用于处理读写事件
List<Worker> workers = new ArrayList<>(); 
for (int i = 0; i < processors; i++) {  
    workers.add(new Worker("worker-" + i));  
}  
ssc.register(selector, SelectionKey.OP_ACCEPT, null);  
ssc.bind(new InetSocketAddress(8080));  
while (true) {  
    selector.select();  
    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();  
    while (iterator.hasNext()) {  
        SelectionKey key = iterator.next();  
        iterator.remove();  
        // 在注册时指定了只关注Accept事件
        if (key.isAcceptable()) {  
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();  
            SocketChannel socketChannel = serverSocketChannel.accept();  
            socketChannel.configureBlocking(false); 
            // 避免极端情况值过大。
            if (active.get() >= limit) {  
                active.set(active.get() % processors);  
            }  
            // 为了Worker能够均匀分配SocketChannel
            int index = active.getAndIncrement() % processors;  
            workers.get(index).register(socketChannel);  
        }  
    }  
}

定义一个Worker,用于处理读写事件
class Worker implements Runnable {  
private Selector selector;  
private Thread thread;  
private String name;  
private volatile boolean running = false;  
  
public Worker(String name) throws IOException {  
    this.name = name;  
    selector = Selector.open();  
}  
  
public void register(SocketChannel sc) throws IOException {  
    if (!running) {  
        thread = new Thread(this);  
        thread.setName(this.name);  
        thread.start();  
        running = true;  
    }  
    sc.register(selector, SelectionKey.OP_READ, null);  
}  
  
@Override  
public void run() {  
    while (true) {  
    try {  
        selector.select();  
        Iterator<SelectionKey> iterator  = selector.selectedKeys().iterator();  
        while (iterator.hasNext()) {  
            SelectionKey key = iterator.next();  
            iterator.remove();  
            if (key.isReadable()) {  
                ...
        }  
    } catch (IOException e) {  
            e.printStackTrace();  
    }
} 

如下图所示,Manager中维护了一个Selector用于管理客户端连接事件,同时维护了一组Worker。在接收到客户端的连接请求时,从Woker组中选择一个Woker,并将建立好的连接通道SocketChannel注册至Woker的Selector中,以此实现通知Woker处理该客户端读写请求。在Woker中同样维护了自己的一个Selector,能够维护多个连接通道(channel),处理多个客户端的请求(多为读写请求)。在这里,一个客户端仅绑定在一个Woker中,避免多个Woker同时处理一个客户端情况下容易产生的问题(如数据丢失,粘包、半包处理难度飙升),同时一个Woker能够处理多个客户端请求,提高了资源利用率。

image.png 网络编程学习笔记。