概述
本文先从基本的 Socket 编程模式说起,介绍了 Java 传统的同步阻塞 IO 网络编程的基本实现,以及存在的性能问题,从而引出 Reactor 设计模式,最后通过 Java NIO 给出单 Reactor 单线程的实现方案。
Socket 编程模式
Unix 有几个统一性的理念或象征,并塑造了它的 API 及由此形成的开发风格。其中最重要的一点应当是“一切皆文件”模型及在此基础上建立的管道概念。
在 Unix/Linux 环境下,网络中的进程通过 Socket 进行通信,Socket 本质上也是一种特殊的文件,可以按照“打开,读写,关闭”的模式来操作。Socket 编程的基本模式如图 1 所示:
-
创建 socket:本质上就是创建一个文件,每个文件都有一个整型的文件描述符(fd)来指代这个文件;
-
绑定端口:一台服务器可以同时运行多个不同的应用,在 TCP/IP 协议下通过端口进行区分,因此接下来需要绑定端口,所有连接到该端口的请求都会被我们的服务处理;
-
监听端口:执行创建 socket 和
bind
之后,socket 还处于closed
状态,不对外监听,需要调用listen
方法,让 socket 进入被动监听状态;其 API 定义如下:
int listen(int sockfd, int backlog);
// 在TCP协议下,建立连接需要完成三次握手,当连接建立完成后会先放到一个连接队列,backlog就是指定这个队列的大小。
-
接收请求:通过调用
accept()
从已完成连接的队列中拿到连接进行处理,如果没有连接则调用会被阻塞。
基于同步阻塞 IO 的 Java Socket 编程
下面介绍在 Java 语言下如何完成 Socket 通信。传统的 Java Socket 编程模式使用同步阻塞 IO,如图 2 所示。
服务端通过new ServerSocket(端口号)
完成了绑定端口和监听端口的工作,接着循环调用 accept()
方法获取客户端请求(如果没有新的请求,程序就会阻塞),并为每一个客户端请求创建一个处理线程,避免因为主线程正在处理请求而无法响应其他连接。具体实现代码如下:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try {
// 绑定并监听端口
ServerSocket server = new ServerSocket(5566);
Socket client;
while (!Thread.interrupted()) {
// 接受请求,没有请求会阻塞
client = server.accept();
new Thread(new Handler(client)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class Handler implements Runnable {
final Socket client;
public Handler(Socket client) {
this.client = client;
}
@Override
public void run() {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(client.getInputStream()));
// 接收客户端发送的内容
String line;
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(client.getOutputStream()));
// 客户端连接未关闭前,readLine返回值不为null,没有数据时会阻塞
while ((line = reader.readLine()) != null) {
writer.println("你输入的是:" + line);
writer.flush();
// 通过约定特定输入,结束通讯
if ("end".equals(line)) {
break;
}
}
writer.close();
reader.close();
client.close();
} catch (IOException e) {
e.printStackTrace();
}</