在HTTP Servlet中正确流送输入和输出

Woj*_*ela 5 java streaming multithreading servlets http

我正在尝试编写将处理POST请求并流式传输输入和输出的servlet。我的意思是它应该读取一行输入,在该行上做一些工作,然后写入一行输出。并且它应该能够处理任意长请求(这样也会产生任意长响应)而不会出现内存不足异常。这是我的第一次尝试:

protected void doPost(HttpServletRequest request, HttpServletResponse response) {
    ServletInputStream input = request.getInputStream();
    ServletOutputStream output = response.getOutputStream();

    LineIterator lineIt = lineIterator(input, "UTF-8");
    while (lineIt.hasNext()) {
        String line = lineIt.next();
        output.println(line.length());
    }
    output.flush();
}
Run Code Online (Sandbox Code Playgroud)

现在,我使用来测试了该servlet curl,它可以工作,但是当我使用Apache HttpClient编写客户端时,客户端线程和服务器线程都会挂起。客户看起来像这样:

HttpClient client = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(...);

// request
post.setEntity(new FileEntity(new File("some-huge-file.txt")));
HttpResponse response = client.execute(post);

// response
copyInputStreamToFile(response.getEntity().getContent(), new File("results.txt"));
Run Code Online (Sandbox Code Playgroud)

问题很明显。客户端在一个线程中按顺序执行它的工作-首先它完全发送请求,然后才开始读取响应。但是服务器为每行输入写入一行输出,如果客户端未读取输出(而顺序客户端未读取),则服务器将被阻止尝试写入输出流。反过来,这会阻止客户端尝试将输入发送到服务器。

我猜想是curl有效的,因为它以某种方式同时发送输入和接收输出(在单独的线程中?)。因此,第一个问题是是否可以将Apache HttpClient配置为与以下行为类似curl

下一个问题是,如何改进servlet,以便使行为不佳的客户端不会导致服务器线程挂起?我的第一个尝试是引入中间缓冲区,该缓冲区将收集输出,直到客户端完成发送输入为止,然后servlet才开始发送输出:

ServletInputStream input = request.getInputStream();
ServletOutputStream output = response.getOutputStream();

// prepare intermediate store
int threshold = 100 * 1024; // 100 kB before switching to file store
File file = File.createTempFile("intermediate", "");
DeferredFileOutputStream intermediate = new DeferredFileOutputStream(threshold, file);

// process request to intermediate store
PrintStream intermediateFront = new PrintStream(new BufferedOutputStream(intermediate));
LineIterator lineIt = lineIterator(input, "UTF-8");
while (lineIt.hasNext()) {
    String line = lineIt.next();
    intermediateFront.println(line.length());
}
intermediateFront.close();

// request fully processed, so now it's time to send response
intermediate.writeTo(output);

file.delete();
Run Code Online (Sandbox Code Playgroud)

这行得通,行为不正常的客户端可以安全地使用我的servlet,但是另一方面,对于像curl此类解决方案这样的并发客户端,这会增加不必要的延迟。并行客户端正在单独的线程中读取响应,因此当随着请求的使用而逐行生成响应时,它将受益。

所以我认为我需要一个字节缓冲区/队列:

  • 可以由一个线程写入,而由另一个线程读取
  • 最初只会在内存中
  • 如有必要,将溢出到磁盘(类似于DeferredFileOutputStream)。

在servlet中,我将生成新线程以读取输入,对其进行处理并将输出写入缓冲区,而主servlet线程将从该缓冲区读取并将其发送给客户端。

您知道有喜欢这样做的图书馆吗?也许我的假设是错误的,我应该做一些完全不同的事情...

小智 2

要实现同时写入和读取,您可以使用 Jetty HttpClient http://www.eclipse.org/jetty/documentation/current/http-client-api.html

我已经使用此代码创建了对您的存储库的拉取请求。

HttpClient httpClient = new HttpClient();
httpClient.start();

Request request = httpClient.newRequest("http://localhost:8080/line-lengths");
final OutputStreamContentProvider contentProvider = new OutputStreamContentProvider();
InputStreamResponseListener responseListener = new InputStreamResponseListener();

request.content(contentProvider).method(HttpMethod.POST).send(responseListener); //async request
httpClient.getExecutor().execute(new Runnable() {
    public void run() {
        try (OutputStream outputStream = contentProvider.getOutputStream()) {
            writeRequestBodyTo(outputStream); //writing to stream in another thread
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

readResponseBodyFrom(responseListener.getInputStream()); //reading response
httpClient.stop();
Run Code Online (Sandbox Code Playgroud)