同步阻塞IO(简称BIO)是最传统的一种IO模型,即在读和写的过程中会发生阻塞现象。
我们编写一个简单的服务端和客户端程序,寻找一下同步阻塞I/O的弊端
服务端代码
TimeServer
public class TimeServer {
public static void main(String[] args) throws IOException {
ServerSocket server = null;
try {
// 如果端口未被占用且合法则创建成功
server = new ServerSocket(8080);
Socket socket = null;
// 通过无限循环来监听客户端连接
while (true) {
// ★ 1. 如果没有客户端接入,主线程阻塞在accept操作上
socket = server.accept();
// ★ 2. 一个客户端连接,启动一个线程进行处理
new Thread(new TimeServerHandler(socket)).start();
}
} catch (Exception e) {
System.out.println("创建serverSocket失败,端口:" + 8080);
e.printStackTrace();
} finally {
System.out.println("The time server close");
if (server != null) {
server.close();
}
}
}
}
服务端创建一个ServerSocket,然后通过无限循环的方式来监听客户端连接,如果没有客户端接入,则主线程会阻塞在accept操作上。当有客户端连接接入后,启动一个线程来处理,具体处理交给TimeServerHandler,这是个Runnable,使用它为构造函数的参数创建一个新的客户端线程处理这条Socket链路。
TimeServerHandler
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime;
String body;
while (true) {
// ★ 读取一行,如果读到输入流尾部,则返回值为null,退出循环
body = in.readLine();
if (null == body) {
break;
}
System.out.println("The time server receive order : " + body);
// 如果读到了非空值,对内容进行判断,如果请求消息为查询时间的指令 QUERY TIME ORDER,则获取当前系统时间
// 通过PrintWriter的println函数发送给客户端,然后退出循环
if ("QUERY TIME ORDER".equalsIgnoreCase(body)) {
currentTime = new Date(System.currentTimeMillis()).toString();
} else {
currentTime = "BAD ORDER";
}
// ★ 通过printWriter发送给客户端
out.println(currentTime);
}
} catch (IOException e) {
System.out.println("exception");
e.printStackTrace();
// 释放输入流
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
// 释放输出流
if (out != null) {
out.close();
}
// 释放socket套接字句柄资源
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
TimeServerHandler通过Socket读取输入流,每次读一行,如果读到了尾部,则返回null,退出循环。如果读到了非空值,对非空值进行判断,如果请求的消息为查询时间的指令,则获取当前最新的世界,通过PrintWriter的println函数发送给客户端,最后退出循环。
客户端代码
TimeClient
public class TimeClient {
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
//
socket = new Socket("127.0.0.1", 8080);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
// ★ 通过PrintWriter向服务端发送 QUERY TIME ORDER 指令
out.println("QUERY TIME ORDER");
System.out.println("send order to server succeed.");
// ★ 通过BufferedReader读取响应并打印
String response = in.readLine();
System.out.println("Now is : " + response);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放输入流
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
// 释放输出流
if (out != null) {
out.close();
}
// 释放socket套接字句柄资源
if (socket != null) {
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
通过PrintWriter向服务端发送指令,然后通过BufferReader的readLine读取响应内容。
分别执行服务端和客户端,执行结果如下:
服务端执行结果:
The time server receive order : QUERY TIME ORDER
客户端执行结果:
send order to server succeed.
Now is : Sat Jul 18 23:19:43 CST 2020
到此同步阻塞IO的示例程序讲解完毕
Talk is cheap , show me the picture
最后通过一张图来说明BIO的通信模型
总结
我们发现,BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理客户端,一个线程只能处理一个客户端连接。在成千上万个客户端并发连接的情况下,这种模型无法满足高性能、高并发接入的场景。
参考资料
- 《Netty权威指南》第二版 by 李林锋