前言
前几天初步接触了解了NIO,发现性能方面完爆BIO。因此决定将之前一个项目的服务端改造成NIO。但是NIO学习难度比BIO大,因此在网上查找相关资料,然后将我自己理解并简化的NIO服务器搭建思路在这里做个记录分享。因此会有一些错误和忽略的地方,若要更加详细请点击下方的原文。
思路来源:《Java NIO文档》非阻塞式服务器,原文:Java NIO Tutorial.
NIO相关的这里就不做相关介绍,毕竟不能算真正的教程。
大致思路如下:
整个系统主要为两个线程:AccepterThread,ProcessorThread。分别用于接收成功连接的Socket对象以及对消息的接收处理。
两个线程之间使用队列通信,用于传递Socket对象
在消息的处理中又对Channel中的操作分为读操作和写操作,这两个操作分别建立两个不同的类用来执行,原作者对此的解释是为了防止读取数据的时候数据缺失以及写数据的时候未完全写入。
读写操作分别又两个对应的Selector对其进行监听。
请求的接收
对请求的接收就和一般的ServerSocket一样,while循环直到有请求进来,这里原文将准备好的SocketChannel包装进另外编写的Socket类中,这个Socket类相关定义如下:
public class Socket {
public long socketId;
public SocketChannel socketChannel;
public MessageReader reader;
public MessageWriter writer;
public Socket(SocketChannel channel) {
this.socketChannel = channel;
}
public int read(Bytebuffer buffer) {
//读方法实现
}
public int write(Bytebuffer buffer) {
//写方法实现
}
}
这里将最重要的读写操作进行包装,并且使用自己的消息读/写器来对Channel中的字节进行读写操作。
Accepter在包装好Socket对象后将该对象add进队列中。
请求的处理
在Processor中,存在一个while(true)循环,该循环遍历连接AccepterThread的队列,一旦队列中有对象,则进行数据的处理操作。
Processor中存在两个Selector,一个负责读,另一个负责写。
包装并注册
在Accepter中包装的Socket对象只有SocketChannel,因此在进行信息的读写之前要将对应的读写器以及SocketId装入Socket对象中。这里原作者对此的操作要规范复杂的多,翻译的原文是:
一个Message Reader一定满足特定的协议。Message Reader需要知道它尝试读取的消息的消息格式。如果我们的服务器可以通过协议来复用,那它需要有能够插入Message Reader实现的功能 – 可能通过接收一个Message Reader工厂作为配置参数。
- 但是我这个系统不需要如此规范,因此只要能够对消息进行读写以及确定消息完全发送完毕即可。
在这里进行包装的好处是能给每个SocketChannel都有读/写器,甚至可以让每个SocketChannel的读写器不同。
再将信息包装好后将Socket中的SocketChannel注册进读Selector中,同时将Socket对象作为附件装入。
等待消息。
读取和处理
当消息到达后触发Selector,将读就绪的SocketChannel获取出来。其中的附件是包装了的Socket对象,里面有这个SocketChannel对应的读写器。将信息完整读取后就是对信息的处理了,在处理这里采用的是函数处理。即在程序的入口处就将处理程序设定完毕,然后将对应的函数传递给Processor,这样当我们需要对处理处理进行修改的时候不需要进入程序的主体,而只要在入口处修改即可。同时读写之间也是采用队列进行通信。信息处理完成后如果需要返回相关细心或者要将信息发送给其他SocketChannel,就可以将该操作offer进写队列当中。
写入
对信息处理完成如果要进行写操作,就将SocketChannel注册进写Selector中。这里一开始觉得为什么不直接从队列中获取到对象就就进行写操作,仔细考虑后发现无法保证这个SocketChannel在执行写的时候Client是没有数据发送过来的,因此要将该SocketChannel注册进写Selector中保证在写就绪状态进行数据写入。 同时在写Selector中也不能保证所有都是对于流程来说可以写就绪的,毕竟空闲的时候就处于写就绪状态,但是此时服务器根本不会主动发送信息给客户端,因此要检测所有在写Selector中注册的SocketChannel中的Message是否写入完毕,如果写入完毕则将其移除Selector如果没有则写入。
不同Channel间通信
只要在获取到包装好的Socket对象后将其保存在Hash表中则可以随时根据Socket的Id获取到对应的SocketChannel然后根据将要进行的操作将该Socket添加到对应的队列中等待注册。