Android中HTTP相关的API

图片 1

你以前是否知道HttpURLConnection的默认实现有个在某些情形下自动重试的特性?好吧,我之前就不知道。当时的情况是,客户端的确触发了超时异常,但是却被HttpURLConnection给捕捉了,而它自己决定重新尝试一次。这就意味着,你调用了HttpURLConnection的read()方法,它阻塞了,你正在等待,看起来就好像是在等待第一次请求的响应一样。但是在HttpURLConnection内部,它作了不止一次尝试,因此创建了不止一个socket连接。这就解释了为什么第二次及以后的请求永远在日志里找不到,因为这些第二次之后的请求是HttpURLConnection内部发起的。

urlConnection.setRequestProperty("Accept-Encoding", "identity");
private static final String[] restrictedHeaders = { /* Restricted by XMLHttpRequest2 */ //"Accept-Charset", //"Accept-Encoding", "Access-Control-Request-Headers", "Access-Control-Request-Method", "Connection", /* close is allowed */ "Content-Length", //"Cookie", //"Cookie2", "Content-Transfer-Encoding", //"Date", //"Expect", "Host", "Keep-Alive", "Origin", // "Referer", // "TE", "Trailer", "Transfer-Encoding", "Upgrade", //"User-Agent", "Via"};

无论如何,先说点背景情况吧,我们的项目里有自动记录活动日志的功能,当某些情况下,比如一个进程启动的时候就会进行记录。这包括我们那出问题的网络服务客户端和服务端,因为它们两者都属于系统的一部分。在某些时候,我们注意到,服务端的响应还没有发出的时候,另外一个来自同样客户端的请求又发了过来。这个真是出乎意料的,因为客户端代码是单线程的,也没有其他的客户端掺和进来。审查代码、测试之后,结论是我们的客户端不可能在第一个请求还没结束的时候再同时发出另外一个。

private void disableConnectionReuseIfNecessary() { // HTTP connection reuse which was buggy pre-froyo if (Integer.parseInt(Build.VERSION.SDK)  Build.VERSION_CODES.FROYO) { System.setProperty("", "false"); }}

当我们在maven仓库中搜索关键字(http
client)时,会出现十几页的搜索结果,可见在Java社区中http
client之多,但是这些当中我们常见的不多。

引言:如果你也是开发者的话,你很可能已经知道PoLA法则(Principle of
Lease
Astonishment)。那么,看看这篇文章讲述的充满奇幻色彩的调试经历,来见识一下PoLA是如何与HttpURLConnection发生了关联。

Android中大多数应用都会发送和接受HTTP请求,在Android
API中主要由两个HTTP请求的相关类,一个是HttpURLConnection,另一个是Apache
HTTP
Client。这两个类实现的HTTP请求都支持HTTPS协议,基于流的上传和下载,可配置超时时间,IPv6和连接池。Apache
HTTP
Client
DefaultHttpClient和同类的AndroidHttpClient都是可扩展的类。它们有大量且灵活的API,适用于网页浏览器开发。同时它们比较稳定并且bug较少。但是繁多的API的现实下,对其改善与保持兼容性不可得兼,明显Android团队的精力已然不在Apache
HTTP
Client。HttpURLConnectionHttpURLConnection是一个通用,轻量的实现,可以满足大多数的程序进行HTTP请求。这个类虽然一开始比较简陋,但是其主要的几个API使得我们更容易进行稳定改善。连接池污染在冻酸奶之前,HttpURLConnection有着一些烦人的bug。最烦人的就是调用一个可读的InputStream的close方法会污染连接池。我们需要禁用连接池绕开这个问题,如下代码可以禁用连接池。

经过我的使用对比,还是觉得Feign的HTTP client使用起来比较方便,推荐使用。

图片 1

当然,这里还需要服务器端设置HTTP缓存相关的头信息。哪家强在2.3之前的版本,Apache的HTTP请求响应实现比较稳定,bug也少,所以在那些版本上它的最好。但是在2.3之后,毫无疑问,HttpURLConnection是最好的。它API精简实用,默认支持压缩,响应缓存等。最重要的这是Android团队重点投入的,而Apache的版本已经被抛弃了。所以还是使用HttpURLConnection吧。原文信息:Android’s
HTTP Clients译文转自:技术小黑屋

Feign使得Java
HTTP客户端编写更方便。Feign灵感来源于Retrofit、JAXRS-2.0和WebSocket。Feign最初是为了降低统一绑定Denominator
到HTTP API的复杂度,不区分是否支持 RESTful。

import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.concurrent.Executors;
import com.sun.net.httpserver.HttpServer;
/**
 * Created by koen on 30/01/16.
 */
public class TestMe {
 public static void main(String[] args) throws Exception {
  startHttpd();
  HttpURLConnection httpURLConnection = (HttpURLConnection) new URL("http://localhost:8080/").openConnection();
  if (!(httpURLConnection instanceof sun.net.www.protocol.http.HttpURLConnection)) {
   throw new IllegalStateException("Well it should really be sun.net.www.protocol.http.HttpURLConnection. "
     + "Check if no library registered it's impl using URL.setURLStreamHandlerFactory()");
  }
  httpURLConnection.setRequestMethod("POST");
  httpURLConnection.connect();
  System.out.println("Reading from stream...");
  httpURLConnection.getInputStream().read();
  System.out.println("Done");
 }
 public static void startHttpd() throws Exception {
  InetSocketAddress addr = new InetSocketAddress(8080);
  HttpServer server = HttpServer.create(addr, 0);
  server.createContext("/", httpExchange -> {
   System.out.println("------> Httpd got request. Request method was:" + httpExchange.getRequestMethod() + " Throwing timeout exception");
   if (true) {
    throw new SocketTimeoutException();
   }
  });
  server.setExecutor(Executors.newCachedThreadPool());
  server.start();
  System.out.println("Open for business.");
 }
}

由于HTTP中的Content-Length头信息返回的是压缩后的大小,所以我们不能使用getContentLength()来计算未压缩数据的大小。正确的做法应该是读取HTTP响应中的字节,直到InputStream.read()方法返回为-1.HTTPs改进从Gingerbread开始,增加了对HTTPs链接的优化。在进行HTTPs请求之前,HttpsURLConnection会尝试使用服务器名字指示(Server
Name
Indication),这种技术可以让多个HTTPs主机共享一个IP地址。在HTTPs请求中,HttpsURLConnection也支持压缩和会话标签。一旦连接失败,HttpsURLConnection会不使用上面的三个特性进行重试。这样即可以保证在连接时高效率地连接到最新的服务器,也可以在不破坏兼容性的同时连接到旧服务器。响应缓存从4.0开始,HttpURLConnection引入了响应缓存机制。一旦缓存创建,后续的HTTP请求会按照下面情况处理完全缓存的响应会直接从本地存储中读取,响应很快,不需要网络连接。有条件的缓存必须由服务端进行freshness验证,比如client发出一个请求,如”Give
me /foo.png if it changed since
yesterday”,然后服务器端要么返回最新的内容,要么返回304未修改的状态。如果内容不变,则不下载。没有缓存的响应需要服务器处理,然后这些请求被缓存下来。对于低于4.0的版本,我们可以使用反射开启响应的缓存机制

  • 连接池
  • 超时间的设置(连接超时、读取超时等)
  • 是否支持异步
  • 请求和响应的编解码
  • 可扩展性

是的,没错,我听明白了,但是客户端的日志该怎么解释?客户端是不是应该抛出一个“ReadTimeoutException”异常,或者类似的玩意,然后可以写到日志里?然而,没错,事实上,并没有。就像现在发现的一样,真正的意外来自HttpURLConnection类的内部(更确切地说,是默认的Oracle的官方实现sun.net.www.protocol.http.HttpURLConnection)。

压缩数据与大小从2.3开始,我们默认对返回的响应进行了压缩,HttpURLConnection会自动为发出去的请求加上Accept-Encoding:
gzip这个头信息。如果gzip压缩的响应有问题,可以通过下面代码禁用gzip。

这几天抽空对HTTP
client的使用做了一些调研,总结出了这篇文章,有不全面的或者偏差的点,请在评论中讨论。

好,先把这事放一边,我想问的是,到底是谁搞出这么个设计来,既没文档描述又没有可配置选项?为啥我做了十五年的Java开发,却对此一无所知?更要命的是,为什么它要对一个构造异常的POST请求进行重试呢?这是对PoLA赤裸裸的违背!

rivate void enableHttpResponseCache() { try { long  = 10 * 1024 * 1024; // 10 MiB File  = new File(getCacheDir(), "http"); Class.forName("android.net.") .getMethod("install", File.class, long.class) .invoke(null, , ); } catch (Exception ) { }}

Feign还以子项目的方式提供了多种Client实现,比如(feign-httpclient、feign-okhttp、feign-ribbon),它们集成了当前比较流行的Http
Client组件,如Apache
HttpClient、Okhttp、Ribbon等,且其默认的Client实现为HttpURLConnection。

如果你和我一样也是开发者的话,你很可能已经听说过“PoLA”原则,或者叫作“产生最少意外”原则。意思非常简单,就是不要让你的用户感到惊讶。或者更明确一些,就像本文这种情况,不要让另外一个开发者感到惊讶。不幸的是,我上个星期就遇到了大大超出我意外的事情,我们有个服务的客户调用端总是发出一些垃圾的请求。

其中每个都有异步的http
client实现,但是已经已经弃用(AsyncRestTemplate已经被WebClient取代)。

好吧,其实也不是。我们发现了罪魁祸首,服务端的容器软件HTTP的读超时设置被调得太低了。服务端的日志显示的确生成了响应,但是客户端却在此之前已经断开了,因为服务器端发生了读超时。这些在服务器端当然没有日志记录,因为这种行为是更低一层协议决定的(HTTP栈),而不是服务端的应用代码。

可以通过在JVM启动参数中添加:-Dsun.net.http.allowRestrictedHeaders=true,来设置允许修改这些HTTP
Header。

本文由码农网 – Sandbox
Wang原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

但是与Feign相比的缺点是:HTTP Get方法不可以在request
body中传递参数,只能是基于URL
Template的方式传递参数(虽然很多人不提倡使用HTTP
get发送数据);调用方式相对繁琐;设置请求的Http
header参数复杂。优点就是有具有spring框架的优良传统,扩展性特别好。

为了完整起见,再提醒一下,如果你让这段代码在容器环境里执行的话,结果可能会不同。你的容器或者你的代码所依赖的库有可能会替换掉Oracle默认的内部实现,请参考URL.setURLStreamHandlerFactory()。现在你可能会问,那个家伙当时为什么要使用HttpURLConnection呢?他难道是坐着演讲巡游车上班吗(原文Wooden
Soapbox,由来参见

if (!this.doOutput) { throw new ProtocolException("cannot write to a URLConnection if doOutput=false - call setDoOutput;} else { if (this.method.equals { this.method = "POST"; }}

运行之,将会得到类似下面的输出。

  • HttpURLConnection 不可以
  • OkHttpClient 报错:method GET must not have a request body.
  • ApacheHttpClient 可以
------> Httpd got request. Request method was:POST Throwing timeout exception
Exception in thread "main" java.net.SocketException: Unexpected end of file from server
 at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:792)
 ...

Retrofit、RestTemplate、Feign这一类HTTP
client是在RESTful标准的微服务中常见的,这一类将client的易用性做到了更好,并且更方便编写restful
api的调用。一般还会提供消息转换、参数映射、提供注解等方式,在使用上写少量的代码即可完成功能,更像是一个RPC调用的client编写方式。

你说垃圾请求吗?是的,就像这样,我们完全不清楚这些请求是从哪里来的。又是这样一个时刻,经理们毫无头绪,抱头乱窜,惊呼“我们肯定是被黑客攻击了”,或者 ”有人把防火墙给关掉了!!”

HTTP的GET方法是否可以带请求体

注意,我们的监听服务被调用了两次,但是我们只发了一个请求。如果我们加上-Dsun.net.http.retryPost=false这个属性再运行一次的话,我们会得到下面的输出:

  • HttpURLConnection
  • Apache Commons HttpClient(或被称为 Apache HttpClient 3.x)
  • Apache HttpComponents Client(或被称为 Apache HttpClient 4.x)
  • OKHttp(Square,Android应用中常见)
  • Asynchronous Http Client
  • google-http-client-xxx(google各种版本)

如果你使用其他更专业的web服务实现的时候(如Spring WS, CXF,
JAX-WS实现等等),他们很可能使用了诸如Apache HTTP
Client的组件。当然了,如果你自己的代码需要发起HTTP连接的话,你也可以使用它。没错,我还是推荐你使用Apache
Commons
HttpClient,虽然这货修改API的频率比普通时尚达人换鞋的频率都还要高。好了,我的牢骚完了。

以上这些HTTP
client相对功能简单的,主要实现的功能是调用http协议饿接口,主要在协议层面的控制,比如,设置http
method、设置超时时间(连接超时、读取超时)、请求参数、header、cookies以及响应的处理等。还有类似于Apache
CXF中的client组件,它是一个经常用在web
service服务开发中的组件。它们是在上述基础的http
client之上封装了一层,对某一特定使用场景做一些定制化的包装,增加使用的便利性。

经过一整天的调试和研究日志发现,事实上,在服务端处理还未结束的时候客户端其实已经断开连接了。所以,这些请求终究并不是同时发生的,但是为什么我们花了一整天的时间才发现呢?这跟我们玩了一整天的星球大战有啥区别?

目前(5.0.4.RELEASE)的 RestTemplate 主要有四种 ClientHttpRequestFactory
的实现,它们分别是:

让我们上一些代码重现一下。

优秀的HTTP client需要具备的特性:

Open for business.
Reading from stream...
------> Httpd got request. Request method was:POST Throwing timeout exception
------> Httpd got request. Request method was:POST Throwing timeout exception
Exception in thread "main" java.net.SocketException: Unexpected end of file from server
 at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:792)
 ...
  1. 默认情况不允许修改受限制的Header中的值,受限制的Header如下:

经过好几天激动人心的调试,最后问题解决的方式却略显轻巧,仅仅指定了一个属性就搞定了。无论如何,这个设计真是着实让我很意外,因此我还专门写了这篇文章来讲述,并且,你也看到了这篇文章。

现在你可能已经猜到了,这是一个BUG(链接:)。当然了,说是BUG并不是指的它的重试机制,而是指它为什么对异常POST请求也会进行重试。按照HTTP
RFC的规范,POST请求并非幂等,因此多次提交POST会带来服务器端数据的改变。但是别担心,Bill早就把这个BUG修改好了。Bill的解决方法是加了一个开关。Bill了解向后兼容原则。Bill认为最好的方法是添加一个默认开启的开关,这样可以保证这个BUG的向后兼容。Bill笑了。Bill已经能够看见全球无数的Java开发者掉进这个大坑时惊愕的面孔。但是,你们都别学Bill好吗?

随着spring
boot快速发展和HTTP2.0的支持力度增加,现在restful标准的微服务接口越来越多,选择一个优秀的HTTP
client也越来越重要了。

后来通过查阅资料发现 RestTemplate 默认是使用 spring 自身的
SimpleClientHttpRequestFactory
创建请求对象和对其进行相关设置(如请求头、请求体等),它只支持 PUT 和
POST 方法带请求体,RestTemplate 的 DELETE 方法不支持传入请求体是因为 JDK
中 HttpURLConnection 对象的 delete 方法不支持传入请求体(如果对
HttpURLConnection 对象的 delete 方法传入请求体,在运行时会抛出
IOException)。我尝试使用HttpComponentsClientHttpRequestFactory创建请求对象,依然能在Get方法中带请求体。

Feign还提供了请求和响应数据格式的编码解码器,用于解析json报文的有feign-gson、feign-jackson等,以及用于解析xml报文的有feign-sax、feign-jaxb等。

RPC的优势在于,它们基本都使用了基于NIO的高效的网络传输模型,并且针对服务调用场景专门设计了协议和序列化技术,还对传输数据做了压缩处理。HTTP的优势在于成熟稳定、实现简单、支持广泛、兼容性良好、防火墙友好、消息的可读性高。一般在开放API、跨平台的服务间调用和对性能要求不苛刻的场景(HTTP/2可提高性能)中广泛使用。

  1. 基于 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory
  2. 基于 Apache HttpComponents Client
    的HttpComponentsClientHttpRequestFactory
  3. 基于 OkHttp 3 的 OkHttpClientHttpRequestFactory
  4. 基于 Netty4 的
    Netty4ClientHttpRequestFactory(已弃用,已经被reactor相关取代)

虽然目前来看服务之间调用大部分还是采用的RPC和消息队列,但是目前随着微服务的解决方案越来越多样性,也有很多人选择的HTTP这种通用性很强的协议。

  1. 在使用feign调用http接口时,如果在请求体中写入数据,GET方法会被转成POST方法发送请求,导致服务端报出不支持POST方法的405错误,其实是get方法不能有request
    body,会制造一定的问题排查困难。查看HttpURLConnection的源码发现如下代码块:

RestTemplate是spring
web框架中提供的restful接口调用工具,它也是针对多个基础的http
client组件做了集成,如Apache
HttpClient、Okhttp等,且其默认的Client实现为HttpURLConnection。

列举几个常见的:

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图