51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

使用 Java ServerSocket 实现简单的 HTTP 服务器

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) {
            // ...
        }
    }
}

上面的方法演示了如何处理与客户端的输入和输出通信。BodyLength 与上一节中的示例相同。

接下来,创建另一个名为 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

赞(0)
未经允许不得转载:工具盒子 » 使用 Java ServerSocket 实现简单的 HTTP 服务器