3

我正在使用 Tomcat 6.0.36 和 JRE 1.5.0,并且正在 Windows 7 上进行开发工作。

作为我正在做的一些工作的概念证明,我从 Java 代码通过 HTTP 将一些 XML 通过套接字发布到 servlet。然后 servlet 回显 xml。在我的第一个实现中,我将两端的输入流交给一个 XML 文档工厂,以提取通过网络发送的 xml。这在 servlet 中运行顺利,但在客户端失败了。事实证明,它在客户端失败了,因为读取响应被阻塞到文档工厂超时并在整个响应到达之前抛出异常的地步。(文档工厂的行为现在没有实际意义,因为正如我在下面描述的,在不使用文档工厂的情况下,我遇到了同样的阻塞问题。)

为了尝试解决这个阻塞问题,我想出了一个更简单的客户端代码和 servlet 版本。在这个更简单的版本中,我从等式中删除了文档生成器。双方的代码现在只是从各自的输入流中读取文本。

不幸的是,我仍然有这个响应阻塞问题,正如我在下面描述的,它并没有通过简单地调用 response.flushBuffer() 来解决。谷歌搜索只检索到一个我能找到的相关主题(Tomcat 不刷新响应缓冲区),但这不是完全相同的问题。

我已经包含了我的代码并解释了下面的确切问题。

这是我的 servlet 代码(请记住,这是简单的概念验证代码,而不是生产代码),

import java.io.InputStreamReader;
import java.io.LineNumberReader;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public final class EchoXmlServlet extends HttpServlet {

    public void init(ServletConfig config) throws ServletException {
        System.out.println("EchoXmlServlet loaded.");
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException {
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException {

        try {
            processRequest(request, response);
        }
        catch(Exception e) {
            e.printStackTrace();
            throw new ServletException(e);
        }

        System.out.println("Response sent.");

        return;
    }

    private final void processRequest(HttpServletRequest request, final HttpServletResponse response) throws Exception {

        String line = null;

        StringBuilder sb = new StringBuilder();
        LineNumberReader lineReader = new LineNumberReader(new InputStreamReader(request.getInputStream(), "UTF-8"));

        while((line = lineReader.readLine()) != null) {

            System.out.println("line: " + line);

            sb.append(line);
            sb.append("\n");
        }

        sb.append("An additional line to see when it turns up on the client.");

        System.out.println(sb);

        response.setHeader("Content-Type", "text/xml;charset=UTF-8");
        response.getOutputStream().write(sb.toString().getBytes("UTF-8"));

        // Some things that were tried.
        //response.getOutputStream().print(sb.toString());
        //response.getOutputStream().print("\r\n");
        //response.getOutputStream().flush();
        //response.flushBuffer();
    }

    public void destroy() {
    }

}

这是我的客户端代码,

import java.io.BufferedOutputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.net.Socket;

public final class SimpleSender {

    private String host;
    private String path;
    private int port;

    public SimpleSender(String host, String path, int port) {

        this.host = host;
        this.path = path;
        this.port = port;

    }

    public void execute() {

        Socket connection = null;
        String line;

        try {
            byte[] xmlBytes = getXmlBytes();
            byte[] headerBytes = getHeaderBytes(xmlBytes.length);

            connection = new Socket(this.host, this.port);

            OutputStream outputStream = new BufferedOutputStream(connection.getOutputStream());
            outputStream.write(headerBytes);
            outputStream.write(xmlBytes);
            outputStream.flush();

            LineNumberReader lineReader
                = new LineNumberReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));

            while((line = lineReader.readLine()) != null) {
                System.out.println("line: " + line);
            }

            System.out.println("The response is read.");
        }
        catch(Exception e) {
            e.printStackTrace();
        }
        finally {
            try {
                connection.close();
            }
            catch(Exception e) {}
        }
    }

    private byte[] getXmlBytes() throws Exception {

        StringBuffer sb = null;

        sb = new StringBuffer()
            .append("<my-xml>\n")
            .append("Hello to myself.\n")
            .append("</my-xml>\n");

        return sb.toString().getBytes("UTF-8");
    }

    private byte[] getHeaderBytes(int contentLength) throws Exception {

        StringBuffer sb = null;

        sb = new StringBuffer()
            .append("POST ")
            .append(this.path)
            .append(" HTTP/1.1\r\n")
            .append("Host: ")
            .append(this.host)
            .append("\r\n")
            .append("Content-Type: text/xml;charset=UTF-8\r\n")
            .append("Content-Length: ")
            .append(contentLength)
            .append("\r\n")
            .append("\r\n");

        return sb.toString().getBytes("UTF-8");
    }

}

