Tomcat - Servlet response blocking - problems with

2019-05-12 08:58发布

问题:

I'm using Tomcat 6.0.36 and JRE 1.5.0, and I'm doing development work on Windows 7.

As a proof of concept for some work I'm doing, from Java code I'm HTTP posting some XML over a socket to a servlet. The servlet then echos back the xml. In my first implementation, I was handing the input stream at both ends to an XML document factory to extract the xml that was sent over the wire. This worked without a hitch in the servlet but failed on the client side. It turned out that it failed on the client side because the reading of the response was blocking to the point that the document factory was timing out and throwing an exception prior to the entire response arriving. (The behaviour of the document factory is now moot because, as I describe below, I am getting the same blocking issue without the use of the document factory.)

To try to work through this blocking issue, I then came up with a simpler version of the client side code and the servlet. In this simpler version, I eliminated the document builder from the equation. The code on both sides now simply reads the text from their respective input streams.

Unfortunately, I still have this blocking issue with the response and, as I describe below, it has not been resolved by simply calling response.flushBuffer(). Google searches retrieved only one relevant topic that I could find (Tomcat does not flush the response buffer) but this was not the exact same issue.

I have included my code and explained the exact issues below.

Here's my servlet code (remember, this is bare-bones proof-of-concept code, not production code),

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() {
    }

}

Here's my client side code,

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");
    }

}

When a request is sent to the servlet via a call to SimpleSender.execute(), the code in the servlet that receives the request reads the xml without a hitch. My servlet code also exits from its processRequest() and doPost() without a hitch. This is the immediate (i.e. there is no blocking between any output line) output on the server:

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.

The output above is exactly as expected.

On the client side, however, the code outputs the following then blocks:

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>

After about 20 seconds of blocking (I timed it), the following lines are output on the client side,

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

Note that the entire output on the server side is fully visible while the blocking is occurring on the client side.

From there, I tried to flush on the server side to try to fix this issue. I independently tried two methods of flushing: response.flushBuffer() and response.getOutputStream().flush(). With both methods of flushing, I still had blocking on the client side (but in a different part of the response), but I had other issues as well. Here's where the client blocked,

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: 

After blocking for about 20 seconds, the following is output on the client side,

The response is read.
GOODBYE FROM MAIN

There are three problems with this output on the client side. Firstly, the reading of the response is still blocking, it's just blocking after a different part of the response. Secondly, I have unanticipated characters returned ("4a", "0"). Finally, the headers have changed. I've lost the Content-Length header, and I have gained the "Transfer-encoding: chunked" header.

So, without a flush, my response is blocking prior to sending the final line and a termination to the response. However, with a flush, the response is still blocking but now I'm getting characters I don't want and a change to the headers I don't want.

In Tomcat, my connector has the default definition,

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

The connectionTimeout is set for 20 seconds. When I changed this to 10 seconds, my client side code blocked for 10 seconds instead of 20. So it appears that it is the connection timeout, as managed by Tomcat, that is causing my response to be fully flushed and terminated.

Is there something additional I should be doing in my servlet code to indicate that my response is finished?

Has anyone got suggestions as to why my response is blocking prior to sending the final line and termination indicator?

Has anyone got suggestions as to why flush is sending unwanted characters and why the response is still blocking after a flush?

If someone has the time, could you tell me if you get the same issues if you try running the code included in this post?

EDIT - In response to Guido's first reply

Guido,

Thanks very much for your reply.

Your client is blocking because you are using readLine to read the body of the message. readLine hangs because the body does not end with a line feed

No, I don't think this is true. Firstly, in my original version of my code, I was not using line readers on either the client or server side. On both sides, I was handing the stream to the xml document factory and letting it read from the stream. On the server, this worked fine. On the client, it timed out. (On the client, I was reading to the end of the headers prior to passing the stream to the document factory.)

Secondly, when I change my client code to not use a line reader, the blocking still occurs. Here's a version of SimpleSender.execute() that does not use a line reader,

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;
}

The above code blocks at,

HELLO FROM MAIN

then 20 seconds later, finishes wtih,

The response is read: 235
GOODBYE FROM MAIN

I think the above shows conclusively the problem is not with the use of a line reader on the client side.

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

The addition of the return in the above line just defers the block to one line later. I had tested this prior to my OP and I just tested again.

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

Yes, I do know that, but in this contrived simple example, it is a moot point. On the client, I am simply outputting the returned HTTP message, headers and all.

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.

Yup, all true but not relevant in this contrived simple example.

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.

I agree entirely. I was actually hoping to pass off the handling of the streams to the xml document factories. As a way of dealing with my blocking issues, I also looked into Apache commons-httpclient. The new version (httpcomponents) still leaves it to the developer to handle the stream of a post return (from what I can tell), so that was of no use. If you can suggest another library, I'd be interested for sure.

I've disagreed with your points but I thank you for your reply and I mean no offense or any negative intimations by my disagreement. I'm obviously doing something wrong or not doing something I should, but I don't think the line reader is the issue. Additionally, where are those funky characters coming from if I flush? Why does the blocking occur when a line reader is not in use on the client side?

Also, I have replicated the issue on Jetty. Hence, this is definetly not a Tomcat issue and very much a 'me' issue. I'm doing something wrong but I don't know what it is.

回答1:

Your server code looks fine. The problem is with your client code. It does not obey the HTTP protocol and is treating the response like a bunch of lines.

Quick fix on the server. Change to:

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

Your client is blocking because you are using readLine to read the body of the message. readLine hangs because the body does not end with a line feed. Finally Tomcat times out, closes the connection, your buffered reader detects this and returns the remaining data.

If you make the change above (to the server), This will make your client appear to work as you expect. Even though it is still wrong.

If you want to do your own HTTP parser, you have to read through the headers until you get two blank lines. 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.



回答2:

LOL Ok, I was doing something wrong (by omission). The solution to my issue? Add the following header to my http request,

Connection: close

That simple. Without that, the connection was staying alive. My code was relying on the server signifying that it was finished, but, the server was still listening on the open connection rather than closing it.

The header causes the server to close the connection when it finishes writing the response (which I guess is signified when its call to doPost(...) returns).

Addendum

With regard to the funky characters when flush() is called...

My server code, now that I'm using Connection: close, does not call flush(). However, if the content to be sent back is large enough (larger than the Tomcat connector's buffer size I suspect), I still get the funky characters sent back and the header 'Transfer-Encoding: chunked' appears in the response.

To fix this, I explicitly call, on the server side, response.setContentLength(...) prior to writing my response. When I do this, the Content-Length header is in the response instead of Transfer-Encoding: chunked, and I don't get the funky characters.

I'm not willing to burn any more time on this as my code is now working, but I do wonder if the funky characters were chunk delimiters, where, once I explicitly set the content length, the chunk delimiters were no longer necessary.