1.1.4 HTTP实体

   请求和响应可以携带一些内容实体作为消息,但也可以不携带,实体是可选的。带有实体的请求

被称为实体封闭请求(entity enclosing requests),HTTP协议指导定义了两种实体封闭请求方法:

POST和PUT。响应通常都带有内容实体,但也有例外,比如响应的HEAD方法和204 无内容,304 未修改,

205 重置内容响应。

   根据消息的产生HttpClient分为三种实体类型:

  1. 基于流的实体:消息来源于流。这种类型的消息实体在HTTP响应中最常见。基于流的消息实体通常不能被重复读取。

  2. 自我包含的实体:消息内容在内容或者消息内容独立于链接或者实体。自我包含的实体通常都是可以被重复读取的,这种类型的实体长用户实体封闭请求中。

  3. 封装的实体:内容包含在其他实体。

   当从HTTP响应流读取信息时对连接管理来说这种实体分类显得尤为重要。对于被应用创建和被用HttpClient发送的请求实体来说,基于流式的实体和自我包含的实体的不同更加重要,在这种情况下,建议使用不可重复读取的实体作为流式的,使用可重复读取的实体作为自我包含的。

1.1.4.1 可重复读取的实体

   一个可被重复读取的实体意味着它的内容可被多次读取。只有自我包含的实体可以被重复读取,例如:ByteArrayEntity和StringEntity。

1.1.4.2 使用HTTP实体

   实体既可以表示二进制内容也可以表示字符文本,而且支持字符编码。

   当执行一个带有信息的请求或者请求成功响应返回信息给客户端时实体被创建。

   为了读取实体内容,可以通过HttpEntity的getConetent()方法,此方法返回输入流对象,即java.io.InputStream,也可以通过提供一个输出流给HttpEntity的writeTo(OutputStream)方法一次性读取所有输入流的内容。

   当实体接受一个输入消息,HttpEntity的getContentType()方法和getContentLength()方法就可以读取到实体内容的类型和长度(如果设置了内容类型),HttpEntity的getContentEncoding()方法用于读取文本(像text/plain、text/html)类型实体的编码字符集。但是如果消息头不可用(没有设置)则获取长度会返回-1,获取消息类型会返回NULL,如果消息头可用则返回消息头对象(Header)。

StringEntity myEntity = new StringEntity("important message",
		ContentType.create("text/plain", "UTF-8"));
System.out.println(myEntity.getContentType());
System.out.println(myEntity.getContentLength());
System.out.println(EntityUtils.toString(myEntity));
System.out.println(EntityUtils.toByteArray(myEntity).length);

输出:

Content-Type: text/plain; charset=utf-8
17
important message
17

1.1.5 确保释放资源

   为了确保系统资源被释放,要么关闭响应输入流,要么直接关闭响应链接。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    HttpEntity entity = response.getEntity();
    if (entity != null) {
        InputStream instream = entity.getContent();
        try {
            // do something useful
        } finally {
            instream.close();
        }
    }
} finally {
    response.close();
}

   关闭响应输入流和关闭响应链接还是有差别的,前者试图保持链接状态,而后者则直接断开连接。

   请注意,当使用HttpEntity的writeTo(OutputStream)方法时,一旦实体内容写入输出流完毕,也要确保系统资源被释放,如果该方法持有被HttpEntity的getContent()方法返回的输入流(java.in.InputStream)对象实例,则最好也在finally语句块中关闭该输入流。

   当使用流式实体时,可以使用EntityUtils的coonsume(HttpEntity)方法确保实体内容被全部读取并且潜在的流被关闭。

   有这么一种情况:只有整个响应内容的一小部分需要被读取,此时,HttpClient为了能读取剩下的内容而保持链接可重用所带来的性能损耗太高,对于这种情况,可以通过关闭响应来终止文本流的传输。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
	HttpEntity entity = response.getEntity();
	if (entity != null) {
		InputStream instream = entity.getContent();
		int byteOne = instream.read();
		int byteTwo = instream.read();
		// Do not need the rest
	}
} finally {
	response.close();
}

  这样,链接就不在可用,但是所有链接持有的资源都会被得到正确的释放。

1.1.6 读取实体内容

   推荐通过HttpEntity类的getContent()方法或者writeTo(OutputStream)方法读取实体内容。HttpClient也提供EntityUtils类,这个类里的几个静态方法可以很容易地从实体读取内容。为了不直接从java.io.InputStream内读取内容,我们可以使用EntityUtils的方法以字符串或者字节数组形式返回整个实体内容,然而,除非响应来自一个可被信任的HTTP服务器并且响应内容是有长度限制的否则强烈不建议使用此类。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    HttpEntity entity = response.getEntity();
    if (entity != null) {
        long len = entity.getContentLength();
        if (len != -1 && len < 2048) {
            System.out.println(EntityUtils.toString(entity));
        } else {
            // Stream content out
        }
    }
} finally {
    response.close();
}

    有些情况需要多次读取实体内容,在这种情况下,实体内容必须以某种形式被缓存到内存或者磁盘,最简单的方法是用BufferedHttpEntity包装原始实体,这样实体内容就会被读进内存缓冲,包装器内实体就会总是原始实体。

CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
    entity = new BufferedHttpEntity(entity);
}

1.1.7 创建实体

    HttpClient提供几个用于通过HTTP链接高效传消息的类。这些类的实例可以将消息内容封装成实体并跟随POST、PUT等请求输出到HTTP链接,HttpClient提供几个常用的用于存储字符串、字节数组、输入流、文件等的实体容器,与其对应的类分别为StringEntity、ByteArrayEntity、InputStreamEntity、FileEntity。

File file = new File("somefile.txt");
FileEntity entity = new FileEntity(file,ContentType.create("text/plain", "UTF-8"));
HttpPost httppost = new HttpPost("http://localhost/action.do");
httppost.setEntity(entity);

    请注意,因为InputeStreamEntity只从流中读取一次数据,所以它是不可被重复读取的,通常建议通过实现普通的HttpEntity接口自定义流读取来替代InputStreamEntity,FileEntity就是一个很好的例子。

1.1.7.1 HTML表单实体

    许多应用需要模仿提交HTML表单的过程,例如,为了登录网站或者提交输入数据,HttpClient提供UrlEncodeFormEntity类来实现这一功能。

List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair("param1", "value1"));
formparams.add(new BasicNameValuePair("param2", "value2"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, Consts.UTF_8);
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);

    类UrlEncodedFormEntity的实例会用制定的URL编码方法编码参数并产生如下内容:

param1=value1&param2=value2

1.1.7.2 消息分包

    通常建议让HttpClient自己选择最佳的消息分包编码策略,但是也可以通过将HttpEntity类的setChunked()方法设置为true来强制分包编码,不过需要注意的是HttpClient只是以这样的方式做一个标记,如果HTTP协议版本不支持分包编码则此标记就会被忽略,例如HTTP/1.0就不支持。

StringEntity entity = new StringEntity("important message",ContentType.create("plain/text", Consts.UTF_8));
entity.setChunked(true);
HttpPost httppost = new HttpPost("http://localhost/acrtion.do");
httppost.setEntity(entity);

1.1.8 响应处理

    最简单、最方便的处理响应的方式是通过使用ResponseHandler接口,这个接口含有handleResponse(HttpResponse response) 方法,此方法完全解除了用户对链接管理的担心。当使用ResponseHandler时,HttpClient会自动管理以确保释放链接资源而不管请求执行成功与否。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/json");
ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>() {
    @Override
    public JsonObject handleResponse(final HttpResponse response) throws IOException {
	StatusLine statusLine = response.getStatusLine();

	HttpEntity entity = response.getEntity();
	if (statusLine.getStatusCode() >= 300) {
	    throw new HttpResponseException(statusLine.getStatusCode(), 
	                statusLine.getReasonPhrase());
	}
	if (entity == null) {
	    throw new ClientProtocolException("Response contains no content");
	}
	Gson gson = new GsonBuilder().create();
	ContentType contentType = ContentType.getOrDefault(entity);
	Charset charset = contentType.getCharset();
	Reader reader = new InputStreamReader(entity.getContent(), charset);
	return gson.fromJson(reader, MyJsonObject.class);
    }
};
MyJsonObject myjson = client.execute(httpget, rh);

1.2 HttpClient接口

    HttpClient接口是HTTP请求执行最重要的规范,他强调在请求执行的过程中不附加任何限制和特定的细节,并且保持链接管理、状态管理、认证和重定向处理的细节独立于实现类,这使得使用额外的功能装饰接口变得更加容易,例如响应内容缓冲。

    通常,HttpClient的实现类充当一些特殊目的处理的门面或者是充当负责HTTP协议的某一方面的处理的策略接口,例如:重定向和认证处理,以及决定是否保持链接处于持续链接状态,这使得用户能够有选择性的使用定制的实现替代默认实现。

ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse response,HttpContext context) {
    long keepAlive = super.getKeepAliveDuration(response, context);
    if (keepAlive == -1) {
        // Keep connections alive 5 seconds if a keep-alive value
        // has not be explicitly set by the server
        keepAlive = 5000;
    }
    return keepAlive;
    }
};
CloseableHttpClient httpclient = HttpClients.custom().
    setKeepAliveStrategy(keepAliveStrat).build();


1.2.1 HttpClient线程安全

    期望HttpClient的实现类是线程安全的,建议相同的实现类的实例对于多请求执行是可重用的。

1.2.2 HttpClient资源释放

    如果一个CloseableHttpClient的实例不再需要或者超出了与之关联的链接管理的范畴,那么一定要通过调用CloseableHttpClient的close()方法关闭它。