Netty学习前置-反应器(Reactor)模式

目录

  • 概览
    • 网络服务特性
    • 扩展可用需求
    • Divide-and-Conquer
  • Reactor 模式
    • 单线程版
    • NIO 重要概念
      • Channels
      • Buffers
      • Selectors
      • SelectionKeys
    • Reactor实现
      • Setup 创建反应器
      • Dispatch Loop 调度工作循环
    • Acceptor 设计
    • Handler setup
    • Request handling 请求处理
  • 分析:也不知道对不对
  • 多线程(池)设计
    • 加入线程池到Handler
    • 反应器多线程
    • 其他的JAVA NIO特性
    • 基于连接的扩展

概览

最近搬砖的帝都民航的啥航电XX设备大数据预测那部分好像黄了。
这边进来一个月不到,我刚动手搬砖就停手项目这套操作是真滴尴尬,不过发现这家有自研测运行维护数据的硬件发送tcp(为啥断断续续短连接不用udp?)报文到InfluxDB使用的是netty,一个臭名昭著大名鼎鼎的玩意,干脆来深入瞎学一下。
但是在看源码之前先复习下反应器模式感觉更好,一种设计模式,在redis,netty等网络应用上使用,看看这个 Doug Lea(令人景仰的神奇大爷)写的NIO.pdf,简单翻译然后自己写点笔记。
发现有错误地方请留言,谢谢。

网络服务特性

  • 高扩展可用网络服务
  • 事件驱动处理
  • 基本版本、多线程版本、其他
  • JAVA非阻塞API
    类似的读、解码、处理服务、发送等

但使用情景不同XML解释、文件传输、网页生成、电脑端服务等
Netty学习前置-反应器(Reactor)模式_第1张图片
然后大多都是有自己线程的Handler

class Server implements Runnable {
    public void run(){
        try{
            ServerSocket ss= new ServerSocket(PORT);
            while (!Thread.interrupted())
                new Thread(new Handler(ss.accept())).start();
        }catch(IOException ex){ /* do something */ }
    }
    
    static class Handler implements Runnable {
        final Socket socket;
        Handler(Socket s) { socket = s }
        public void run() {
            try{
                /* do something */
            }catch(IOException ex){ /* do something */ }
        }
        private byte[] process(byte[] cmd){ /* do something */ }
    }
}

扩展可用需求

  • 更好处理高负载
  • 随资源提高性能(更好的cpu,内存,硬盘,带宽)
  • short latencies短等待、达到peak需求、可调优服务质量

分而治之的思想一般是最好的解决上述问题办法

Divide-and-Conquer

  • 切分一个处理为多个小任务
    • 而每个任务表现为非阻塞行为
  • 一旦创建,立即执行每个任务,由每个IO事件触发
  • JAVA支持的非阻塞机制
    • NIO读写
    • 用IO事件触发Dispatch调度任务
  • 高可变的事件驱动设计
  • 比alternative更有效(这个没看懂可替代指啥)
    • 更少资源
      • 不常需要占用一个线程来服务一个客户
    • 少上头?(less overhead)
      • 少上下文切换context switching,少阻塞
    • 调度dispacth可能慢点
      • 多manually绑定行为到事件上
  • 更难的编程
    • 要分散进简单的NIO行为
      • 细分进GUI事件行为驱动
      • 不能终结所有的阻塞(GC,页面错误等)
    • 必须保持追踪服务的逻辑状态

例子java.awt.event:java中awt的事件触发,事件驱动的IO类似这种结构但设计不同
Netty学习前置-反应器(Reactor)模式_第2张图片

Reactor 模式

  • 反应器通过Dispacth调度正合适的Hanlder对IO事件响应,类似AWT thread
  • Handlers表现为NIO行为,类似AWT ActionListeners
  • 通过绑定handlers到事件来管理,类似AWT addActionListeners

单线程版

Netty学习前置-反应器(Reactor)模式_第3张图片

NIO 重要概念

Channels

连接文件,sockets等来支持NIO读取

Buffers

一种类数组对象,可以直接由Channels读写

Selectors

有IO事件的Channels组

SelectionKeys

Maintain维护IO事件状态和绑定

Reactor实现

Setup 创建反应器

我的理解是运行反应器同时构建selector和channel并绑定channel到socket port。同时设置NIO然后注册channel到selector将返回的selectionkey绑定到一个acceptor实例待执行

class Reactor implements Runnable {
    final Selector selector;
    final ServerSocketChannel serverSocket;
    
    // 构造方法初始化反应器
    Reactor(int port) throws IOException {
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open()// 绑定channel到port
        serverSocket.socket.bind(new InetSocketAddress(port));
        // NIO设置
        serverSocket.configureBlocking(false);
        // 注册channel到selector并生成accept后的selectionkey(绑定accepyor实例)
        SelectionKey sk = serverSocket.register(selector,SelectionKey.OP_ACCEPT);
        sk.attach(new Acceptor());
    }
    /*
        // 可替代做法:直接使用清楚的SPI生成者类
        // Alternatively, use explicit SPI provider:
        SelectorProvider p = SelectorProvider.provider();
        selector = p.openSelector();
        serverSocket = p.openServerSocketChannel();
    */

Dispatch Loop 调度工作循环

获取selector中所有的selecitonkeys并迭代遍历调度执行(新线程)相应的acceptor实例

    // 接上Reactor类里
    public void run() { 
            // 新线程内容
            try {
                while (!Thread.interrupted()) {
                    selector.select();
                    // 获取selector里已绑定的keys
                    Set selected = selector.selectedKeys();
                    // 迭代处理selectedKeys,调度其绑定的selectors
                    Iterator it = selected.iterator();
                    // 当前是否有待执行
                    while (it.hasNext())
                        // 调度sk的attachment(acceptor或handler)
                        dispatch((SelectionKey)(it.next()));
                    selected.clear();
                }
            } catch (IOException ex) { /* ... */ }
        }
        
