Http客户端常规使用说明

Http客户端常规使用说明

HttpClient出自Apache基金会,历史非常悠久,经过长足的发展,功能非常丰富,Api也越来越易用.经过大规模应用,表现非常稳定.被定制打包在Android SDK中,可以在Android客户端中非常方便地使用.

HTTP协议简单说明

HTTP协议描述了一个”请求-响应”模型.
一个请求消息是由请求行、请求头字段、一个空行和消息主体构成
客户端通过”请求头”与服务端进行内容协商,例如:

  • 是否使用长连接,连接的过期策略
  • Cookie交换
  • 访问权限凭证
  • 当前访问的内容是否可以缓存,是否有更新
  • 是否接受所访问资源的压缩形式,可以使用哪种压缩方法
  • 是否需要提升为加密连接,所支持的加密算法有哪些

对应的”响应头”中也包含上述协商内容,服务端在接收到客户端请求后会首先读取请求行,识别请求方法和资源定位地址,然后根据请求头的描述返回客户端需要的且能识别的内容.

Apache HttpClient(AHC)基础

客户端发送一个Http请求通常要考虑如下问题:

  1. 如何组装一个请求的请求行,请求头,消息体
  2. 不同类型的数据如何编码封装到消息体中
  3. 请求发生异常时如何处理(是否需要重试?)
  4. 如何忽略响应中断一个请求
  5. 如何处理重定向响应

HttpClient在API设计上充分考虑了http协议的方方面面:

HttpClient http = HttpClients.createDefault();

//HTTP GET
String uri = "http://www.baidu.com";
HttpGet get = new HttpGet(uri);

ClosableHttpResponse resp = http.execute(get);
//get.abort() 可以中断请求的执行,由于execute方法是阻塞的,所以abort方法只能由其他线程来执行
final HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() == 200) {
    //处理响应内容
    EntityUtils.consume(entity)
}
resp.close();//可以随时调用该方法,中断请求,在一定的情况下可以避免完整下载响应消息体,节省带宽资源

//HTTP POST
String uri = "https://life.meizu.com/login";
HttpPost post= new HttpPost(uri);
//设置请求消息体
List<NameValuePair> nvps = new ArrayList<>(2);
nvps.add(new BasicNameValuePair("userName","life_user_name"));
nvps.add(new BasicNameValuePair("password","life_password"));
post.setEntity(new UrlEncodedFormEntity(nvps,UTF_8));
//设置请求头
post.setHeader(HttpHeaders.REFERER,"http://life.meizu.com/index.html");
ClosableHttpResponse resp = http.execute(post);
final HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() == 200) {
    //处理响应内容
    EntityUtils.consume(entity)
}
resp.close();

上述示例代码稍显复杂,每次都需要显式关闭资源,如下:

HttpClient http = HttpClients.createDefault();

//HTTP GET
String uri = "http://www.baidu.com";
HttpGet get = new HttpGet(uri);
//修改为自动管理连接资源后,减少了样板的代码,并且可以直接返回需要的类型.
String http.execute(get,new ResponseHandler<String>() {

    //回调该方法,自动管理连接资源
    public String handleResponse(final HttpResponse response) throws IOException {
        final StatusLine statusLine = response.getStatusLine();
        final HttpEntity entity = response.getEntity();
        if (statusLine.getStatusCode() >= 300) {
            EntityUtils.consume(entity);
            //log error
        }
        return entity == null ? null : EntityUtils.toString(entity, UTF_8);
    }
});

下面看一下,如何使用HttpClient的自动异常重试和重定向支持:

//默认设置只会对任何GET,HEAD进行自动重定向,认为POST/PUT需要用户确认
//默认的异常处理机制会认为GET,HEAD是幂等请求进行重试,但是如果异常是发生在请求发送完成之后则不重试
HttpClient http = HttpClients.createDefault();

//如果要自定义重定向处理策略和异常处理策略,可以使用自己的策略来初始化HttpClient
HttpClient http = HttpClients.custom()
                             .setRetryHandler(/*异常重试处理器*/)
                             .setRedirectStrategy(/*重定向策略*/)
                             .build();

