导论
前面几篇文章我们分别对一、JAVA IO/NIO体系介绍 、二、网络IO原理-创建ServerSocket-彻底弄懂IO 、三、JAVA中ServerSocket调用Linux系统内核 、四、「大厂职员教你」IO进化过程之BIO 、五、「大厂职员教你」Java-IO进化过程之NIO 、六、Selector实现Netty中Reactor单线程模型 等几个纬度对JavaIO和NIO体系做了详细介绍,并由简到深的根据IO体系的升级过程做了系统分析。今天我们开始讲解NIO体系下的多路复用器(Selector),并用实例教你如何实现Netty中Reactor主从模型。这篇文章是与上篇文章【六、Selector实现Netty中Reactor单线程模型 】紧紧结合的,因此要读懂这篇文章,请先以上篇文章做铺垫。
多路复用器-selector多线程版本- Reactor主从模型
代码示例
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;import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.atomic.AtomicInteger;/** * Created by Bruce on 2020/9/18 * * 多路复用器-selector多线程版本 * * 网络IO之SELECT-服务端 * * * * SELECT-写法 * * * ServerSocketChannel **/public class SocketServerSelectorMultiplexingThreads { /** * 服务端通道 */ private ServerSocketChannel serverSocketChannel; /** * 服务端端口 */ private int serverPort; /** * 主选择器-主要用于客户端的的接入-OP_ACCEPT-事件 */ private Selector bossSelector; /** * 任务选择器-主要用于客户端数据的传入处理-OP_READ-事件 */ private Selector[] workerSelectors; /** * 任务选择器数量 */ private int workerNum; /** * @param serverPort 服务端端口 * @param workerNum 任务选择器数量 * @throws IOException */ public SocketServerSelectorMultiplexingThreads(int serverPort ,int workerNum) throws IOException { this.serverPort = serverPort; this.workerNum = workerNum; /** * 创建并绑定端口号 */ serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(serverPort)); serverSocketChannel.configureBlocking(false); System.out.println("step1 : new ServerSocket(" + serverPort+ ") "); /** * 创建主(boss)选择器 */ bossSelector = Selector.open(); /** * serverSocket注册到 */ serverSocketChannel.register(bossSelector, SelectionKey.OP_ACCEPT); /** * 初始化任务选择器-根据设定的任务选择器数量 */ initWorderSelector(); } /** * 初始化 任务选择器 * @throws IOException */ private void initWorderSelector() throws IOException { workerSelectors = new Selector[workerNum]; for(int i = 0; i < workerNum; i++){ workerSelectors[i] = Selector.open(); } } public Selector getBossSelector() { return bossSelector; } public Selector[] getWorkerSelectors() { return workerSelectors; } public int getWorkerNum() { return workerNum; } public static void main(String[] args) throws IOException, InterruptedException { int serverPort = 8080; int workerNum = 3; System.out.println("准备启动服务端端口号:" + serverPort + "---准备启动worker数量:" + workerNum); SocketServerSelectorMultiplexingThreads server = new SocketServerSelectorMultiplexingThreads(serverPort,workerNum); NioThread bossThreadRunnable = new NioThread(server.getBossSelector(),workerNum); Thread bossThread = new Thread(bossThreadRunnable); bossThread.start(); Thread.currentThread().sleep(2000); Thread workerThread = null; Selector[] workerSelectors = server.getWorkerSelectors(); for(Selector selector : workerSelectors){ NioThread workerNioThread = new NioThread(selector); workerThread = new Thread(workerNioThread); workerThread.start(); } }}class NioThread implements Runnable{ /** * 传入的 选择器-私有 */ private Selector selector; /** * 任务选择器数量-多线程可见 */ private static int workerSelectorNum; /** * 每个任务选择器对应一个 队列 -多线程可见 */ static BlockingQueue[] clientSocketChannelQueues; /** * 任务选择器下标的数值-用于需任务选择器队列中获取自己的阻塞队列 * 每个worker生成自己的ID。 * * 这个workerID是与
BlockingQueue[] clientSocketChannelQueues
中阻塞队列进行间接绑定了的 */ int id = 0; /** * 是否为工作任务的选择器线程 * 默认为工作任务的选择器线程 * 如果为非工作任务的选择器线程则设置为false */ private boolean workerSign = true; /** * 自增ID-多线程可见 */ static AtomicInteger idx = new AtomicInteger(); /** * Boss 选择器专用构造器 * @param selector boss任务选择器 * @param workerSelectorNum 任务选择器数量 */ public NioThread(Selector selector, int workerSelectorNum){ this.selector = selector; this.workerSelectorNum = workerSelectorNum; this.workerSign = false; clientSocketChannelQueues = new LinkedBlockingQueue[workerSelectorNum]; initWorderSocketChannelQueues(); System.out.println("Boss 启动"); } /** * 初始化任务选择器所需的阻塞队列数组 * 一个阻塞队列对应一个任务选择器 * @throws IOException */ private void initWorderSocketChannelQueues() { for (int i = 0; i < workerSelectorNum; i++){ clientSocketChannelQueues[i] = new LinkedBlockingQueue<>(); } } /** * worker 任务选择器专用构造器 * @param selector */ public NioThread(Selector selector) { this.selector = selector; /** * 每个worker生成自己的ID。 */ id = idx.getAndIncrement() % workerSelectorNum;//任务选择器队列下标 System.out.println("任务线程 【worker---" + id + "】启动"); } @Override public void run() { try { while (true){ if(selector.select(10) > 0){//10毫秒延迟获取 不完全阻塞 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()){ SelectionKey selectionKey = iterator.next(); iterator.remove(); if(selectionKey.isConnectable()){ System.out.println("---------------selectionKey.isConnectable()......"); }else if(selectionKey.isAcceptable()){//客户端请求连接事件---其时只有boss选择器才能走到这一步 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()......"); } } } /** * boss 不参与这个过程 * 只有工作任务的选择器线程 * 且 * 对应的阻塞队列不为空的时候,才会执行 */ if(workerSign && !clientSocketChannelQueues[id].isEmpty()){ //默认创建一个8字节的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8192); //从阻塞队列中取出对应的客户端 SocketChannel clientSocketChannel = clientSocketChannelQueues[id].take(); clientSocketChannel.register(selector,SelectionKey.OP_READ,byteBuffer); System.out.println("-----------------------------------------------------"); System.out.println("客户端连接进入:" + clientSocketChannel.socket().getPort() + "分配到workder ---" + id); System.out.println("-----------------------------------------------------"); } } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 其时这一步 只有 boss选择器可以进入 * @param selectionKey * @throws IOException */ private void acceptHandler(SelectionKey selectionKey) throws IOException { /** * 从选择器中获取服务端注册时的通道 */ ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel(); /** * 接收传入的客户端SocketChannel */ SocketChannel clientSocketChannel = serverSocketChannel.accept(); clientSocketChannel.configureBlocking(false);//设置客户端类型为非阻塞 /** * 由于boss选择器所在的线程并不会处理客户端的链接, * 他只是把接受到的选择器按照自增的规则取模后放到不同的阻塞队列当中 * 每次来一个客户端 idx都会自增1 ,然后取模后放到不同的任务队列。 */ //轮询分配 int index = idx.getAndIncrement() % workerSelectorNum; /** * 把接受到的客户端放入不同任务选择器归属的阻塞队列中 */ clientSocketChannelQueues[index].add(clientSocketChannel); } /** * 其时这一步只有worker * @param selectionKey * @throws IOException */ private void readHandler(SelectionKey selectionKey) throws IOException { SocketChannel clientSocketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment(); byteBuffer.clear(); int readNum = 0; try { while (true){ readNum = clientSocketChannel.read(byteBuffer); if(readNum < 0){ System.out.println("client port --" + clientSocketChannel.socket().getPort() + "---offline---"); selectionKey.cancel(); clientSocketChannel.socket().close(); clientSocketChannel.close(); }else if(readNum == 0){ break; }else { byteBuffer.flip();//每次读取之前都要反转一次 byte[] bytes = new byte[readNum]; byteBuffer.get(bytes); String clientStr = new String(bytes); System.out.println("client port --" + clientSocketChannel.socket().getPort() + "---data---" + clientStr); String returnStr = "server get client data" + clientStr; byteBuffer.clear();//把数据返回 byteBuffer.put(returnStr.getBytes()); byteBuffer.flip();//每次写出之前都要反转一次 while (byteBuffer.hasRemaining()){//判断当前缓冲区中是否有数据 clientSocketChannel.write(byteBuffer);//把当前缓冲区中数据写回客户端。 } byteBuffer.clear();//写完之后清空。 } } }catch (IOException e) { e.printStackTrace(); System.out.println("client port --" + clientSocketChannel.socket().getPort() + "---offline---"); selectionKey.cancel(); clientSocketChannel.socket().close(); clientSocketChannel.close(); } }}
多路复用器-selector多线程版本- Reactor主从模型-打印输出
在linux环境或者windows环境下使用nc命令链接服务端,查看服务端打印过程。
具体linux系统或者windows系统如何安装nc命令,请从网络搜索或查看目录文档 ‘网络IO涉及到的-linux指令.docx’。
1.nc客户端1打印(Windows-nc命令打印)
C:甥敳獲Administrator>nc 127.0.0.1 8080nc111server get client datanc111client111server get client dataclient111
2.nc客户端2打印(Windows-nc命令打印)
C:甥敳獲Administrator>nc 127.0.0.1 8080nc222server get client datanc222client222server get client dataclient222
3. nc客户端2打印(Windows-nc命令打印)
C:甥敳獲Administrator>nc 127.0.0.1 8080nc333server get client datanc333chient333server get client datachient333
4.nc客户端2打印(Windows-nc命令打印)
C:甥敳獲Administrator>nc 127.0.0.1 8080nc444server get client datanc444client444server get client dataclient444
5.服务端打印
准备启动服务端端口号:8080---准备启动worker数量:3step1 : new ServerSocket(8080)Boss 启动任务线程 【worker---0】启动任务线程 【worker---1】启动任务线程 【worker---2】启动-----------------------------------------------------客户端连接进入:8822分配到workder ---0----------------------------------------------------------------------------------------------------------客户端连接进入:8825分配到workder ---1----------------------------------------------------------------------------------------------------------客户端连接进入:8828分配到workder ---2----------------------------------------------------------------------------------------------------------客户端连接进入:8830分配到workder ---0-----------------------------------------------------client port --8822---data---nc111 client port --8825---data---nc222 client port --8828---data---nc333 client port --8830---data---nc444 client port --8830---data---client444 client port --8828---data---chient333 client port --8825---data---client222 client port --8822---data---client111
多线程版本的多路复用器图示-Reactor主从模型示意图
多线程版本的多路复用器的几种模型-Reactor模型的几种类型
往期文章链接
一、JAVA IO/NIO体系介绍
二、网络IO原理-创建ServerSocket-彻底弄懂IO
三、JAVA中ServerSocket调用Linux系统内核
四、「大厂职员教你」IO进化过程之BIO
五、「大厂职员教你」Java-IO进化过程之NIO
六、Selector实现Netty中Reactor单线程模型
如需了解更多更详细内容也可关注本人CSDN博客:不吃_花椒