1、不得不说的NIO框架
解析传统IO问题,一般使用多线程来解决,一个读写阻塞不会影响其它的线程,但这也有问题,线程上下文切换是有开销的,另外设置过多的线程,也是一种开销。传统IO的根本原因是IO是阻塞,所以一种新的IO模型就产生了NIO(Non-Blocking IO),NIO三把斧(select、channel、buffer)刚好解决了传统IO的问题,具体分析可以详见 漫谈NIO。支持NIO要在操作系统内核支持,应用层调用系统函数进行处理操作,可以使用JDK中的类,但JDK存在一些问题,所以就诞生了NIO框架,但它们的原理都是相似的,只是在细节处理上不同。现在比较流行的NIO框架有Netty、Mina,两个框架都是出于同一个作者。
2、NIO示例
public class NIOServerDemo {
private static final int BUF_SIZE = 100;
private static final int TIME_OUT = 10000;
public static void main(String args[]) throws Exception {
// 打开服务端 Socket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 打开 Selector
Selector selector = Selector.open();
// 服务端 Socket 监听8080端口, 并配置为非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(8081));
serverSocketChannel.configureBlocking(false);
// 注册OP_ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 通过调用 select 方法, 阻塞地等待 channel I/O 可操作
if (selector.select(TIME_OUT) == 0) {
continue;
}
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 先移去
keyIterator.remove();
// 可连接
if (key.isAcceptable()) {
System.out.println("one connect is coming");
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
// 注册读事件
clientChannel.register(key.selector(), OP_READ, ByteBuffer.allocate(BUF_SIZE));
}
// 可读
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = clientChannel.read(buf);
if (bytesRead == -1) {
clientChannel.close();
} else if (bytesRead > 0) {
key.interestOps(OP_READ | SelectionKey.OP_WRITE);
System.out.println("Get data ===>: " + new String(buf.array()));
}
}
}
}
}
}
说明:
- ServerSocketChannel 注册请求(SelectionKey.OP_ACCEPT)事件,并设置监听端口,设置非阻塞请求
- selector.select() 阻塞式的监听事件,如果有事件返回,它就会有返回,事件类型在selector.selectedKeys()
- SelectionKey 可以获取SocketChannel,可以把它当作一个客户端连接
上面一个例子"麻雀虽小,但五脏俱全",它基本上把NIO的原理讲清楚了。
3、网络的本质
网络的本质是"请求-处理-响应(返回)",我们平时写的网络程序,基本上都遵循这个规律,一般的步骤是:
- 1) 服务器监听请求,发现有请求来了,转到具体的处理类上;
- 2)处理请求也有一系列的步骤:
- a. 解析请求,转成对应的对象
- b. 具体业务处理
- 3)按约定格式返回处理结果
不管使用最基础的Servlet,还是SpringMVC,或者SpringCloud,基本都是上面的步骤。
4、Netty原理
Netty是一个NIO框架,在Netty里,有一种线程模型Reactor,用下面的图来说明。
说明:
- 体现了单一职现原理,将一个网络请求分为:请求+处理,每个专注的事情不一样
- 左边的boss线程池(一个端口只对应一个线程,所以一般把它设置一个线程),右边是work线程池,每个线程都有一个select,监听channel注册的读写事件
- boss线程等待连接事件到来,如果到了,就从work线程池中取一个线程出来,让它来等待读写事件到来
- 事件到来是通过selector.select()来判断的,依赖于操作系统底层支持,所说的异步就是通过这种事件
再回过头来看上面NIO 示例的时候,是不是很清晰了。Netty封装了这两个线程池,里面稍微有一些复杂,让一些初学者感觉到害怕,一步一步的深入的时候,发现原理就是那样的。
源码说明:
- EventLoopGroup 是池线程池,真正的线程是NioEventLoop
- 每个线程都有一个selector,它会监听注册的channel事件发生
- bossGroup设置成单线程,一个端口只会对应一个线程,多个线程监听一个端口会报错的,端口占用,workerGroup可以是多线程,线程数默认是核数*2,假设是4核8线程,每个线程处理1000个请求,共计可处理8000个请求
- 每个channel有一个pipeline,里面可以加一些handler,如编码、解码、业务处理等,是典型的责任链模式