1. 涉及的网络基础知识
Socket:
操作系统提供的api,介于应用层和tcp/ip层之间的软件层,封装服务器客户端之间网络通信相关内容,方便调用
IO多路复用:
(I/O Multiplexing)是一种IO操作模式,它允许单个进程或线程同时处理多个输入输出(IO)操作。
怎么算一个连接:
tcp层中,客户端ip、客户端端口、服务器ip、服务器端口,这4个算一个连接。
通信接收流程:
3步:接收连接、读取数据、写入数据
服务器端接收连接、服务器端读取客户端发来的数据、服务器端写入客户端发来的数据并进行业务处理。
应用程序java在处理通信的两个前提:
1、客户端a与服务器b通信,a的数据到达b的操作系统的socket的缓冲区中。这时候java要如何接收并处理这些数据呢?java会调用操作系统提供的socket api进行操作。
2、如果很多客户端与服务器b通信,并发量上来了,java又如何管理和处理这么多的连接呢?java有BIO(Blocking I/O,阻塞IO)和NIO(no-blocking I/O,非阻塞)两种
2. Java NIO和BIO的区别
- 阻塞与非阻塞:
- BIO是同步阻塞的。当线程发起I/O请求时,如果没有数据可读或可写,线程会一直阻塞,直到数据准备就绪。
- NIO是同步非阻塞的。线程发起I/O请求后,如果没有数据可读或可写,线程不会阻塞,而是可以去做其他事情。当数据准备就绪时,线程会被通知去处理数据。
- I/O模型:
- BIO是基于流的。数据被顺序地从一个流读取到另一个流,期间没有缓存,且不能前后移动流中的数据。
- NIO是基于缓冲区的。数据被读取到一个缓冲区中,需要时可以在缓冲区中前后移动,增加了处理过程中的灵活性。
- 线程管理:
- BIO中,每个连接都需要一个独立的线程来处理。这种方式在连接数较少时可行,但在连接数较多时会导致线程数量过多,系统资源消耗大。
- NIO中,一个线程可以处理多个连接。这是通过Selector和Channel实现的,Selector可以监听多个Channel上的事件,从而允许单个线程管理多个连接。这就是IO多路复用。
- 适用场景:
- BIO适用于连接数较少且固定的场景。
- NIO适用于连接数多且连接时间较短的场景,如聊天服务器、弹幕系统等。它能够有效减少线程数量,降低系统资源消耗,提高并发处理能力。
3、BIO和NIO详解
如果服务器b上同时有多个连接,下面我称为连接1号、连接2号等。
-
基础版本:java原生bio
流程:读取连接1号socket中的数据,如果数据没有准备好,或者遇到一些其他问题,就会阻塞,一直等到问题解决好。连接1号处理好后,开始处理连接2号,获取连接2号数据与业务处理。
缺点:阻塞,并发量小
-
改进版:伪异步IO
流程:acceptor+处理器。acceptor用来处理连接,处理器用来接收和处理数据。连接1号来了,acceptor就开一个新线程交给一个处理器a去获取连接1号socket数据并业务处理数据。接着acceptor给连接2号开个新线程,交给处理器b去接收并业务处理连接2号。这样连接1号和2号就同时处理了。
优点:并发量大,因为开了线程
缺点:如果并发量多,线程多,消耗多,可能内存爆掉,这点可以通过线程池来控制线程量。
但是因为处理器本身还是阻塞的,如果连接1号的数据没准备好,读取不了数据,这个时候处理器a会等待,也就是cpu在等待,cpu时间其实就浪费了,所以如果并发量很大,性能还是不高
-
改进版:非阻塞IO
流程:如果连接1号的处理器的数据还没准备好,读取不了,这个时候cpu不等待,cpu可以先去处理其他的连接,这个就是不阻塞啦,cpu处理好其他连接,回来处理连接1号时,此时连接1号的数据准备好了,cpu就能顺利的接收连接1号的数据并进行业务处理。
缺点:这个是让cpu在所有处理器(连接1号、连接2号...)中做个轮询,但可能这次轮询到1号,发现1号还是没有准备好,所以还有没有更好的办法呢?
-
改进版:原生jdk网络编程——NIO
1、Reactor反应器模式
就是一种倒转思维,由处理器主动发布信号
接着上边的讲。如果cpu轮询,发现1号还是没有准备好,那不是也浪费了cpu资源?
所有最好是1号准备好了,来叫我,这时候我cpu去处理就比较顺利。每个连接准备好了,给个信号,我cpu只去处理有这个信号的连接,这样就更快一点。这就是反应器模式!
2、三大组件:selector、channel、buffer
selector:
相当于cpu、acceptor,主要起到记录和调度作用。记录事件与channel的关系,从而帮助分配channel
channel:
相当于处理器。主要有两个
ServerSocketChannel:相当于acceptor,是一个可以监听TCP连接请求的通道。当服务器程序创建一个ServerSocketChannel
并绑定到一个特定的端口后,它就开始监听该端口上的连接请求。需ServerSocketChannel
本身并不直接“接收”连接,而是触发了一个“连接到达”的事件,即给个“有连接啦”的信号,告知一下
SocketChannel:相当于处理器。一个socketChannel处理一个连接,一个连接可能有多个socketChannel。主要是接收这个连接的数据,并调用业务代码处理数据。
buffer:
用于存储数据,channel从buffer中读写数据
3、服务器端流程:
接收连接
ServerSocketChannel去selector注册,关注OP_ACCEPT接收事件。ServerSocketChannel绑定8808端口,会监听tcp层8808端口,如果发现有连接到达,ServerSocketChannel会给信号,就是触发一个OP_ACCEPT接收事件。
selector一直在轮询,查看注册下的channel中是否有事件触发,有的话就回去执行channel。这时候发现ServerSocketChannel触发注册时关注的OP_ACCEPT事件了,selector就调用ServerSocketChannel的accept()方法开始接收连接,ServerSocketChannel就会创建并返回一个SocketChannel,然后把这个SocketChannel注册到selector上,并写上关注OP_READ事件,然后ServerSocketChannel就接着去监听还有没有新的连接到来了。接收并处理这个连接的内容是由这个SocketChannel去做的。
处理连接数据
连接socket的数据准备好了,操作系统会更新数据状态,channel就会知道(因为channel的底层是socket),然后就会给出信号,触发一个OP_READ可读事件。
selector一直在轮询,查看注册下的channel中是否有事件触发,发现这个SocketChannel注册时关注的OP_READ事件触发了,就会调用SocketChannel的read()方法,执行相关操作。这个channel可以设置为非阻塞,非阻塞的话就是,如果出现问题,直接返回一个值。也可以设置为阻塞,阻塞的话,就是出现问题,一直等待。
4、理解
所以这里的cpu是selector,selector一直在轮询那些事件触发了(也就是准备好了的,大概率不会阻塞的)的channel,然后去执行channel,这样cpu就不会闲置,通过selector就能管理多个channel,即管理多个连接。而channel呢,就不是被动接受,而是主动告知selector,channel自己监控自己,当准备好了,就触发下事件,主动给告知selector。
优点:最大限度压榨cpu性能,让cpu不闲着