1. 现象
最近在一次项目编码中遇到这么一个情况: 程序本身是一个中间处理器的角色,功能可简述为接收硬件设备的报文,然后对某些报文处理(其他报文直接转发),然后发送服务端处理;服务端处理后回复,程序接收服务端回复,然后直接转发给硬件设备。在测试时发现,硬件设备第一次发送报文,能够正常处理,但是当硬件设备第二次发送报文时,程序中Java NIO会持续触发读事件,导致服务端持续回复报文,从而设备持续收到回复报文。
2. 问题排查
由于程序中很多地方使用到线程池、队列等,一开始排查是否是这些因素导致的,但是经过仔细的控制变量的测试,发现并不是这些导致的。在这个问题上纠结了半天多,感觉崩溃的时候,决定去网上找找思路,然后看到一个同仁遇到类似问题,其中说到NIO中的边缘触发和水平触发,于是仔细看了下这两个概念:
(1) 水平触发是当就绪的fd未被用户进程处理,下一次select()查询依旧会返回,这是select和poll的触发方式。
(2) 边缘触发是无论就绪的fd是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发。
由于开发、测试是在windows平台下进行的,windows中Java NIO的底层网络模型是select模型,所以其触发方式为水平触发。所以上面遇到的情况很可能是由于就绪的fd未被处理,导致每次都会返回该事件。于是沿着这个方向排查。
经过排查,最终发现了原因:由于服务端在处理读事件时,将通道中的数据读到ByteBuffer中。将数据写入ByteBuffer后,经过一系列处理,该ByteBuffer的limit和position相等了,第二次再使用这个ByteBuffer来读通道中的数据时,将无法将数据写入该ByteBuffer,即就绪fd未被处理,导致下次selector调用select()方法时,又会返回该事件。
3. 解决方法
找到