Httpclient4.4之原理(请求执行)

Apache Httpclient基于java BIO实现的,也是基于apache HttpCore项目。他最基本的功能是执行HTTP方法。HttpClient的API的主要入口就是HttpClient接口,看看这个示例:

1. HTTP请求

所有的http请求都由:方法名,请求url,HTTP协议组成。HttpClient支持HTTP/1.1支持的所有方法:GET,HEAD,POST,PUT,DELETE,TRACE和OPTIONS,HttpClient中都有一个特定的类与之对应:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace和HttpOptions。

HTTP请求URI包括协议,主机名,可选的端口,资源路径,可选的查询条件等。如下例:

HttpGet httpget = new HttpGet("http://www.google.com/search?hl=en"
                    + "&q=httpclient&btnG=Google+Search&aq=f&oq=");
 

HttpClient提供了URIBuilder通用类来创建或修改请求URI,如例:

URI uri = new URIBuilder()
    .setScheme("http")
    .setHost("www.google.com")
    .setPath("/search")
    .setParameter("q", "httpclient")
    .setParameter("btnG", "Google Search")
    .setParameter("aq", "f")
    .setParameter("oq", "")
    .build();
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI())
 

2. HTTP响应

HTTP响应是HTTP请求发送到服务端处理后响应到客户端的消息。响应第一行是协议与协议版本号,接着是数字状态码和一些文本信息,示例演示一下看看执行结果:

package httpclienttest;
 
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.message.BasicHttpResponse;
 
public class T2 {
    public static void main(String[] args) {
        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
                HttpStatus.SC_OK, "OK");
        System.out.println(response.getProtocolVersion());
        System.out.println(response.getStatusLine().getStatusCode());
        System.out.println(response.getStatusLine().getReasonPhrase());
        System.out.println(response.getStatusLine().toString());
    }
}
 

输出为:
 
HTTP/1.1
200
OK
HTTP/1.1 200 OK
 

3. HTTP消息头

HTTP消息头(header)包含多个消息描述的信息,例如:内容长度,内容类型等。HttpClient提供的方法有检索,添加,删除和枚举等操作。 示例:

package httpclienttest;
 
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.message.BasicHttpResponse;
 
public class T3 {
    public static void main(String[] args) {
        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
                HttpStatus.SC_OK, "OK");
        response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
        response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");
        Header h1 = response.getFirstHeader("Set-Cookie");
        System.out.println(h1);
        Header h2 = response.getLastHeader("Set-Cookie");
        System.out.println(h2);
        Header[] hs = response.getHeaders("Set-Cookie");
        System.out.println(hs.length);
    }
}
 

输出:
 
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
2
 

更有效率的方法是通过HeaderIterator接口获得所有的header信息,示例:

package httpclienttest;
 
import org.apache.http.HeaderIterator;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.message.BasicHttpResponse;
 
public class T4 {
    public static void main(String[] args) {
        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
                HttpStatus.SC_OK, "OK");
        response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
        response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");
        HeaderIterator it = response.headerIterator("Set-Cookie");
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
 

输出结果:
 
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
 

他也提供了更便利的方法来解析HTTP消息并获得header中一个个独立的header元素,示例:

package httpclienttest;
 
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicHttpResponse;
 
public class T5 {
    public static void main(String[] args) {
        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
                HttpStatus.SC_OK, "OK");
        response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
        response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");
        HeaderElementIterator it = new BasicHeaderElementIterator(
                response.headerIterator("Set-Cookie"));
        while (it.hasNext()) {
            HeaderElement elem = it.nextElement();
            System.out.println(elem.getName() + " = " + elem.getValue());
            NameValuePair[] params = elem.getParameters();
            for (int i = 0; i < params.length; i++) {
                System.out.println(" " + params[i]);
            }
        }
    }
}
 

输出信息:

c1 = a
path=/
domain=localhost
c2 = b
path=/
c3 = c
domain=localhost
 

4. HTTP Entity

HTTP消息能携带与请求或响应有关的实体内容,它只是可选的,能在有些请求或响应中找到。请求消息中使用实体是针对entity enclosing request的,HTTP规范中定义了两个entity enclosing request方法:POST和PUT。响应通常都是包装一个实体的,当然也有例外!

HttpClient区分三种实体,根据其内容来源:

•streamed(流):内容从流中接收的。流实体是不可重复的。


•self-contained(自包含):内容在内存中或者从连接或其它实体自主获得的。自包含的实体通常都是可重复的。这种类型的实体通常用于entity enclosing request。


•wrapping(包装):内容从其它实体获得。


从HTTP响应中流出内容时,对于连接管理(connection management)这种区别还是很重要的。对于请求实体是通过应用程序创建的并且只是使用HttpClient发送,这种区别对于streamed与self-contained意义不大。这种情况下,建议把不可重复的实体当作streamed,把那些可重复的当作self-contained。

4.1 可重复的实体

一个实体能被重复获取,意味着内容能被读多次。这是唯一可能的自包含实体(如:ByteArrayEntity或StringEntity)。

4.2 使用HTTP实体

因为实体可以代表二进制和字符内容,它是支持字符集的(支持后者,即文字内容)。

从实体中读取内容,可能通过HttpEntity的getContent()方法检索输入流,它返回一个java.io.InputStream,或者可以提供一个输出流作为HttpEntity的writeTo(OutputStream)方法的参数,将返回的所有内容写入给定的流。

当实体收到一个进来的消息时,HttpEntity的getContentType方法和getContentLength方法能被用来读取请求header里的元数据:Content-Type和Content-Length(如果它们可用)。由于header的Content-Type能包含像text/plain或text/html这样的MIME类型的字符集(编码),HttpEntity的getContentEncoding()方法用来读取这个信息。 如果header不可用,长度将返回-1和内容类型为NULL。如果header的Content-Type是可用的,这个Header对象将返回。

