大家儿童节快乐,我是小黑。
今天和大家分享网络编程中的阻塞式IO模型。网络编程中有5钟常见的IO模型:同步阻塞式IO模型、同步非阻塞式IO模型、多路复用IO模型、信号驱动IO模型和异步IO模型。这篇博客暂时先分享阻塞式IO模型,其他的IO模型会在后续的博客里面持续更新。
什么是同步阻塞式IO模型
网络传输中,数据通过计算机网络传输到机器,直到程序读取到数据,会经历两个步骤:
- 数据从网卡拷贝到内核空间
- 数据从内核空间拷贝到用户空间,供应用程序读取
各种IO模型的区别就在于这两个步骤的方式不一样,同步阻塞式IO的做法是:用户线程发起 read() 调用后就阻塞了,让出CPU。内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程唤醒。整个过程都是阻塞住的,如图所示。
单线程同步阻塞式IO模型的实现
这里实现一个最简单的单线程版本阻塞式IO模型,但麻雀虽小五脏俱全。
示例是一个Echo服务器,客户端发送什么数据,服务端就在加上一个日期然后原样返回。
我们先看看服务端怎么实现,代码如下。主体是一个死循环,阻塞在 accept() 方法上,有连接建立才会返回。返回后会调用 handle() 方法进行处理,一般都分为三个大步骤:读取客户端数据;处理客户端数据;结果返回给客户端。
public class EchoServer {
private ServerSocket serverSocket;
public static void main(String[] args) throws IOException {
new EchoServer().start();
}
public EchoServer() throws IOException {
serverSocket = new ServerSocket(8001); // 初始化一个ServerSocket,绑定8001端口
}
public void start() {
for (; ; ) {
Socket socket = null;
try {
socket = serverSocket.accept(); // 接收到一个客户端的连接
handle(socket); // 进行处理,主要的步骤是:read->process->write
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handle(Socket socket) {
BufferedReader bufferedReader = null;
PrintWriter printWriter = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
printWriter = new PrintWriter(socket.getOutputStream(), true);
String line = null;
while ((line = bufferedReader.readLine()) != null) { // read 读取客户端数据
String echoMsg = new Date() + ": " + line; // process 处理客户端数据
printWriter.println(echoMsg); // write 结果返回给客户端
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (Objects.nonNull(printWriter)) {
printWriter.close();
}
if (Objects.nonNull(bufferedReader)) {
bufferedReader.close();
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
复制代码
再看看客户端代码。首先和服务端连接,然后读取控制台的数据发送到服务端,并等待服务端响应。
public class EchoClient {
private Socket socket;
public EchoClient() {
socket = new Socket();
}
public static void main(String[] args) throws IOException {
new EchoClient().start();
}
public void start() throws IOException {
socket.connect(new InetSocketAddress("localhost", 8001)); // 建立连接
try {
BufferedReader localReader = new BufferedReader(new InputStreamReader(System.in));
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
BufferedReader bufReader = new BufferedReader(new InputStreamReader(in));
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(out), true);
String msg = null;
while ((msg = localReader.readLine()) != null) {
printWriter.println(msg); // 发送数据
System.out.println(bufReader.readLine()); // 接受响应
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.isNull(socket)) {
socket.close();
}
}
}
}
复制代码
跑起来的示例如下,绿色字体是客户端在控制台输入的数据,黑色字体是服务端响应的数据。
单线程版本的同步阻塞式IO模型有哪些问题
我们启动一个EchoServer,然后并行启动多个EchoClient,就能发现单线程版本的同步阻塞式IO模型都会有哪些问题。EchoServer只有一个线程,这个线程要独自一人读取客户端数据(read),进行业务处理(process),把响应发回给客户端(write)。中间任何一个步骤都可能会阻塞或者占用大块的CPU时间,如果连接一多,整个服务器势必会阻塞住很多客户端的连接,影响通信性能。
现实中,有许多实际应用要求服务器具有同时为多个客户端提供服务的能力,比如我们每天都在用的HTTP服务器,如果因为客户端过多造成长时间等待,会使得网站失去信誉,从而降低访问量。所以,单线程版本的同步阻塞式IO模型的问题在于:它是单线程的,不能充分发挥现在机器多线程的能力,无法支持并行处理多个用户请求的场景。
想要解决这个问题,就要用多线程,详情见下一篇:同步阻塞式IO模型-多线程版本,马上更新。
白白。
Ref
- 极客时间:《深入拆解Tomcat&Jetty》
- 孙卫琴:《Java网络编程精解》
- Budi Kurniawan、Paul Deck:《深入剖析Tomcat》
- 葛一鸣:《实战Java高并发程序设计》