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 DrivenAIO
无论怎么怎样的模型…行为上都可以拆成两个步骤
- 1 等待data就绪
- 2 数据从kernel buffer -> 应用程序(JVM实例)
不同IO模型拥有不同的上述行为特征
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容器
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的栗子嘛)
完整的容器设计
论证代码
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