IO模型实战—实现Netty模型

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平平安安年年

一起学习,一起成长

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值