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

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

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

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

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

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

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

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("\n") .append("Hello to myself.\n") .append("\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:  line: Hello to myself. line:   Hello to myself.  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:  line: Hello to myself. line:  

在阻塞大约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:  line: Hello to myself. line:  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”标头。

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

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

  

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

在上面一行中添加返回只是将块推迟到一行。 我在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问题,而是一个“我”问题。 我做错了什么,但我不知道它是什么。

您的服务器代码看起来很好 问题出在您的客户端代码上。 它不遵守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编写器/解析器的内容,以便为您处理这些详细信息。

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

 Connection: close 

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

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

附录

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

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

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

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