作者:Grey

原文地址:Java IO学习笔记七:多路复用从单线程到多线程

前面提到的多路复用的服务端代码中, 我们在处理读数据的同时,也处理了写事件:

  1. public void readHandler(SelectionKey key) {
  2. SocketChannel client = (SocketChannel) key.channel();
  3. ByteBuffer buffer = (ByteBuffer) key.attachment();
  4. buffer.clear();
  5. int read;
  6. try {
  7. while (true) {
  8. read = client.read(buffer);
  9. if (read > 0) {
  10. buffer.flip();
  11. while (buffer.hasRemaining()) {
  12. client.write(buffer);
  13. }
  14. buffer.clear();
  15. } else if (read == 0) {
  16. break;
  17. } else {
  18. client.close();
  19. break;
  20. }
  21. }
  22. } catch (IOException e) {
  23. e.printStackTrace();
  24. }
  25. }

为了权责清晰一些,我们分开了两个事件处理:

一个负责写,一个负责读

读的事件处理, 如下代码

  1. public void readHandler(SelectionKey key) {
  2. System.out.println("read handler.....");
  3. SocketChannel client = (SocketChannel) key.channel();
  4. ByteBuffer buffer = (ByteBuffer) key.attachment();
  5. buffer.clear();
  6. int read = 0;
  7. try {
  8. while (true) {
  9. read = client.read(buffer);
  10. if (read > 0) {
  11. client.register(key.selector(), SelectionKey.OP_WRITE, buffer);
  12. } else if (read == 0) {
  13. break;
  14. } else {
  15. client.close();
  16. break;
  17. }
  18. }
  19. } catch (IOException e) {
  20. e.printStackTrace();
  21. }
  22. }

其中read > 0 即从客户端读取到了数据,我们才注册一个写事件:

  1. client.register(key.selector(), SelectionKey.OP_WRITE, buffer);

其他事件不注册写事件。(PS:只要send-queue没有满,就可以注册写事件)