对于客户端调用来说,自动重试机制是把双刃剑,一方面可能由于服务端压力大导致请求失败,此时自动重试只会加剧这种情况,重试没有意义.实际经验表明,重试还是交由业务代码根据实际情况处理比较安全.可以通过如下代码禁用自动重试功能.

HttpClient http = HttpClients.custom().disableAutomaticRetries().build();

连接

  1. 持久化连接

    扫描二维码关注公众号,回复: 2610971 查看本文章

    每一次的http交互都基于一条TCP连接,而建立TCP连接的消耗是可观的,所以HTTP/1.1默认情况下会保持连接,可以重用连接来完成多次http请求交互.

    Connection,这个头可以用来告知http交互的双方如何处理连接.有三种取值close,keepalive.

    • 在请求中:

      close : 告知服务端处理结束后关闭连接

      ​keepalive: 告知服务端处理结束后需要保持连接

    • 在响应中:

      close: 告知客户端处理结束后服务端会关闭连接

      keepalive: 告知客户端处理结束后服务端会保持连

  2. keepalive策略

    HTTP/1.1,默认情况下会无限期保持,对于服务端而言需要同时服务大量用户,而资源有限(例如:内存),所以需要设置保持连接的时间.

    Keep-Alive,客户端请求中包含这个头可以指定期望服务端保持连接的时长.

    AHC中有个”路由”概念,在初始化连接池时需要设置全局最大连接数对每个目标域的最大连接数.尤其需要注意后者,其默认值是2.在服务端调用第三方API时往往会成为并发瓶颈.

    HttpClients.custom()
              //默认的keep-alive策略为使用响应中Keep-Alive的超时时间
           .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
              .setMaxConnTotal(100)//全局最大连接数,默认为20
              .setMaxConnPerRoute(100);//每个域的最大连接数,默认为2
  3. 超时设置

    管理连接池的客户端1,在一次http请求的过程中有三个可能超时的机会:

    • 从连接池中获取连接超时(根据连接池设置,可能当前没有空闲连接)
    • 建立TCP连接超时(连接池中没有可用连接,但也没满的时候,会创建新连接)
    • 传输数据过程超时(Java可能SocketTimeout)

    AHC可以使用如下代码进行设置:

    HttpClients.custom()
              .setDefaultRequestConfig(
                RequestConfig.custom()
                             .setConnectTimeout(500)
                             .setConnectionRequestTimeout(100)
                             .build())
              .setDefaultSocketConfig(
                SocketConfig.custom()
                            .setSoTimeout(30000)
                            .build())
  4. 安全连接(https)

    https通过证书机制校验访问的目标主机是否合法,并使用加密算法保护交互信息不被中间人截获.但是建立一条https连接的成本更高.

    在C/S模式中使用时,往往我们并不关心目标主机的合法性(都是合作方或内部服务使用自签名证书),只希望使用加密.Apache HttpClient中默认情况是会使用严格校验的,需要设置关闭校验.

    HttpClients.custom()
       .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

缓存

为了节省带宽,加速客户端的响应速度,http1.1规定了缓存相关的头.通过这些头,客户端和服务端来协商内容是否可以缓存,什么时候过期,什么时候需要更新.

  1. Cache-Control
常用取值 含义 示例
max-age 缓存内容的过期时间,以秒为单位. Cache-Control: max-age=10
no-cache/no-store 表示客户端不能缓存内容 Cache-Control: no-cache

max-age 值为0时表示,每次都要跟服务端交互校验缓存有效性

  1. Last-Modified/If-Modified-Since

    这两个头的值都是一个具体的时间,例如Last-Modified: Tue, 15 Nov 2016 13:43:21 GMT

    响应头中可以使用Last-Modified告知客户端内容的最后更新时间,客户结合Cache-Control的值来判断下次请求相同内容时是否需要真正发交互,或者仅仅是想服务端校验缓存内容有效性.

    客户端可以使用If-Modified-Since请求服务端校验本地缓存的有效性,如果服务端校验有效则直接返回304状态码,无须传输具体内容.

  2. E-Tag/If-None-Match

    跟上面一对请求头类似,这两个头也是用于协商内容缓存的.不同的是这两个头是利用内容的唯一标识校验缓存内容的有效性.示例:E-Tag:3691308f2a4c2f6983f2880d32e29c84

    响应头中使用E-Tag告知客户端内容的标识信息,客户端将该标识与内容一同缓存起来.

    客户端可以使用If-None-Match请求服务端校验本地缓存是否发生变更,如果没有变,服务端直接返回304状态码,无须传输具体内容.

