有了网络IO知识之后,开始看java层面是如何来对多路复用器进行封装.
单线程版多路复用
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.sql.ClientInfoStatus;
import java.util.Iterator;
import java.util.Set;
/**
* Created by 祝程 on 12/25/21.
*/
public class MultiSingleThread {
private ServerSocketChannel server;
/**
* 一个多路复用器的抽象, 底层可能是 select poll epoll
*/
private Selector selector;
private void initServer() {
try {
// 对应的系统调用是 socket = fd6
server = ServerSocketChannel.open();
server.configureBlocking(false);
// 对应的系统调用是 bind(fd6,9090) && listen(fd6)
server.bind(new InetSocketAddress(9090));
/**
* 如果是
* select poll 则是在jvm层面开辟一个空间
* epoll 则 调用系统调用的 epoll_create=fd4 开辟内核空间
*/
selector = Selector.open();
/**
* 如果是 select poll 则 把 这个 文件描述符放入jvm空间
* 如果是 epoll 则调用 epoll_ctl(fd4,fd6,read)
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
}
}
public void start() {
initServer();
System.out.println("server start");
try {
while (true) {
Set<SelectionKey> keys = selector.keys();
System.out.println("keysize....." + keys.size());
/**
* 如果是 select poll 就是把 jvm里面的文件描述符传入到内核
* 内核做遍历之后返回一共有几个可以读写 select(fds) poll(fds)
* 如果是 epoll 则是调用 epoll_wait 内核把链表空间的文件描述符返回
*/
while (selector.select(500) > 0) {
// 返回了有状态的 fd集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
/**
* 有数据的文件描述符可能是服务端的fd 那么就需要处理新的客户端链接
* 也有可能是客户端链接产生的数据,那么就需要进行业务处理
*/
if (key.isReadable()) {
handlerRead(key);
} else if (key.isAcceptable()) {
handlerAccept(key);
}
}
}
}
} catch (Exception e) {
}
}
/**
* 处理服务端收到的客户端链接请求
*
* @param key
* @throws Exception
*/
private void handlerAccept(SelectionKey key) throws Exception {
// 收到新的客户端链接,需要把这个客户端注册到多路复用器中
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(8096);
client.register(selector, SelectionKey.OP_READ, buf);
System.out.println("新客户端链接" + client.getRemoteAddress());
}
/**
* 处理客户端发送来的数据
*
* @param key
* @throws Exception
*/
private void handlerRead(SelectionKey key) throws Exception {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
byteBuffer.clear();
while (true) {
int read = client.read(byteBuffer);
if (read > 0) {
// 客户端发什么就往回写什么,这是属于具体业务范畴
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
client.write(byteBuffer);
}
byteBuffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
}
public static void main(String[] args) {
new MultiSingleThread().start();
}
}
额外补充, 客户端断开,但是服务端没有close client会怎么样
客户端启动
首先以 poll 的方式启动服务器
strace -ff -o poll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider MultiSingleThread
查看服务端的文件描述符 4号对应的是服务端的serversocket的文件描述符.
查看网络状态,也可以看到9090正在被监听
推论当前系统调用
- socket() = 4
- bind(4,9090)
- listen(4)
- 不断的 循环 poll(4)
客户端链接
看到三次握手
网络状态看到服务端和客户端建立连接
查看服务端的文件描述符,发现多了一个 7 就是对于客户端这个socket的文件描述符
系统调用推断
- poll 返回了一个 4,然后 accept(4) = 7 . 表示serversocket4返回数据,然后读取4返回了一个文件描述符7
- 因为是poll 所以 java层面把7存在了jvm空间,接下来就是继续重复调用 poll,只是里面的fd多了7
验证通过
客户端登出
网络抓包看到 客户端给服务端发送了一个 fin 服务端回了一个fin之后就结束了
查看服务端文件描述符状态,看到7的状态变成 CLOSE_WAIT
网络状态也可以看到变为 CLOSE_WAIT
并且服务端一直在空轮询系统调用,不断读取到文件描述符7上面的空,因为服务端没有处理这个事件,所以一直被读取.
CLOSE_WAIT 含义
当一方收到了 FIN 请求之后,状态就会变成 CLOSE_WAIT, 自己如果也发出 FIN请求就会关闭该socket
TIME_WAIT 含义
当服务端开启了 client.close() 之后, 再次测试链接断开.发现客户端产生了 TIME_WAIT状态
客户端断开链接的过程如下,当客户端最后一次发送ACK的时候服务端直接关闭资源了,但是客户端不知道这个ack有没有到达服务端,所以等待两倍的MSL,自己到服务端最大一次MSL,服务端来最长一次MSL,超过这个时间就表示请求肯定送达了
TIME_WAIT影响
当一个客户端处于短链接场景中,建立一个socket发送一个请求然后马上断开.然后再建立请求.这时候 客户端 的socket可能有大量的处于 TIME_WAIT 状态的socket,这种socket也是不能被重复使用的.
epoll方式的调用流程
- socket bind listen 都不变
- 内核中开辟一个空间,文件描述符7来表示
- 把4放到7的空间中,然后开始监听7
- 客户端链接服务端后. epoll把7空间里面的 红黑树里面的4 挪到了 链表 里面, 调用epoll_wait 发现链表里面有数据,就返回了4的文件描述符. accept 4 读取到了客户端链接 8. 再把8用 epoll_ctl 放入红黑树7中
write 事件
之前的模型里面都只是处理 accept 事件和 read事件. 分别表示有新的客户端建立连接和已有客户端发送来数据.
write事件的响应只依赖于 socket里面的 send-Q 是不是可以写入
所以注册write监听的事件时机是基于业务的 在之前的模型里面读取到了事件之后马上就对client进行了写操作,实际场景中读写可能是分离的.先读取到数据之后,觉得可以写了之后 再注册写事件,下次轮询就判断 send-Q 是不是可以写入,如果可以写入就进行写入操作.
多线程版多路复用器
在单线程的版本里面 读写操作可能包含很多业务逻辑 如果一个连接是处于一个耗时操作那么所有其他的连接都得等待. 所以设计一个模型是主线程处理连接任务,当有读写请求的时候放到子线程中处理.
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* Created by 祝程 on 12/26/21.
*/
public class MultiThread {
private ServerSocketChannel server;
/**
* 一个多路复用器的抽象, 底层可能是 select poll epoll
*/
private Selector selector;
private void initServer() {
try {
// 对应的系统调用是 socket = fd6
server = ServerSocketChannel.open();
server.configureBlocking(false);
// 对应的系统调用是 bind(fd6,9090) && listen(fd6)
server.bind(new InetSocketAddress(9090));
/**
* 如果是
* select poll 则是在jvm层面开辟一个空间
* epoll 则 调用系统调用的 epoll_create=fd4 开辟内核空间
*/
selector = Selector.open();
/**
* 如果是 select poll 则 把 这个 文件描述符放入jvm空间
* 如果是 epoll 则调用 epoll_ctl(fd4,fd6,read)
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
}
}
public void start() {
initServer();
System.out.println("server start");
try {
while (true) {
Set<SelectionKey> keys = selector.keys();
/**
* 如果是 select poll 就是把 jvm里面的文件描述符传入到内核
* 内核做遍历之后返回一共有几个可以读写 select(fds) poll(fds)
* 如果是 epoll 则是调用 epoll_wait 内核把链表空间的文件描述符返回
*/
while (selector.select(500) > 0) {
// 返回了有状态的 fd集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
/**
* 有数据的文件描述符可能是服务端的fd 那么就需要处理新的客户端链接
* 也有可能是客户端链接产生的数据,那么就需要进行业务处理
*/
if (key.isReadable()) {
key.cancel();
handlerRead(key);
} else if (key.isAcceptable()) {
handlerAccept(key);
}else if (key.isWritable()){
key.cancel();
handlerWrite(key);
}
}
}
}
} catch (Exception e) {
System.out.println("dd");
}
System.out.println("ds");
}
/**
* 处理写事件。
* 只要send-Q是空的就是可以写
* @param key
* @throws Exception
*/
private void handlerWrite(SelectionKey key)throws Exception{
new Thread(()->{
try {
System.out.println("write");
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
SocketChannel client = (SocketChannel) key.channel();
client.write(buffer);
buffer.clear();
client.register(selector,SelectionKey.OP_READ,buffer);
}catch (Exception e){
}
}).start();
}
/**
* 处理服务端收到的客户端链接请求
*
* @param key
* @throws Exception
*/
private void handlerAccept(SelectionKey key) throws Exception {
// 收到新的客户端链接,需要把这个客户端注册到多路复用器中
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(8096);
client.register(selector, SelectionKey.OP_READ, buf);
System.out.println("新客户端链接" + client.getRemoteAddress());
}
/**
* 处理客户端发送来的数据
*
* @param key
* @throws Exception
*/
private void handlerRead(SelectionKey key) throws Exception {
new Thread(()->{
try {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
byteBuffer.clear();
while (true) {
int read = client.read(byteBuffer);
if (read > 0) {
// 客户端发什么就往回写什么,这是属于具体业务范畴
client.register(selector,SelectionKey.OP_WRITE,byteBuffer);
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
}catch (Exception e){
}
}).start();
}
public static void main(String[] args) {
new MultiThread().start();
System.out.println("dsd");
}
}
上面的模型属于单 selector 多线程版本. selector取到 fd之后开辟线程来处理业务,不阻塞对selector的轮询. 但是这个模型有一个缺点是: 因为 handlerRead 和 handlerWrite 都是在子线程当中,主线程下次轮询可能子线程的这个fd还没有处理完成, 内核中还是有相应的标志位,就可能导致重复消费. 所以需要不断的 key.cancel(); 然后在处理完读写请求之后再重新注册回selector. 这样就造成了很多的 ==系统调用==
多selector多线程模型
我们再来重新回想一下模型的推导过程.
多selector多线程模型的好处就是,既利用了多核cpu增加处理速度,又不会需要额外的 注销注册系统调用. ==创建多个 selector每个selector里面是单线程.==
多selector多线程模型代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* Created by 祝程 on 12/27/21.
* 多selector模型
*/
public class MultiSelector {
private Selector mainSelector;
private Selector selector01;
private Selector selector02;
private void initServer() throws Exception {
mainSelector = Selector.open();
selector01 = Selector.open();
selector02 = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(9090));
server.configureBlocking(false);
server.register(mainSelector, SelectionKey.OP_ACCEPT);
new Thread(() -> {
while (true) {
try {
while (selector01.select(500) > 0) {
System.out.println("selector01监听到事件" + selector01.keys().size());
Set<SelectionKey> keys = selector01.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isReadable()) {
handlerRead(key);
} else if (key.isWritable()) {
handlerWrite(key);
}
}
}
} catch (Exception e) {
System.out.println("f");
}
}
}).start();
new Thread(() -> {
while (true) {
try {
while (selector02.select(500) > 0) {
Set<SelectionKey> keys = selector02.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
if (key.isReadable()) {
handlerRead(key);
} else if (key.isWritable()) {
handlerWrite(key);
}
}
}
} catch (Exception e) {
}
}
}).start();
System.out.println("server up 9090");
}
/**
* 处理客户端发送来的数据
*
* @param key
* @throws Exception
*/
private void handlerRead(SelectionKey key) throws Exception {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
byteBuffer.clear();
while (true) {
int read = client.read(byteBuffer);
if (read > 0) {
// 客户端发什么就往回写什么,这是属于具体业务范畴
byteBuffer.flip();
client.register(key.selector(),SelectionKey.OP_WRITE,byteBuffer);
} else if (read == 0) {
break;
} else {
client.close();
key.cancel();
break;
}
}
}
private void handlerWrite(SelectionKey key) throws Exception {
System.out.println("线程--" + Thread.currentThread().getName() + "收到write请求,开始写入");
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
SocketChannel client = (SocketChannel) key.channel();
client.write(buffer);
buffer.clear();
key.cancel();
}
private void start() throws Exception {
initServer();
int i = 0;
while (true) {
while (mainSelector.select(1000) > 0) {
Set<SelectionKey> keys = mainSelector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
/**
* 分发到不同的selector中
*/
ByteBuffer buffer = ByteBuffer.allocate(8096);
if (i % 2 == 0) {
System.out.println("线程--" + Thread.currentThread().getName() + "收到accept请求,开始分发 selector01");
client.register(selector01, SelectionKey.OP_READ, buffer);
} else {
System.out.println("线程--" + Thread.currentThread().getName() + "收到accept请求,开始分发 selector02");
client.register(selector02, SelectionKey.OP_READ, buffer);
}
}
}
}
}
public static void main(String[] args) throws Exception {
new MultiSelector().start();
}
}
架构图
上面的架构图和netty的架构图就有那么一点意思了.