NIO:非阻塞Socket的理解

java nio socket相较于传统socket主要优势就是“非阻塞”。这里针对java初学者谈一下nio非阻塞机制的粗略原理,帮助理解和学习。

首先要说明的是,nio socket并非100%取消了阻塞,该阻塞还是要阻塞,只是它允许程序以更高效的方式来阻塞。

 

传统socket编程的服务器端主要代码是:

new ServerSocket(8888).accept();

这个accept等待客户端连接的操作是阻塞的,直到有客户端连入。连入之后通常还会有一系列消息交互。

一个客户端与服务器程序交互的过程往往是这样:

如果你的服务器程序是单线程的,那么这个交互过程就一直占据当前唯一的主线程,红色框代表了对主线程的占用。

如果这个服务器程序只接待唯一的一个客户端,或者,接待多个客户端但每个客户端都长话短说迅速离去,那么传统的这种服务器端程序就不会有严重的问题,全新的nio也显不出任何优势。

但现实残酷,请你开发软件的老板肯定希望客户越多越好,而且客户在线时间越长越好。

所以,在多客户端、长时间交互的情况下,对于其他客户端来说,它必须等到前一个客户断开连接才有机会连入,如下图:

对主线程的占用必须排排队(红色框)。如果前一个连接一直持续了1小时,那么后面的连接必须等待1小时以上。

解决这个问题显而易见的办法,就是为每一个连接开辟一个线程,如下图:

每个红色框代表一个独立的线程,这样服务器程序就可以同时接待多个客户端了,代码大概如下(只表大意,忽略细节):

ServerSocket server = new ServerSocket(8888);
while(true) {
	Socket socket = server.accept();
	new Thread() {
		public void run() {
			InputStream in = socket.getInputStream();
			in.read();
		}
	}.start();
}

代码中,每收到一个客户端连接,就将其扔进新的线程中去处理,有数据就读出来处理,没数据就阻塞在流对象的read()上一直等待。而ServerSocket早就重新进入等待(下一个客户端连接)。这段代码可以进一步优化,如使用线程池来更高效地复用线程,此处按下不表。

对初学者来说,线程是很神奇又很厉害的武器,但是对于实际的生产环境来说,一分货一分钱,线程代价不菲。几十几百个线程没问题,但几千几万几十万呢?线程池也不是法力无边。

所以要进一步优化。

nio在思想上就进是一步优化上面这个模型,但也绝不是彻底推翻这个模型。从某种意义上,你可以认为nio的底层还是使用传统的那些socket对象,只是更聪明地使用他们,这种更聪明的使用方式我们自己也可以写出来,只是nio帮我们写好了,我们直接拿来用即可。——nio socket并不是全新实现的高级技术,只是合理使用了对初学者来说有点玄妙的所谓“设计模式”而已。

nio优化的着眼点,是看到了每个线程中“浪费”的部分。如下图:

上图绿色块部分,程序在执行什么?——是在执行InputStream.read()这个阻塞的操作。“阻塞”就是“傻等”、“死等”。实际上我们只希望InputStream.read()这个阻塞操作在读取消息的黄色块那部分发挥它的作用。

如此一来,很容易就能点透解决问题的出发点(但完整解决问题还有很多详细工作要做):

InputStream上还有一个非阻塞的available()方法,当前线程可以通过这个方法得知连接上有多少字节可读,并且方法是立即返回的。

nio采用了如下策略解决问题:

  1. 把所有连接集中到一个专门的线程中,只做非阻塞的轮询
  2. 轮询中发现任何连接上有可读的数据,就通知事先设好的观察者
  3. 观察者负责I/O操作处理可读数据,此时为了缓解I/O阻塞,建议在新线程中处理,处理完马上释放;

那个专门轮询的线程,对应了nio中的Selector.select()或selectNow()——当然,select()是阻塞的(selectNow非阻塞),那只是select给自己加戏了:所有连接都没有可读数据的话,我自己就反复去问这些连接呀(循环轮询),这个循环构成了阻塞而已。

同样的策略,有客户端连接进来的操作,那个accept(),因为也是阻塞的,所以也由Selector一并处理了。用熟知的SelectionKey.OP_ACCEPT(有客户端连入)和SelectionKey.OP_READ(有数据可读)来区分不同的通知。

注意Selector并不直接处理I/O,而只是发出通知,这个通知自然就是SelectionKey咯。

SelectionKey.isReadable()或SelectionKey.isAcceptable() 为 true,并不表示读取过了、连接过了,而是表示:此处应该有代码来读取数据了、处理连接了。

完整的流程往往如下:

  1. 新建ServerSocketChannel对象,可以认为这就是服务器端的抽象;
  2. 把ServerSocketChannel注册到Selector对象上,监听OP_ACCEPT事件(客户端连入);
  3. Selector.select()进入“轮询”
  4. 当有客户端连入,Selector.select()返回并提供SelectionKey(要确认isAcceptable()==true,因为完整程序Selector还会通知其他类型)
  5. ServerSocketChannel现在可以accept(),接受连接,产生SocketChannel对象(代表与客户端的连接管道)
  6. 此时不要思维定式去读取SocketChannel对象上的数据,而是nio的思想把SocketChannel对象注册到Selector对象上,监听OP_READ时间(数据可读)
  7. Selector.select()继续“轮询”
  8. 当有数据可读,Selector.select()返回并提供SelectionKey(要确认isReadable()==true)
  9. 此时可以从SocketChannel读取数据了(当然要使用推荐的Buffer咯)

 

以上,是给nio socket初学者的一份参考,希望有用。因为是帮助理解的,所以存在很多不严谨之处,希望初学者入门后自行详阅官方文档纠偏。大佬路过请斧正。谢谢

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值