当通过调用 SimpleSender.execute() 将请求发送到 servlet 时,servlet 中接收请求的代码会顺利读取 xml。我的 servlet 代码也顺利退出了它的 processRequest() 和 doPost()。这是服务器上的即时(即任何输出行之间没有阻塞)输出:

line: <my-xml>
line: Hello to myself.
line: </my-xml>
<my-xml>
Hello to myself.
</my-xml>
An additional line to see when it turns up on the client.
Response sent.

上面的输出完全符合预期。

然而,在客户端,代码输出以下 then 块:

HELLO FROM MAIN
line: HTTP/1.1 200 OK
line: Server: Apache-Coyote/1.1
line: Content-Type: text/xml;charset=UTF-8
line: Content-Length: 74
line: Date: Sun, 18 Nov 2012 23:58:43 GMT
line:
line: <my-xml>
line: Hello to myself.
line: </my-xml>

阻塞了大约 20 秒后(我计时了),在客户端输出以下几行,

line: An additional line to see when it turns up on the client.
The response is read.
GOODBYE FROM MAIN

请注意,当阻塞发生在客户端时,服务器端的整个输出是完全可见的。

从那里,我尝试在服务器端刷新以尝试解决此问题。我独立尝试了两种刷新方法:response.flushBuffer() 和 response.getOutputStream().flush()。使用这两种刷新方法,我仍然在客户端遇到阻塞(但在响应的不同部分),但我也遇到了其他问题。这是客户端阻止的地方,

HELLO FROM MAIN
line: HTTP/1.1 200 OK
line: Server: Apache-Coyote/1.1
line: Content-Type: text/xml;charset=UTF-8
line: Transfer-Encoding: chunked
line: Date: Mon, 19 Nov 2012 00:21:53 GMT
line:
line: 4a
line: <my-xml>
line: Hello to myself.
line: </my-xml>
line: An additional line to see when it turns up on the client.
line: 0
line: 

阻塞约20秒后,在客户端输出如下,

The response is read.
GOODBYE FROM MAIN

此输出在客户端存在三个问题。首先,响应的读取仍然是阻塞的,它只是在响应的不同部分之后阻塞。其次,我返回了意想不到的字符(“4a”,“0”)。最后,标题已更改。我丢失了 Content-Length 标头,并且获得了“Transfer-encoding: chunked”标头。

因此,如果没有刷新,我的响应会在发送最后一行和终止响应之前阻塞。但是,在刷新时,响应仍然是阻塞的,但现在我得到了我不想要的字符以及对我不想要的标题的更改。

在 Tomcat 中,我的连接器具有默认定义,

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

connectionTimeout 设置为 20 秒。当我将其更改为 10 秒时,我的客户端代码阻塞了 10 秒而不是 20 秒。因此,由 Tomcat 管理的连接超时似乎导致我的响应被完全刷新并终止。

我应该在我的 servlet 代码中做些什么来表明我的响应已完成?

在发送最后一行和终止指示符之前,是否有人对为什么我的响应被阻止提出建议?

有没有人对为什么刷新发送不需要的字符以及为什么刷新后响应仍然阻塞提出建议?

如果有人有时间,如果您尝试运行本文中包含的代码,您能否告诉我是否遇到同样的问题?

编辑 - 回应 Guido 的第一个回复

圭多,

非常感谢您的回复。

您的客户端正在阻塞,因为您正在使用 readLine 来读取消息的正文。readLine 挂起,因为正文没有以换行符结尾

不,我不认为这是真的。首先,在我的原始代码版本中,我没有在客户端或服务器端使用行阅读器。在双方,我将流交给 xml 文档工厂并让它从流中读取。在服务器上,这工作正常。在客户端,它超时。(在客户端上,在将流传递到文档工厂之前,我正在阅读标题的末尾。)

其次,当我将客户端代码更改为不使用行阅读器时,仍然会发生阻塞。这是一个不使用行阅读器的 SimpleSender.execute() 版本,

public void execute() {

    Socket connection = null;
    int byteCount = 0;

    try {
        byte[] xmlBytes = getXmlBytes();
        byte[] headerBytes = getHeaderBytes(xmlBytes.length);

        connection = new Socket(this.host, this.port);

        OutputStream outputStream = new BufferedOutputStream(connection.getOutputStream());
        outputStream.write(headerBytes);
        outputStream.write(xmlBytes);
        outputStream.flush();

        while(connection.getInputStream().read(new byte[1]) >= 0) {
            ++byteCount;
        }

        System.out.println("The response is read: " + byteCount);
    }
    catch(Exception e) {
        e.printStackTrace();
    }
    finally {
        try {
            connection.close();
        }
        catch(Exception e) {}
    }

    return;
}

