1、概览 {#1概览}
HTTP 服务器通常用于为发起请求的客户端提供资源。Java 中有一系列生产级 Web 服务器。
本文将带你了解如何使用 ServerSocket
类实现一个简单的 Web 服务器,从而了解 HTTP 服务器是如何工作的。注意,此服务器仅用于教学目的,不适合用于生产。
2、ServerSocket 基础 {#2serversocket-基础}
首先,服务器会监听客户端应用程序的连接。客户端应用程序可以是浏览器、其他程序、API 工具等。连接成功后,服务器会响应客户端连接,向客户端提供资源。
ServerSocket
类提供了在指定端口上创建服务器的方法。它使用 accept()
方法监听指定端口上的传入连接。
accept()
方法会阻塞,直到建立连接,并返回一个 Socket
实例。Socket
实例为服务器和客户端之间的通信提供了对输入和输出流的访问。
3、创建 ServerSocket 实例 {#3创建-serversocket-实例}
首先,创建一个监听指定端口的 ServerSocket
对象:
int port = 8080;
ServerSocket serverSocket = new ServerSocket(port);
接着,使用 accept()
方法接受一个传入连接:
while (true) {
Socket clientSocket = serverSocket.accept();
// ...
}
如上,使用 while
循环等待连接。然后,调用 ServerSocket
对象上的 accept()
方法来监听和接受连接。
连接建立后,该方法会返回一个 Socket
对象,允许服务器和客户端通过已建立的网络进行通信。
4、处理输入和输出 {#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、多线程服务器 {#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、测试 {#6测试}
通过在 main
方法中实例化服务器来进行测试:
static void main(String[] args) throws IOException {
int port = 8080;
SimpleHttpServerMultiThreaded server = new SimpleHttpServerMultiThreaded(port);
server.start();
}
启动程序,在浏览器中打开 http://localhost:8080
就可以看到服务器的响应结果。
7、总结 {#7总结}
本文介绍了如何使用 ServerSocket
类来实现一个简单的 HTTP 服务器,以及它的多线程版本。
Ref:https://www.baeldung.com/java-serversocket-simple-http-server