Apache HTTPClient 流式 HTTP POST 请求?

6
我正在尝试使用Apache HTTPClient构建“全双工”HTTP流请求。
在我的第一次尝试中,我尝试使用以下请求代码:
URL url=new URL(/* code goes here */);

HttpPost request=new HttpPost(url.toString());

request.addHeader("Connection", "close");

PipedOutputStream requestOutput=new PipedOutputStream();
PipedInputStream requestInput=new PipedInputStream(requestOutput, DEFAULT_PIPE_SIZE);
ContentType requestContentType=getContentType();
InputStreamEntity requestEntity=new InputStreamEntity(requestInput, -1, requestContentType);
request.setEntity(requestEntity);

HttpEntity responseEntity=null;
HttpResponse response=getHttpClient().execute(request); // <-- Hanging here
try {
    if(response.getStatusLine().getStatusCode() != 200)
        throw new IOException("Unexpected status code: "+response.getStatusLine().getStatusCode());

    responseEntity = response.getEntity();
}
finally {
    if(responseEntity == null)
        request.abort();
}

InputStream responseInput=responseEntity.getContent();
ContentType responseContentType;
if(responseEntity.getContentType() != null)
    responseContentType = ContentType.parse(responseEntity.getContentType().getValue());
else
    responseContentType = DEFAULT_CONTENT_TYPE;

Reader responseStream=decode(responseInput, responseContentType);
Writer requestStream=encode(requestOutput, getContentType());

请求在上面指示的行卡住了。看起来代码试图在获得响应之前发送整个请求。回想一下,这很有道理。然而,这不是我所希望的。 :)

相反,我希望使用 Transfer-Encoding: chunked 发送请求标头,接收一个带有自己的 Transfer-Encoding: chunked 标头的 HTTP/1.1 200 OK 响应标头,然后我就可以拥有一个全双工流式 HTTP 连接来处理。

令人高兴的是,我的 HTTPClient 还有另一个基于 NIO 的异步客户端,有很好的使用示例(比如 this one)。我的问题是:

  1. 我对同步 HTTPClient 的行为的解释正确吗?或者是否有什么我可以做的,以继续以我描述的方式使用(更简单的)同步 HTTPClient?
  2. 基于 NIO 的客户端是否等待发送整个请求才寻求响应?或者我能否同时逐步发送请求和逐步接收响应?
如果HTTPClient不支持这种方式,是否有其他的HTTP客户端库可以支持?还是我应该计划编写一个(最小化的)HTTP客户端来支持这种方式?
2个回答

1

以下是我对代码的浏览方式的看法:

  1. 我不能完全同意非200响应代表失败这一事实。所有2XX响应大多数都是有效的。详情请查看wiki

  2. 对于任何TCP请求,我建议接收整个响应以确认其有效性。我这么说是因为,部分响应大多数情况下会被视为坏的响应,因为大多数客户端实现无法使用它。 (想象一种情况,服务器正在响应2MB的数据,并在此期间关闭)


这些是很好的观点。我只接受200,因为我处于测试模式;你说得对,我应该接受2XX表示成功。不过对于后一点,整个实现的目的是随时间接收和处理响应。 - sigpwned
这些不是程序挂起的原因。这是由于管道输入流造成的。 - Robert Christian

0
必须有一个单独的线程写入OutputStream,你的代码才能正常工作。
上面的代码为HTTPClient提供了一个PipedInputStream。
PipedInputStream会在相应的OutputStream中写入字节时使其可用。
上面的代码没有写入OutputStream(必须由单独的线程完成)。
因此,代码正好停在你的评论处。
在幕后,Apache客户端说“inputStream.read()”,在管道流的情况下,这要求先调用outputStream.write(bytes)(由单独的线程调用)。
由于你没有从单独的线程将字节泵入相关的OutputStream中,所以InputStream只是坐等OutputStream被“其他线程”写入。
从JavaDocs中:

一个管道输入流应该连接到一个管道输出流;然后管道输入流提供任何写入管道输出流的数据字节。

通常情况下,一个线程从PipedInputStream对象读取数据,而另一个线程将数据写入相应的PipedOutputStream。

不建议尝试在单个线程中同时使用两个对象,因为这可能会导致线程死锁。

管道输入流包含一个缓冲区,在一定限度内解耦读操作和写操作。如果向连接的管道输出流提供数据字节的线程不再活动,则称管道已“破裂”。

注意:看起来对于您的问题陈述来说,既没有提到管道流也没有提到并发性,因此这不是必需的。首先尝试使用ByteArrayInputStream()将其包装到实体对象中进行健全性检查...这应该有助于您缩小问题范围。

更新

顺便提一下,我编写了一个Apache HTTP客户端API [PipedApacheClientOutputStream] 的反转版本,它使用Apache Commons HTTP Client 4.3.4提供了一个HTTP POST的OutputStream接口。这可能是你正在寻找的东西...

调用代码看起来像这样:

// Calling-code manages thread-pool
ExecutorService es = Executors.newCachedThreadPool(
  new ThreadFactoryBuilder()
  .setNameFormat("apache-client-executor-thread-%d")
  .build());


// Build configuration
PipedApacheClientOutputStreamConfig config = new      
  PipedApacheClientOutputStreamConfig();
config.setUrl("http://localhost:3000");
config.setPipeBufferSizeBytes(1024);
config.setThreadPool(es);
config.setHttpClient(HttpClientBuilder.create().build());

// Instantiate OutputStream
PipedApacheClientOutputStream os = new     
PipedApacheClientOutputStream(config);

// Write to OutputStream
os.write(...);

try {
  os.close();
} catch (IOException e) {
  logger.error(e.getLocalizedMessage(), e);
}

// Do stuff with HTTP response
...

// Close the HTTP response
os.getResponse().close();

// Finally, shut down thread pool
// This must occur after retrieving response (after is) if interested   
// in POST result
es.shutdown();

注意 - 在实践中,同一客户端、执行器服务和配置可能会在应用程序的整个生命周期内被重复使用,因此上述示例中的外部准备和关闭代码可能会直接嵌入到OutputStream实例化的代码中,而是放置在引导/初始化和最终化代码中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接