Java中使用套接字进行HTTP 1.1持久连接

4
假设我有一个Java程序,使用HTTP 1.1在服务器上进行HTTP请求,并且没有关闭连接。我发出了一个请求,并从绑定到套接字的输入流中读取所有返回的数据。然而,在进行第二个请求时,我没有从服务器收到任何响应(或者流存在问题 - 它不提供任何更多的输入)。如果按顺序进行请求(请求,请求,读取),它可以正常工作,但是(请求,读取,请求,读取)则不行。
有人能否解释一下为什么会发生这种情况?(代码片段如下所示)。无论我做什么,第二个读取循环的isr_reader.read()始终只返回-1。
try{
        connection = new Socket("SomeServer", port);
        con_out = connection.getOutputStream();
        con_in  = connection.getInputStream();
        PrintWriter out_writer = new PrintWriter(con_out, false);
        out_writer.print("GET http://somesite HTTP/1.1\r\n");
        out_writer.print("Host: thehost\r\n");
        //out_writer.print("Content-Length: 0\r\n");
        out_writer.print("\r\n");
        out_writer.flush();

        // If we were not interpreting this data as a character stream, we might need to adjust byte ordering here.
        InputStreamReader isr_reader = new InputStreamReader(con_in);
        char[] streamBuf = new char[8192];
        int amountRead;
        StringBuilder receivedData = new StringBuilder();
        while((amountRead = isr_reader.read(streamBuf)) > 0){
            receivedData.append(streamBuf, 0, amountRead);
        }

// Response is processed here.

        if(connection != null && !connection.isClosed()){
            //System.out.println("Connection Still Open...");

        out_writer.print("GET http://someSite2\r\n");
        out_writer.print("Host: somehost\r\n");
        out_writer.print("Connection: close\r\n");
        out_writer.print("\r\n");
        out_writer.flush();

        streamBuf = new char[8192];
        amountRead = 0;
        receivedData.setLength(0);
        while((amountRead = isr_reader.read(streamBuf)) > 0 || amountRead < 1){
            if (amountRead > 0)
                receivedData.append(streamBuf, 0, amountRead);
        }
}
        // Process response here
    }

问题的回答:

是的,我正在从服务器接收分块响应。

由于外部限制,我正在使用原始套接字。

对于代码的混乱表示歉意 - 我正在根据记忆重写它,并似乎引入了一些错误。

所以共识是,我要么执行(请求,请求,读取)并让服务器在我到达末尾时关闭流,或者,如果我执行(请求,读取,请求,读取),则在到达流的末尾之前停止,这样流就不会被关闭。

5个回答

5
根据你的代码,只有在服务器关闭输出流(即输入流)并接收/响应第一个请求后,才会进入处理发送第二个请求的语句。
原因是你的代码只读取第一个响应。
while((amountRead = isr_reader.read(streamBuf)) > 0) {
  receivedData.append(streamBuf, 0, amountRead);
}

该方法会一直阻塞,直到服务器关闭输出流(即当 read 返回 -1)或者读取超时时间到。如果读取超时,则会抛出异常,并且您将无法发送第二个请求。

HTTP响应的问题在于它们不会告诉您从流中读取多少字节,直到响应结束。对于HTTP 1.0响应来说这并不是什么大问题,因为服务器在响应后简单地关闭连接,从而使您可以通过读取流中的所有内容(状态行+头部+正文)来获取响应。

但是,在HTTP 1.1持久连接中,您不能简单地读取流中的所有内容直到其结束。您首先需要逐行读取状态行和头部,然后根据状态代码和头部(如Content-Length)决定要读取多少字节以获取响应正文(如果有的话)。如果您正确执行上述操作,则在连接关闭或超时之前,您的读取操作将完成,并且您将准确地读取服务器发送的响应。这将使您能够发送下一个请求,然后以与第一个响应完全相同的方式读取第二个响应。

P.S. 如果您的服务器支持请求管线化,则请求、请求、读取可能会“工作”,从而使您的服务器接收并处理两个请求,您因此将两个响应读入一个缓冲区作为您的“第一个”响应。

P.P.S 请确保您的 PrintWriter 使用 US-ASCII 编码。否则,根据您的系统编码,您的HTTP请求的请求行和头部可能会格式不正确(编码错误)。


3
编写一个符合RFC标准的简单的HTTP/1.1客户端并不是一项难以完成的任务。为了解决Java中读取套接字时的阻塞I/O访问问题,您必须使用Java.nio类。SocketChannels提供了执行非阻塞I/O访问的可能性。
这是发送HTTP请求到持久连接的必要条件。此外,nio类将提供更好的性能。
我的压力测试给出了以下结果:
- HTTP/1.0(java.io)-> HTTP/1.0(java.nio)= +20%更快 - HTTP/1.0(java.io)-> HTTP/1.1(java.nio与持久连接)= +110%更快

0

请确保您的请求中有Connection: keep-alive。尽管这可能是一个无关紧要的问题。

服务器返回了什么样的响应?您是否使用分块传输?如果服务器不知道响应正文的大小,它无法提供Content-Length头,并必须在响应正文结束时关闭连接,以向客户端指示内容已结束。在这种情况下,keep-alive 将无法工作。如果您正在使用 PHP、JSP 等动态生成内容,则可以启用输出缓冲,检查累积主体的大小,推送Content-Length头并刷新输出缓冲区。


服务器正在使用分块传输。然而,由于我可以执行请求请求读取操作,因此服务器在接收第一个请求后不会关闭连接。 - Zxaos
如果您能捕获所有请求和响应头并将其附加到您的问题中,我认为这将极大地帮助解决此问题的故障排除。 - Ates Goral

0

你使用原始套接字而不是Java的URL连接或Commons HTTPClient,有特定的原因吗?

HTTP并不容易实现。我知道Commons HTTP Client可以像你尝试做的那样重用连接。

如果你没有特别的原因使用套接字,这就是我推荐的方法 :)


这个项目有一个外部限制,不幸的是只能使用原始套接字,其他方式都不合适。 - Zxaos
1
无论如何,您可以查看Apache HttpClient的源代码,找出发生了什么。 - anjanb

0
编写自己正确的客户端HTTP/1.1实现并非易事;历史上,我见过大多数尝试编写它的人都做错了。他们的实现通常忽略了规范,只是针对一个特定的测试服务器做出了可以工作的处理 - 特别是,他们通常忽略了处理分块响应的要求。
编写自己的HTTP客户端可能不是个好主意,除非你有一些非常奇怪的需求。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接