zk源码阅读33:Server与Client的网络IO(二):ServerCnxn子类NIOServerCnxn

zk源码阅读33:Server与Client的网络IO(二):ServerCnxn子类NIOServerCnxn,第1张

本节讲解ServerCnxn的NIO实现方式,也就是NIOServerCnxn(NettyServerCnxn就不讲了)
NIOServerCnxn继承了ServerCnxn抽象类,用NIO来处理与客户端之间的通信,单线程处理。

主要讲解

源码中,体现为两种

该类用来将给客户端的响应进行分块
避免response太大,没有写完而一直占用空间,因此对response分块

用于处理ServerCnxn中的定义的命令,如"ruok","stmk"其主要逻辑定义在commandRun方法中,在子类中各自实现,每个命令使用单独的线程进行处理。

它有很多个子类,这里只列举一个DumpCommand,感受一下即可,实现commandRun,完成对应 *** 作,写入PrintWriter就行。

对Socket通道进行相应设置,如设置TCP连接无延迟、获取客户端的IP地址并将此信息进行记录
最后设置SelectionKey感兴趣的 *** 作类型为READ,准备读取后续消息

核心函数 doIO

主要逻辑就是

在上面doIO中,调用到了如下函数

//读取前4个字节代表int,如果还没有初始化,并且int值是特定cmd对应的int,那么就当成是cmd,否则给incomingBuffer分配对应len的空间

验证int值是否对应特定的cmd,是的话写入对应回复到PrintWriter

读取payload请求,即非cmd的请求

读取连接请求,调用ZooKeeperServer相关逻辑

读取非连接的请求

doIO里面并没有直接利用发送相关的函数

发送"关闭"的buffer

同步发送

发送的核心函数

主要逻辑如下
如果不是"关闭"的ByteBuffer,如果能用NIO的方式就用NIO的方式,加入outgoingBuffers队列,否则就直接同步发送了

有很多,这里只用列举一个即可

//增加尚未处理的请求个数

实现接口方法process

里面调用了函数sendResponse,主要是进行一些序列化的 *** 作,然后把对应长度len写入,方便client读,完成一些数据统计,更新的 *** 作

里面调用的sendBuffer在上面已经讲过了

cleanupWriterSocket函数完成对应cmd处理器写完printWriter之后进行关闭

在前面阅读源码18中讲过,ClientCnxnSocketNIO#doIO,逻辑如下

因为先要完成server 接受 client的连接,那么这一步在哪完成,这里简要带过后面才讲的
NIOServerCnxnFactory

server对ACCEPT事件的注册,NIOServerCnxnFactory#configure
server接受client连接,在读取client的请求,如下
NIOServerCnxnFactory#run

是NIOServerCnxnFactory#run中根据SelectionKey的attachment获取NIOServerCnxn对象,读写就绪时调用的,对应源码NIOServerCnxnFactory#run,这里不展开

在上面介绍payload和非payload过程中,其实都是根据开头4个字节得到int,而判断是不是非payload就根据mapping规则来,比如如果int值是"conf"对应的1668247142,那么就认为是conf的cmd,而不会认为是有1668247142这么大的byteBuffer。感觉是一个bug。如果刚好长度这么大,就会被认为成conf的cmd了。

这个也没办法,这就是nio的实现

>IO通常分为几种,BIO(阻塞 Blocking IO)、NIO(非阻塞 Non-Blocking IO)、AIO(异步非阻塞)。

在JDK14出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要建立一堆线程等待请求,而客户端发送请求后,先询问服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求。BIO模型图如下:

优缺点很明显。这里主要说下缺点:主要瓶颈在线程上。每个连接都会建立一个线程。虽然线程消耗比进程小,但是一台机器实际上能建立的有效线程有限,以Java来说,15以后,一个线程大致消耗1M内存!且随着线程数量的增加,CPU切换线程上下文的消耗也随之增加,在高过某个阀值后,继续增加线程,性能不增反降!而同样因为一个连接就新建一个线程,所以编码模型很简单!

