Tomcat - Servlet响应阻塞 - 刷新问题

时间:2012-11-19 01:20:24

标签: java tomcat response blocking flush

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

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

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

不幸的是,我仍然对响应有这个阻塞问题,正如我在下面描述的那样,它还没有得到解决 只需调用response.flushBuffer()。 Google搜索只检索了一个我能找到的相关主题 (Tomcat does not flush the response buffer)但这不完全相同 问题。

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

这是我的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.

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

然而,在客户端,代码输出以下内容然后阻止:

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

客户端此输出有三个问题。首先,响应的读取仍然是阻塞的,它是 只是在响应的不同部分后阻塞。其次,我有意想不到的字符返回(&#34; 4a&#34;,&#34; 0&#34;)。 最后,标题已更改。我已经丢失了Content-Length标题,我已经获得了 &#34;传输编码:chunked&#34;报头中。

因此,如果没有刷新,我的响应会在发送最后一行并终止响应之前阻塞。然而, 同花顺,响应仍然阻止,但现在我得到了我不想要的角色,并改变了标题我 不想要。

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

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

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

我的servlet代码中是否还有其他内容表明我的响应已完成?

在发送最终一行和终止指示符之前,是否有人建议我的回复为什么会阻止?

有没有人得到关于为什么flush发送不需要的字符以及为什么响应仍然阻塞之后的建议 同花顺?

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

编辑 - 回应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");

在上面一行中添加返回只是将块推迟到一行。我在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)仍然留给开发人员来处理回复的流(从我可以告诉的),所以没有用。如果您可以推荐另一个图书馆,我肯定会感兴趣。

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

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

2 个答案:

答案 0 :(得分: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编写器/解析的内容替换为预先编写的客户端库,以便为您处理这些详细信息。

答案 1 :(得分:1)

大声笑好吧,我做错了什么(省略了)。解决我的问题?将以下标头添加到我的http请求

Connection: close

那很简单。没有它,连接就会保持活力。我的代码依赖于服务器表示它已完成,但是,服务器仍在监听打开的连接而不是关闭它。

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

<强>附录

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

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

为了解决这个问题,我在编写响应之前在服务器端显式调用了response.setContentLength(...)。当我这样做时,Content-Length标头在响应中而不是Transfer-Encoding:chunked,我没有得到时髦的角色。

由于我的代码现在正在工作,我不愿意再烧这个了,但我确实想知道这个时髦的字符是否是块分隔符,一旦我明确设置了内容长度,就不再有块分隔符了必要的。