创建一个输出消息的实体时,该数据是由实体的创建者提供,示例:
 
package httpclienttest;
 
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
 
public class T6 {
    public static void main(String[] args) throws Exception{
        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
 

5. 确保底层资源的释放

为了确保系统资源的释放,必须关闭与实体相关的内容流或者response自身:

package httpclienttest;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
 
public class T7 {
    public static void main(String[] args) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpget = new HttpGet("http://ifeng.com");
        //使用java7中的语法,自动调用close()方法,所以这里没有显示调用
        try(CloseableHttpResponse response = httpclient.execute(httpget)){
            HttpEntity entity = response.getEntity();
            try(BufferedReader reader = new BufferedReader(new InputStreamReader(
                    entity.getContent(),StandardCharsets.UTF_8))){
                Stream<String> sm = reader.lines();
                sm.forEach(System.out::println);
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
 

关闭内容流与关闭response之间的区别是,前者试图保持消费实体内容的基本连接是活的,而后者关闭连接并丢弃!请注意:一旦实体被完全写出,HttpEntity的writeTo(OutputStream)方法也要确保系统资源释放。如果调用HttpEntity的getContent()方法获得一个java.io.InputStream流的实例,在最后也是要关闭并释放资源的。

当使用流实体工作时,EntityUtils的consume(HttpEntity)方法能确保实体内容完全被消费掉,并自动后台关闭流来释放资源。

有种极少见的情况,请求响应的实体内容只有一小部分需要被检索,剩下的都不需要,而消费剩下的内容又有性能损耗,造成重用连接很高。这种情况下,可以直接关闭response来终止内容流!如下例:

package httpclienttest;
 
import java.io.InputStream;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
 
public class T8 {
    public static void main(String[] args) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpget = new HttpGet("http://ifeng.com");
        try (CloseableHttpResponse response = httpclient.execute(httpget)) {
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                InputStream instream = entity.getContent();
                int byteOne = instream.read();
                int byteTwo = instream.read();
                System.out.printf("%d,%d",byteOne,byteTwo);
                // instream中剩下的内容不需要了!直接关闭response
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
 

这样连接不会被重用,而且所有级别的资源都会被释放!

6. 消费实体内容

消费实体内容的推荐方式是使用HttpEntity的getContent()或HttpEntity的writeTo(OutputStream)方法。HttpClient还配备了EntityUtils类,它提供了几个静态方法让读取实体内容与信息更容易,而不是直接读java.io.InputStream。能通过EntityUtils的这些静态方法检索整个内容体到这符串/字节数组。不管怎样,强烈建议不要使用EntityUtils,除非响应实体产生自一个可信的HTTP服务端并且知道是有限长度的。示例:
 
package httpclienttest;
 
import java.nio.charset.StandardCharsets;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
 
public class T9 {
    public static void main(String[] args) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpget = new HttpGet("http://www.ifeng.com/");
        try (CloseableHttpResponse response = httpclient.execute(httpget)){
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                System.out.println(EntityUtils.toString(entity, StandardCharsets.UTF_8));
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
 

在某些情况下,可能需要读实体内容不止一次。这种情况下,实体内容在某种程度上必须缓冲,无论在内存还是磁盘。最简单的方法是通过BufferedHttpEntity类来包装原始的实体,这将使原始实体的内容读到内存缓冲区。示例:
 
package httpclienttest;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
 
public class T10 {
    public static void main(String[] args) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpget = new HttpGet("http://www.ifeng.com/");
        try (CloseableHttpResponse response = httpclient.execute(httpget)){
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                //实体进行缓冲,可重复使用
                entity = new BufferedHttpEntity(entity);
                try(BufferedReader reader = new BufferedReader(new InputStreamReader(
                        entity.getContent(),StandardCharsets.UTF_8))){
                    Stream<String> sm = reader.lines();
                    sm.forEach(System.out::println);
                }
                System.out.println("读第二次!");
                try(BufferedReader reader = new BufferedReader(new InputStreamReader(
                        entity.getContent(),StandardCharsets.UTF_8))){
                    Stream<String> sm = reader.lines();
                    sm.forEach(System.out::println);
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

7. 创建实体内容

HttpClient提供了几个通过HTTP connection有效地流出内容的类。这些类的实例与POST和PUT这样的entity enclosing requests有关,为了把这些实体内容放进即将发出的请求中。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);
 

请注意:InputStreamEntity是不可重复的,因为它只能从底层数据流读一次。通常推荐使用InputStreamEntity来实现一个自定义的self-contained的HttpEntity类。

7.1 HTML表单

许多应用程序需要模拟提交一个HTML表单的过程,例如,为了登录一个web应用程序或者提交输入数据,HttpClient提供一个实体类UrlEncodedFormEntity来帮助完成这个过程。

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
 

7.2 HTTP分块

通常推荐HttpClient选择适当的基于HTTP消息传输特性的传输编码。然而,有个可能是通知HttpEntity优先使用分块编码(chunk coding),通过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);
 

8. response处理

处理response最简单,最方便的方式是使用ResponseHandler接口,它包含handleResponse(HttpResponse respnse)方法。这种方法让用户完全不用担心连接的管理。使用ResponseHandler时,HttpClient将自动释放连接并把连接放回连接管理器中,不论请求执行成功还是失败。示例:
 
package httpclienttest;
 
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
 
public class T14 {
    public static void main(String[] args) {
        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 = httpclient.execute(httpget, rh);
    }
}

相关推荐