背景
这里的I/O主要指网络编程,即客户端和服务器之间的通讯。任何高并发高性能的网络通讯都是从单一的客户端服务端应答逐步演化而来的。此文主要介绍这个演化的过程,以及各种I/O模型对于实现高并发高性能的做法。这也是《Netty权威指南(第二版)》第二章给出的几个例子的深入解读,其中包含了自己的理解。采用的源代码可以从这个项目下载
https://github.com/wuyinxian124/nettybook2
其中Netty部分用5.0.0作为示例,这个版本已经被废除了,所以我尝试改用最新的推荐稳定版本4.1.25,但例子不能很好的兼容,所以重新改写了示例,所以和书中原例略有不同。
网络编程之前没有接触过,所以是0基础。刚开始,只需要知道一个服务器(以下统一称server)在自己的某端口提供服务,一个客户端(以下统一称client)根据server的ip和端口,和服务器建立连接,之后client向server发请求(以下统一称request),并从server获取响应(以下统一称response)。
以下不论哪种I/O模型,从业务上都是一个事情,即client和server连接后,向server请求当前时间(服务端的唯一业务逻辑及返回就是
new java.util.Date(System.currentTimeMillis()).toString()
),并获取到正确的结果。不同之处在于实现手段和线程的行为存在着差异,这种差异也造成了实现的难度和对高性能的支持程度。
所谓实现手段,可以归结为以下几类
- 原始unix的操作系统级别调用,当然这类在Java开发层面是不需要的,但原理应该掌握,即使是使用后面几种方式,最底层还是unix的系统调用;
- Java原生API,不论使用阻塞式还是NIO,都可以理解为调用相关类(server端或client端)来实现连接和通讯;
- Netty,其简化了调用原生API的步骤,并隐藏了实现高性能的相关细节,本质还是对原生API或系统的调用。
阻塞式I/O(BIO & PIO)
阻塞式I/O(Blocking I/O)是最简单的一种C/S通讯模式。所谓阻塞,就是client在发出request和获得response之间的时间里,是停止的,不执行任何代码的,就像堵在那里一样,是为阻塞。
BIO是基于流的,所以在代码中必须有各种读(read)和写(write)的stream,stream的使用也是阻塞的原因之一。
在网络条件较好,且业务比较简单,client的数量又相对较少的情况下,这种原始的方式是可以接受的。阻塞的时间会短到无法察觉,所以并不是说BIO不好,只是这是一种非常简单的实现方式,而且是学习网络编程的一个重要起点。
Server创建及建立连接
说明:除非需要大段说明,我把一些自己的理解以注释的方式和代码结合在一起,下同
try {
// Server端首先需要一个ServerSocket对象,会绑定一个port,用于监听来自于client的request
server = new ServerSocket(port);
System.out.println("The time server is start in port : " + port);
Socket socket = null;
// 无限循环可以一直等待请求,除非强制关闭
while (true) {
// accept方法根据JavaDoc的描述,当有一个连接来的时候,这里会返回一个Socket对象,如果没有连接,会一直block当前的代码
// Socket对象是一个连接点(endpoint),用于两个机器(即client和server)之间的通讯
socket = server.accept();
// 一但Socket对象有了,下面进入实际的处理,在Handler里再展开
new Thread(new TimeServerHandler(socket)).start();
}
} finally {
if (server != null) {
System.out.println("The time server close");
server.close();
server = null;
}
}
Server读Request并写Response
try {
// 从socket这个连接点获取读和写的流的句柄
in = new BufferedReader(new InputStreamReader(
this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime = null;
String body = null;
while (true) {
// 从输入流按行读取数据,如果读不到东西了,就退出
body = in.readLine();
if (body == null)
break;
System.out.println("The time server receive order : " + body);
// 如果读到的数据(即request)是QUERY TIME ORDER,则生成一个结果(即当前时间)
currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
// 往输出流写入这个结果当做response,注意此处的out是socket的输出,即指向client,不是在屏幕上打印
out.println(currentTime);
}
} catch (Exception e) {
// ...
}
Client建立连接,发送request和获取response
客户端在main函数中用一个socket连接发送request并等待response
try {
// new一个Socket相当于和服务端建立一个连接,即Socket的endpoint功能
socket = new Socket("127.0.0.1", port);
// in表示从该通道读取数据,即response
in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
// out表示向该通道写入数据,即request
out = new PrintWriter(socket.getOutputStream(), true);
out.println("QUERY TIME ORDER");
System.out.println("Send order 2 server succeed.");
// 等待读服务器的response
// 此处如果网络不好或业务量巨大,客户端会被迫一直等待,是为阻塞
String resp = in.readLine();
System.out.println("Now is : " + resp);
} catch (Exception e) {
e.printStackTrace();
}
PIO的改进仅在线程的创建
在原始的BIO编程模型中,server每接受到一个连接请求,就new一个新的线程进行处理,如下
new Thread(new TimeServerHandler(socket)).start();
为了避免无限制的多开线程导致系统性能的急剧下降,可以采用线程池来控制线程数量和管理线程,在收到连接后,用线程池启动线程,实现如下
try {
server = new ServerSocket(port);
System.out.println("The time server is start in port : " + port);
Socket socket = null;
// 提前创建一个I/O任务线程池
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(
50, 10000);
while (true) {
socket = server.accept();
// 用线程池启动线程,替换new Thread().start()方法
singleExecutor.execute(new TimeServerHandler(socket));
}
}
TimeServerHandlerExecutePool
的构造函数如下
// 根据线程池定义,构造一个大小合适的线程池
public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime()
.availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS,
new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
}
PIO本质还是阻塞式I/O,只不过在线程的管理上有了一些改进。
【未完待续,下一部分开始进入NIO的奇妙世界】
网友评论