使用 Java ServerSocket 实现简单的 HTTP 服务器
1、概览
HTTP 服务器通常用于为发起请求的客户端提供资源。Java 中有一系列生产级 Web 服务器。
本文将带你了解如何使用 ServerSocket
类实现一个简单的 Web 服务器,从而了解 HTTP 服务器是如何工作的。注意,此服务器仅用于教学目的,不适合用于生产。
2、ServerSocket 基础
首先,服务器会监听客户端应用程序的连接。客户端应用程序可以是浏览器、其他程序、API 工具等。连接成功后,服务器会响应客户端连接,向客户端提供资源。
ServerSocket
类提供了在指定端口上创建服务器的方法。它使用 accept()
方法监听指定端口上的传入连接。
accept()
方法会阻塞,直到建立连接,并返回一个 Socket
实例。Socket
实例为服务器和客户端之间的通信提供了对输入和输出流的访问。
3、创建 ServerSocket 实例
首先,创建一个监听指定端口的 ServerSocket
对象:
int port = 8080;
ServerSocket serverSocket = new ServerSocket(port);
接着,使用 accept()
方法接受一个传入连接:
while (true) {
Socket clientSocket = serverSocket.accept();
// ...
}
如上,使用 while
循环等待连接。然后,调用 ServerSocket
对象上的 accept()
方法来监听和接受连接。
连接建立后,该方法会返回一个 Socket
对象,允许服务器和客户端通过已建立的网络进行通信。
4、处理输入和输出
通常,服务器接收来自客户端的输入并发送适当的响应。我们可以使用 Socket
类的 getInputStream()
和 getOutputStream()
方法,通过提供流来读取和写入数据到客户端,从而进行通信。
扩展示例,读取和写入数据流:
while (true) {
// ...
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream())
);
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(clientSocket.getOutputStream())
);
// ...
}
如上,我们使用 clientSocket
对象上的 getInputStream()
方法来获取与客户端和服务器之间活动连接相关联的输入流。为了更高效地读取文本数据,我们将输入流封装在 BufferedReader
中。
同样,getOutputStream()
被封装在 BufferedWriter
中,这样服务器就能方便地向客户端发送响应。
在本例中,输入包括一个 HTTP 请求,例如对 URL - http://localhost:8080
的 GET 请求。
接下来,让我们通过调用 BufferedWriter()
对象上的 write()
方法来往客户端写入服务器响应。一个典型的 HTTP 响应有 Header 和 Body。
首先,定义响应的内容,即 Body:
String body = """
<html>
<head>
<title>Baeldung Home</title>
</head>
<body>
<h1>Baeldung Home Page</h1>
<p>Java Tutorials</p>
<ul>
<li>
<a href="/get-started-with-java-series"> Java </a>
</li>
<li>
<a href="/spring-boot"> Spring </a>
</li>
<li>
<a href="/learn-jpa-hibernate"> Hibernate </a>
</li>
</ul>
</body>
</html>
""";
如上,我们创建了一个简单的 HTML 页面作为响应体。接下来,计算内容长度,并将其添加到 Header 中:
// 注意,如果包含了非 ASCII 字符(如,汉字)的话,则需要统计字节数
int length = body.length();
接下来,把 HTTP Header 和 Body 写入输出流:
while (true) {
// ...
String clientInputLine;
while ((clientInputLine = in.readLine()) != null) {
if (clientInputLine.isEmpty()) {
break;
}
out.write("HTTP/1.0 200 OK\r\n");
out.write("Date: " + now + "\r\n");
out.write("Server: Custom Server\r\n");
out.write("Content-Type: text/html\r\n");
out.write("Content-Length: " + length + "\r\n");
out.write("\r\n");
out.write(body);
}
}
如上,使用 write()
方法写入了 HTTP Header 和 Body。注意,我们使用 \r\n
(空行)来分隔 Header 和 Body,以表示 Header 的结束。
5、多线程服务器
目前,我们的服务器只通过单线程处理请求,这影响了性能。服务器必须能够同时处理多个请求。
重构示例,在单独的线程上处理每个请求。首先,创建一个名为 SimpleHttpServerMultiThreaded
的类:
class SimpleHttpServerMultiThreaded {
private final int port;
private static final int THREAD_POOL_SIZE = 10;
public SimpleHttpServerMultiThreaded(int port) {
this.port = port;
}
}
如上,我们定义了两个字段来表示端口号和线程池大小。端口号在创建服务器对象时通过构造函数传递。
接下来,定义一个方法来处理客户端通信:
void handleClient(Socket clientSocket) {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(clientSocket.getOutputStream()))
) {
String clientInputLine;
while ((clientInputLine = in.readLine()) != null) {
if (clientInputLine.isEmpty()) {
break;
}
}
LocalDateTime now = LocalDateTime.now();
out.write("HTTP/1.0 200 OK\r\n");
out.write("Date: " + now + "\r\n");
out.write("Server: Custom Server\r\n");
out.write("Content-Type: text/html\r\n");
out.write("Content-Length: " + length + "\r\n");
out.write("\r\n");
out.write(body);
} catch (IOException e) {
// ...
} finally {
try {
clientSocket.close();
} catch (IOException e) {
// ...
}
}
}
上面的方法演示了如何处理与客户端的输入和输出通信。Body
和 Length
与上一节中的示例相同。
接下来,创建另一个名为 start()
的方法,在单独的线程上处理每个连接:
void start() throws IOException {
try (ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
Socket clientSocket = serverSocket.accept();
threadPool.execute(() -> handleClient(clientSocket));
}
}
}
如上,通过实例化 ExecutorService
创建了一个线程池。然后,调用 threadPool
对象上的 execute()
方法,为每个客户端连接提交一个任务。
通过将客户端连接分配给线程池中的线程,服务器可以同时处理多个请求,从而显著提高性能。
6、测试
通过在 main
方法中实例化服务器来进行测试:
static void main(String[] args) throws IOException {
int port = 8080;
SimpleHttpServerMultiThreaded server = new SimpleHttpServerMultiThreaded(port);
server.start();
}
启动程序,在浏览器中打开 http://localhost:8080
就可以看到服务器的响应结果。
7、总结
本文介绍了如何使用 ServerSocket
类来实现一个简单的 HTTP 服务器,以及它的多线程版本。
Ref:https://www.baeldung.com/java-serversocket-simple-http-server