本文主要用JDK中的NIO包中类和方法,完成一个“客户端-服务端的网络通讯demo代码”的阻塞->非阻塞->异步演进过程。
本文涉及的代码可在这里获取
V1.0 阻塞版
服务端
主体程序
1 |
|
消息处理方法
1 |
|
说明:服务端每收到一个连接请求,就起一个线程去连接并处理消息。
缺点:
- 每个连接一个线程,不能支持高并发,此外,线程切换的开销也比较大;
- 当接收连接请求后就新建线程去处理,但此时可能没有数据,读取数据的操作(SocketChannel#read)就阻塞了。
客户端
一个简单地客户端,发送消息并接收服务端返回消息
1 |
|
V2.0 非阻塞版
相对于阻塞版,非阻塞版通过使用多路复用器(Selector)去管理多个通道。看代码:
服务端
1 |
|
客户端可继续沿用V1中的客户端。
用了多路复用器后,可以只用一个线程来轮询这个 Selector,看看是否有通道是准备好的(有事件)。 当通道准备好可读或可写,然后才去开始真正的读写,这样速度就很快了。完全没有必要给每个通道都起一个线程。
Selector简介
NIO 中 Selector 是对底层操作系统实现的一个抽象,管理通道状态其实都是底层系统实现的,这里简单介绍下在不同系统下的实现。
select
:上世纪 80 年代就实现了,它支持注册 FD_SETSIZE(1024) 个 socket,在那个年代肯定是够用的,不过现在嘛,肯定是不行了。
poll
:1997 年,出现了 poll 作为 select 的替代者,最大的区别就是,poll 不再限制 socket 数量。
select 和 poll 都有一个共同的问题,那就是它们都只会告诉你有几个通道准备好了,但是不会告诉你具体是哪几个通道。所以,一旦知道有通道准备好以后,自己还是需要进行一次扫描,显然这个不太好,通道少的时候还行,一旦通道的数量是几十万个以上的时候,扫描一次的时间都很可观了,时间复杂度 O(n)。所以,后来才催生了以下实现。
epoll
:2002 年随 Linux 内核 2.5.44 发布,epoll 能直接返回具体的准备好的通道,时间复杂度 O(1)。
除了 Linux 中的 epoll,2000 年 FreeBSD 出现了 Kqueue,还有就是,Solaris 中有 /dev/poll。
在Window中,非阻塞IO只能使用select。
V3.0 异步IO版
个人感觉,异步和非阻塞有一个区别是: 非阻塞只是不让你等待在那里啥也不干,如V2.0里所现,有事件了通知你去处理; 而异步的话则更进一步,有事件,ta直接调用回调函数给你处理完了。
JDK1.7后有了异步IO的接口,异步IO提供了两种使用方式:使用Future
,或使用回调函数CompletionHandler
1 |
|
服务端
相比非阻塞版本,此处的服务端的ServerSocketChannel
变成了AsynchronousServerSocketChannel
。
相似的,SocketChannel
和FileChannel
的AIO版本也分别加了 Asynchronous 前缀。
主体程序
1 |
|
回调函数
1 |
|
附加信息类 Attachment
1 |
|
客户端
1 |
|
Group简介
在代码中并没有涉及到group这个概念,但还是有必要介绍一下,group可以理解为Channel的集合。
在AIO中,AsynchronousChannelGroup
(下文简称 ACG)在linux中的实现类是EPollPort
,在windows中是Iopc
。
ACG 内持有fileDescriptor
到channel
的映射,从epoll
返回的事件可以间接的找到fileDescriptor
,通过映射找到channel
,从而完成io;
ACG 还持有线程池,自动开启,用于异步处理io,执行CompletionHandler
。
AsynchronousServerSocketChannels
和 AsynchronousSocketChannels
是属于 group 的。
当我们调用 AsynchronousServerSocketChannel
或 AsynchronousSocketChannel
的 open()
方法的时候,相应的 channel 就属于默认的 group,这个 group 由 JVM 自动构造并管理。
也可在open()中指定group参数。
如果我们想要配置这个默认的 group,可以在 JVM 启动参数中指定以下系统变量:
java.nio.channels.DefaultThreadPool.threadFactory
用于设置 ThreadFactory
应该是 java.util.concurrent.ThreadFactory 实现类的全限定类名。一旦指定了这个 ThreadFactory 以后,group 中的线程就会使用该类产生。java.nio.channels.DefaultThreadPool.initialSize
用于设置线程池的初始大小
若想使用自己定义的 group,以便其中的线程进行更多的控制,使用以下几个方法即可:
- ACG#
withCachedThreadPool(ExecutorService executor, int initialSize)
- ACG#
withFixedThreadPool(int nThreads, ThreadFactory threadFactory)
- ACG#
withThreadPool(ExecutorService executor)