epoll是个好东西好多地方都在直接或间接的用,nginx用event库用nio用你用我用大家用。LT模式省心ET模式牛掰,处理得当效率那真是杠杠的,C、C++的用法可以参考“项目”通用库https://git.oschina.net/gonglibin/GlbLib-1.0.0中的封装,也可以参考“代码片段”中的用例https://git.oschina.net/gonglibin/codes/kev675liw8unz3j4psft134。由于一个潜在的项目迁移计划导致未来存在基于java技术路线中涉及线程池大并发长(短)连接的业务,所以一个框架就显得特别重要,兵马未动粮草先行。其实不同的业务包含各自不同的应用场景,就通信而言其实差别有时候还是挺大的,同样TCP连接,有的长有的短,有的求多应少,有的求少应多,还有一问一答的,更有一言不合就shutdown的,对底层资源的管理不上心有时候真不行,大半夜没起过夜绝对不算老司机。
说说nio踩过的坑吧,下面列举的仅仅是让发动机托底的大坑,其中崴脚的小坑更是不计其数,首先打开地址https://git.oschina.net/gonglibin/codes/2jagu36pq7dwsc1eylkr916。首先注意一点的是nio的许多操作不是线程安全的,用不对各种坑有的踩了。大致思路首先创建TCP服务,构造里建线程池,主进程里给selector对象画个圈圈,这个对象的所有操作限制在listen方法里,把对业务的处理全部浸泡在读事件中,为啥读完不注册写信号让写事件处理呢,一会后面说吧,要是忘了千万别提醒我。
坑一,cli.register(sel, SelectionKey.OP_READ, ByteBuffer.allocate(TU_SERSIZE)),在注册新连接的时候顺带把allocate的buffer赋值给SelectionKey对象的attachment,接下来read并判断读取长度,不大于零和对端一起关闭,否则把SelectionKey对象往线程池里一派发就完事了,流程自己继续跑它自己的路,代码看起来简洁潇洒大气木毛病,正嘚瑟着问题来了,对端一关闭傻眼了,新请求直接把attachment擦除了,所以还是别顺带了,老老实实给每个任务开个buffer吧。
坑二,这些年搬砖一直追求精益求精对资源少占少用,C、C++一个套接字底层对应两块buffer,一读一写井水不犯河水相安无事,ByteBuffer对象是环形buffer,通过flip方法实现读写缓存的逆转,把position游标值和limit提交值一变轻松搞定,好是好但在线程池里维护一个连接上的各个buffer就够费劲的这还非要把读写合并一堆儿,真考验人呀,加锁呗,行是行就是显得low。
坑三,把SelectionKey对象的实例作为线程入口参数派发,表面看没毛病,但在完成各个事件处理以后重新注册监听事件的时候线程间同步又成了问题,其实这个坑已经解答了上面的疑问,读写分离按需注册固然完美,但每个线程的执行时间是不同的,CPU时间片调度也只能相对均衡,保不齐谁早点谁晚点,一个close操作秒完,那边任务才跑一半,咔嚓一下就把注册的监听事件给改了,结果这边的注册失效了,傻了吧唧还等呢,左等不来右等不来结果下班了。
坑四,SocketChannel对象关闭以后把SelectionKey实例的valid记做false,打标记倒是无所谓啦,不过无效的对象是不是就别再放在监听集合里处理是不是好点呀,免得每次还要判断,不判断就抛出一个异常,你看人家C库epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)设置EPOLL_CTL_DEL清理的干干净净,或许这么玩有人家自己的道理吧,相信别人这么做是有道理的。
把接收缓存和频道对象作为入参传给线程池进行任务处理,保证每次请求都能被唯一的指派,强调服务的一对一特性,处理完流程内部直接返回了,能做成读写异步自然好呀,不过写这个框架是有私心的,业务指向十分明确,其他的需求还是另起炉灶或许更好一些。最后还是要强调一下,不同的业务对通信方式的要求不尽相同,所以一个框架解决不了所有问题,还是应该具体问题具体分析。
难得一本正经的写个东西浅显地剖析一个不是问题的问题,口号喊起来:励斌出品,必是精品,欧耶!