就性能瓶颈这一点,就确定了BIO并不适合进行高性能服务器的开发!像Tomcat这样的Web服务器,从7开始就从BIO改成了NIO,来提高服务器性能!

NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题:在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且 *** 作系统本身也对线程的总数有一定的限制。如果客户端过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。

NIO基于Reactor,当socket有流可读或可写入socket时, *** 作系统会相应的通知应用程序进行处理,应用再将流读取到缓冲区或写入 *** 作系统。 也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程。NIO模型图如下:

NIO的优缺点和BIO就完全相反了!性能高,不用一个连接就建一个线程,可以一个线程处理所有的连接!相应的,编码就复杂很多。还有一个问题,由于是非阻塞的,应用无法知道什么时候消息读完!

BIO和NIO的对比图:

AIO没有前两者普及,暂不讨论!

在java2以前,传统的socket IO中,需要为每个连接创建一个线程,当并发的连接数量非常巨大时,线程所占用的栈内存和CPU线程切换的开销将非常巨大。java5以后使用NIO,不再需要为每个线程创建单独的线程,可以用一个含有限数量线程的线程池,甚至一个线程来为任意数量的连接服务。由于线程数量小于连接数量,所以每个线程进行IO *** 作时就不能阻塞,如果阻塞的话,有些连接就得不到处理,NIO提供了这种非阻塞的能力。
NIO 设计背后的基石:反应器模式,用于事件多路分离和分派的体系结构模式。
反应器(Reactor):用于事件多路分离和分派的体系结构模式
通常的,对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞 与非阻塞 。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待 。
一种常用做法是:每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。
另一种较高效的做法是:服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读 *** 作;如果发现某个 Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写 *** 作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。
传统的阻塞式IO,每个连接必须要开一个线程来处理,并且没处理完线程不能退出。
非阻塞式IO,由于基于反应器模式,用于事件多路分离和分派的体系结构模式,所以可以利用线程池来处理。事件来了就处理,处理完了就把线程归还。而传统阻塞方式不能使用线程池来处理,假设当前有10000个连接,非阻塞方式可能用1000个线程的线程池就搞定了,而传统阻塞方式就需要开10000个来处理。如果连接数较多将会出现资源不足的情况。非阻塞的核心优势就在这里。
为什么会这样,下面就对他们做进一步细致具体的分析:
首先,我们来分析传统阻塞式IO的瓶颈在哪里。在连接数不多的情况下,传统IO编写容易方便使用。但是随着连接数的增多,问题传统IO就不行了。因为前面说过,传统IO处理每个连接都要消耗一个线程,而程序的效率当线程数不多时是随着线程数的增加而增加,但是到一定的数量之后,是随着线程数的增加而减少。这里我们得出结论,传统阻塞式IO的瓶颈在于不能处理过多的连接。
然后,非阻塞式IO的出现的目的就是为了解决这个瓶颈。而非阻塞式IO是怎么实现的呢?非阻塞IO处理连接的线程数和连接数没有联系,也就是说处理 10000个连接非阻塞IO不需要10000个线程,你可以用1000个也可以用2000个线程来处理。因为非阻塞IO处理连接是异步的。当某个链接发送请求到服务器,服务器把这个连接请求当作一个请求"事件",并把这个"事件"分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还。这样一个线程就可以异步的处理多个事件。而阻塞式IO的线程的大部分时间都浪费在等待请求上了。
所谓阻塞式IO流,就是指在从数据流当中读写数据的的时候,阻塞当前线程,直到IO流可以
重新使用为止,你也可以使用流的avaliableBytes()函数看看当前流当中有多少字节可以读取,这样
就不会再阻塞了。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/13384055.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-07-25
下一篇 2023-07-25

发表评论

登录后才能评论

评论列表(0条)

保存