目录
概览
最近搬砖的帝都民航的啥航电XX设备大数据预测那部分好像黄了。
这边进来一个月不到,我刚动手搬砖就停手项目这套操作是真滴尴尬,不过发现这家有自研测运行维护数据的硬件发送tcp(为啥断断续续短连接不用udp?)报文到InfluxDB使用的是netty,一个臭名昭著大名鼎鼎的玩意,干脆来深入瞎学一下。
但是在看源码之前先复习下反应器模式感觉更好,一种设计模式,在redis,netty等网络应用上使用,看看这个 Doug Lea(令人景仰的神奇大爷)写的NIO.pdf,简单翻译然后自己写点笔记。
发现有错误地方请留言,谢谢。
网络服务特性
- 高扩展可用网络服务
- 事件驱动处理
- 基本版本、多线程版本、其他
- JAVA非阻塞API
类似的读、解码、处理服务、发送等
但使用情景不同XML解释、文件传输、网页生成、电脑端服务等
然后大多都是有自己线程的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,页面错误等)
- 必须保持追踪服务的逻辑状态
- 要分散进简单的NIO行为
例子java.awt.event:java中awt的事件触发,事件驱动的IO类似这种结构但设计不同
Reactor 模式
- 反应器通过Dispacth调度正合适的Hanlder对IO事件响应,类似AWT thread
- Handlers表现为NIO行为,类似AWT ActionListeners
- 通过绑定handlers到事件来管理,类似AWT addActionListeners
单线程版
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里面有我就不献丑了