IO Reactor模式 JAVA

Motivation

  • 每天与IO 打交道, 都熟悉, 脑中图片未必一致
  • 对IO “精通” 又 “抠脚” -> 缺少一条线串联
  • 高级语言 框架透明了 IO相关API 使用细节 -> 难点已被前人铺平
  • 太基础了…不 “高大上” …常用关联用语: “就这?” “这不是很简单嘛” “你说的我都会”…“F Word”
  • 时间…“我干嘛不花这些时间学算法 架构 业务 ???”

讲讲大家都了解的才有趣

Bound Context

IO 涉及面太广了… 减少歧义 -> 约束下本次分享的边界:

  • 网络传输Socket关联的IO
  • 操作系统: Linux
  • 语言 : JAVA
  • TCP/IP协议 : >= TCP (很少再往下了…)
  • NIO -> JAVA NEW I/O(包括blocking & non-blocking)

JAVA 使用者 能触碰多高?

高级编程语言很"弱"很"弱":

  • 函数调用 -> 快…但很多事做不了
  • Kernel调用 -> 叽里咕噜用户/内核态切换…慢…但不得不用

哪怕一句

 System.out.println("HelloWorld")
JVM : 啥是控制台? 打印机在哪? 帮我装个驱动!
Kernel : 你你你...还是让我来吧
.........
.........
软中断 -> 寄存器记录kernel函数&参数 -> 内核执行write.....终于....."HelloWorld"

在这里插入图片描述
(图片来自https://pediaa.com/difference-between-kernel-and-shell)

Kernel -> 一个向上提供系统调用 向下管理IO设备的软件 -> Linux = Kernel + 杂七杂八的Tools

啥是软中断…内核切换…内核调用? -> www.baidu.com
不细谈…但是它耗时间(对我们开发而言往往无感知)

正题: IO

JAVA里的IO到底是啥

API ?

FileInputStream

  /**
     * Reads a byte of data from this input stream. This method blocks
     * if no input is yet available.
     *
     * @return     the next byte of data, or <code>-1</code> if the end of the
     *             file is reached.
     * @exception  IOException  if an I/O error occurs.
     */
    public int read() throws IOException {
        return read0();
    }

    private native int read0() throws IOException;

只是调用函数read0()就能与IO设备通讯?.. 似乎少了点啥

请添加图片描述
(引用reference图片)

JVM: Kernel你去跟IO设备通讯下(同上述HelloWorld例子)

那么…不管JAVA怎么演进 只要JVM还不能直接与IO硬件直接交互… Kernel就扮演真正交互的角色

到这儿…JAVA I/O相关API最终映射为->对应的SystemCall (未来…JVM如果能直接和硬件交互了…将会是新的篇章)

Kernel怎么与IO设备交互的?

Richard Steven 的中五大IO模型

  • BIO (Java stream-related BIO API/NIO API)
  • NIO(Java channel-related NIO API )
  • IO Multiplexing (Java channel-related NIO API )
  • Signal Driven
  • AIO
无论怎么怎样的模型…行为上都可以拆成两个步骤
  • 1 等待data就绪
  • 2 数据从kernel buffer -> 应用程序(JVM实例)

不同IO模型拥有不同的上述行为特征

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GGKPAQcV-1681457354887)(./bio.png)]
图片地址

BIO: 简单的来讲就是-> 进程/线程进行systemCall后, 在Kernel对应读函数执行完毕前会一直Blocked(卡着)

在这里插入图片描述
图片地址

NIO:

  • 在数据就绪前, read相关 systemCall会立即返回 -> 不阻塞进程/线程(Non-blocking)
  • 数据就绪后, read相关 systemCall 阻塞 进程/线程, 直到数据拷贝到应用程序

在这里插入图片描述
图片地址

Multiplexing:

  • 调用select 相关systemCall时, 参数携带对应的文件描述符组(可以理解为sockets组)
  • 在所有sockets组数据都未就绪场景下-> 调用select 相关systemcall的进程/线程处于阻塞状态
  • 数据就绪后, 进程/线程恢复运行状态, 且调用read相关systemCall读取数据(此时read相关systemCall必然能返回一部分数据)
  • read相关systemCall往往设置为NIO模式
  • 从功能上来看, 非常像 “BIO+每个连接独占一个线程” -> 一个进程/线程调用select相关systemCall, 进入阻塞并且监听多个文件描述符(socket)

来个小总结

JDK 1.5前的BIO相关API 只涉及Kernel BIO相关的IO模型对应的systemCall

JDK 1.5后的NIO(new I/O)相关API 涉及Kernel & BIO NIO & Multiplexing 对应的systemCall