上面的代码块在,

HELLO FROM MAIN

然后 20 秒后,完成,

The response is read: 235
GOODBYE FROM MAIN

我认为以上内容最终表明问题不在于在客户端使用行阅读器。

sb.append("An additional line to see when it turns up on the client.\n");

在上面的行中添加 return 只是将块推迟到后面的一行。我在我的 OP 之前测试过这个,我刚刚再次测试。

If you want to do your own HTTP parser, you have to read through the headers until you get two blank lines.

是的,我确实知道,但在这个人为的简单示例中,这是一个有争议的问题。在客户端,我只是输出返回的 HTTP 消息、标头和所有内容。

Then you need to scan the headers to see if you had a Content-Length header. If there is no Content-Length then you are done. If there is a Content-Length you need to parse it for the length, then read exactly that number of additional bytes from the stream. This allows HTTP to transport both text data and also binary data which has no line feeds.

是的,在这个人为的简单示例中,所有内容都是正确的,但不相关。

I recommend you replace the guts of your client code HTTP writer/parse with a pre-written client library that handles these details for you.

我完全同意。我实际上希望将流的处理传递给 xml 文档工厂。作为处理我的阻塞问题的一种方式,我还研究了 Apache commons-httpclient。新版本(httpcomponents)仍然让开发人员处理返回后的流(据我所知),所以这没有用。如果你能推荐另一个图书馆,我肯定会感兴趣。

我不同意你的观点,但我感谢你的回复,我的意思不是冒犯或任何负面暗示。我显然做错了什么或没有做我应该做的事情,但我不认为线路阅读器是问题所在。此外,如果我冲洗,那些时髦的角色是从哪里来的?为什么在客户端不使用线路阅读器时会发生阻塞?

另外,我已经在 J​​etty 上复制了这个问题。因此,这绝对不是 Tomcat 问题,而是非常“我”的问题。我做错了什么,但我不知道它是什么。

4

2 回答 2

2

您的服务器代码看起来不错。问题出在您的客户端代码上。它不遵守 HTTP 协议,并将响应视为一堆行。

服务器上的快速修复。改成:

    sb.append("An additional line to see when it turns up on the client.\n");

您的客户端正在阻塞,因为您正在使用 readLine 来读取消息的正文。readLine 挂起,因为正文没有以换行符结尾。最后 Tomcat 超时,关闭连接,您的缓冲阅读器检测到这一点并返回剩余数据。

如果您进行上述更改(对服务器),这将使您的客户端看起来像您预期的那样工作。即使它仍然是错误的。

如果你想做你自己的 HTTP 解析器,你必须通读标题,直到你得到两个空行。然后您需要扫描标头以查看是否有 Content-Length 标头。如果没有 Content-Length,那么你就完成了。如果有 Content-Length ,您需要解析它的长度,然后从流中准确读取该数量的附加字节。这允许 HTTP 传输文本数据和没有换行符的二进制数据。

我建议你用一个为你处理这些细节的预先编写的客户端库替换你的客户端代码 HTTP writer/parse 的胆量。

于 2012-11-19T03:55:25.940 回答
1

大声笑好吧,我做错了什么(通过遗漏)。我的问题的解决方案?将以下标头添加到我的 http 请求中,

Connection: close

就这么简单。没有它,这种联系仍然存在。我的代码依赖服务器表示它已完成,但是,服务器仍在侦听打开的连接而不是关闭它。

标头导致服务器在完成写入响应时关闭连接(我猜这在它对 doPost(...) 的调用返回时表示)。

附录

关于调用 flush() 时的时髦字符......

我的服务器代码,现在我正在使用 Connection: close,不会调用 flush()。但是,如果要发回的内容足够大(大于我怀疑的 Tomcat 连接器的缓冲区大小),我仍然会收到发回的时髦字符,并且标题“Transfer-Encoding: chunked”出现在响应中。

为了解决这个问题,我在服务器端明确调用 response.setContentLength(...),然后再写我的响应。当我这样做时,Content-Length 标头在响应中,而不是 Transfer-Encoding: 分块,并且我没有得到时髦的字符。

由于我的代码现在正在运行,我不想再花时间在这上面,但我想知道这些时髦的字符是否是块分隔符,一旦我明确设置内容长度,块分隔符就不再需要了。

于 2012-11-19T06:51:27.703 回答