写事件的处理逻辑如下:

  1. private void writeHandler(SelectionKey key) {
  2. System.out.println("write handler...");
  3. SocketChannel client = (SocketChannel) key.channel();
  4. ByteBuffer buffer = (ByteBuffer) key.attachment();
  5. buffer.flip();
  6. while (buffer.hasRemaining()) {
  7. try {
  8. client.write(buffer);
  9. } catch (IOException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. buffer.clear();
  14. key.cancel();
  15. try {
  16. client.close();
  17. } catch (IOException e) {
  18. e.printStackTrace();
  19. }
  20. }

写完后,调用key.cancel() 取消注册,并关闭客户端。既然分了读和写的不同处理流程,那么在主方法里面调用的时候:

  1. while (iter.hasNext()) {
  2. SelectionKey key = iter.next();
  3. iter.remove();
  4. if (key.isAcceptable()) {
  5. acceptHandler(key);
  6. } else if (key.isReadable()) {
  7. readHandler(key);
  8. } else if (key.isWritable()) {
  9. writeHandler(key);
  10. }
  11. }

增加了

  1. if (key.isWritable()) {
  2. writeHandler(key);
  3. }

测试一下,运行SocketMultiplexingV2.java

并通过一个客户端连接进来:

  1. nc 192.168.205.1 9090

客户端发送一些内容:

  1. nc 192.168.205.1 9090
  2. asdfasdfasf
  3. asdfasdfasf

可以正常接收到数据。

考虑有一个fd执行耗时,在一个线性里会阻塞后续FD的处理,同时,考虑资源利用,充分利用cpu核数。

我们来实现一个基于多线程的多路复用模型。

将N个FD分组(这里的FD就是Socket连接),每一组一个selector,将一个selector压到一个线程上(最好的线程数量是: cpu核数或者cpu核数*2)

每个selector中的fd是线性执行的。假设有100w个连接,如果有四个线程,那么每个线程处理25w个。

分组的FD和处理这堆FD的Selector我们封装到一个数据结构中,假设叫:SelectorThread,其成员变量至少有如下:

  1. public class SelectorThread {
  2. ...
  3. Selector selector = null;
  4. // 存Selector对应要处理的FD队列
  5. LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();
  6. ...
  7. }

由于其处理是线性的,且我们要开很多个线程来处理,所以SelectorThread本身是一个线程类(实现Runnable接口)

  1. public class SelectorThread implements Runnable {
  2. ...
  3. }

在run方法中,我们就可以把之前单线程处理selector的常规操作代码移植过来:

  1. ....
  2. while (true) {
  3. ....
  4. if (selector.select() > 0) {
  5. Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
  6. while (iter.hasNext()) {
  7. SelectionKey key = iter.next();
  8. iter.remove();
  9. if (key.isAcceptable()) {
  10. acceptHandler(key);
  11. } else if (key.isReadable()) {
  12. readHandler(key);
  13. } else if (key.isWritable()) {
  14. }
  15. }
  16. }
  17. ....
  18. }
  19. ....

SelectorThread设计好以后,我们需要一个可以组织SelectorThread的类,假设叫SelectorThreadGroup,这个类的主要职责就是安排哪些FD由哪些Selector来接管,这个类里面持有两个SelectorThread数组,一个用于分配服务端,一个用于分配每次客户端的Socket请求。

  1. // 服务端,可以启动多个服务端
  2. SelectorThread[] bosses;
  3. // 客户端的Socket请求
  4. SelectorThread[] workers;

构造器中初始化这两个数组

  1. SelectorThreadGroup(int bossNum, int workerNum) {
  2. bosses = new SelectorThread[bossNum];
  3. workers = new SelectorThread[workerNum];
  4. for (int i = 0; i < bossNum; i++) {
  5. bosses[i] = new SelectorThread(this);
  6. new Thread(bosses[i]).start();
  7. }
  8. for (int i = 0; i < workerNum; i++) {
  9. workers[i] = new SelectorThread(this);
  10. new Thread(workers[i]).start();
  11. }
  12. }

以下代码是针对每次的请求,如何分配Selector:

  1. ...
  2. public void nextSelector(Channel c) {
  3. try {
  4. SelectorThread st;
  5. if (c instanceof ServerSocketChannel) {
  6. st = nextBoss();
  7. st.lbq.put(c);
  8. st.setWorker(workerGroup);
  9. } else {
  10. st = nextWork();
  11. st.lbq.add(c);
  12. }
  13. st.selector.wakeup();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. private SelectorThread nextBoss() {
  19. int index = xid.incrementAndGet() % bosses.length;
  20. return bosses[index];
  21. }
  22. private SelectorThread nextWork() {
  23. int index = xid.incrementAndGet() % workers.length; //动用worker的线程分配
  24. return workers[index];
  25. }
  26. ...

这里要区分两类Channel,一类是ServerSocketChannel,即我们每次启动的服务端,另外一类就是连接服务端的Socket请求,这两类最好是分到不同的SelectorThread中的队列中去。分配的算法是朴素的轮询算法(除以数组长度取模)

这样我们主函数只需要和SelectorThreadGroup交互即可:


  1. public class Startup {
  2. public static void main(String[] args) {
  3. // 开辟了三个SelectorThread给服务端,开辟了三个SelectorThread给客户端去接收Socket
  4. SelectorThreadGroup group = new SelectorThreadGroup(3,3);
  5. group.bind(9999);
  6. group.bind(8888);
  7. group.bind(6666);
  8. group.bind(7777);
  9. }
  10. }

启动Startup,

开启一个客户端,请求服务端,测试一下:

  1. [root@io io]# nc 192.168.205.1 7777
  2. sdfasdfs
  3. sdfasdfs

客户端请求的数据可以返回,服务端可以监听到客户端的请求:

  1. Thread-1 register listen
  2. Thread-0 register listen
  3. Thread-2 register listen
  4. Thread-1 register listen
  5. Thread-1 acceptHandler......
  6. Thread-5 register client: /192.168.205.138:44152

因为我们开了四个端口的监听,但是我们只设置了三个服务端SelectorThread,所以可以看到Thread-1监听了两个服务端。

新接入的客户端连接是从Thread-5开始的,不会和前面的Thread-0,Thread-1,Thread-2冲突。

再次来一个新的客户端连接

  1. [root@io io]# nc 192.168.205.1 8888
  2. sdfasdfas
  3. sdfasdfas

输入一些内容,依然可以得到服务端的响应

服务端这边日志显示:

  1. Thread-3 register client: /192.168.205.138:33262
  2. Thread-3 read......

显示是Thread-3捕获了新的连接,也不会和前面的Thread-0,Thread-1,Thread-2冲突。

完整源码:Github

Java IO学习笔记七:多路复用从单线程到多线程的更多相关文章

  1. Java IO学习笔记七

    System对IO的支持 System是系统的类,其中的方法都是在控制台的输入和输出,但是通过重定向也是可以对文件的输入输出 System中定义了标准输入.标准输出和错误输出流,定义如下: stati ...

  2. Java IO学习笔记六:NIO到多路复用

    作者:Grey 原文地址:Java IO学习笔记六:NIO到多路复用 虽然NIO性能上比BIO要好,参考:Java IO学习笔记五:BIO到NIO 但是NIO也有问题,NIO服务端的示例代码中往往会包 ...

  3. Java IO学习笔记八:Netty入门

    作者:Grey 原文地址:Java IO学习笔记八:Netty入门 多路复用多线程方式还是有点麻烦,Netty帮我们做了封装,大大简化了编码的复杂度,接下来熟悉一下netty的基本使用. Netty+ ...

  4. Java IO学习笔记:概念与原理

    Java IO学习笔记:概念与原理   一.概念   Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...

  5. Java IO学习笔记总结

    Java IO学习笔记总结 前言 前面的八篇文章详细的讲述了Java IO的操作方法,文章列表如下 基本的文件操作 字符流和字节流的操作 InputStreamReader和OutputStreamW ...

  6. Java IO学习笔记三

    Java IO学习笔记三 在整个IO包中,实际上就是分为字节流和字符流,但是除了这两个流之外,还存在了一组字节流-字符流的转换类. OutputStreamWriter:是Writer的子类,将输出的 ...

  7. Java IO学习笔记二

    Java IO学习笔记二 流的概念 在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成. 程序中的输入输 ...

  8. Java IO学习笔记一

    Java IO学习笔记一 File File是文件和目录路径名的抽象表示形式,总的来说就是java创建删除文件目录的一个类库,但是作用不仅仅于此,详细见官方文档 构造函数 File(File pare ...

  9. Java IO学习笔记一:为什么带Buffer的比不带Buffer的快

    作者:Grey 原文地址:Java IO学习笔记一:为什么带Buffer的比不带Buffer的快 Java中为什么BufferedReader,BufferedWriter要比FileReader 和 ...

随机推荐

  1. 求曲线y=lnx在区间(2,6)内的一条切线,使得该切线与直线x=2,x=6及曲线y=lnx所围成的图形的面积最小。

    求曲线y=lnx在区间(2,6)内的一条切线,使得该切线与直线x=2,x=6及曲线y=lnx所围成的图形的面积最小. 1.先画图. 2.设切点为(a,lna) (2<a<6) 3.切线方程 ...

  2. vmware vpshere 安装完的必备工作

    1:例如:vCenter计算机地址为:192.168.0.200, 访问地址:https://192.168.0.200,安装证书: 参考教程:https://blog.csdn.net/cooljs ...

  3. 前端面试 CSS三大特性

    CSS的三大特性 1.层叠性 代码由上向下执行,相同选择器设置到同一元素上,样式冲突的,会执行比较靠近html的样式,样式不冲突的情况下不影响 代码如下 <!DOCTYPE html> & ...

  4. 成功的多项目管理都有哪些"制胜之道"?

    实施多项目管理,一个重要原因就是提高项目的效率和管理水平.除了满足时间.成本.业绩和客户需求之外,项目管理办公室(PMO)经理的预期产出还包括有效利用组织资源.下面是影响多项目管理成功的几个关键因素, ...

  5. 排坑&#183;ASCII码为160的空格(nbsp)

    阅文时长 | 2.83分钟 字数统计 | 1345.2字符 『排坑·ASCII码为160的空格(nbsp)』 编写人 | SCscHero 编写时间 | Wednesday, September 9, ...

  6. [Java] Spring 示例

    (一)IoC/DI 功能 配置解析:将配置文件解析为BeanDefinition结构,便于BeansFactory创建对象 对象创建:BeansFactory 根据配置文件通过反射创建对象,所有类对象 ...

  7. make clean 清除之前编译的可执行文件及配置文件。 make distclean 清除所有生成的文件。

    https://blog.csdn.net/bb807777/article/details/108302105 make clean 清除之前编译的可执行文件及配置文件.make distclean ...

  8. inux软件安装管理之——dpkg与apt-*详解

    inux软件安装管理之--dpkg与apt-*详解 Nosee123关注 0.5922017.09.12 17:47:44字数 3,894阅读 8,565 [Linux软件安装管理系列]- - 传送门 ...

  9. USB中TOKEN的CRC5与CRC16校验(神奇的工具生成Verilog实现)

    USB2.0IP设计 最近,在学习USB2.0IP的设计,其中包含了CRC校验码的内容,之前学习千兆以太网曾经用到过CRC32校验(https://www.cnblogs.com/Xwangzi66/ ...

  10. linux中用iptables开启指定端口

    linux中用iptables开启指定端口   centos默认开启的端口只有22端口,专供于SSH服务,其他端口都需要自行开启. 1.修改/etc/sysconfig/iptables文件,增加如下 ...