NIO的一坑一惑小记
-
前言
不知不觉,已那么长时间没有更新东西了,说来真是汗颜啊。(主要是最近在技术上豁然开朗的感觉越来越少了-_-|||)
最近一直在学习Linux相关的东西。又一次接触到了I/O复用模型(select/poll/epoll),由于好久没在用NIO写过代码了,今天就小试写个例子,以巩固下对I/O复用模型的理解。这不,遇到了一个坑,也产生了一点疑惑。^_^。
-
一坑
简单描述:Selector的select方法返回的key集合中有一个SelectionKey是可读的,但是调用与此SelectionKey关联的channel的read方法,总是返回读取长度是-1。既然返回-1,可以说明tcp链接已经断开。在下次调用select方法不应再返回这个SelectionKey,也不应该此SelectionKey是可读状态的。但事实并非如此:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public class NIOMain { public static void main(String[] args) throws Exception { Selector selector = Selector.open(); ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking( false ); serverChannel.socket().bind( new InetSocketAddress( 9000 ), 10 ); serverChannel.register(selector, SelectionKey.OP_ACCEPT); doSelect(selector); } public static void doSelect(Selector selector) throws Exception{ while ( true ) { int srt=selector.select(); if (srt<= 0 ){ continue ; } Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); while (iter.hasNext()){ SelectionKey key = iter.next(); if (key.isAcceptable()){ ServerSocketChannel sChannel= (ServerSocketChannel) key.channel(); SocketChannel cChannel = sChannel.accept(); cChannel.configureBlocking( false ); cChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()){ SocketChannel cChannel = (SocketChannel) key.channel(); ByteBuffer bb = ByteBuffer.allocate( 1024 ); int len =cChannel.read(bb); bb.flip(); if (bb.hasArray() && len> 0 ){ System.out.println( "from client " + ":" + new String(bb.array(), 0 ,len)); int newInterestOps = key.interestOps(); newInterestOps |= SelectionKey.OP_WRITE; key.interestOps(newInterestOps); } else if (len==- 1 ){ System.out.println( "no data" ); //在这里不能忘记关闭channel } bb.clear(); } iter.remove(); } } } } |
运行此代码,然后在浏览器里输入127.0.0.1:9000,回车。结果是控制台里首先打印出http协议的信息。然后就是死循环打印no data。原因可想而知,浏览器在发起http请求后,一定时间没有得到服务器端的相应,便会断开tcp链接。此时channel的read方法就会返回-1。坑的是,链接都已经断开了,Selector还能将它select出来,并且一直是可读状态。这就导致了一直死循环打印no data。如果这种事情发生在生产环境,后果真是不堪设想啊。
解决方式虽然比较简单,但却不能疏忽遗漏。当channel的read方法返回-1时。调用channel的close方法关闭channel。上边代码就是在打印no data的地方添加一行:cChannel.close()。这样channel对应的SelectionKey也就不会再被select出来了。也就不再发生死循环了。
-
一惑
NIO编程中我一直有一个疑惑或者说不确定,就是什么时候调用channel的write方法将数据返回给客户端。
在网上看到的一些例子代码中无非两种。
- 直接返回---服务器端读取到客户端发过来的数据后,直接调用channel的write方法将数据返回给客户端。
- 注册Writable事件,可写事件发生后再返回---服务器读取到客户端发来的数据后,然后将channel注册到selector对Writable感兴趣。当可写后,再调用channel.write写数据。但这个方式一定得注意:当写完数据后,一定取消对Writable事件的感兴趣。否则服务器又得忙到崩溃。
这两个方式似乎都可以工作,跑一些例子也都没发现什么问题。但是心里总是感觉有一点不够明确不够开朗(可能就是因为对系统底层的实现不够明确的原因)。Java有一些成熟的开源的NIO框架,比如netty、mina。何不去看看他们是如何处理的呢?好,接下来就看看mina的实现方式。(我这里看的是mina2.0.2版本)
接下来是我追踪到AbstractPollingIoProcessor的flushNow方法的代码
由于篇幅就不贴上writeBuffer方法的全部代码,其关键调用:,writeBuffer方法也是将write方法返回的localWrittenBytes返回。接下来让我们抓紧看看write方法的实现吧。并看看到底返回的是什么东西
抛开其他的细节不管,咱们先看看如何实现向客户返回数据的,mina直接从session中拿到关联的SocketChannel,然后直接调用SocketChannel的write方法写数据到客户端,并将write写出去数据的长度记录下来。
让我们返回到最开始flushNow方法:
可以看到,当channel写出去的数据长度大于零,并且buff里还有数据要写时。调用了setInterestedInWrite方法,通过方法名也知道是在注册对写事件感兴趣是吧,看下代码明确下吧
没错,确实是在注册对写事件感兴趣。在flushNow方法后边还有一个对localWrittenBytes等于零的判断:
通过源代码里的注释,就知道,当localWrittenBytes等于零时,也就是调用channel的write没有写出任何数据,此时就是内核的Buufer满了,是不可写状态。所以这里也调用setInterestedInWrite方法注册可写感兴趣,以待可写事件发生后再发送数据到客户端。
总结一下mina的实现就是:读取到客户端请求的数据后,就调用channel的write方法向客户返回数据,如果channel的write方法没有把所要返回的数据全部发送完,就注册对可写感兴趣,以待下次可写事件触发时再继续发送。
就写到这吧,有啥说的不清楚,说的不准确的地方,还望高手不吝指教(*^__^*) ……
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?