来个粗犷点的描述:以功能上来看 JAVA NIO >= JAVA BIO API

正题: JAVA Reactor 模式

为啥用NIO?

BIO 的局限性…大概能搜到一大坨吧(www.baidu.com):

  • 线程
  • 并发量
  • 资源

  • 贵在简单
为啥用JAVA NIO必须扯到 Reactor 模式?

当我们站在巨人的肩膀上时…可还记得地面的触感?

首先…不使用select(multiplexing)相关api能搭建一个基于NIO的应用嘛? YES

代码为证

//@author : Yukai
 public static void main(String[] args) throws IOException {
        List<SocketChannel> socketChannels = new ArrayList();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(9999));

        while (true) {
            /**
            ** 注意了...这儿正在疯狂地进行systemCall !!!!
            * 虽然你感受不到它 
            **/
            SocketChannel sc = ssc.accept();
            if (sc != null) {
                sc.configureBlocking(false);
                socketChannels.add(sc);
                socketChannels.forEach(JavaPureNioDemo::printReadInfo);
            }
        }
    }
    @SneakyThrows
    public static void printReadInfo(SocketChannel sc) {
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 4);
        SocketAddress localAddress = sc.getLocalAddress();
        sc.read(buffer);
        buffer.flip();
        byte[] sys_buffer = new byte[buffer.remaining()];
        buffer.get(sys_buffer);
        System.out.println("socketChannel:"
                + localAddress.toString() + " "
                + new String(sys_buffer));
    }

然而…这样用NIO 往往比Multithreading + BIO 糟糕得多:

BIO+多线程NIO+疯狂轮训
单连接 一次systemCall -> 阻塞 -> 一次请求读取单连接 无限systemcall -> 一次次尝试读取(有时读不到)
一个连接一个线程一个线程管理多个连接
就第一条…就能彻底否决应用这种模式了
那么…能否一个线程管理多个连接同时避免疯狂的systemCall? YES
Reactor 模式 -> 融入Multiplexing 相关 API(Selector)

来自官方长者的推荐(Doug Lea) Scalable IO in Java

Reactor: 将IO事件分发到合适处理器

跟操作系统的IO模型不同, Reactor是一种编程模式…在应用这个模式时会:

  • 调用JAVA Selector相关API -> Kernel Multiplexing相关systemCall
  • 调用JAVA NIO相关API -> Kernel NIO相关systemCall
  • 尝试拆分复杂的问题
  • 应用程序会用适当的阻塞替代"无限轮训"

举个Reactor的应用栗子
我们要写一个简单的嵌入式NIO Web容器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c6xkd96F-1681457354887)(./questionMark.jpg)]

Divide-and-conquer

首先…给自己定个小目标:
  • A 嵌入式
  • B 应用JAVA NIO
  • C 应用Reactor模式
  • D 能跑…能解析http协议下的业务数据包
Then…理一理API层面上,BIO NIO的共性与差异

最大的共性大概是: 都是Stream相关的API, 处理的都是字节流 (byte[])

最大的差异大概是: 数据如何从对应socket中被读取 Link

  • BIO 一次读取(从阻塞状态恢复后, read相关API一个字节一个字节读, 读到 -1为止) -> 读到的数据必定能反序列成一个完整的业务数据包

在这里插入图片描述
(引用reference图)

  • NIO 一次读取(一次read事件下),读取到的byte[]不确定是否对应一个完整的业务数据包: e.g HTTP协议下一次read只读到一个byte, 这是不能反序列化成一个报文的

解决方案: 缓存

在这里插入图片描述

1 用一个对象(XSocket)wrap 每个连接绑定的SocketChannel对象
2 XSocket对象维护一个弹性的(可扩容) 的缓存
3 每次NIO Read事件 -> 尽可能地读(read函数返回>0), 无脑丢该缓存
4 读完后用特定协议对应的解析组件尝试反序列化业务数据包(解析成功后需要对缓存进行操裁剪)
5 执行业务处理逻辑
6 重复步骤 3, 4, 5

然而…上述那些…不用Selector好像也能实现诶
Selecor…到底扮演了什么角色?
我的理解是: 通过使主Loop对应的线程适当阻塞来大幅度减少systemcall (还记得刚才那个不用selector的栗子嘛)


完整的容器设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6G2q5Iq3-1681457354889)(./ssnioServer.jpg)]

论证代码

Reference:

http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
http://tutorials.jenkov.com/java-nio/non-blocking-server.html
W. Richard Stevens - Unix Network Programming Volume 1 3rd Edition - The Sockets Networking API

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值