Java IO学习笔记七:多路复用从单线程到多线程
作者:Grey
原文地址:Java IO学习笔记七:多路复用从单线程到多线程
在前面提到的多路复用的服务端代码中, 我们在处理读数据的同时,也处理了写事件:
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
为了权责清晰一些,我们分开了两个事件处理:
一个负责写,一个负责读
读的事件处理, 如下代码
public void readHandler(SelectionKey key) {
System.out.println("read handler.....");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
client.register(key.selector(), SelectionKey.OP_WRITE, buffer);
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
其中read > 0 即从客户端读取到了数据,我们才注册一个写事件:
client.register(key.selector(), SelectionKey.OP_WRITE, buffer);
其他事件不注册写事件。(PS:只要send-queue没有满,就可以注册写事件)
写事件的处理逻辑如下:
private void writeHandler(SelectionKey key) {
System.out.println("write handler...");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
while (buffer.hasRemaining()) {
try {
client.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
buffer.clear();
key.cancel();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
写完后,调用key.cancel() 取消注册,并关闭客户端。既然分了读和写的不同处理流程,那么在主方法里面调用的时候:
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key);
} else if (key.isWritable()) {
writeHandler(key);
}
}
增加了
if (key.isWritable()) {
writeHandler(key);
}
测试一下,运行SocketMultiplexingV2.java
并通过一个客户端连接进来:
nc 192.168.205.1 9090
客户端发送一些内容:
nc 192.168.205.1 9090
asdfasdfasf
asdfasdfasf
可以正常接收到数据。
考虑有一个fd执行耗时,在一个线性里会阻塞后续FD的处理,同时,考虑资源利用,充分利用cpu核数。
我们来实现一个基于多线程的多路复用模型。
将N个FD分组(这里的FD就是Socket连接),每一组一个selector,将一个selector压到一个线程上(最好的线程数量是: cpu核数或者cpu核数*2)
每个selector中的fd是线性执行的。假设有100w个连接,如果有四个线程,那么每个线程处理25w个。
分组的FD和处理这堆FD的Selector我们封装到一个数据结构中,假设叫:SelectorThread,其成员变量至少有如下:
public class SelectorThread {
...
Selector selector = null;
// 存Selector对应要处理的FD队列
LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();
...
}
由于其处理是线性的,且我们要开很多个线程来处理,所以SelectorThread本身是一个线程类(实现Runnable接口)
public class SelectorThread implements Runnable {
...
}
在run方法中,我们就可以把之前单线程处理selector的常规操作代码移植过来:
....
while (true) {
....
if (selector.select() > 0) {
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key);
} else if (key.isWritable()) {
}
}
}
....
}
....
SelectorThread设计好以后,我们需要一个可以组织SelectorThread的类,假设叫SelectorThreadGroup,这个类的主要职责就是安排哪些FD由哪些Selector来接管,这个类里面持有两个SelectorThread数组,一个用于分配服务端,一个用于分配每次客户端的Socket请求。
// 服务端,可以启动多个服务端
SelectorThread[] bosses;
// 客户端的Socket请求
SelectorThread[] workers;
构造器中初始化这两个数组
SelectorThreadGroup(int bossNum, int workerNum) {
bosses = new SelectorThread[bossNum];
workers = new SelectorThread[workerNum];
for (int i = 0; i < bossNum; i++) {
bosses[i] = new SelectorThread(this);
new Thread(bosses[i]).start();
}
for (int i = 0; i < workerNum; i++) {
workers[i] = new SelectorThread(this);
new Thread(workers[i]).start();
}
}
以下代码是针对每次的请求,如何分配Selector:
...
public void nextSelector(Channel c) {
try {
SelectorThread st;
if (c instanceof ServerSocketChannel) {
st = nextBoss();
st.lbq.put(c);
st.setWorker(workerGroup);
} else {
st = nextWork();
st.lbq.add(c);
}
st.selector.wakeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private SelectorThread nextBoss() {
int index = xid.incrementAndGet() % bosses.length;
return bosses[index];
}
private SelectorThread nextWork() {
int index = xid.incrementAndGet() % workers.length; //动用worker的线程分配
return workers[index];
}
...
这里要区分两类Channel,一类是ServerSocketChannel,即我们每次启动的服务端,另外一类就是连接服务端的Socket请求,这两类最好是分到不同的SelectorThread中的队列中去。分配的算法是朴素的轮询算法(除以数组长度取模)
这样我们主函数只需要和SelectorThreadGroup交互即可:
public class Startup {
public static void main(String[] args) {
// 开辟了三个SelectorThread给服务端,开辟了三个SelectorThread给客户端去接收Socket
SelectorThreadGroup group = new SelectorThreadGroup(3,3);
group.bind(9999);
group.bind(8888);
group.bind(6666);
group.bind(7777);
}
}
启动Startup,
开启一个客户端,请求服务端,测试一下:
[root@io io]# nc 192.168.205.1 7777
sdfasdfs
sdfasdfs
客户端请求的数据可以返回,服务端可以监听到客户端的请求:
Thread-1 register listen
Thread-0 register listen
Thread-2 register listen
Thread-1 register listen
Thread-1 acceptHandler......
Thread-5 register client: /192.168.205.138:44152
因为我们开了四个端口的监听,但是我们只设置了三个服务端SelectorThread,所以可以看到Thread-1监听了两个服务端。
新接入的客户端连接是从Thread-5开始的,不会和前面的Thread-0,Thread-1,Thread-2冲突。
再次来一个新的客户端连接
[root@io io]# nc 192.168.205.1 8888
sdfasdfas
sdfasdfas
输入一些内容,依然可以得到服务端的响应
服务端这边日志显示:
Thread-3 register client: /192.168.205.138:33262
Thread-3 read......
显示是Thread-3捕获了新的连接,也不会和前面的Thread-0,Thread-1,Thread-2冲突。
完整源码:Github
Java IO学习笔记七:多路复用从单线程到多线程的更多相关文章
- Java IO学习笔记七
System对IO的支持 System是系统的类,其中的方法都是在控制台的输入和输出,但是通过重定向也是可以对文件的输入输出 System中定义了标准输入.标准输出和错误输出流,定义如下: stati ...
- Java IO学习笔记六:NIO到多路复用
作者:Grey 原文地址:Java IO学习笔记六:NIO到多路复用 虽然NIO性能上比BIO要好,参考:Java IO学习笔记五:BIO到NIO 但是NIO也有问题,NIO服务端的示例代码中往往会包 ...
- Java IO学习笔记八:Netty入门
作者:Grey 原文地址:Java IO学习笔记八:Netty入门 多路复用多线程方式还是有点麻烦,Netty帮我们做了封装,大大简化了编码的复杂度,接下来熟悉一下netty的基本使用. Netty+ ...
- Java IO学习笔记:概念与原理
Java IO学习笔记:概念与原理 一.概念 Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...
- Java IO学习笔记总结
Java IO学习笔记总结 前言 前面的八篇文章详细的讲述了Java IO的操作方法,文章列表如下 基本的文件操作 字符流和字节流的操作 InputStreamReader和OutputStreamW ...
- Java IO学习笔记三
Java IO学习笔记三 在整个IO包中,实际上就是分为字节流和字符流,但是除了这两个流之外,还存在了一组字节流-字符流的转换类. OutputStreamWriter:是Writer的子类,将输出的 ...
- Java IO学习笔记二
Java IO学习笔记二 流的概念 在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成. 程序中的输入输 ...
- Java IO学习笔记一
Java IO学习笔记一 File File是文件和目录路径名的抽象表示形式,总的来说就是java创建删除文件目录的一个类库,但是作用不仅仅于此,详细见官方文档 构造函数 File(File pare ...
- Java IO学习笔记一:为什么带Buffer的比不带Buffer的快
作者:Grey 原文地址:Java IO学习笔记一:为什么带Buffer的比不带Buffer的快 Java中为什么BufferedReader,BufferedWriter要比FileReader 和 ...
随机推荐
- Object划分
Object划分 1.PO(persistantobject)持久对象 PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合.PO中应该不包 含任何对数据库的操作. 2.DO(Domain ...
- .NET 反射(Reflection)
这节讲一下.NET 中的一个技术:反射(Reflection). 反射是一种很重要的技术,它可以在程序运行时,动态的获取类的实例,并调用实例中的任何方法.它就像一面镜子,映射出一个类的所有细节. ...
- 从0开始fastjson漏洞分析
关于fastjson漏洞利用参考:https://www.cnblogs.com/piaomiaohongchen/p/10799466.html fastjson这个漏洞出来了很久,一直没时间分析, ...
- C++ primer plus读书笔记——第10章 对象和类
第10章 对象和类 1. 基本类型完成了三项工作: 决定数据对象需要的内存数量: 决定如何解释内存中的位: 决定可使用数据对象执行的操作或方法. 2. 不必在类声明中使用关键字private,因为这是 ...
- HEVC学习(一) —— HM的使用
http://blog.csdn.net/hevc_cjl/article/details/8169182 首先自然是先把这个测试模型下载下来,链接地址如下:https://hevc.hhi.frau ...
- 字体:等宽字体与比例字体 - Monospaced font & Proportional font
字体:等宽字体与比例字体 - Monospaced font & Proportional font 量子波儿 2013-08-24 16:54:12 7101 收藏 1分类专栏: 计算机常识 ...
- 使用sosreport命令生成诊断包
[RHEL]-7-常用系统状态检测命令 引言 这篇文章介绍RHEL中最常用的几个系统状态检测命令,包括:网络状态.内存.主机名及其架构.系统负载及其用户登录状态等. 文章目录 0×1.查看网络状态 0 ...
- Linux压力测试软件Stress安装及使用指南2
stress工具使用指南和结果分析 Linux压力测试软件Stress安装及使用指南 一.Stress是什么 stress是一个linux下的压力测试工具,专门为那些想要测试自己的系统,完全高 ...
- Linux_配置主DNS服务(基础)
[RHEL8]-DNSserver:[Centos7.4]-DNSclient !!!测试环境我们首关闭防火墙和selinux(DNSserver和DNSclient都需要) [root@localh ...
- IT菜鸟之计算机硬件
现在的人们几乎无时无刻都会碰到计算机!不管是桌面计算机.笔记本电脑.平板计算机.智能型手机等等,这些东西都算计算机.虽然接触的怎么多,但是,我们一般很少会专门了解计算机内部的构成,下面就是自己在听课结 ...