NIO的一坑一惑小记

  • 前言

  不知不觉,已那么长时间没有更新东西了,说来真是汗颜啊。(主要是最近在技术上豁然开朗的感觉越来越少了-_-|||)

  最近一直在学习Linux相关的东西。又一次接触到了I/O复用模型(select/poll/epoll),由于好久没在用NIO写过代码了,今天就小试写个例子,以巩固下对I/O复用模型的理解。这不,遇到了一个坑,也产生了一点疑惑。^_^。

  • 一坑

  简单描述:Selector的select方法返回的key集合中有一个SelectionKey是可读的,但是调用与此SelectionKey关联的channel的read方法,总是返回读取长度是-1。既然返回-1,可以说明tcp链接已经断开。在下次调用select方法不应再返回这个SelectionKey,也不应该此SelectionKey是可读状态的。但事实并非如此:

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方法将数据返回给客户端。

  在网上看到的一些例子代码中无非两种。

  1. 直接返回---服务器端读取到客户端发过来的数据后,直接调用channel的write方法将数据返回给客户端。
  2. 注册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方法没有把所要返回的数据全部发送完,就注册对可写感兴趣,以待下次可写事件触发时再继续发送。

  就写到这吧,有啥说的不清楚,说的不准确的地方,还望高手不吝指教(*^__^*) ……

时间: 2024-10-16 20:12:30

NIO的一坑一惑小记的相关文章

记一次服务器高CPU的排查思路

现象 排查思路 另一台服务器CPU正常,由于消息中心有部分老接口是域名调用的,网关已做负载均衡,并且pinpoint上的两台服务器gc如图,初步猜测是否是负载不均衡导致. 经运维调试nginx权重无效,证明与负载均衡无关.那么先看子线程,这种情况必定由某几个线程引起 ps -mp pid -o THREAD,tid,time命令查看子线程,如图 这个数据上,分两部分看,1.有3个占用高的线程,2.执行时间可以注意到分别是4天,1天,23小时.说明线程出于某种情况,死锁,死循环. 由于这时候jst

kafka与zookeeper管理之kafka-manager踩坑小记

在elk集群搭建过程中,为了极大程度的利用服务器资源,kafka.zookeeper.logstash规划混跑在了同一组服务器上.随着业务量的增加,要频繁增加调整kafka的topic,出现问题时还要去服务器敲命令查看kafka和zookeeper的相关信息,效率低而且不方便,于是就考虑到用kafka的管理工具kafka-manager,安装配好后,整个集群的状态一目了然,而且可以方便的进行topic的操作.消费情况的查询.broker各种状态指标的查询等,非常方便,各种信息一目了然,安装配置过

Ubuntu 16.04 安装Mysql 5.7 踩坑小记

title:Ubuntu 16.04 安装Mysql 5.7 踩坑小记 date: 2018.02.03 安装mysql sudo apt-get install mysql-server mysql-client 测试是否安装成功 sudo netstat -tap | grep mysql 相关操作 登录 mysql -uroot -p 检查MySQL服务器占用端口 netstat -nlt|grep 3306 检查MySQL服务器系统进程 ps -aux|grep mysql 查看数据库的

算法踩坑小记

经过前面研究图像算法和近阶段研究视频和音频算法的经历经验. 在2019年快要来临的时候,写下这篇小记. 目的很简单,总结过往,展望未来. 这里列举一些本人在算法上踩过的坑和出坑思路. 主要是数据标准化问题. 1.临界值问题  (最大值,最小值,阈值,无穷小,无穷大) 最早做一键修图的时候,在这个坑上踩了太多次. 简单描述就是, (示例伪代码例子仅供理解思考参考,不具有实际意义) 1.1 梯度消失 如果一个算法在计算过程中,存在最小值(无穷小,一般为0或接近0的数), 那就很可能出现"梯度消失&q

vue项目实战爬坑小记002

1.如何使用vuex来保存数据(需要传参的情况下) 实例说明:登录->缓存用户信息->跳转到首页->从state获取用户信息显示在页面上 step1: 新建store.js作为初始化vuex的主文件,可在里面创建state对象,缓存数据字段,初始化vuex, 目录结构如下: store.js代码如下: 1 import Vue from 'vue'; 2 import Vuex from 'vuex'; 3 import * as actions from './actions'; 4

Guava Lists.transform踩坑小记&lt;转&gt;

1.问题提出 1.前段时间在项目中用到Lists.transform返回的List,在对该list修改后发现修改并没有反映在结果里,研究源码后发现问题还挺大.下面通过单步调试的结果来查看Guava Lists.transform使用过程中需要注意的地方. a.对原有的list列表修改会影响Lists.transform已经生成列表 由上图可以看出,对原数据集personDbs的修改会直接影响到Lists.transform方法返回的结果personVos,这是很危险的,如果在使用的过程中不注意的

vue项目实战爬坑小记003

太久没更新了.其实期间遇到了很多问题,都想着来整理和记录一下.可能是懒癌发作了吧,一直没动手记录.今天突破懒癌,重新来记录一波~~ 页面上如果要显示可编辑的保留数据格式的文本,可以有2种方式: 1. textarea标签中直接插入该数据的对象格式.有一个需要注意的坑是:如果修改里面的内容,容器不重绘的话,多个切换的时候内容不会变!!! 应该有人会吐槽: 不会用v-model 吗?小菜鸡~~~ 如果用了,确实能响应,但是不会显示这个格式, 而且如果直接绑定对象,就只会显示object文本. 1 <

async语法升级踩坑小记

从今年过完年回来,三月份开始,就一直在做重构相关的事情. 就在今天刚刚上线了最新一次的重构代码,希望高峰期安好,接近半年的Node.js代码重构. 包含从callback+async.waterfall到generator+co,统统升级为了async,还顺带推动了TypeScript在我司的使用. 这些日子也踩了不少坑,也总结了一些小小的优化方案,进行精简后将一些比较关键的点,拿出来分享给大家,希望有同样在做重构的小伙伴们可以绕过这些. 为什么要升级 首先还是要谈谈改代码的理由,毕竟重构肯定是

springcloud采坑--Zuul上传文件报java.nio.charset.IllegalCharsetNameException: UTF-8;boundary=sqgzzmMxl1UPdIp0IAYnQgUIAr9yNewVAzKIX

报错日志: 2018-12-17 10:01:19,688 ERROR [io.undertow.request] (default task-3) UT005023: Exception handling request to /xxx/app/bannerMaterialManager/uploadBannerSysGoodsPicture: java.nio.charset.IllegalCharsetNameException: UTF-8;boundary=sqgzzmMxl1UPdI