You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HTTP Connection keep-alive未维持持久连接问题排查

问题:设置Keep-Alive后仍新建Socket连接的原因及解决方法

我用ServerSocket实现了一个简易Java服务器,浏览器连接时会启动新线程(ClientHandler)处理请求。处理器响应GET请求并返回一个带按钮的HTML页面,点击按钮通过AJAX发送携带"Hello from browser"的POST请求,页面不刷新。但每次发送POST请求都会新建Socket连接,明明已经在响应头设置了keep-alive。

现象

  • 服务器终端:每次点击按钮都会打印新的Client: X Joined日志,说明每次POST都建立了新连接
  • 浏览器开发者工具网络面板:每个POST请求的连接状态都是新建立的,没有复用之前的连接

相关代码

Server.java

import java.io.*;
import java.net.*;

public class Server {
    private static final int PORT = 8080;
    private static int count = 0;
    private static final String HTML_FILE_PATH = "src/main/java/org/example/index.html";

    public static void main(String[] args) throws Exception {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("Server started on port " + PORT);

            while (true) {
                try {
                    Socket clientSocket = serverSocket.accept();
                    System.out.println("Client: " + ++count + " Joined");
                    ClientHandler clientHandler = new ClientHandler(clientSocket);
                    // Handle the client connection in a new thread 
                    new Thread(clientHandler).start();
                } catch (IOException e) {
                    System.err.println("Error accepting client connection: " + e.getMessage());
                }
            }
        }
    }
}

ClientHandler.java

import java.io.*;
import java.net.*;
import java.nio.file.Files;
import java.util.concurrent.atomic.AtomicInteger;

public class ClientHandler implements Runnable {
    private static final AtomicInteger nextId = new AtomicInteger(0);
    private Socket clientSocket;
    private int clientId;
    private static final String HTML_FILE_PATH = "src/main/java/org/example/index.html";

    public ClientHandler(Socket clientSocket) {
        this.clientSocket = clientSocket;
        this.clientId = nextId.incrementAndGet();
    }

    @Override
    public void run() {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {

            String line;
            StringBuilder requestBuilder = new StringBuilder();
            while (!(line = reader.readLine()).isBlank()) {
                requestBuilder.append(line).append("\r\n");
            }
            String request = requestBuilder.toString();
            String[] requestLines = request.split("\r\n");
            String[] requestLine = requestLines[0].split(" ");
            String method = requestLine[0];
            String path = requestLine[1];
            if ("GET".equals(method) && "/".equals(path)) {
                serveFile(HTML_FILE_PATH, writer);
            } else if ("POST".equals(method)) {
                // Extract the Content-Length from the headers
                int contentLength = getContentLength(requestLines);
                char[] buffer = new char[contentLength];

                // Read the POST data from the input stream
                reader.read(buffer, 0, contentLength);
                String postData = new String(buffer);

                // Print the POST data in the server terminal
                System.out.println("POST Data: " + postData);

                // Continue with sending the response
                String responseMessage = "Hello Client " + clientId + ", This is server.";
                writeResponse(writer, "HTTP/1.1 200 OK", "text/plain", responseMessage.getBytes(), true);
            } else {
                writeResponse(writer, "HTTP/1.1 404 Not Found", "text/plain", "Not Found".getBytes(), false);
            }
        } catch (IOException e) {
            System.err.println("Error handling client " + clientId + ": " + e.getMessage());
        }
    }

    private void serveFile(String filePath, BufferedWriter writer) throws IOException {
        File file = new File(filePath);
        if (file.exists()) {
            byte[] fileContent = Files.readAllBytes(file.toPath());
            writeResponse(writer, "HTTP/1.1 200 OK", "text/html", fileContent, true);
        } else {
            writeResponse(writer, "HTTP/1.1 404 Not Found", "text/plain", "File not found".getBytes(), false);
        }
    }

    private void writeResponse(BufferedWriter writer, String statusLine, String contentType, byte[] content, boolean keepAlive) throws IOException {
        writer.write(statusLine + "\r\n");
        writer.write("Content-Type: " + contentType + "\r\n");
        writer.write("Content-Length: " + content.length + "\r\n");
        if (keepAlive) {
            writer.write("Connection: keep-alive\r\n");
            writer.write("Keep-Alive: timeout=60, max=100\r\n");
        } else {
            writer.write("Connection: close\r\n");
        }
        writer.write("\r\n");
        writer.write(new String(content));
        writer.flush();
    }
    private int getContentLength(String[] requestHeaders) {
        for (String header : requestHeaders) {
            if (header.toLowerCase().startsWith("content-length:")) {
                return Integer.parseInt(header.substring("content-length:".length()).trim());
            }
        }
        return 0;
    }
}

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Tic-Tac-Toe</title>
</head>
<body>
<button id="helloButton">Say Hello</button>
<div id="responseText"></div>
<script>
    document.getElementById('helloButton').onclick = function() {
        fetch('/', {
            method: 'POST',
            headers: {
                'Content-Type': 'text/plain'
            },
            body: 'Hello from browser'
        })
            .then(response => response.text())
            .then(data => document.getElementById('responseText').innerText = data)
            .catch(error => console.error('Error:', error));
    };
</script>
</body>
</html>

原因分析

  1. ClientHandler单次请求即终止:当前ClientHandler的run方法处理完一个请求后,就会退出try-with-resources代码块。由于BufferedReader和BufferedWriter是基于Socket的流创建的,try-with-resources会自动关闭这些流,进而触发Socket连接关闭。即使设置了Keep-Alive响应头,线程结束后连接也会被关闭,无法复用。
  2. 缺少循环处理多请求逻辑:HTTP持久连接要求服务器在处理完一个请求后,保持Socket处于打开状态,持续监听该连接上的后续请求,而不是处理完一个就直接关闭连接。

