nio是JDK1.4中引入的,用于区别于传统的IO,所以nio也可以称之为new io。
nio的三大核心是Selector,channel和Buffer,本文我们将会深入探究NIO和netty之间的关系。
在讲解netty中的NIO实现之前,我们先来回顾一下JDK中NIO的selector,channel是怎么工作的。对于NIO来说selector主要用来接受客户端的连接,所以一般用在server端。我们以一个NIO的服务器端和客户端聊天室为例来讲解NIO在JDK中是怎么使用的。
因为是一个简单的聊天室,我们选择Socket协议为基础的ServerSocketChannel,首先就是open这个Server channel:
然后向server channel中注册selector:
虽然是NIO,但是对于Selector来说,它的select方法是阻塞方法,只有找到匹配的channel之后才会返回,为了多次进行select操作,我们需要在一个while循环里面进行selector的select操作:
selector中会有一些SelectionKey,SelectionKey中有一些表示操作状态的OP Status,根据这个OP Status的不同,selectionKey可以有四种状态,分别是isReadable,isWritable,isConnectable和isAcceptable。
当SelectionKey处于isAcceptable状态的时候,表示ServerSocketChannel可以接受连接了,我们需要调用register方法将serverSocketChannel accept生成的socketChannel注册到selector中,以监听它的OP READ状态,后续可以从中读取数据:
当selectionKey处于isReadable状态的时候,表示可以从socketChannel中读取数据然后进行处理:
上面的serverResponse方法中,从selectionKey中拿到对应的SocketChannel,然后调用SocketChannel的read方法,将channel中的数据读取到byteBuffer中,要想回复消息到channel中,还是使用同一个socketChannel,然后调用write方法回写消息给client端,到这里一个简单的回写客户端消息的server端就完成了。
接下来就是对应的NIO客户端,在NIO客户端需要使用SocketChannel,首先建立和服务器的连接:
然后就可以使用这个channel来发送和接受消息了:
向channel中写入消息可以使用write方法,从channel中读取消息可以使用read方法。
这样一个NIO的客户端就完成了。
虽然以上是NIO的server和client的基本使用,但是基本上涵盖了NIO的所有要点。接下来我们来详细了解一下netty中NIO到底是怎么使用的。
以netty的ServerBootstrap为例,启动的时候需要指定它的group,先来看一下ServerBootstrap的group方法:
ServerBootstrap可以接受一个EventLoopGroup或者两个EventLoopGroup,EventLoopGroup被用来处理所有的event和IO,对于ServerBootstrap来说,可以有两个EventLoopGroup,对于Bootstrap来说只有一个EventLoopGroup。两个EventLoopGroup表示acceptor group和worker group。
EventLoopGroup只是一个接口,我们常用的一个实现就是NioEventLoopGroup,如下所示是一个常用的netty服务器端代码:
这里和NIO相关的有两个类,分别是NioEventLoopGroup和NioServerSocketChannel,事实上在他们的底层还有两个类似的类分别叫做NioEventLoop和NioSocketChannel,接下来我们分别讲解一些他们的底层实现和逻辑关系。
NioEventLoopGroup和DefaultEventLoopGroup一样都是继承自MultithreadEventLoopGroup:
他们的不同之处在于newChild方法的不同,newChild用来构建Group中的实际对象,NioEventLoopGroup来说,newChild返回的是一个NioEventLoop对象,先来看下NioEventLoopGroup的newChild方法:
这个newChild方法除了固定的executor参数之外,还可以根据NioEventLoopGroup的构造函数传入的参数来实现更多的功能。
这里参数中传入了SelectorProvider、SelectStrategyFactory、RejectedExecutionHandler、taskQueueFactory和tailTaskQueueFactory这几个参数,其中后面的两个EventLoopTaskQueueFactory并不是必须的。
最后所有的参数都会传递给NioEventLoop的构造函数用来构造出一个新的NioEventLoop。
在详细讲解NioEventLoop之前,我们来研读一下传入的这几个参数类型的实际作用。
SelectorProvider是JDK中的类,它提供了一个静态的provider()方法可以从Property或者ServiceLoader中加载对应的SelectorProvider类并实例化。
另外还提供了openDatagramChannel、openPipe、openSelector、openServerSocketChannel和openSocketChannel等实用的NIO操作方法。
SelectStrategyFactory是一个接口,里面只定义了一个方法,用来返回SelectStrategy:
什么是SelectStrategy呢?
先看下SelectStrategy中定义了哪些Strategy:
SelectStrategy中定义了3个strategy,分别是SELECT、CONTINUE和BUSY_WAIT。
我们知道一般情况下,在NIO中select操作本身是一个阻塞操作,也就是block操作,这个操作对应的strategy是SELECT,也就是select block状态。
如果我们想跳过这个block,重新进入下一个event loop,那么对应的strategy就是CONTINUE。
BUSY_WAIT是一个特殊的strategy,是指IO 循环轮询新事件而不阻塞,这个strategy只有在epoll模式下才支持,NIO和Kqueue模式并不支持这个strategy。
RejectedExecutionHandler是netty自己的类,和 java.util.concurrent.RejectedExecutionHandler类似,但是是特别针对SingleThreadEventExecutor来说的。这个接口定义了一个rejected方法,用来表示因为SingleThreadEventExecutor容量限制导致的任务添加失败而被拒绝的情况:
EventLoopTaskQueueFactory是一个接口,用来创建存储提交给EventLoop的taskQueue:
这个Queue必须是线程安全的,并且继承自java.util.concurrent.BlockingQueue.
讲解完这几个参数,接下来我们就可以详细查看NioEventLoop的具体NIO实现了。
首先NioEventLoop和DefaultEventLoop一样,都是继承自SingleThreadEventLoop:
表示的是使用单一线程来执行任务的EventLoop。
首先作为一个NIO的实现,必须要有selector,在NioEventLoop中定义了两个selector,分别是selector和unwrappedSelector:
在NioEventLoop的构造函数中,他们是这样定义的:
首先调用openSelector方法,然后通过返回的SelectorTuple来获取对应的selector和unwrappedSelector。
这两个selector有什么区别呢?
在openSelector方法中,首先通过调用provider的openSelector方法返回一个Selector,这个Selector就是unwrappedSelector:
然后检查DISABLE_KEY_SET_OPTIMIZATION是否设置,如果没有设置那么unwrappedSelector和selector实际上是同一个Selector:
DISABLE_KEY_SET_OPTIMIZATION表示的是是否对select key set进行优化:
如果DISABLE_KEY_SET_OPTIMIZATION被设置为false,那么意味着我们需要对select key set进行优化,具体是怎么进行优化的呢?
先来看下最后的返回:
最后返回的SelectorTuple第二个参数就是selector,这里的selector是一个SelectedSelectionKeySetSelector对象。
SelectedSelectionKeySetSelector继承自selector,构造函数传入的第一个参数是一个delegate,所有的Selector中定义的方法都是通过调用
delegate来实现的,不同的是对于select方法来说,会首先调用selectedKeySet的reset方法,下面是以isOpen和select方法为例观察一下代码的实现:
selectedKeySet是一个SelectedSelectionKeySet对象,是一个set集合,用来存储SelectionKey,在openSelector()方法中,使用new来实例化这个对象:
netty实际是想用这个SelectedSelectionKeySet类来管理Selector中的selectedKeys,所以接下来netty用了一个高技巧性的对象替换操作。
首先判断系统中有没有sun.nio.ch.SelectorImpl的实现:
SelectorImpl中有两个Set字段:
这两个字段就是我们需要替换的对象。如果有SelectorImpl的话,首先使用Unsafe类,调用PlatformDependent中的objectFieldOffset方法拿到这两个字段相对于对象示例的偏移量,然后调用putObject将这两个字段替换成为前面初始化的selectedKeySet对象:
如果系统设置不支持Unsafe,那么就用反射再做一次:
还记得前面我们提到的selectStrategy吗?run方法通过调用selectStrategy.calculateStrategy返回了select的strategy,然后通过判断
strategy的值来进行对应的处理。
如果strategy是CONTINUE,这跳过这次循环,进入到下一个loop中。
BUSY_WAIT在NIO中是不支持的,如果是SELECT状态,那么会在curDeadlineNanos之后再次进行select操作:
如果strategy >0,表示有拿到了SelectedKeys,那么需要调用processSelectedKeys方法对SelectedKeys进行处理:
上面提到了NioEventLoop中有两个selector,还有一个selectedKeys属性,这个selectedKeys存储的就是Optimized SelectedKeys,如果这个值不为空,就调用processSelectedKeysOptimized方法,否则就调用processSelectedKeysPlain方法。
processSelectedKeysOptimized和processSelectedKeysPlain这两个方法差别不大,只是传入的要处理的selectedKeys不同。
处理的逻辑是首先拿到selectedKeys的key,然后调用它的attachment方法拿到attach的对象:
如果channel还没有建立连接,那么这个对象可能是一个NioTask,用来处理channelReady和channelUnregistered的事件。
如果channel已经建立好连接了,那么这个对象可能是一个AbstractNioChannel。
针对两种不同的对象,会去分别调用不同的processSelectedKey方法。
对第一种情况,会调用task的channelReady方法:
对第二种情况,会根据SelectionKey的readyOps()的各种状态调用ch.unsafe()中的各种方法,去进行read或者close等操作。
NioEventLoop虽然也是一个SingleThreadEventLoop,但是通过使用NIO技术,可以更好的利用现有资源实现更好的效率,这也就是为什么我们在项目中使用NioEventLoopGroup而不是DefaultEventLoopGroup的原因。
WebSocket是一种规范,是Html5规范的一部分,websocket解决什么问题呢?解决http协议的一些不足。我们知道,http协议是一种无状态的,基于请求响应模式的协议。
网页聊天的程序(基于http协议的),浏览器客户端发送一个数据,服务器接收到这个浏览器数据之后,如何将数据推送给其他的浏览器客户端呢?
这就涉及到服务器的推技术。早年为了实现这种服务器也可以像浏览器客户端推送消息的长连接需求,有很多方案,比如说最常用的采用一种轮询技术,就是客户端每隔一段时间,比如说2s或者3s向服务器发送请求,去请求服务器端是否还有信息没有响应给客户端,有就响应给客户端,当然没有响应就只是一种无用的请求。
这种长轮询技术的缺点有:
1)响应数据不是实时的,在下一次轮询请求的时候才会得到这个响应信息,只能说是准实时,而不是严格意义的实时。
2)大多数轮询请求的空轮询,造成大量的资源带宽的浪费,每次http请求携带了大量无用的头信息,而服务器端其实大多数都不关注这些头信息,而实际大多数情况下这些头信息都远远大于body信息,造成了资源的消耗。
拓展
比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
WebSocket一种在单个 TCP 连接上进行 全双工通讯 的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket API也被W3C定为标准。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许 服务端主动向客户端推送数据 。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
websocket的出现就是解决了客户端与服务端的这种长连接问题,这种长连接是真正意义上的长连接。客户端与服务器一旦连接建立双方就是对等的实体,不再区分严格意义的客户端和服务端。长连接只有在初次建立的时候,客户端才会向服务端发送一些请求,这些请求包括请求头和请求体,一旦建立好连接之后,客户端和服务器只会发送数据本身而不需要再去发送请求头信息,这样大量减少了
网络带宽。websocket协议本身是构建在http协议之上的升级协议,客户端首先向服务器端去建立连接,这个连接本身就是http协议只是在头信息中包含了一些websocket协议的相关信息,一旦http连接建立之后,服务器端读到这些websocket协议的相关信息就将此协议升级成websocket协议。websocket协议也可以应用在非浏览器应用,只需要引入相关的websocket库就可以了。
HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket使用ws或wss的统一资源标志符,类似于HTTPS,其中wss表示在TLS之上的Websocket。如:
优点
浏览器页面向服务器发送消息,服务器将当前消息发送时间反馈给浏览器页面。
服务器端
服务器端初始化连接
WebSocketServerProtocolHandler :参数是访问路径,这边指定的是ws,服务客户端访问服务器的时候指定的url是: ws://localhost:8899/ws 。
它负责websocket握手以及处理控制框架(Close,Ping(心跳检检测request),Pong(心跳检测响应))。 文本和二进制数据帧被传递到管道中的下一个处理程序进行处理。
桢 :
WebSocket规范中定义了6种类型的桢,netty为其提供了具体的对应的POJO实现。
WebSocketFrame:所有桢的父类,所谓桢就是WebSocket服务在建立的时候,在通道中处理的数据类型。本列子中客户端和服务器之间处理的是文本信息。所以范型参数是TextWebSocketFrame。
自定义Handler
页面 :
启动服务器,然后运行客户端页面,当客户端和服务器端连接建立的时候,服务器端执行 handlerAdded 回调方法,客户端执行 onopen 回调方法
服务器端控制台:
页面:
客户端发送消息,服务器端进行响应,
服务端控制台打印:
客户端也收到服务器端的响应:
打开开发者工具 :
在从标准的HTTP或者HTTPS协议切换到WebSocket时,将会使用一种升级握手的机制。因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确定时刻特定与应用程序;它可能会发生在启动时候,也可能会发生在请求了某个特定的IURL之后。
参考技术
java web 服务器推送技术--comet4j
Comet:基于 HTTP 长连接的“服务器推”技术
客户端发起http请求,请求Netty服务器进行WebSocket连接,服务器接收后请求后进行注册信道并登记客户端IP地址,如此一来就建立了WebSocket通讯连接。
上面的论述可以得出,我们可以比较Http和WebSocket两者之间的关系和区别
基于上述WebSocket的特点,可以实现在线设备的消息推送
创建线程启动Netty服务器
也许这里,有些疑问为什么需要创建线程来启动Netty服务器,这里尝试一下如果没有创建线程,直接在Main主线程启动,看看会有什么问题?这个问题留着后面再去说明
在配置文件中进行WebSocket的端口配置
上面的步骤进行完成了一个Netty服务器的初始化启动和端口监听,这时候会有疑问了,消息推送功能还没有实现。所以下面就是我们如何去实现消息处理逻辑
欢迎分享,转载请注明来源:夏雨云
评论列表(0条)