AHC中可以通过设置上述的协议头,来自己控制缓存.当然也提供了简单的配置能力,自动处理缓存.

//初始化一个具有自动处理缓存能力的httpclient
CloseableHttpClient hc = CachingHttpClients.createFileBound(new File("/data/cache"));

//或者如下代码
hc =  CachingHttpClients.custom().setCacheDir(new File("/data/cache")).build();

Session&Cookie

通常一个业务可能需要横跨多个请求-响应才能完成,我们称之为一个会话(Session).电商业务中的购物车过去是一个比较常用的场景,用户会选取多个商品,添加到购物车,然后阶段.此类会话应用必然包含状态数据,有状态的服务在分布式服务中是个大难题.对于使用Servlet技术实现的服务端,过去会使用HttpSession来支持会话能力.但实践证明该方式完全无法应对互联网应用需求.因为服务端保存会话数据在分布式环境中会引发不一致问题,进而需要更复杂的架构来解决,并在性能和可分区性之间[^CAP]做出取舍.

著名的CAP原则,即在分布式系统中,绝对的数据一致性,原子性和可分区性,同时只能满足其中两个.

Cookie作为一种客户端状态方案,在一定的场景下可以取代服务端Session.例如:保存用户的访问计数器,状态数据量小(不超过4K),且无安全隐患.

AHC中默认会启用Cookie能力,可以通过如下代码定制Cookie的存储方式:

//通过实现CookieStore接口,可以自定义Cookie存储方式
CloseableHttpClient hc = HttpClients.custom()
                            .setDefaultCookieStore(new BasicCookieStore()).build();

内容协商

在HTTP协议中,客户端/服务端可以通过头部,协商交互内容的方式.相关的主要头部如下:

头部名称 使用位置 含义 值示例
Accept 请求 可接受的响应内容类型 text/html,application/json
Accept-Encoding 请求 可接受的响应内容编码方式 gzip, deflate
Accept-Language 请求 可接受的响应语言列表 en-US, zh-CN
Accept-Charset 请求 可接受的响应内容文字编码 UTF-8,GBK
Content-Type 请求/响应 请求/响应内容的类型 application/json; charset=utf-8
Content-Encoding 请求/响应 请求/响应内容的编码方式 gzip

为了有效降低网络延时,压缩请求/响应体内容是比较常用的方法.利用Accept-Encoding/Content-Encoding,可以达成此目的.

这些头部的存在,令程序自动处理压缩/解压成为可能.在AHC中,如果不显式禁用,默认情况下会自动添加Accept-Encoding: gzip,deflate头部.服务器返回的响应中包含Content-Encoding: gzip也会被自动处理解压.

如果要禁用该特性需要使用如下代码:

CloseableHttpClient hc = HttpClients.custom().disableContentCompression().build();

Apache HttpClient 完整配置示例

CloseableHttpClient hc = CachingHttpClients.custom()
    .setCacheDir(new File("/data/cache"))//设置本地缓存目录
    .setHostnameVerifier(ALLOW_ALL_HOSTNAME_VERIFIER) //https不校验域名
    .disableAutomaticRetries()//禁用自动重试
    .setMaxConnTotal(100)//全局最大连接数
    .setMaxConnPerRoute(100)//单个域的最大连接数
    .setDefaultRequestConfig(
      RequestConfig.custom()
                   .setConnectTimeout(500)//建立连接超时时间设置
                   .setConnectionRequestTimeout(100)//从池中获取连接的超时时间
                   .build())
    .setDefaultSocketConfig(
      SocketConfig.custom()
                  .setSoTimeout(30000)//socket传输超时时间
                  .build())
    .setDefaultCookieStore(new BasicCookieStore())
    //可以通过系统属性值配置指定构建参数,例如-Dhttp.keepAlive=true表示开启Keep-Alive
    .useSystemProperties()
    .build();

OkHttpClient