解决方法

修改ClientHandler的run方法,添加循环逻辑,持续处理同一Socket连接上的多个请求,直到连接被客户端关闭或超时。同时要注意正确处理每个请求的边界(比如请求头结束的空行、POST请求的body长度)。

修改后的ClientHandler代码:

import java.io.*;
import java.net.*;
import java.nio.file.Files;
import java.util.concurrent.atomic.AtomicInteger;

public class ClientHandler implements Runnable {
    private static final AtomicInteger nextId = new AtomicInteger(0);
    private Socket clientSocket;
    private int clientId;
    private static final String HTML_FILE_PATH = "src/main/java/org/example/index.html";
    // 记录该连接剩余可处理的请求数
    private int keepAliveMax = 100;

    public ClientHandler(Socket clientSocket) {
        this.clientSocket = clientSocket;
        this.clientId = nextId.incrementAndGet();
        // 设置Socket超时,避免无限等待
        try {
            clientSocket.setSoTimeout(60000); // 60秒超时
        } catch (SocketException e) {
            System.err.println("Error setting socket timeout: " + e.getMessage());
        }
    }

    @Override
    public void run() {
        BufferedReader reader = null;
        BufferedWriter writer = null;
        try {
            reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

            boolean keepAlive = true;
            while (keepAlive && !clientSocket.isClosed() && keepAliveMax > 0) {
                String line;
                StringBuilder requestBuilder = new StringBuilder();
                // 读取请求头,直到空行或客户端关闭连接
                while ((line = reader.readLine()) != null && !line.isBlank()) {
                    requestBuilder.append(line).append("\r\n");
                }
                // 如果读取到null,说明客户端关闭了连接
                if (line == null) {
                    break;
                }
                String request = requestBuilder.toString();
                String[] requestLines = request.split("\r\n");
                String[] requestLine = requestLines[0].split(" ");
                String method = requestLine[0];
                String path = requestLine[1];

                // 处理请求
                if ("GET".equals(method) && "/".equals(path)) {
                    serveFile(HTML_FILE_PATH, writer);
                } else if ("POST".equals(method)) {
                    int contentLength = getContentLength(requestLines);
                    char[] buffer = new char[contentLength];
                    reader.read(buffer, 0, contentLength);
                    String postData = new String(buffer);
                    System.out.println("POST Data: " + postData);

                    String responseMessage = "Hello Client " + clientId + ", This is server.";
                    keepAlive = writeResponse(writer, "HTTP/1.1 200 OK", "text/plain", responseMessage.getBytes(), true);
                } else {
                    keepAlive = writeResponse(writer, "HTTP/1.1 404 Not Found", "text/plain", "Not Found".getBytes(), false);
                }
                // 每次处理请求后递减max值
                keepAliveMax--;
            }
        } catch (IOException e) {
            System.err.println("Error handling client " + clientId + ": " + e.getMessage());
        } finally {
            // 最后统一关闭资源
            try {
                if (reader != null) reader.close();
                if (writer != null) writer.close();
                if (clientSocket != null) clientSocket.close();
            } catch (IOException e) {
                System.err.println("Error closing resources: " + e.getMessage());
            }
        }
    }

    private void serveFile(String filePath, BufferedWriter writer) throws IOException {
        File file = new File(filePath);
        if (file.exists()) {
            byte[] fileContent = Files.readAllBytes(file.toPath());
            writeResponse(writer, "HTTP/1.1 200 OK", "text/html", fileContent, true);
        } else {
            writeResponse(writer, "HTTP/1.1 404 Not Found", "text/plain", "File not found".getBytes(), false);
        }
    }

    // 修改返回值,告知是否需要保持连接
    private boolean writeResponse(BufferedWriter writer, String statusLine, String contentType, byte[] content, boolean keepAlive) throws IOException {
        writer.write(statusLine + "\r\n");
        writer.write("Content-Type: " + contentType + "\r\n");
        writer.write("Content-Length: " + content.length + "\r\n");
        if (keepAlive && keepAliveMax > 0) {
            writer.write("Connection: keep-alive\r\n");
            writer.write("Keep-Alive: timeout=60, max=" + keepAliveMax + "\r\n");
        } else {
            writer.write("Connection: close\r\n");
            keepAlive = false;
        }
        writer.write("\r\n");
        writer.write(new String(content));
        writer.flush();
        return keepAlive;
    }

    private int getContentLength(String[] requestHeaders) {
        for (String header : requestHeaders) {
            if (header.toLowerCase().startsWith("content-length:")) {
                return Integer.parseInt(header.substring("content-length:".length()).trim());
            }
        }
        return 0;
    }
}

修改要点

  1. 移除try-with-resources包裹流:避免处理完一次请求就自动关闭流和Socket
  2. 添加循环处理请求:持续读取同一Socket上的后续请求,直到连接关闭、超时或达到Keep-Alive的max限制
  3. 设置Socket超时:防止线程因客户端无请求而无限阻塞
  4. 动态更新Keep-Alive的max值:每次处理请求后递减max,符合HTTP规范(表示该连接剩余可处理的请求数)
  5. 判断客户端关闭连接:当reader.readLine()返回null时,说明客户端关闭了连接,终止循环

内容的提问来源于stack exchange,提问作者Teahana

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 08:07:05