二Netty--NIO入门
二Netty--NIO入门
第二章 NIO入门
网络编程基本模型:
server和client模型,两个进程相互通信。server端提供位置(ip+port),client端通过connect向服务端监听的地址(ip+port)发起连接请求。通过三次握手,建立连接,然后通过socket进行通信。
2.1 传统BIO编程
传统同步阻塞IO开发模型:
serverSocket绑定IP、启动监听端口port;socket发起连接操作。连接成功后,双方通过inputStream和outputStream进行同步阻塞通信。
2.1.1 BIO通信模型图
server端:一个独立的acceptor线程,监听client端的连接,它收到client端连接请求后,为每个client创建一个新的线程进行链路处理,处理完毕,通过outputStream返回给client,然后线程销毁。
缺点:缺乏弹性伸缩能力。服务端线程数和客户端并发访问数1:1,系统性能急剧下降,直至对外停止服务。
2.1.2 同步阻塞IO涉及的流程
BIO的输入/输出流进行 同步阻塞通信:
(BIO的代码,可以在其中找到和上面BIO通信模型对应的部分)
//client端,创建socket连接IP和PORT
Socket socket = new Socket("192.111", 80);
//server端,绑定监听端口port
ServerSocket ss = new ServerSocket(80);
Socket accSocket = ss.accept();
// server端接收连接请求
/**读入流*/
BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
String body = in.readLine();
/** 输出流*/
PrintWriter out= new PrintWriter(this.accSocket.getOutputStream(), true);
out.println("search");
BIO核心的组件及流程如上。
注意:
BIO的阻塞,是由于InputStream.read和OutputStream.write(输入、输出流的读取和写入)是同步阻塞的。
2.1.3 同步阻塞IO源码
2.1.3.1 Server端
public class TimerServer {
public static void main(String[] args) throws IOException {
int port=8080;
ServerSocket server = null;
try {
/** serverSocket绑定端口port*/
server = new ServerSocket(port);
Socket socket = null;
/** server端启动单独accptor线程,while循环,处理连接请求*/
while (true) {
socket = server.accept();
/**处理的client连接建立后,为client创建新线程*/
new Thread(new TimerServerHandler(socket)).start();
}
}finally {
/** finally,关闭server*/
if (server!=nullX)
server.close();
server = null;
}
}
}
11行,应用程序进程用while(true)来循环监听客户端连接,没有client接入,主线程阻塞在ServerSocket的accept上。
TimeServerHandler处理新建立socket链路的runnable的线程。
public class TimeServerHandler implements Runnable {
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
private Socket socket;
@Override
public void run() {
// 读取InputStream的reader
BufferedReader in = null;
// 输出响应的writer
PrintWriter out = null;
try {
// 获取socket对应的reader
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream(), String.valueOf(true)));
// 创建输出writer
out = new PrintWriter(this.socket.getOutputStream(), Boolean.parseBoolean(String.valueOf(true)));
// 输出内容body
String body = null;
while (true) {
body = in.readLine();
// 跳出循环条件——in读到输入流尾部时,返回值为null
if (body==null) break;
/** 对读到的body内容,进行server端业务处理*/
//// TODO: 2022/9/16 业务逻辑代码处理
out.println("输出业务处理结果");
}
} catch (IOException e) {
//程序结束,关闭和清理资源
//释放输入、输出、socket的句柄资源,才能够让线程最后自动销毁并被虚拟机回收(需要对各个资源=null)
in.close();
out.close();
socket.close();
in,out,socket=null;
}
}
}
2.1.3.2 Client端
public class TimeClient {
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
//client端socket指定要访问的IP和port,进行通信连接
socket = new Socket("127.0.0.1", 80);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//client发送信息、指令
out.println("指令");
//client等待server的回复
/** in.readLine为同步阻塞,直至有信息到来*/
String resp = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}finally {
/** 释放资源以及句柄*/
in.close();
out.close();
socket.close();
in,out,socket.close();
}
}
总结:(一连接一线程模型)
BIO问题在于,每一个client请求接入,server需要创建一个thread处理接入的client。BIO的通信模型,无法满足高性能的服务器,需要面对高性能、高并发接入的场景。
2.2 伪异步IO编程
改造BIO,将一个acceptor线程创建1:1的连接线程改进,通过acceptor把连接请求包装成Task,投递给后端的线程池ThreadPool或者消息队列MQ,通过N个线程处理M个client连接请求的模型。由于底层通信仍然是同步阻塞的IO(因为仍旧使用BIO的inputStream、outputStream的阻塞模型),所以被称为伪异步IO。
2.2.1 伪异步IO通信模型图
客户端M:线程池最大线程N,其中M可以远大于N。
伪异步IO模型原理:
当有新的client接入请求,server端把socket包装成Task(该任务实现Runnable接口),投递到后端ThreadPool中进行处理。线程池中有消息队列和N个线程,来处理传入消息队列的请求。
ThreadPool可以设置消息队列和活跃线程数量,因此可以控制占用的资源。
2.2.2 伪异步IO源码
仍旧以TimeServer为例,进行改造。只有server端需要改造
需要改造Server服务端、再增加后端的线程池
public class TimeServerFIO {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(80);
Socket socket = null;
//创建后端的任务线程池
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50, 10000);
//单独的acceptor线程,处理client连接,包装成Task,投递给后端的线程池
while (true) {
socket = serverSocket.accept();
//包装为task,投递给后端线程池
singleExecutor.execute(new TimeServerHandler(socket));
}
}
finally {
serverSocket.close();
serverSocket = null;
}
}
}
封装的线程池ExecutePool,如下:
public class TimeServerHandlerExecutePool {
private ExecutorService executor;
public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
//实现pool的execute方法
public void execute(java.lang.Runnable task) {
executor.execute(task);
}
}
由于线程池和消息队列有界,因此,无论client并发连接多大,都不会导致线程个数过于膨胀或者内存溢出。相比BIO是一种改进。
但是,由于伪IO底层通信依然是同步阻塞模型,因此无法从根本解决BIO的问题。
总结
对于BIO的InputStream.read和OutputStream.write方法都是同步阻塞的,阻塞时间取决于对方IO线程处理速度和网络IO传输速度。因此,无论BIO,还是线程池改进的伪IO,底层通信都使用输入和输出流,因此都无法解决同步IO导致的通信线程阻塞问题。层层的IO同步阻塞,会导致系统级联故障,使系统崩溃。
2.3 NIO编程
NIO称为非阻塞IO。
NIO提供了Channel通道,包括SocketChannel(对应Socket)和ServerSocketChannel(对应ServerSocket)。这两种新增的通道,支持BIO和NIO两种通信模式。
使用:
低负载、低并发,使用阻塞模式,可以降低代码复杂度;高负载、高并发,使用NIO的非阻塞模式,性能和可靠性更高。
2.3.1 NIO类库介绍
NIO引入新的功能和组件:
首先,提供高速、面向块(缓冲区buffer为数据,构成成块的结构)的IO功能;其次,定义了包含数据的类,通过以块的形式(块,就是缓冲区构成的块结构),处理数据,且NIO不用本机代码就可以低级优化。
1 缓冲区Buffer
buffer作为对象,包含要写入和读出的数据。
区别BIO与NIO中:
BIO:数据直接write或者read到Stream对象;
NIO:所有数据用buffer处理,数据分别read或write到buffer中,然后任何时刻访问NIO的数据,都是通过buffer操作。
Buffer本质:数组,通常是字节数组ByteBuffer,且buffer提供了对数据块的结构化访问,以及维护读写位置limit等信息。
每个buffer类,不只是存储数据的数组,还提供了一系列的方法,对数组访问。
2 通道channel
网络数据通过channel读取和写入数据。channel是双全工的,可以双向传输,可以更好的映射底层操作系统API。
区别channel和stream:
stream只是在一个方向上移动(只能是inputStream或者outputStream的子类);channel是双向的,且读和写可以同时进行。
主要的类如下:
3 多路复用器Selector
多路复用器能够选择已经就绪的任务(channel),是NIO多路复用模型的核心。且JDK使用epoll,没有连接数限制,一个selector可以负责上万client的请求。
原理:
selector轮询注册在其上的channel,如果某些channel发生write或者read事件,这个channel会处于就绪状态(调用channel的回调函数,通知selector当前channel就绪),会被selector轮询出来,然后通过SelectionKey可以获取就绪的channel集合,再进行后续的IO操作。
区别BIO:
BIO用while(true)内的serverSocket.accept,来循环的逐个获取就绪的连接请求。while以及accept的阻塞,以及新开启的处理线程,都是系统性能的瓶颈。
2.3.2 NIO服务端序列图
//step1 打开ServerSockerChannel,监听client的连接,是所有client连接的父管道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//step2 绑定监听端口,设置连接非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(getByName("IP")), 80);
serverSocketChannel.configureBlocking(false);
//step3 创建多路复用器selector并打开;创建Reactor线程并启动
//reactor线程启动,会调用selector来监听和轮询事件(可知,NIO中,reactor也是单独开启的线程,负责通过selector轮询就绪状态)
Selector selector = Selector.open();
new Thread(new ReactorTask()).start();
//step4 serverSocketChannel注册到Reactor线程的selector上,并设置监听ACCEPT事件
SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, ioHandler);
//step5 多路复用器selector,在reactor线程中,在while(it.hasNext)的无限循环体内,轮询就绪的Key
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
//处理IO中就绪的channel对应的key
}
//step6 selector监听到有client的连接请求,此时调用serverSocketChannel建立链路连接
SocketChannel channel = serverSocketChannel.accept();
//step7 设置client端链路为非阻塞模式
channel.configureBlocking(false);
channel.socket().setReuseAddress(true);
//step8 新接入client端channel注册到reactor线程的selector上,监听读操作,用来read客户端发送的信息
//理解:server端,socketChannel在selector上注册read监听,就是server端用来读取client发来的信息
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, ioHandler);
//step9 异步读取客户端channel请求消息,到缓冲区receivedBuffer中
ByteBuffer receivedBuffer = ByteBuffer.allocate(1024);
int readNumber = channel.read(receivedBuffer);
//step10 对读取到缓冲区buffer消息进行解码decode,将解码成功的消息,封装成Task
//投递到业务线程池中,进行业务的逻辑处理
Object message = null;
while (receivedBuffer.hasRemaining()) {
Object message = decode(receivedBuffer);
messageList.add(message);
}
for (Object messageE : messageList)
handleTask(messageE);
//step11 将处理过的结果POJO对象,encode成ByteBuffer,调用socketChannel.write的异步写入方法,将消息异步发送给client
channel.write(buffer);
注意:
如果轮询到的channel为write,则说明数据没有发送完成,需要继续发送。
2.3.3 NIO客户端序列图
//step1 打开SocketChannel,会绑定client的本地地址
SocketChannel clientChannel = SocketChannel.open();
//step2 设置SocketChannel为非阻塞模式,同时设置socketChannel.socket()的客户端连接的TCP参数
clientChannel.configureBlocking(false);
clientChannel.socket().setReuseAddress(true);
clientChannel.socket().setReceiveBufferSize(1024);
clientChannel.socket().setSendBufferSize(1024);
//step3 异步连接服务器端
boolean connected = clientChannel.connect(new InetSocketAddress("111", 80));
//step4 判断client是否连接成功,如果true,则直接注册读READ状态到多路复用器
if (connected) {
clientChannel.register(selector, SelectionKey.OP_READ, ioHandler);
} else {
//step5 如果没有连接成功,向Reactor线程的selector注册CONNECT状态,监听server端的ACK连接响应
clientChannel.register(selector,SelectionKey.OP_CONNECT,ioHandler);
}
//step6 创建多路复用器selector,并创建reactor线程,并启动
Selector selector = Selector.open();
new Thread(new ReactorTask()).start();
//step7 selector在线程run方法的无限循环内,轮询就绪的key
/** 这里的selector是client端的,它轮询的是是否有连接自身channel的CONNECT事件*/
int num = selector.select();
Set selectorKeys = selector.selectedKeys();
Iterator it = selectorKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
//然后对轮询出的就绪状态的channel,通过key,进行IO处理
//// TODO: 2022/9/17
//step8 接收connect事件,并进行处理
if (key.isConnectable())
handlerConnect();
}
//step9 判断连接结果,如果连接成功,把当前socketChannel注册到selector上,监听READ事件
/** 这里的监听READ事件,是监听server端返回的响应信息*/
if (clientChannel.finishConnect())
registerRead();
//step10 注册读事件到selector(step10是step9的方法内容)
clientChannel.register(selector,SelectionKey.OP_READ,ioHandler);
//step11 异步读read客户端client请求消息到缓冲区buffer(这是读取准备发送的消息,先存入缓冲区)
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readNum=clientChannel.read(readBuffer);
//step12 对readBuffer进行编解码,读取消息,然后封装message到Task,投递到后端业务处理线程池中
Object message = null;
while (receivedBuffer.hasRemaining()) {
Object message = decode(receivedBuffer);
messageList.add(message);
}
for (Object messageE : messageList)
handleTask(messageE);
//step13 将POJO对象encode成byteBuffer,调用socketChannel的异步writer方法,消息异步发送给client端
clientChannel.write(sendbuffer);
2.5 4种IO对比
2.5.1 概念梳理
1 异步非阻塞IO
NIO,是多路复用器模型(主要由selector实现),其只是IO复用模型实现的非阻塞IO,并不是异步IO。java1.5虽然底层使用epoll代替poll,提升了性能,但是没有改变底层模型,仍是多路复用模型;
AIO,才是真正的异步非阻塞IO模型。JDK1.7的NIO2.0提供了异步的socketChannel,是真正的异步IO。在异步IO操作时候,会传递信号变量,当操作完成后会操作对应的回调方法。