这是最近一个比较火的http客户端库,使用java编写,出自Square.库本身比较精简,依赖简单.默认提供以下支持

  • HTTP/2,允许对同一主机的所有请求共享同一条连接
  • 非HTTP/2环境,提供连接池以降低请求延迟
  • 透明处理GZIP压缩,减少传输占用带宽
  • 缓存响应内容,避免重复发送相同请求.

对比Apache HttpClient

Apache HttpClient OkHttp
协议支持 HTTP1.0,1.1
依赖 http-core,commons-logging,commons-codec,jdk6+
异步支持 通过HttpAsyncClient支持
相关模块 httpmime,fluent-hc, httpclient-cache,httpclient-win,httpclient-osgi
代码复杂度 拆分成多个模块,每个模块都拆分成功能内聚的多个类
API易用性 通过fluent-hc提供简单的api
文档 文档丰富完整
支持平台 Java,提供单独的HttpClient for Android

上述只是初步的简单对比,另外根据网络上的资料,Android已经删除了预置的httpclient,推荐使用okhttp.

okhttp在官网上标榜自己的设计实现目标是高效,具体性能没有实际测试,不做过多讨论.作为相对年轻的http客户端库,文档虽然简单,但是源码比较少,API设计简洁,也是比较容易理解使用的.

AHC历史悠久已经对非常多的需求做了十分精细的覆盖,例如要调用一个使用自签名证书的https服务.在AHC中需直接提供了相关的常量,便于调用者去配置.而okhttp并没有提供.

//Apache HttpClient
//AHC中提供了大量类似的常量或者默认实现,一种大而全的感觉.
//使用AHC时,有什么需求可以先去doc中查找一下,基本都可以找到支持类
public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactory {

    public static final String TLS   = "TLS";
    public static final String SSL   = "SSL";
    public static final String SSLV2 = "SSLv2";

    //直接提供三种域名校验方法的常量
    public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
        = new AllowAllHostnameVerifier();

    public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
        = new BrowserCompatHostnameVerifier();

    public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
        = new StrictHostnameVerifier();

    //....其他代码
}
//直接使用
HttpClients.custom()
    .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

//okhttp 需要自己提供实现
new OkHttpClient.Builder().hostnameVerifier(new HostnameVerifier(){

     @Override
     public boolean verify(String hostname, SSLSession session){
         return true;
     }
})

对于缓存AHC也提供了很多细致的调整选项,而Okhttp相对简单.

//Apache HttpClient内置文件和内存两种缓存方式,通过CacheConfig类提供了如下属性用来控制缓存行为
long maxObjectSize;
int maxCacheEntries;
int maxUpdateRetries;
boolean allow303Caching;
boolean weakETagOnPutDeleteAllowed;
boolean heuristicCachingEnabled;
float heuristicCoefficient;
long heuristicDefaultLifetime;
boolean isSharedCache;
int asynchronousWorkersMax;
int asynchronousWorkersCore;
int asynchronousWorkerIdleLifetimeSecs;
int revalidationQueueSize;
boolean neverCacheHTTP10ResponsesWithQuery;

//而Okhttp中只有Cache只能通过构造函数传递缓存的最大尺寸,以及本地缓存文件夹路径
Cache(File directory, long maxSize) 

丰富的可调整参数,对服务端程序是有意义的,因为任何一个配置都可能对性能产生影响.在大量并发下,效果会成倍放大.但对于客户端程序而言,影响微乎其微.

所以关注并发性能的服务端程序,推荐使用AHC,而客户端使用Okhttp可以获得非常好用简单的API.

使用示例

OkHttpClient client = new OkHttpClient();
//get
String get(String url) throws IOException {
  Request request = new Request.Builder()
                               .url(url)
                               .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

//post
public static final MediaType JSON
    = MediaType.parse("application/json; charset=utf-8");
String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(JSON, json);
  Request request = new Request.Builder()
                               .url(url)
                               .post(body)
                               .build();
  Response response = client.newCall(request).execute();
  return response.body().string();
}

参考资料 :

OkHttp:Java 平台上的新一代 HTTP 客户端

Retrofit: A type-safe HTTP client for Android and Java


  1. 比如配置了PoolingHttpClientConnectionManager的Apache HttpClient.

猜你喜欢

转载自blog.csdn.net/u012631045/article/details/81480093