一.预备知识
1.1 Linux网络IO模型
- fd:文件描述符
- socketfd:socket描述符
- 描述符是一个数字,指向内核的一个结构体
1.2 Java的5种IO模型
- 阻塞IO
- 非阻塞IO
- IO复用模型:把多个IO的阻塞复用到同一个select阻塞上,不必新开线程处理;select、poll顺序扫描fd是否就绪;epoll基于事件驱动;
- 信号驱动模型:内核通知何时开始IO操作(内核缓存写入数据就通知)
- 异步IO:内核通知IO操作何时完成(用户缓存写入数据才通知)
1.3 epoll优势
- 单个进程可以打开的socketFD无限制(与内存相关)
- 事件驱动而不是线性扫描,效率不会随FD数量增大而降低
- 内核与用户空间消息传递使用mmap同一块内存
二.BIO
2.1 server
import java.net.ServerSocket;
import java.net.Socket;
//连接
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
//接收后进行回复
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String body = in.readLine();
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("server response...");
2.2 client
//连接
Socket socket = new Socket("ip", 8080);
//发送后等待回复
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("client request...");
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String body = in.readLine();
三.NIO
3.1 基本概念
- 缓冲区
BIO读取写入数据都是直接操作stream对象,效率低,NIO使用缓冲区解决。缓冲区本质是一个数组,提供对数据的结构化访问以及维护读写位置等。
- 通道
Channel主要分为用于网络读写的SelectableChannel和用于文件操作的FileChannel,SelectableChannel主要包括SocketChannel和ServerSocketChannel
- 多路复用器
Selector不断轮询注册在其上的Channel,若某个Channel上有新的TCP接入、读、写则说明这个通道以及就绪,将被Selector轮询选择到SelectionKey.
3.2 server
- 创建父管道
- 创建Reactor线程并启动,selector开始轮询
- 将父管道注册到selector,监听ACCEPT事件
- 有ACCEPT事件触发,建立新的客户端channel并注册到selector,监听读事件
- 有READ事件触发,处理相应的客户端channel
//1.服务端创建父管道,处理所有客户端连接
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress("ip",8080));
serverSocketChannel.configureBlocking(false);
//2.创建Reactor线程
Selector selector = Selector.open();
//创建一个selector线程并启动,轮询就绪的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isValid()){
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//有新客户端请求接入时的情况,详见4
}
if(key.isReadable()){
SocketChannel sc = (SocketChannel) key.channel();
//有新客户端请求进来的情况,详见5
}
}
}
//3.将父管道注册到选择器,监听客户端的accept事件
SelectionKey selectionKey = ServerSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//4.选择器监听到ACCEPT事件后(即有新客户端请求进来),为每个请求生成一个channel(建立TCP连接)并注册read事件到选择器
SocketChannel clientChannel = serverSocketChannel.accept();//serverSocketChannel即为2中的ssc
clientChannel.configureBlocking(false);
SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ);
//5.选择器监听到read事件后(即客户端请求进来了),异步读取请求信息到缓冲区、解码,最后放到工作线程池处理
ByteBuffer dst = ByteBuffer.allocate(1024);
clientChannel.read(dst);//clientChannel即为2中的sc
dst.flip();
byte[] bytes = new byte[dst.remaining()];
dst.get(bytes);
String clientInfo = new String(bytes, "utf-8");
//6.服务端处理完毕后,把返回值写入到clientChannel返回给客户端
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
String serverResponse = "xxx";
byte[] serverBytes = serverResponse.getBytes();
writeBuffer.put(serverBytes);
writeBuffer.flip();
clientChannel.write(dst);
3.3 client
//1.创建客户端通道
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
//2.建立Reactor线程,首先判断连接是否建立详见3,再轮询激活事件详见4
Selector selector = Selector.open();
//3.判断客户端是否与服务端建立了连接,连接成功则注册read事件,否则注册connect事件
if(clientChannel.connect(new InetSocketAddress("ip",8080))){
clientChannel.register(selector, SelectionKey.OP_READ);
//TODO 处理写操作,即发送请求到服务端
}
else {
clientChannel.register(selector, SelectionKey.OP_CONNECT);
}
//4.轮询所有就绪事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isValid()){
SocketChannel sc = (SocketChannel) key.channel();
//客户端的连接请求,如果已建立连接则注册read事件准备接受服务端回复,并发送相应请求
if(key.isConnectable()){
if(sc.finishConnect()){
sc.register(selector, SelectionKey.OP_READ);
//TODO 处理写操作,即发送请求到服务端
}
else {
System.exit(1);//服务端返回ACK但是连接失败,异常
}
}
//客户端接受到服务端回复后的处理
if(key.isReadable()){
//TODO 处理读操作,即接受服务端的回复并相应处理
}
}
}
四.AIO
真正的异步非阻塞IO,对应Unix网络编程的事件驱动IO,无需selector轮询即可实现异步读写。
server
- AsynchronousServerSocketChannel创建父管道
- 父管道通过accept方法,异步接受客户端连接
- accept方法入参CompletionHandler<AsynchronousSocketChannel,? super A> handler用于回调
- CompletionHandler接口包括completed和failed两个接口
- completed方法里,首先继续调用accept方法,即一个客户端连接成功之后接着异步接受其它客户端,然后再异步调用read方法
- 异步调用read方法回调成功后,再异步调用write方法
client
- AsynchronousSocketChannel创建客户端管道
- 异步调用管道的connect方法,建立连接
- connect方法异步执行成功,回调方法执行write方法异步发送请求
- write方法异步执行成功,回调方法执行异步read异步读取回包