大家都知道传统的bio(即阻塞io)的并发性能不高,原因是什么呢?
- ServerSocket要accept接受客户端socket是阻塞的,那么这个线程就一直被占用,这样才能保证客户端的连接都能被处理
- accept到客户端的socket连接之后要再开一个线程来处理读写操作,这样又一个线程被占用
- 所以当并发访问量非常高时,服务器的线程调度数量到达峰值,就导致响应客户端性能下降
那么怎么解决io线路复用的问题呢,答案就是nio(非阻塞io)的出现,下面我们来看下nio的基本操作方法
public class NioServer {
ServerSocketChannel ssc;
private final Selector selector;
public NioServer() throws IOException {
//打开serversocketchannel
ssc = ServerSocketChannel.open();
//设置为非阻塞
ssc.configureBlocking(false);
//绑定端口
ssc.socket().bind(new InetSocketAddress(8000));
//创建selector
selector = Selector.open();
//注册channel到selector 并且绑定accept事件
ssc.register(selector,SelectionKey.OP_ACCEPT);
//处理服务端channel接受问题
handleAccept();
}
selector是一个底层的轮询器,他可以去轮询ServerSocketChannel是否有新连接accept,有阻塞和非阻塞轮询方式,如果ServerSocketChannel关注是否有客户端accept进来,就需要向selector注册accept事件
private void handleAccept() throws IOException {
for (;;){
//阻塞轮询 阻塞3s来轮询是否有新连接到来 num返回SelectionKey的个数
int num = selector.select(3 * 1000L);
if(num == 0)continue;
//拿到selectKey的迭代器 返回的是一个set集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//拿到有accept事件的selectKey 从集合中移除
iterator.remove();
//验证key的有效性
if(!key.isValid())continue;
handlekey(key);
}
}
}
这段代码主要就是调用selector去阻塞的轮询客户端到来的连接,拿到SelectionKey 进行io操作
private void handlekey(SelectionKey key) throws IOException {
//事件是accept
if(key.isAcceptable()){
//拿到服务端的channel 调用accept方法 返回客户端的channel连接
SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();
//设置channel非阻塞
socketChannel.configureBlocking(false);
//把channel注册到selector上 对读事件感兴趣
socketChannel.register(selector,SelectionKey.OP_READ);
}
if(key.isReadable()){
//可读的事件 客户端channel
SocketChannel channel = (SocketChannel) key.channel();
//创建ByteBuffer 从channel往buf写数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
if(count==-1)return;
//重置buf 到可读状态
buffer.flip();
String content = new String(buffer.array());
System.out.println(content);
//返回数据
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("hellow word".getBytes());
buf.flip();
//channel 写数据
channel.write(buf);
}
}
拿到channel注册到selector轮询器read事件,在read事件到来的时候读数据,向客户端写数据,这种nio的模型为实现io复用提供了很大的方便,单个线程可以同时处理很多的请求,在read事件准备好之前不会阻塞线程,这段时间可以处理其他已经准备就绪的io操作,极大的提高了io线路复用率
再仔细考虑一下单线程模型下单个selector轮询accept read write等全部事件,虽然说非阻塞但还是单线程,必然造成请求的响应缓慢问题,参考一下Netty的Reactor模型
- 服务端又一个专门负责轮询accept的线程(boss线程)
- 轮询到的key交给work线程来继续轮询read write操作以及pipline的执行逻辑
- work线程和boss线程都可以设置成多线程的线程池,这样就提高了io链路的吞吐量
下一篇开始分析netty的源码流程