基本使用
网络框架中服务端最基本的功能是需要监听客户端的连接事件、读写请求。下面代码片段模拟了一个最简单的服务端监听程序。
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();
}
}
上述代码开启了一个阻塞式的监听程序,不难看出其中的几个问题:
ssc.accept()方法会阻塞当前线程,在等待连接时,无法读取之前已连接的客户端发送的数据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能够处理多个客户端请求,提高了资源利用率。
网络编程学习笔记。