导论
前面几篇文章我们分别从
一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
等几个纬度对JavaIO和NIO体系做了详细介绍,并由简到深的根据IO体系的升级过程做了系统分析。今天我们开始讲解NIO体系下的多路复用器(Selector),并用实例教你如何实现Netty中Reactor单线程模型。
解读
多路复用IO模型:
多路复用IO模型是目前使用得比较多的模型。JavaNIO实际上就是多路复用IO。在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件时才会使用IO资源,所以它大大减少了资源占用。在JavaNIO中,是通过select.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在哪里。因此这种方式会导致用户线程的阻塞。多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因为,多路复用IO比较适合连接数比较多的情况。
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时是通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,效率要比在用户线程高的多。
不过需要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续事件迟迟得不到处理,并且会影响新的事件轮询。
需要注意的是,多路复用器只是告诉了我们每个client(客户端)的IO状态。
具体的读取和写入还是需要用户自己去做。
----这个整个过程称之为同步。不论这个数据的过程是在当前线程去处理,
还是去创建新线程去处理,整个过程依旧是同步模型。
全图(下面有分解图)
图解
单线程版本的多路复用器图示
释义
1.NIO解决了BIO模型的阻塞状态问题,SELECT(多路复用器/选择器)模型解决了NIO模型的以下几个问题->
①.c10K(一万个客户端)问题,高并发问题。如果有一万个客户端连入了当前ServerSocket服务端,由于所有客户端的数据处理都是在当前主线程中进行的,虽然现在新客户端的进入不再阻塞,但是当前线程每进行一次recvfrom操作都需要对一万个客户端进行循环问询,客户端是否有数据传入。有多少个客户端就需要循环进行多少次问询。每循环内会有O(N)复杂度的系统调用,但是在O(N)复杂度内可能只有两个客户端发来了数据。这样在问询过程当中会有9998次的问询是没有效果的,是无效调用。长时间下来就进行了无数次的无效的有系统调用成本的的系统调用轮询。
②.为了解决以上无效系统调用的问题,我们引入了*选择器(SELECT)*的概念。也就是说有N个客户端连入了ServerSocket服务端,其中只有M个客户端发送了数据,那么我们就可以紧对M个客户端轮询即可。这样系统调用的时间复杂度就由O(N)降低到了O(M)。M永远都是小于等于N的,只对有数据传入的客户端进行accept操作。
2.SELECT(多路复用器/选择器)模型命令也是系统调用中的2类命令,通过linux系统的 man 2 select命令即可查看调用方法。
现阶段已知的操作系统中的多路复用器有:
select(大部分系统都有)、
poll(大部分系统都有)、
epoll(linux系统)、
kqueue(unix系统)
3.系统内核提供了SELECT(多路复用器/选择器)模型命令,服务端向系统内核传入N个客户端的文件描述符,系统内核返回有数据传入的M个客户端的文件描述符。这样就减少了客户端轮询的时间复杂度。
4.NIO解决了BIO模型的阻塞状态问题,SELECT模型解决了NIO模型的无数次无效重复系统调用问题,降低了时间复杂度。但是SELECT(多路复用器/选择器)模型还是存在以下几个问题->
①.SELECT(多路复用器/选择器)模型解决了应用层面的无数次重复系统调用及上下文切换的问题,但是他并没有真正解决掉系统调用层面的无数次重复系统调用问题,他只是把应用层的N次循环切换到了SELECT(多路复用器/选择器)中,SELECT(多路复用器/选择器)模型帮应用进行了N次循环。
②.为了解决以上问题,也为了更加高效的利用CPU,提出了事件驱动模型(EVENT)
selector单线程版本-Netty单线程模型
服务端代码:
import java.io.IOException;
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 Bruce on 2020/9/17
* 多路复用器-selector单线程版本
* 网络IO之SELECT-服务端
* * * SELECT-写法
* * ServerSocketChannel
**/
public class SocketServerSelectorSingleThread {
/**
* 服务器端通道
*/
private static ServerSocketChannel serverSocketChannel;
/**
* 服务器端多路复用器
*/
private static Selector selector;
/**
* 服务器端端口号
*/
private static Integer serverPort = 8080;
/**
* 初始化服务器端信息
*/
private void initServerSocket(){
try {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(serverPort));//绑定端口号
System.out.println("step1 : new ServerSocket(" + serverPort+ ") ");
serverSocketChannel.configureBlocking(false);//设置服务端非阻塞模式
/**
* //打开多路复用器
*/
selector = Selector.open();
/**
* 把服务端注册到多路复用器当中,并监听一个网络行为---》OP_ACCEPT
*
* Interest Set
* 监听的Channel通道触发了一个事件意思是该事件已经就绪。
* 一个channel成功连接到另一个服务器称为”连接就绪“。
* 一个server socket channel准备号接收新进入的连接称为”接收就绪“。
* 一个有数据可读的通道可以说是”读就绪“。
* 一个等待写数据的通道可以说是”写就绪“。
* 这四种事件用SelectionKey的四个常量来表示:
* SelectionKey.OP_CONNECT
* SelectionKey.OP_ACCEPT
* SelectionKey.OP_READ
* SelectionKey.OP_WRITE
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
initServerSocket();
System.out.println("服务端已经启动..........");
try {
while (true){
/**
* select方法---》
* 参数:
* 如果为正数,则阻塞最长<tt>超时</tt>在等待一个通道准备就绪;
* 如果为零,则无限期阻塞;
* *不能为负数
* 返回值:
钥匙的数量,可能是零,其就绪操作集已更新
*/
if(selector.select(0) > 0){//无限阻塞,如果有数据或者有事件传入行为到达则返回值大于0
/**
* 返回有数据传入或有事件的SocketChannel集合。
* 从多路复用器取出有效的连接-selectionKeys-
*/
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
/**
* 循环迭代每个有数据传入或有事件的SocketChannel,
* 并根据他们的行为做特殊处理
*/
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();//获取到选择秘钥
/**
* 获取之后从原迭代器中删除,防止重复循环
*/
iterator.remove();
if(selectionKey.isConnectable()){
System.out.println("---------------selectionKey.isConnectable()......");
}else if(selectionKey.isAcceptable()){//客户端请求连接事件
acceptHandler(selectionKey);
}else if(selectionKey.isReadable()){//客户端数据到达事件
readHandler(selectionKey);
}else if(selectionKey.isValid()){
System.out.println("---------------selectionKey.isValid()......");
}else if(selectionKey.isWritable()){
System.out.println("---------------selectionKey.isWritable()......");
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端连接建立处理类
* @param selectionKey
*/
private void acceptHandler(SelectionKey selectionKey){
try {
/**
* 由于在之前已经把ServerSocketChannel
* <p>serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);</p>
* 已经注册到多路复用器(selector)中,因此现阶段可以直接把服务端通道直接取出
*/
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
/**
* 调用accept接口从服务端通道中获取客户端连接
*/
SocketChannel clientSocketChannel = serverSocketChannel.accept();
/**
* 设置客户端连接也为非阻塞状态
*/
clientSocketChannel.configureBlocking(false);
/**
* 创建一个8字节的数据缓冲区
* 暂且不论该缓冲区大小是否足够
* 该缓冲区的作用为:******一个客户端通道对应一个数据缓冲区,防止缓冲区公用
*/
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8192);
/**
* 把新的客户端连接也注册到多路复用器当中,并且注册监听事件为读取就绪
* 这样在新的<p>SelectionKey</p> 中就即可以拿到自己的客户端,也可以拿到与客户端绑定的缓冲区
*
* 附加选项---byteBuffer
*/
SelectionKey clientSelectionKey = clientSocketChannel.register(selector,SelectionKey.OP_READ,byteBuffer);
System.out.println("--------------------------------------------------");
System.out.println("------新客户端进入------:" + clientSocketChannel.getRemoteAddress());
System.out.println("--------------------------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端数据读取处理类
* @param selectionKey
*/
private void readHandler(SelectionKey selectionKey){
/**
* 由于在之前<p>acceptHandler </p>方法中已经把SocketChannel(客户端连接)
* <p> SelectionKey clientSelectionKey = clientSocketChannel.register(selector,SelectionKey.OP_READ,byteBuffer);</p>
* 已经注册到多路复用器(selector)中,因此现阶段可以直接把客户端连接从选择器中取出
*/
SocketChannel clientSocketChannel = (SocketChannel)selectionKey.channel();
/**
* 附加选项---byteBuffer
* 由于在之前<p>acceptHandler </p>方法中已经把ByteBuffer(缓冲区)
* * <p> SelectionKey clientSelectionKey = clientSocketChannel.register(selector,SelectionKey.OP_READ,byteBuffer);</p>
* * 已经注册到多路复用器(selector)中,因此现阶段可以直接把客户端连接对应的byteBuffer(缓冲区)从选择器中取出。
*/
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
byteBuffer.clear();//清空缓冲区
try {
int read = 0;
while (true){
read = clientSocketChannel.read(byteBuffer);//从客户端中读取数据到缓冲区
if(read > 0){//读取到数据
byteBuffer.flip();//准备开始从缓冲区中读取数据,指针反转
byte[] bytes = new byte[read];
byteBuffer.get(bytes);//从缓冲区中获取数据到字节数组
String clientStr = new String(bytes);
//打印客户传入的数据信息
System.out.println("客户端【" + clientSocketChannel.getRemoteAddress() + "】有数据传入:" + clientStr);
/**
* 数据读取完毕,开始准备写入数据
*/
byteBuffer.clear(); //清空缓冲区
String returnClientStr = "---server port---" + serverPort +"---accept client port---" + clientSocketChannel.getRemoteAddress() + "---data---" + clientStr;
byteBuffer.put(returnClientStr.getBytes());
byteBuffer.flip();//准备开始从缓冲区中读取数据,指针反转
while (byteBuffer.hasRemaining()){//判断当前缓冲区中是否有数据
clientSocketChannel.write(byteBuffer);//把当前缓冲区中数据写回客户端。
}
byteBuffer.clear();
}else if(read == 0){//没有数据传入跳过
break;
}else { //-1 close_wait bug 客户端可能断开
System.out.println("---client port---" + clientSocketChannel.getRemoteAddress() + "---offline---");
/**
* 检测到客户端关闭,删除selectionKey监听事件,
* 否则会一直受到这个selectionKey的动作。
*/
selectionKey.cancel();
clientSocketChannel.close();
}
}
} catch (IOException e) {
e.printStackTrace();
try {
/**
* 如果这里不做处理 在 read读取数据的时候就可能会报错
* <p>
* java.io.IOException: 远程主机强迫关闭了一个现有的连接
*
* java.io.IOException: 远程主机强迫关闭了一个现有的连接。
* at sun.nio.ch.SocketDispatcher.read0(Native Method)
* at sun.nio.ch.SocketDispatcher.read(Unknown Source)
* at sun.nio.ch.IOUtil.readIntoNativeBuffer(Unknown Source)
* at sun.nio.ch.IOUtil.read(Unknown Source)
* at sun.nio.ch.SocketChannelImpl.read(Unknown Source)
* </p>
* 主要原因是客户端强制关闭了连接(没有调用SocketChannel的close方法),服务端还在read事件中,此时读取客户端的信息时会报错。
* 代码不够健壮导致的;
* 可以把这个catch中的代码注释掉,重启serversocket,然后使用nc命令连接后再强制关闭,看一下错误日志信息打印。
*/
System.out.println("---client port---" + clientSocketChannel.getRemoteAddress() + "---offline---");
/**
* 检测到客户端关闭,删除selectionKey监听事件,
* 否则会一直受到这个selectionKey的动作。
*/
selectionKey.cancel();
clientSocketChannel.socket().close();
clientSocketChannel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
服务端测试代码:
/**
* Created by Bruce on 2020/9/17
* 多路复用器-selector单线程版本-测试类
* * 网络IO之SELECT-服务端
* * * * SELECT-写法
* * * ServerSocketChannel
**/
public class SocketServerSelectorSingleThreadTest {
public static void main(String[] args) {
SocketServerSelectorSingleThread serverSocket = new SocketServerSelectorSingleThread();
serverSocket.start();
}
}
selector单线程版本Reactor单线程模型-打印输出
在linux环境或者windows环境下使用nc命令链接服务端,查看服务端打印过程。
具体linux系统或者windows系统如何安装nc命令,请从网络搜索或查看目录文档 ‘网络IO涉及到的-linux指令.docx’。
- nc客户端1打印(Windows-nc命令打印)
C:\Users\Administrator>nc 127.0.0.1 8080
789456123
---server port---8080---accept client port---/127.0.0.1:60697---data---789456123
C:\Users\Administrator>nc 127.0.0.1 8080
789456
---server port---8080---accept client port---/127.0.0.1:60820---data---789456
123456
---server port---8080---accept client port---/127.0.0.1:60820---data---123456
2. nc客户端2打印(Windows-nc命令打印)
C:\Users\Administrator>nc 127.0.0.1 8080
aaa
---server port---8080---accept client port---/127.0.0.1:60855---data---aaa
bbb
---server port---8080---accept client port---/127.0.0.1:60855---data---bbb
ccc
---server port---8080---accept client port---/127.0.0.1:60855---data---ccc
3. 服务端打印
step1 : new ServerSocket(8080)
服务端已经启动..........
--------------------------------------------------
------新客户端进入------:/127.0.0.1:60820
--------------------------------------------------
客户端【/127.0.0.1:60820】有数据传入:789456
客户端【/127.0.0.1:60820】有数据传入:123456
--------------------------------------------------
------新客户端进入------:/127.0.0.1:60855
--------------------------------------------------
客户端【/127.0.0.1:60855】有数据传入:aaa
客户端【/127.0.0.1:60855】有数据传入:bbb
客户端【/127.0.0.1:60855】有数据传入:ccc
往期JavaIO文章
一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
整体JavaIO体系文章概览