NIO模型
Java网络编程模型中,从JDK1.4 之后提出了NIO模型。我们来描述一下NIO是如何处理多个客户端连接的。
在NIO模型中,服务器端可以仅创建一个线程,在这个线程中使用Java NIO提供的多路复用器(Selector)来为客户端的多个连接提供服务。
NIO模型原理
Selector内部维护着一个列表, 当一个客户端连接到来时,不再为每个连接单独创建一个线程,而是把该客户端连接注册到selector上,这样在某个时刻,selector上可能已经被注册了多个连接。然后开始检查该selector,如果该selector所维护的列表中有一个或多个连接有数据可读,该selector则返回数据已经就绪的连接列表。这样用一个线程便可轮询该列表,读出多个连接上的数据。
理解了NIO模型的原理之后,我们来看下用Java所提供的API如何来完成NIO服务端的开发,仍然沿用阻塞模型中的客户端和服务端 PING消息 发送流程。
Java NIO 服务器
NIOServer
public class NIOServer {
private static final int port = 8000;
public static void main(String[] args) {
try {
Selector serverSelector = Selector.open();
ServerSocketChannel listenChannel = ServerSocketChannel.open();
listenChannel.socket().bind(new InetSocketAddress(port));
// (1) 配置为非阻塞模式
listenChannel.configureBlocking(false);
// (2) 监听新的客户端连接事件(ACCEPT)
listenChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
int counter = 0;
while (true) {
int num = 0;
try {
System.out.println("select ... " + counter++);
num = serverSelector.select();
} catch (Exception e) {
e.printStackTrace();
break;
}
if (num > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> it = set.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
try {
// (3) 接收到新的客户端连接
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
// (4) 新的客户端连接配置为非阻塞模式
clientChannel.configureBlocking(false);
// (5) 监听新客户端连接的读事件
clientChannel.register(serverSelector, SelectionKey.OP_READ);
} catch (Exception e) {
e.printStackTrace();
}
}
if (key.isReadable()) {
try {
// (6) 客户端有数据可读
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(4096);
int count = clientChannel.read(buf);
if (count > 0) {
buf.flip();
// (7) 输出接收到的数据
System.out.println(Charset.defaultCharset().newDecoder().decode(buf).toString());
} else {
key.cancel();
clientChannel.close();
}
} catch (Exception e) {
key.cancel();
e.printStackTrace();
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 服务端监听socket设置为非阻塞模式。
- 服务端监听socket,注册ACCEPT事件,来关注该事件,当有新的客户端连接到来时,会得到通知。
- 从多路复用器selector中获取所有的连接,如果该连接isAcceptable()为真,则表明监听socket上有新的客户端连接到来。
- 新的客户端连接仍然要配置为非阻塞模式。
- 为客户端连接注册OP_READ读事件,当该客户端socket有数据可读时,会得到通知。
- 该连接的socket有读事件发生,从通道中读取数据。
- 把该数据输出到终端。
服务端输出
首先运行NIO服务器,客户端仍然沿用之前的阻塞IO客户端IOClient.java,运行后,服务端输出结果如下:
Select ... 1
Sat Oct 26 13:20:56 CST 2019: PING
select ... 2
Sat Oct 26 13:20:58 CST 2019: PING
select ... 3
Sat Oct 26 13:21:00 CST 2019: PING
select ... 4
Sat Oct 26 13:21:02 CST 2019: PING
select ... 5
Sat Oct 26 13:21:04 CST 2019: PING
select ... 6
Sat Oct 26 13:21:06 CST 2019: PING
NIO编程思考
哇哦,我们用Java NIO模型实现了高效率的服务器,而且解决了之前阻塞模型BIO的问题,看起来好像不错。但你有没有发现,这里直接使用了原来的阻塞模型的客户端逻辑IOClient来与NIO服务器交互,而没有实现NIO模型的客户端。 是因为使用Java原生API来完成NIO模型,复杂度实在是有些高。
- 编程复杂。
- JDK底层的epoll实现, 存在导致CPU占用100%的Bug。
- 想完全依赖Java原生NIO API,来实现一个高效的NIO模型框架,需要有足够的耐心和底层基础知识,很难保证没有Bug。
那么问题来了, 如果既想使用NIO模型的高性能优势, 又不想处理Java原生NIO编程的复杂性,该怎么办呢? 于是来了个牛哄哄的家伙: Netty, 我们慢慢道来......
进入Netty之前
在进入更高层的NIO框架之前,先简单看一下计算机编程的整体框架和底层的基础知识,做到知其然而知其所以然,这样在编程时, 才知道各个逻辑单元在整个大厦中所处的位置。
整套Java API框架的实现包含了很多IO操作,包括各种文件IO和网络socket IO, 但这些Java API不能直接和外设交互,必须依赖操作系统提供的基础服务,于是乎,负责处理Java语言的虚拟机JVM在执行Java字节码的时候,需要与操作系统交互来实现IO操作, 而大部分操作系统都是由原生的宿主native语言(C语言)来开发实现,因此这之间的交互需要有一个叫jni (Java Native Interface)的通讯桥梁来打通。
整个调用栈的流程如上图,现在自底向上来看下各个部分。
- IO硬件。这里指各种外部输入输出设备,着重说明实现网络通讯的网卡设备。
- 操作系统。其实在操作系统和硬件设备之间有一层驱动(Driver)层,以Linux操作系统为例,驱动开发人员,可以遵循Linux的驱动框架,比如字符设备,块设备(网卡属于块设备), 并参考相关硬件设备厂商的规范手册,来开发驱动程序,并把驱动程序集成并入加载到操作系统中,和操作系统一起运行在权限最高的特权级别,来最终实现和IO硬件的通讯。
- epoll。这里就是NIO底层网络的核心实现机制,基于事件驱动机制,在操作系统内核层注册各种关心的事件,当这些事件就绪时,会向关注者发送通知。
如上三个epoll相关的系统调用,就是操作系统内核提供给我们实现高效NIO编程的制胜法宝。核心思想是,通过epoll_create来创建一个epoll内核对象,然后通过epoll_ctl可以动态注册事件,修改事件,删除事件,也就是说把所关心的事件都通过该系统调用交给内核来维护,操作系统内核内部会用一个高效的数据结构来维护该注册信息, 当网卡驱动有数据到来时,操作系统内核会收到通知,把这些处于就绪状态的事件转移到另一个就绪列表中。 当外部通过epoll_wait来获取就绪事件时,内核可直接把该就绪列表返回,不可谓不高效。
运行时库。操作系统提供的系统调用一般是该操作系统,最基础的,最关键的,必不可少的核心API,仅仅提供原子的基础能力,这样能够最大力度简化内核模块的设计与开发。但系统调用在用户的易用性上不一定能满足需求,于是运行时库担负起了这一职责,在linux系统上也就是glibc运行时库。该运行时库会对系统调用进行封装,提供更高层次的API。
JNI。看了操作系统底层最关键的基础设施之后,这里就是Java语言和下层本地操作系统的基础通讯桥梁。
NIO 框架。通过操作系统提供的基础NIO能力,Netty在这里完成了NIO类库的核心逻辑,和JVM携手联合在一起,在Java世界提供了一整套完善的NIO网络编程框架。
应用App。 这里才是上层的应用开发逻辑,包括实际的业务系统, 以及各种中间件系统,RPC框架等等都可以在这里实现。海深凭鱼跃,天高任鸟飞,凭借Netty提供的高效且方便的NIO框架,我们马上要开始后续的征程,奔跑吧,少年...... (见下一篇)