异步非阻塞网络通讯技术研究
2019-12-03段楠
段楠
(吉林大学软件学院,长春130000)
1 网络通讯的方式
在网络通讯的开发中,存在着同步/异步、阻塞/非阻塞的多种方式,不同方式的软件性能与开发成本各不相同,可根据软件需求进行选择。以下介绍同步/异步、阻塞/非阻塞在网络通讯中的概念,和应用程序与操作系统内核之间同步/异步、阻塞/非阻塞的概念不尽相同。
1.1 同步与异步
同步与异步在网络通讯环境下更倾向于对客户端通讯方式的描述。
同步是指客户端向服务器发送请求后,必须等到服务器的响应到来才可进行下一步操作。
异步是指客户端向服务器发送请求后,不必等待服务器响应并可继续其他操作,待服务器发来响应后,客户端会得到I/O 操作完成的通知,只需对结果进行处理即可。
1.2 阻塞与非阻塞
阻塞与非阻塞在网络通讯环境下更倾向于对服务器通讯方式的描述。
阻塞是指服务器在接受某个客户端的请求后,触发相应函数,无论能不能执行都会一直等待,直到当前函数执行结束后才能进行下一步操作。
非阻塞是指服务器在接收客户端的请求后,向操作系统注册I/O 监听,如果当前不能读写会立刻返回并执行其他操作。当读写可执行时,操作系统会通知服务器应用程序执行读写操作,触发相应函数。
1.3 Java的三种网络通讯方式
Java 在网络通讯方面最早只有实现同步阻塞方式的BIO 组件,BIO 服务器的实现方式为给每一个客户端的连接提供一个进程。即使当前客户端没有执行任何操作,该线程也会一直阻塞等待。这种方式显然会带来很大的资源浪费。在这种方式下实现并发连接需要对服务器使用多线程技术,为每个客户端连接增加一个新的线程。可见,这种方式会消耗大量的资源。
在JDK 1.4 之后,Java 开始支持同步非阻塞的通讯方式,即NIO 组件。NIO 服务器的实现方式为给每个客户端的请求提供一个线程。客户端的连接会注册到多路复用器上,多路复用器进行轮询,当某个连接有I/O请求时启动一个线程进行处理。
在JDK 7 之后,Java 推出了具有异步非阻塞通讯功能的AIO 组件。AIO 服务器的实现方式为给每个有效请求提供一个进程。即操作系统对I/O 请求执行完之后,再通知服务器应用程序启动线程进行处理。
以上三种通讯方式可根据软件应用的具体需求进行选择。BIO 适合并发量较小的应用,且对服务器的资源要求较高。但实现方式简单,可快速开发。NIO适合并发量较大且多数为短连接的应用,实现方式较为复杂。AIO 适合并发量较大且多数为长连接的应用,会调动操作系统实现并发操作,实现方式较为复杂。
2 异步非阻塞通讯
2.1 异步非阻塞
同步/异步、阻塞/非阻塞原本是应用程序与操作系统交互时的一组概念。同步与异步指的是应用程序的方法调用是否需要等待结果的返回,才能进行其他操作。阻塞与非阻塞指的是一个读写操作是否需要等待操作系统内核的所有读写完成,才算完成。而在网络通讯中这组概念使用的也是系统内核中的基本原理,因此了解系统内核中异步非阻塞的原理是十分有必要的。
在应用程序与系统内核交互中,异步指的是,应用程序调用读写操作方法后,不须一直等待结果的返回,而是可以继续进行其他的操作。当读写完成后,会由操作系统通知到应用程序,再由应用程序对结果进行处理。
非阻塞是指在系统底层,进行一个读写操作时CPU 无须等待当前操作的所有内核I/O 全部完成,而是每个内核I/O 完成之后会立刻返回一个状态,此时CPU 就可以继续执行其他任务。当某个读写操作的内核I/O 全部完成之后,该读写操作完成。而CPU 需要判断某个读写操作当前是否有内核I/O 请求,以及确认读写操作是否完成并取得数据,这就需要CPU 对读写操作的控制机制。主要有“轮询”和“中断”两种机制。“轮询”是指CPU 通过循环对所有I/O 访问,得知当前读写操作的状态。轮询过程中应用程序需要等待CPU的询问,因此是一种同步非阻塞的方式。“中断”是指读写操作有I/O 请求时主动请求CPU 为其分配内核资源。而应用程序在发送读写请求后只需等待系统将结果返回即可,此时可执行其他操作,因此时一种异步非阻塞的方式。
系统内核层面的异步非阻塞,与网络通讯层面的异步非阻塞原理大致相同,只是描述对象有所差别。上述“轮询”方式类似于Java NIO 的实现方式,服务器对客户端的请求进行轮询处理,来查找某个客户端是
否有数据请求。上述“中断”方式类似于Java AIO 的实现方式,客户端的请求到来后,首先由操作系统进行I/O操作,然后将结果通知给服务器应用程序,进行一些处理后再返回响应给客户端。此时客户端不需要等待服务器的轮询,只需等待结果即可。Java AIO 显然是一种异步非阻塞的通讯方式,以下详细阐述该技术。
2.2 Java异步非阻塞通讯原理
Java 异步非阻塞通讯组件AIO 使用“订阅-通知”方式进行实现。即服务器应用程序向操作系统注册I/O监听,当操作系统完成I/O 操作之后,通知服务器应用程序进行处理,触发相应函数。
在服务器应用程序中,当需要读写时只需调用read 和write 方法。这两种方法都是非阻塞的。进行read 操作时,操作系统将客户端的I/O 请求处理完成后,将数据放入read 的缓冲区,并通知服务器应用程序对数据进行处理。进行write 操作时,服务器应用程序将数据写入write 缓冲区,操作系统从缓冲区取得数据并进行I/O 操作,操作完成后通知服务器应用程序进行回调操作。read 和write 可在读写操作完成后通过回调函数的方式进行回调操作。回调方法包括completed方法和failed 方法。completed 方法在读写操作成功后回调执行,一般会在其中对ByteBuffer 数据进行业务逻辑处理,以及递归调用下一个读写操作。failed 方法在读写操作失败后回调执行,一般向控制台或日志文件打印异常信息。
AIO 中有如下几个重要元素:
Channel:是应用程序与操作系统之间的通道,通过该通道可实现应用程序与操作系统之间的数据传输。Channel 包括:AsynchronousServerSocketChannel,实现服务器应用程序对操作系统的监听与操作系统对服务器应用程序的通知。AsynchronousSocketChannel,实现服务器对TCP 套接字的监听。DatagramChannel,实现服务器对UDP 套接字的监听。AsynchronousFileChannel,实现服务器应用程序对文件数据I/O 的监听。
ByteBuffer:为每一种Channel 提供的数据缓存区,用于Channel 中数据的交换。ByteBuffer 中有一个指向当前读写位置的索引,每次读写结束后会停留在最后数据的位置。因此每个ByteBuffer 对应的通道在每次读操作前,需要使用flip 方法将该索引回到初始位置。每次读操作后,需要使用clear 方法将ByteBuffer清空,以便下次读入。
Attachment:通道的附件,在嵌套或递归的读写操作中起到上下文的作用。
3 Java异步非阻塞通讯实现
以时间发送程序为例,给出Java AIO 实现的一个简单示范。服务器每收到一个客户端请求,就发送系统当前时间响应给客户端。
3.1 服务器关键代码
首先创建AsynchronousServerSocketChannel,可以用线程池的方式创建。
ExecutorService executor=Executors.newFixedThreadPool(20);
AsynchronousChannelGroup group=AsynchronousChannel-Group.withThreadPool(executor);
AsynchronousServerSocketChannel serverSocketChannel=AsynchronousServerSocketChannel.open(group);
然后对指定的主机及端口进行绑定,并监听发来的连接请求。监听方法中一定要绑定回调方法来执行监听成功后的下一步操作。需要自己编写Completion-Handler 接口的实现类,并重写completed 与failed方法。
serverSocketChannel.bind (new InetSocketAddress ("0.0.0.0",33335));
serverSocketChannel.accept(null,new AcceptHandler(serverSocketChannel));
以下为CompletionHandler 接口的实现类AcceptHandler 的completed 方法,参数需要Asynchronous-SocketChannel 作为当前连接客户端的通道,attachment可根据需要决定是否传入有效数据。
public void completed(AsynchronousSocketChannel socketChannel,Object attachment)
递归进行客户端连接监听。
serverSocketChannel.accept(attachment,this);
读取客户端发来的数据。将需要发送的数据用ByteBuffer 封装并传入第一个参数。第二个参数为attachment,如不需要上下文对象可传入null。第三个参数即回调方法类,可使用匿名内部类的方式实现。
ByteBuffer buffer=ByteBuffer.allocate(1024);
socketChannel.read(buffer,null,new CompletionHandler
读取客户端发来的消息并解码。
buffer.flip();
String request=StandardCharsets.UTF_8.decode(buffer).toString();
处理业务逻辑。write 方法向客户端写入响应数据,由于不需要递归调用所以只传入数据缓冲参数即可。
if(request.equals("time")){
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date=sdf.format(new Date());
socketChannel.write(ByteBuffer.wrap(date.getBytes("utf-8"))).get();
}else{
socketChannel.write(ByteBuffer.wrap("非法输入".get-Bytes("utf-8"))).get();
}
递归进行下次数据读取。
buffer.clear();
socketChannel.read(buffer,null,this);
3.2 客户端关键代码
客户端首先要打开AsynchronousSocketChannel 并连接服务器。
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect (new InetSocketAddress ("127.0.0.1",33335)).get();
执行写操作,给服务器发送请求。
socketChannel.write (ByteBuffer.wrap (request.getBytes("utf-8")),null,newCompletionHandler
读取服务器发来的响应。
ByteBuffer buffer=ByteBuffer.allocate(1024);
socketChannel.read(buffer,null,new CompletionHandler
进行输出,之后清空缓冲区。
buffer.flip();
String response=StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(response);
buffer.clear();
递归调用下次写操作,实现客户端可重复向服务器发送数据。
String request=in.readLine();
socketChannel.write (ByteBuffer.wrap (request.getBytes("utf-8")),null,this);
4 结语
本文首先介绍了同步与异步、阻塞与非阻塞的概念,从操作系统内核的原始原理到网络通讯的具体情形对异步非阻塞进行了具体阐述。然后以Java 开发为例,给出了异步非阻塞网络通讯的具体实例。阐述了Java AIO 的基本原理,示范了其实现的具体方式。