大家好,这里是五彩石编程,我是军哥。java开发中,绕不开的一个技术点就是nio
,和nio
相关的概念也非常之多,经常一起出来的还有BIO
、AIO
、channel
、epoll
等,很容易就把人搞得头晕了,今天军哥就带大家梳理下思路,用10分钟的时间来摸透nio
技术。
nio的故事
java早期只有io包,没有nio包,但io包在使用的过程中出现了很多问题,比如io是阻塞的,io操作也比较慢等等。后来,几位java开发者某天中午去了一家汉堡店吃汉堡,那时正是饭点,汉堡店的人非常多,3个点餐员前面,都排起了长队,他们每个人都点过汉堡后,就坐在靠近橱窗的位置,一边等着叫号,一边聊天,还好出餐的速度比较快,他们点的汉堡很快就做好了,吃完后他们还不想回去,就继续坐着,很快,其它客人就都吃完离开了,离开前都自觉的把餐盘送到了收餐具的窗口。最后只剩下了他们几个,这时候,其中一个开发者提了一个话题,说这家店的客人这么多,效率怎么还能这么高呀?其它人听完后,就开始分析,他们分析出来的要点主要有以下几个:
- 点餐员只负责点餐,客人多的时候就动态增加一个,直到最多3个点餐员
- 后厨负责查看单子,制作汉堡等各种耗时的操作,和点餐员的工作互不影响
- 后厨制作好一份食物后,就把食物放到取餐口,再通过叫号系统叫一下号,对应的客人听到后,就会自己来取自己点的食物,客人就不必一直等在取餐口了。
分析完了之后,一位开发者又提出了一个问题,如果我们也按这个步骤来改造io包,是不是也就解决了io阻塞的问题,还能提高io的效率呀!其他人听了之后,感觉有点意思,于是就快速回到了办公室,开始了改造工作。
首先客人来到汉堡店,一定是要完成一项吃东西的任务,而每个人的任务都不太一样,那就需要先把客人抽象出来,而每一个点餐员都会负责处理多个客人的任务,还可以动态增加点餐员,这个也得抽象出来,最好用线程池来实现动态增加的功能。餐做好了去通知客人,刚好可以用linux的epoll
技术来完成,而做好的餐放在出餐口,等待客人来取,那么出餐口也比较重要,也得抽象出来。客人走的时候,都把餐盘送到收餐具的口,要处理这么多餐具,那么收餐具的口也比较重要,也得抽象出来,这样一来,所有需要的东西就都抽象出来了,接下来开始进行整理。
一个点餐员处理那么多的客人,好像和一台电视机可以处理多个频道的功能比较类似,那干脆把客人的抽象叫做channel
频道吧,好直白的命名呀!他们貌似也意识到了这个问题,那么点餐员的这个抽象就绝对不能叫做tv,那就显得有点low了,既然点餐员可以选择为某个客户服务,也可以选择不为某个客户服务,那干脆叫selector
选择器吧,貌似又有点直白了,但总比tv好吧,就这样定吧!出餐口经常会临时放几份做好的食物,不就是提供了一个缓冲的空间嘛,那干脆称其抽象为buffer
吧,收餐具口貌似和出餐口功能类似,也会临时放几个餐具,那干脆把二者合二为一吧,大家都叫buffer
,这下需要的抽象就都齐活了。
以上故事纯属虚构,只是为了帮助大家理解nio中核心的三大件:selector
、channel
和buffer
。
nio的名字诞生
通过上面的故事,向大家描述了nio
中三大件的由来,那么怎么实现这个思路呢?如果直接改造io包,那么对于已经大量使用io包的企业来说,升级的成本就太高了,既然这个思路和之前的io思路不一样,这个思路又这么新颖,干脆就叫new io吧,但new io的名字貌似又有点low,那么就用缩写这个常见技巧,立马就会显得高大上了,于是一个开创新的io时代的名字诞生了:nio
。
nio的设计及实现
有了nio的三大件抽象,就开始整起来吧。
selector
的功能是平台相关的,linux
和windows
在实现上各不相同。
channel
用来代表一个具体的连接,具体是连接到一个文件,还是连接到一个硬件,还是连接到一个socket
,还是其它的任何能连接的东西,反正channel
就代表了这个连接,这个连接也代表了自己本身的属性,具体来说就是我们常见的FileChannel
、ByteChannel
、DataGramChannel
、ServerSocketChannel
、SocketChannel
等各个具体的channel
、接口或抽象类,功能上针对各自的应用场景做了扩展而已。
buffer
用来代表客人取食物以及放回餐盘的缓冲区,也即channel
的缓冲区,channel
要想拿到东西,或者放一个东西,必须要和buffer
打交道,哎,好像附带着把连接和读写的功能给解耦了呀,想之前的io包,stream还需要负责读或写的功能,看现在功能解耦之后,好一个清爽的世界呀!一句话:channel
就是连接,读写要找buffer
,搞定。
但这样一来,buffer
的速度还是不够高呀,这个时候,那几个java开发人员相视一笑,嘿嘿,炫技的时刻到了,一通输出之后,写出来了76个buffer
相关的类,全面覆盖了各种类型的buffer
,如常用的ByteBuffer
、CharBuffer
等,基于直接内存的各种类型的buffer
,基于堆的各种类型的buffer
,以及使用内存映射的MappedByteBuffer
等,还有基于ByteBuffer
来读取转换为其它各种类型数据的工具类。这样既解决了读写的性能问题,又解决了数据的转换问题,还顺便解决了大文件的读写问题。把简单的思路玩成别人难人比肩的高度,这就是大神!
单单这样就够了吗?no no no,too young to simple了,不是说原先的io包不好用吗?不是说原先的文件管理功能太简单吗?干脆玩彻底一点,把原来的io包的功能也顺便扩展一下,加入nio
中的channel
,悄悄的让它也加入到nio
的世界中来,还写了一个强大的Files
工具类,把大家想要的文件管理相关的功能全都加上了,又加一个路径管理工具Path,让大家用起来更方便,这下没有什么好吐槽的了吧!大神们终于可以休息一段时间了。
nio的应用示例
上面讲了这么多,好像没有看到一行代码,那么到底应该怎么使用nio
呀?按照军哥的惯例,肯定不会让大家失望的,下面依然没有一行代码。
走起。
读写文件示例
要想读取文件,肯定得知道文件的位置,也就是文件的路径,比如"/xx/xx/xx.log",有了路径,就可以实例化一个FileInputStream
了,有了InputStream
,通过它的getChannel
方法,就可以获取到这个文件对应的FileChannel
了,有了channel
,就要考虑buffer
了,这里暂时用不到selector
,所以不用管它,怎么创建一个buffer
呢?java已经替我们想好了,ByteBuffer
类中有现成的方法allocate
,想用直接内存?没有关系,换个方法allocateDirect
就搞定了,有了buffer
,直接调用channel
中的read
方法,把buffer
传进去,让它读到这个buffer
中不就可以了嘛!写文件的话是同样的思路,调用 channel
中的write
方法,就可以把buffer
中的数据写进去了,在这里就不再多说了。
服务端socket读写示例
socket
通常代表网络连接,对一个socket
执行读操作,就相当于读取用户传过来的数据,对一个socket
执行写的操作,就相当于给用户发送数据。
以服务器程序为例,想用nio
来操作socket
,就需要先有一个channel
,服务端的channel
已经有现成的实现了ServerSocketChannel
,直接调用它里面的open
方法,就可以创建一个服务器端的socketChannel
了,有了channel
还不够,还需要为它配置参数,先用它的configureBlocking
方法把它配置成非阻塞的,再用它里面的bind
方法来配置一个它要监听的地址和端口,这时候就需要用到selector
了,以便让服务器在有连接或其它相关事件发生时,来通知我们的程序。所以我们先调用Selector
中的open
方法,来获取一个selector
,再使用channel
中的register
方法,把我们关心的事件注册到selector
上,比如客户端连接事件,注册好之后,就可以循环调用selector
中的select
方法来接收事件列表了,此时代码就会阻塞了,直到有事件进来,一旦有连接事件进来,就可以通过之前创建的ServerSocketChannel
中的accept
方法,来获取到客户的连接对应的SocketChannel
了,终于可以看见客户端了,有了客户端的channel
,先把它设置为非阻塞,再把它也注册进selector
中,此时可以关注它的可读事件,一旦有客户端发来了数据,服务器就会通过selector
来通知我们,通过selector
的selectedKeys
方法就可以获取到事件列表,然后区分出可读的事件类型就可以进行读取了,读取方法就不用多说了吧,使用channel
中的read
方法,读到byteBuffer
里就可以了。
在这里,selector
、channel
和buffer
全都聚齐了。并且实现了一个服务器的功能。
至于向客户端发数据,应该也不用多说了吧,通过channel
的write
方法,把buffer
中的数据写进去,就相当于给客户端发送数据了。
客户端socket读写示例
客户端向服务器发送及接收数据,在这里也大概说一下吧,思路和服务器端的一样,只不过客户端使用的channel
为SocketChannel
,它也不需要bind
,而是需要调用connect
方法来连接到远程服务器,同样也需要selector
,把连接事件先绑定上,等到连接成功后,再把可读事件也绑定到selector
上,等到服务器有数据发过来时,就可以通过buffer
来接收数据了,向服务器发送数据就更简单了,还是调用channel
中的write
方法就可以了。
现在,你已经对java的nio有比较系统的了解了吧,但这样处理socket是不是太麻烦了,有没有简单一点的方法呢?当然有了,可以使用netty嘛!想了解netty及更多java相关技术,请微信搜索并关注 “五彩石编程”公众号,军哥的文章将会持续同步更新。
下课