美文网首页
nio和netty(上)

nio和netty(上)

作者: 艾尔之子 | 来源:发表于2018-03-15 18:50 被阅读0次

    客户端和服务器的通信有bio和nio之分,bio一般被描述为同步阻塞io。其实现一般如下:服务端会监听一个端口,从而等待客户端发起请求连接服务端,此时服务器会阻塞在那里,以直等到有请求进来才会继续执行下面流程。而这个acceptor会处于while循环内一直等待接收客户端的请求,每当客户端有请求进来,服务端就分配其一个线程去处理客户端socket,这样一来一个客户端请求对应一个服务端线程,当客户端并发非常大时服务端线程数也非常大,很容易造成线程堆oom,内存撑爆。所以这种模型只是初识io时使用的io通信的方式,在真实环境中不可能用此方式来实现io。

    基于同步io中的线程会不断增加,导致内存溢出,一种线程池模型的同步io就理所应当地出现了,其不同于同步io的地方就是在服务端accept客户端socket后不是立即创建新线程,而是把这个socket封装成task丢入线程池,用jdk线程池去execute新的任务,这样就引入了消息队列即阻塞队列的概念。这种方式能解决线程无限扩容的问题,但他的io实现还是同步的,就避免不了阻塞的问题,先来看下jdk的两个阻塞read和write方法的api说明。read方法会阻塞读,直到有可用数据进行读取,或者数据已读完,或者发生空指针或io异常才会退出这个方法,那就意味着如果read方法的线程的io能力差,或者网络差,那么整个读方法就会阻塞很久。设想一下客户端并发写入服务器数据,而如果服务端线程池内所有正在执行read方法的线程都阻塞了将近一分钟,那么消息队列中的其他任务就不得不等待这么长时间,其他客户端的写入请求将得不到及时响应,直至服务端阻塞读取完成才能最终得到read完成的反馈,那么这种网络通信系统的性能实在时令人堪忧的。write方法api也是如此,如果服务端写出数据到客户端,那么它将会被阻塞,知道所有要发送的字节全部写入完毕,或者发生异常,如果服务端写出数据的速度受网络影响比较严重,同样会产生超时严重的问题。所以这种俗称伪异步的io模型同样是阻塞的,在一端网络延迟的情况下,很容易产生众多io线程被阻塞,影响整个服务器和客户端的并发通信能力。

    同步阻塞io的好处是使用简单,所以在并发量不高的简单场景下,使用同步阻塞io来处理网络通信也是足够的。但作为一个支持大量请求的服务器开发者来说,了解并使用nio是很有必要的。nio顾名思义就是非阻塞io,也可以叫做new io,这种io会有个读取和写入块的概念,我想主要是通过其缓冲区来实现的。nio有三个基本组件,buffer,channel和selector,buffer是处于消息发送两端的缓冲组件,有bytebuffer,charbuffer,longbuffer等,本质都是通过把传输的字符转化为byte来实现的,所以最根本的是bytebuffer,buffer中有很多操作,比如position,mark,limit,capacity,用来控制buffer的指针,方便对buffer进行读写。channel是进行数据传输的通道,他是全双工通信的,即可以同时在一个channel上进行读写,而流因为必须是InputStream或OutputStream的子类,所以流只能进行单向传输。Selector是一个多路复用器,多个channel的互动是通过selector来协调的,一般一个selector可以持有多个channel信息的引用,即不同的channel可以注册到同一个selector上,注册时会同时注册selector所监听的channel事件,之后selector可以在一个while循环里不断执行select方法,来获得注册在其上的channel事件,这种事件抽象到了SelectionKey上。一个典型的服务器场景如下:selector和serverChannel开启服务,serverChannel监听了某个端口,等待客户端接入,并把channel模式设为非阻塞,则之后的读写操作就都是非阻塞的了,然后把serverChannel的accept事件注册到selector上。接下来在while循环里selector就可以遍历其内部的SelectionKey,当有客户端请求时,select方法就会返回1,代表此时selector捕捉到一个事件,之后匹配事件时就会知道时accept事件,接下来serverChannel执行accept方法的到socketChannel,客户端channel就会注册读事件到selector上,之后有客户端写入数据时,selector就会意识到channel的读事件发生,进行数据读取。此时的read方法是非阻塞的,他有三种返回情况,当返回值大于0,都到了字节,对字节进行编码等后续操作;等于0,没有读到字节,属于正常场景,忽略(这里就是非阻塞的表现,并不会阻塞在此一直等待有数据读取);等于-1,表示链路已关闭,需要关闭客户端channel释放资源。写操作也类似。

    所以我们可以发现accept,connect,select,read,write等方法都没有发生阻塞,要执行accept必须在selector捕捉到了serverChannel这个事件后才会发生,所以是立即产生效果;客户端发起的connect方法是异步的,如果没有立即连接成功服务器,则客户端channel会在selector上注册自己的connect事件,等到服务器确认和客户端连接时,selector监测到此事件,就可以进行连接;select方法也有有参和无参两种方式,无参则在没有事件后立即返回0,有参则是在参数(timeout)时间范围内没有事件,则返回0,都不会发生很长事件的阻塞,在while循环内返回后会立即执行下一个select;read和write也是在selector捕捉到了客户端channel的这些事件时才进行真实操作,操作本身也不会发生阻塞。所以nio的这一机制的关键就是selector,主线程会分配一个线程去专门处理selector的注册和轮询 ,这个线程俗称reactor线程,这种模式也被称为reactor模式,即有个专门线程去接收客户端的连接,读写等各种事件,也只是去接收这种事件,而把真正的事件处理工作分配给另一个线程去做,这样一来如果具体的读写业务逻辑很复杂很费事,也就不会影响reactor线程的吞吐量,这对于内存执行密集型的系统来说是很有必要的。那么bio和现在的nio有什么本质区别吗?注意到如果一个一步读写线程在处理复杂业务或者因为网络原因执行了很长时间,并不会影响其他客户端请求的进入,因为读写线程是异步的,而主要的reactor线程还是在不断的做selector轮询,当有新的请求发生,还是照样可以捕捉,并把这些客户端的读写交给新的子线程处理,当然了这些子线程的分配肯定也有相应线程池技术来保证线程不会撑爆内存。

    关于ByteBuffer的作用再多说一句,通过channel.read(byteBuffer),channel把客户端数据通过channel读入buffer,之后再把buffer内容get至byte数组中,进行输出或操作;write操作也相似,先把数据写入缓冲区,然后把缓冲区内容写入channel。

    AIO则是异步非阻塞IO,这里的异步是真正的异步,正因为有了异步,AIO去除了selector的协调。AIO中最关键的是CompletionHandler接口,其中有两个方法completed和failed,nio内部在方法执行完时会自动执行completed方法进行回调。在异步服务器Channel的accept方法里会传入当前server作为attachment和AcceptCompletionHandler,在completed方法内会继续调用当前server的accept方法去接收其他客户端的连接。如果failed,则调用当前server的latch countdown,使得外层latch的await通过,serverhandler结束。write方法也相似,在completed方法内继续看传入的attachment还有没有剩余字节,如果有就继续调用channel的write方法继续走异步写方法,传进来的attachment其实是一个byteBuffer,也是通过先写入缓存,然后把缓存内容写入channel的方式进行写,只是多了个异步不断写的过程。

    相关文章

      网友评论

          本文标题:nio和netty(上)

          本文链接:https://www.haomeiwen.com/subject/bozhqftx.html