    void dispatch(SelectionKey k) {
        // 新线程运行selectedKey上面相应附加的acceptor
        Runnable r = (Runnable)(k.attachment());
        if (r != null)
            r.run();
    }

Acceptor 设计

独立线程,调度执行sk对应的acceptor看看有无事务

    //接上面Reactor类里面
    class Acceptor implements Runnable { 
        // inner
        public void run() {
            try {
                // 检查channel里有没有东西
                SocketChannel c = serverSocket.accept();
                if (c != null)
                    // 启动执行事务操作该channel里的东西
                    new Handler(selector, c);
            }catch(IOException ex) { /* ... */ }
        }
    }
}

Handler setup

处理程序的线程类,也就是实际初始化(也是绑定sk)和执行netty程序的地方

final class Handler implements Runnable {
    final SocketChannel socket;
    final SelectionKey sk;
    // 分配IO流大小
    ByteBuffer input = ByteBuffer.allocate(MAXIN);
    ByteBuffer output = ByteBuffer.allocate(MAXOUT);
    static final int READING = 0, SENDING = 1;
    // 默认状态为读取
    int state = READING;
    
    // 构造函数处理传入Selector和Channel
    Handler(Selector sel, SocketChannel c) throws IOException {
        socket = c; 
        // NIO设置
        c.configureBlocking(false);
        // 注册事务到channel0(运行)到当前selector并生成selectionkey(绑定handler实例)
        sk = socket.register(sel, 0);
        // 当前channel中的第一个selectionkey绑定当前handler实例
        sk.attach(this);
        sk.interestOps(SelectionKey.OP_READ);
        sel.wakeup();
    }
    
    boolean inputIsComplete() { /* ... */ }
    boolean outputIsComplete() { /* ... */ }
    void process() { /* ... */ }

Request handling 请求处理

一般搬砖要写的应用操作在这里

    // 接上面Handler类
    // 分读写操作
    public void run() {
        try {
            if (state == READING) read();
            else if (state == SENDING) send();
            } catch (IOException ex) { /* ... */ }
        }
        
    void read() throws IOException {
        socket.read(input);
        if (inputIsComplete()) {
        process();
        state = SENDING;
        // Normally also do first write now
        sk.interestOps(SelectionKey.OP_WRITE);
        }
    }
        
    void send() throws IOException {
        socket.write(output);
        if (outputIsComplete()) 
            sk.cancel();
    }
}

分析:也不知道对不对

初始化时绑定port到channel(属于selector)并生成其所属的selectionkey(绑定acceptor实例),在调度器while中一直检查selector然后调度每一个里面的selectionkey,并创建新线程执行初始化绑定的acceptor实例来处理(若真有东西则new handler实例)

上文中acceptor线程中若真有东西则new handler来构造具体事务并绑定selectionkey到一个独特的channel(本代码中是channel0)等待反应器进程中的while去发现sk执行它,另补充handler实例也就是最后目标程序实现功能的那个线程

port内的事务经过accept到达channel属于selector由reactor调度执行selector里所有的selectionkey对应绑定的的acceptor实例后注册成为handler实例绑定的selectionkey等待执行

多线程(池)设计

  • 策略性的为高扩展可用增加线程设计如下两种
  • 工作线程池
    • 反应器应该快速触发handler(使反应器变慢的元凶大头所在)
    • Offload NIO 处理到其他线程
  • 反应器多线程
    • 反应器线程可沉浸处理IO事务u到其他反应器(负载均衡匹配cpu和io率)

加入线程池到Handler

多线程读写事务

class Handler implements Runnable {
    // uses util.concurrent thread pool
    static PooledExecutor pool = new PooledExecutor(...);
    static final int PROCESSING = 3;
     
    synchronized void read() { // ...
         socket.read(input);
         if (inputIsComplete()) {
             state = PROCESSING;
             pool.execute(new Processer());
         }
     }
     
    synchronized void processAndHandOff() {
        process();
        state = SENDING; // or rebind attachment
        sk.interest(SelectionKey.OP_WRITE);
    }
         
    // 多线程读写提高性能
    class Processer implements Runnable {
        public void run() { processAndHandOff(); }
    }
}

反应器多线程

让反应器动态使用独立的selector、线程、调度循环

Selector[] selectors; // also create threads
int next = 0;
class Acceptor { // ...
    public synchronized void run() { ...
        Socket connection = serverSocket.accept();
        if (connection != null)
            new Handler(selectors[next], connection);
            if (++next == selectors.length) 
                next = 0;
    }
}

其他的JAVA NIO特性

  • 每个反应器多selector
    • 绑定不同的handler处理不同的IO事件可能要小心同步去协调处理的问题
  • 文件传输
  • 内存映射文件–通过buffer获取文件
  • 直接用字节流–需要设置启动和终止,适合长连接服务或应用

基于连接的扩展

  • 相比较单单的服务请求,还能做到处理client连接、多消息/请求传输、终止
  • 例如数据库和事务监控,多人游戏啥的
  • 可扩展基本的网络服务模式
    • 处理相对长的连接client
    • 追踪client和session状态(包括drop)
    • 多hosts分布式服务

这位大爷的API参考
Buffer
ByteBuffer(CharBuffer, LongBuffer, etc not shown.)
Channel
SelectableChannel
SocketChannel
ServerSocketChannel
FileChannel
Selector
SelectionKey
具体设计PDF里面有我就不献丑了

你可能感兴趣的:(杂谈)