1、NIO实现Client-Server通讯

我们将NIO实现Client-Server通讯的流程分为4步,见下面伪代码:

class NioServer{    
main(){
初始化 nio 相关组件
for(循环处理监听端口的事件){
1、与客户端建立连接;
2、读取客户端发送的数据;
3、处理数据;
4、发送响应给客户端
}
}
}

流程示意图如下

一步步Netty的基石 - Reactor模式_服务端

图 1-1

我们写代码实现一下

public class NioServer {
public static void main(String[] args) throws IOException {
// 初始化 nio 相关组件
Selector sel = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 开始监听9000端口
ssc.bind(new InetSocketAddress(9000));
// 循环处理9000端口的事件
while (!Thread.interrupted()) {
// 将当前所有的事件封装为SelectionKey放入Set中
sel.select();
Set keySet = sel.selectedKeys();
Iterator it = keySet.iterator();
// 遍历Set中的事件
while (it.hasNext()) {
SelectionKey key = (SelectionKey) (it.next());
if (key.isAcceptable()) {
// 1、accept建立连接
SocketChannel channel = ssc.accept();
channel.register(sel, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 2、读取客户端发送的数据
String readedStr = SocketReader.read(channel);
// 3、处理数据
String processedStr = process(readedStr);
// 4、发送响应给客户端
channel.write(ByteBuffer.wrap(processedStr.getBytes()));
}
it.remove();
}
}
}

public static String process(String str) {
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "processed string:" + str;
}
}

一步一图分析处理流程

初始化nio服务端

创建一个Selector,Selector下有个Set

一步步Netty的基石 - Reactor模式_数据_02


创建一个ServerSocketChannel,并将其注册到Selector

一步步Netty的基石 - Reactor模式_客户端_03


注册结果返回一个SelectioinKey,我们用下图表示

一步步Netty的基石 - Reactor模式_数据_04


当一个客户端client-1发送连接请求时,ServerSocketChannel会监听到这个请求事件,select方法会将该事件对应SelectionKey放入Set中,调用ServerSocketChannel的accept方法创建一个SocketChannel与客户端建立连接

一步步Netty的基石 - Reactor模式_客户端_05

1、与客户端建立连接

假设有个client-1创建一个SocketChannel发送一个请求过来,服务端创建一个SocketChannel与之建立连接,可以把这两个SocketChannel看做网络连接两头的端点

一步步Netty的基石 - Reactor模式_客户端_06

client-2、…client-n 发送连接请求也是如此

一步步Netty的基石 - Reactor模式_数据_07

2、读取客户端发送的数据

客户端与服务端建立连接后,client就可以向

3、处理数据

4、发送响应给客户端

1 ~ 4 步是串行阻塞执行的,无法充分利用现代多核CPU的性能,这个实现性很低!

如何改进呢? 使用多线程使得1 ~ 4步并行执行。

我们一步步来重构这个流程。

业务数据处理的时间一般都比较长,所以我们首先使用单独的线程来执行第3步 ,因为第4步依赖第3步,所以第4步也放入线程中,示例代码如下:

class Server{
main(){
for(循环处理监听端口的事件){
1、accept建立连接;
2、读取客户端发送的数据;
new Thread(new Runnable(){
3、处理数据;
4、发送响应给客户端
}).start();
}
}
}

这里


Reactor 反应器模式,角色有

1、Reactor

负责派发IO事件给对应的处理器处理。

连接请求发送给连接处理器-Acceptor,

2、 Acceptor

负责接受client的连线请求,然后给client绑定一个Handler并注册IO事件到Reactor上监听。

3、Handler
负责处理与client交互的事件或行为。

通常因为Handler要处理与所对应client交互的多个事件或行为,可以状态模式来实现Handler。

Reactor 将不同的事件转发到不同的 “事件处理器” 进行处理

1、Reactor 流程

我们以单个客户端连接为例,介绍一下整个流程。

1.1、初始化NIO网络的设置

大家需要明白 SelectionKey 中有 Selector和Channel(ServerSocketChannel 或 SocketChannel)的引用,见下面第一张图 (第二张关系图有兴趣可以看看):

一步步Netty的基石 - Reactor模式_服务端_08

可以通过下面两个方法获取 ​​Selector​​​ 和 ​​Channel​​ :

SelectableChannel channel = SelectionKey.channel();
Selector selector = SelectionKey.selector();

|SelectionKey 简略图|

一步步Netty的基石 - Reactor模式_数据_09

三者关系图

1.2、启动服务端

启动服务端就是使用ServerSocketChannel 监听端口,循环执行 ​​Selector.select()​​​ 这个阻塞方法来获取 ​​SelectionKey​​​集合,即在有事件发生时,JDK 底层会基于OS,将所有事件封装为Set集合 ​​Set<SelectionKey>​​​ 作为 ​​select​​ 方法的返回值

1.3、处理客户端连接请求

服务启动后,一个客户端发送连接请求过来,​​select​​​ 方法退出阻塞,返回 ​​Set<SelectionKey>​

一步步Netty的基石 - Reactor模式_服务端_10

图1 - 启动服务端

下面我们来完成服务端的部分功能:网络服务初始化工作,代码如下:

伪代码:

class STReactor {
STReactor (){
1、监听指定的端口;
2、向Selector注册关注的 “连接请求事件”;
}
run(){
while(true){
if(selector.select()>0){
3、获取请求事件;
//4、TODO 处理请求
}
}
}
}

具体实现代码:

public class STReactor implements Runnable {
private final ServerSocketChannel ssc;
private final Selector selector;
public STReactor(int port) throws IOException {
// 1、监听指定的端口
selector = Selector.open();
ssc = ServerSocketChannel.open();
InetSocketAddress addr = new InetSocketAddress(port);
ssc.bind(addr);
ssc.configureBlocking(false);
// 2、向Selector注册关注的 “连接请求事件”
SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT);
}
@Override
public void run() {
while (!Thread.interrupted()) {
try {
if (selector.select() == 0){
continue;
}
} catch (IOException e) {
e.printStackTrace();
}
Set<SelectionKey> selectedKeys = selector.selectedKeys(); //已就绪事件的key集合
Iterator<SelectionKey> it = selectedKeys.iterator();
while (it.hasNext()) {
// 3、获取请求事件
SelectionKey sk = (it.next());
// 4、TODO 处理请求
it.remove();
}
}
}
}

我们使用 ​​AcceptHandler​​​ 来处理连接请求,最终建立连接,所谓的连接,从JDK的角度来看就是 ​​SocketChannel​​​ ,且以 ​​SelectionKey​​​的形式存在于 ​​Selector​​中

一步步Netty的基石 - Reactor模式_服务端_11

1.4、处理客户端消息

建立好连接后,客户端就可以发送消息了,服务端我们使用 ​​RWHandler​​ 处理器来处理客户端发送过来的消息


一步步Netty的基石 - Reactor模式_服务端_12

不同的事件需要不同的 “事件处理器” 来处理,此时我们就需要一个 “路由转发机制”(​​SelectionKey​​​ 的​​attach()​​方法了解一下)

+++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++

一步步Netty的基石 - Reactor模式_客户端_13

参考资料

​​网络编程基础(3) : IO多路复用(单线程)​​网络编程基础(4) : IO多路复用(多线程)
网络编程基础(5) : IO多路复用(多Reactor)(主从式Reactor)