IO模型实战—实现Netty模型
引言
近日学习了IO的相关的知识,其中包括内存与IO,磁盘IO和网络IO,获益良多。看了某大牛的手写netty视频,感叹于其中奇妙,不禁想动手自己实现一下。一是对于IO知识的总结,二是沉迷于其中的逻辑。本文将基于Netty的工作架构图,简单实现一个netty模型。
知识点
先备知识:FD,epoll,多线程,响应式编程,负载均衡
重要涉及类:Selector,ByteBuffer,ServerSocketChannel,SocketChannel
问题背景与思路
1.NIO是同步非阻塞的,对于已经启动的server来说,进行一次Selector.select()。它会得到NIO Channel的状态,包括连接状态,读状态和写状态,进行处理时是同步的,这里就存在响应延迟的问题。
2.网络NIO Channel的状态类型可以分为两类,一类是ServerSocketChannel的连接信息,会调用accept创建新的SocketChannel。另一类是SocketChannel接收的读写信息。若所有Channal注册在同一个Selector上,会造成业务的耦合。
从上面两个问题,netty的采用多线程创建Selector,并将不同类别的Channal分类注册到不同的Selector,并分组进行Selector的管理。
具体步骤
第一步,创建selectorThread类,持有Selector引用,继承Runnable,一个线程对应一个Selector。
public class SelectorThread implements Runnable{
SelectorThread(SelectorThreadGroup stg) {
try {
// 底层调用epoll_create,开辟内核空间
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第二步,在run方法中实现Selector常规的功能。
@Override
public void run() {
// loop,事件环
while (true) {
int num = 0;
// epoll_wait,阻塞
num = selector.select();
if(num > 0) {
// selectorKeys
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.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()){
}
}
}
}
第三步,实现连接事件处理
private void acceptHandler(SelectionKey key) {
// 多态
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
try {
SocketChannel client = serverSocketChannel.accept();
client.configureBlocking(false);
// 注册client到对应Selector
stg.nextSelectorV2(client);
} catch (IOException e) {
e.printStackTrace();
}
}
第四步,实现read事件
private void readHandler(SelectionKey key) {
// 获取该key的缓存通道
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel client = (SocketChannel) key.channel();
buffer.clear();
while (true) {
try {
int num = client.read(buffer);
if(num > 0) {
buffer.flip(); // 将读到内容反转。
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if(num == 0) {
break;
} else if(num < 0) {
// 客户端断开,又有很多原因.将注册的FD剔除
key.cancel();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
第五步:考虑到多个Selector线程的管理和分类,创建Selector的组对象。
public class SelectorThreadGroup {
SelectorThread[] selectorThreads;
SelectorThreadGroup(int num) {
selectorThreads = new SelectorThread[num];
for (int i = 0; i < selectorThreads.length; i++) {
selectorThreads[i] = new SelectorThread(this);
new Thread(selectorThreads[i]).start();
}
}
第六步:参考netty架构图,考虑到不同类型的组,这里我们考虑持有SelectorThreadGroup的引用,默认为BOSS组(注册accept),可以设置为work组(注册read&write)
// 默认就是boss
SelectorThreadGroup stg = this;
public void setWork(SelectorThreadGroup stg) {
this.stg = stg;
}
第七步:实现bind方法,绑定端口,ServerSocketChannel的实现与监听。
// 绑定接口
public void bind(int port) throws IOException {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
// 选择selector并注册
nextSelector(server);
}
第八步:实现注册的负载均衡,同时完成注册。这里为了保证select()阻塞被wakeUp后能顺利进行key的注册,给SelectThread引入一个队列。为实现Selecor的分类注册,实现两个选择器选择方法,选择不同的组进行注册。
public void nextSelectorV2(Channel c) {
if(c instanceof ServerSocketChannel) {
// 在main线程的堆里取到selectorThread对象
SelectorThread st = next();
// 通过队列传输数据
st.lbq.add(c);
// 设置work组
st.setWorkGroup(stg);
// 打断阻塞
st.selector.wakeup();
}else if(c instanceof SocketChannel) {
// 在main线程的堆里取到selectorThread对象
SelectorThread st = nextV2();
// 通过队列传输数据
st.lbq.add(c);
// 打断阻塞
st.selector.wakeup();
}
}
private SelectorThread next() {
// 实现负载均衡
int index = atomicInteger.incrementAndGet() % selectorThreads.length;
return selectorThreads[index];
}
private SelectorThread nextV2() {
int index = stg.atomicInteger.incrementAndGet() % stg.selectorThreads.length;
return stg.selectorThreads[index];
}
第九步:创建Main方法进行实验
public class MainThread {
// 用来启动,不做IO业务处理
public static void main(String[] args) {
// 创建boss组
SelectorThreadGroup boss = new SelectorThreadGroup(2);
// 创建worker组
SelectorThreadGroup work = new SelectorThreadGroup(3);
// 设置work组
boss.setWork(work);
try {
// 将server注册到某一个selector上
stg.bind(9999);
stg.bind(9998);
} catch (IOException e) {
e.printStackTrace();
}
}
}
实验
实验目的
1.验证selector分组
2.验证多线程负载均衡
实验设计
1.boss组启动了两个线程,分别绑定9999和9998端口。work组启动了三个线程。
2.linux开启四个客户端进行连接
3.linux四个客户端分别发送消息
实验结果
Thread[Thread-1,5,main]listen...../0:0:0:0:0:0:0:0:9999
Thread[Thread-0,5,main]listen...../0:0:0:0:0:0:0:0:9998 // 监听在线程0,1
acceptHander....
Thread[Thread-1,5,main]accept...
Thread[Thread-3,5,main]register.....
acceptHander....
Thread[Thread-1,5,main]accept...
Thread[Thread-4,5,main]register.....
acceptHander....
Thread[Thread-0,5,main]accept... // boss组两个selector都接收accept消息,分别绑定不同端口
Thread[Thread-2,5,main]register.....
acceptHander....
Thread[Thread-0,5,main]accept...
Thread[Thread-3,5,main]register..... // 连接轮询注册在不同线程
Thread[Thread-3,5,main]read.....
Thread[Thread-2,5,main]read.....
Thread[Thread-4,5,main]read.....
Thread[Thread-3,5,main]read..... // read()负载在组work不同线程
源码地址
链接:https://pan.baidu.com/s/164wzckpQcYM7H5fiT2cpbg
提取码:e6p7