1、NIO实现Client-Server通讯
我们将NIO实现Client-Server通讯的流程分为4步,见下面伪代码:
class NioServer{
main(){
初始化 nio 相关组件
for(循环处理监听端口的事件){
1、与客户端建立连接;
2、读取客户端发送的数据;
3、处理数据;
4、发送响应给客户端
}
}
}
流程示意图如下
图 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
创建一个ServerSocketChannel,并将其注册到Selector
注册结果返回一个SelectioinKey,我们用下图表示
当一个客户端client-1发送连接请求时,ServerSocketChannel会监听到这个请求事件,select方法会将该事件对应SelectionKey放入Set中,调用ServerSocketChannel的accept方法创建一个SocketChannel与客户端建立连接
1、与客户端建立连接
假设有个client-1创建一个SocketChannel发送一个请求过来,服务端创建一个SocketChannel与之建立连接,可以把这两个SocketChannel看做网络连接两头的端点
client-2、…client-n 发送连接请求也是如此
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)的引用,见下面第一张图 (第二张关系图有兴趣可以看看):
可以通过下面两个方法获取 Selector
和 Channel
:
SelectableChannel channel = SelectionKey.channel();
Selector selector = SelectionKey.selector();
|SelectionKey 简略图|
|
三者关系图 |
1.2、启动服务端
启动服务端就是使用ServerSocketChannel 监听端口,循环执行 Selector.select()
这个阻塞方法来获取 SelectionKey
集合,即在有事件发生时,JDK 底层会基于OS,将所有事件封装为Set集合 Set<SelectionKey>
作为 select
方法的返回值
1.3、处理客户端连接请求
服务启动后,一个客户端发送连接请求过来,select
方法退出阻塞,返回 Set<SelectionKey>
|
图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
中
1.4、处理客户端消息
建立好连接后,客户端就可以发送消息了,服务端我们使用 RWHandler
处理器来处理客户端发送过来的消息
不同的事件需要不同的 “事件处理器” 来处理,此时我们就需要一个 “路由转发机制”(SelectionKey
的attach()
方法了解一下)
+++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++
参考资料
网络编程基础(3) : IO多路复用(单线程)网络编程基础(4) : IO多路复用(多线程)
网络编程基础(5) : IO多路复用(多Reactor)(主从式Reactor)