最近在整理Android常用第三方框架相关的东西,说道Android的框架,无外乎就是Android开发中常见的网络、图片缓存、数据交互、优化、页面等框架,其中网络作为一个基础部分,我相信大家更多的是使用OkHttp,而在长连接中有Socket和webSocket等,今天给大家总结下OkHttp相关的内容,部分参考网络资源。

OkHttp简介

OkHttp作为时下Android最火的Http第三方库可以说被大多数的Android客户端程序所使用,Retrofit底层也是使用OkHttp,与Volley等网络请求框架相比,OkHttp具有如下的一些特点:

  • HTTP/2支持所有访问相同主机的请求共享一个套接字。也就是说支持Google的SPDY协议,如果 SPDY 不可用,则通过连接池来减少请求延时。
  • 连接池减少了请求延迟(如果HTTP/2不可用)。
  • 透明GZIP压缩减少了下载大小。
  • 响应缓存完全避免了重复请求的网络使用。
  • 当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址

OkHttp官网地址:http://square.github.io/okhttp/
OkHttp GitHub地址:https://github.com/square/okhttp

使用示例

OkHttp的使用也非常简单,支持Get、Post等多种请求方式,并且支持文件等的上传下载等多种功能,可以说现在你业务中能涉及到的情况,OkHttp都能解决。下面是一些简单的使用示例。

同步Get请求

private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    Request request = new Request.Builder()        .url("http://publicobject.com/helloworld.txt")        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    Headers responseHeaders = response.headers();    for (int i = 0; i < responseHeaders.size(); i++) {      System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));    }    System.out.println(response.body().string());  }

不过需要注意的是,作用在响应主体上的string()方法对于小文档来说是方便和高效的,但是如果响应主体比较大(大于1MB),应避免使用string(),因为它会加载整个文档到内存中。

异步Get请求

异步使用enqueue进行请求,例如:

private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    Request request = new Request.Builder()        .url("http://publicobject.com/helloworld.txt")        .build();    client.newCall(request).enqueue(new Callback() {      @Override public void onFailure(Call call, IOException e) {        e.printStackTrace();      }      @Override public void onResponse(Call call, Response response) throws IOException {        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);        Headers responseHeaders = response.headers();        for (int i = 0, size = responseHeaders.size(); i < size; i++) {          System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));        }        System.out.println(response.body().string());      }    });  }

设置Header

典型的HTTP头工作起来像一个Map< String, String >,每一个字段有一个值或没有值。但是有一些头允许多个值,像Guava的Multimap。

使用Request进行请求头信息的设置时,有些信息再次设置是不会被覆盖的,例如addHeader(name, value),使用addHeader(name, value)来添加一个头而不移除已经存在的头。

private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    Request request = new Request.Builder()        .url("https://api.github.com/repos/square/okhttp/issues")        .header("User-Agent", "OkHttp Headers.java")        .addHeader("Accept", "application/json; q=0.5")        .addHeader("Accept", "application/vnd.github.v3+json")        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    System.out.println("Server: " + response.header("Server"));    System.out.println("Date: " + response.header("Date"));    System.out.println("Vary: " + response.headers("Vary"));  }

上传字符串

使用HTTP POST来发送请求(比如文件)主体到服务器,因为整个请求主体同时存在内存中,应避免使用这个API上传大的文档大于1MB。如果是大文件,可以使用OKHttp的断点续传功能。

public static final MediaType MEDIA_TYPE_MARKDOWN      = MediaType.parse("text/x-markdown; charset=utf-8");  private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    String postBody = ""        + "Releases\n"        + "--------\n"        + "\n"        + " * _1.0_ May 6, 2013\n"        + " * _1.1_ June 15, 2013\n"        + " * _1.2_ August 11, 2013\n";    Request request = new Request.Builder()        .url("https://api.github.com/markdown/raw")        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    System.out.println(response.body().string());  }

当然,OkHttp也支持以stream的形式来上传文件等请求主体。

public static final MediaType MEDIA_TYPE_MARKDOWN      = MediaType.parse("text/x-markdown; charset=utf-8");  private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    RequestBody requestBody = new RequestBody() {      @Override public MediaType contentType() {        return MEDIA_TYPE_MARKDOWN;      }      @Override public void writeTo(BufferedSink sink) throws IOException {        sink.writeUtf8("Numbers\n");        sink.writeUtf8("-------\n");        for (int i = 2; i <= 997; i++) {          sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));        }      }      private String factor(int n) {        for (int i = 2; i < n; i++) {          int x = n / i;          if (x * i == n) return factor(x) + " × " + i;        }        return Integer.toString(n);      }    };    Request request = new Request.Builder()        .url("https://api.github.com/markdown/raw")        .post(requestBody)        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    System.out.println(response.body().string());  }

文件上传

文件的上传相对简单,直接提供File的路径即可。

public static final MediaType MEDIA_TYPE_MARKDOWN      = MediaType.parse("text/x-markdown; charset=utf-8");  private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    File file = new File("README.md");    Request request = new Request.Builder()        .url("https://api.github.com/markdown/raw")        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    System.out.println(response.body().string());  }

上传表格参数

OkHtpp支持使用FormBody.Builder来构建一个工作起来像HTML< form >标签的请求主体。键值对会使用一个兼容HTML form的URL编码进行编码。

private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    RequestBody formBody = new FormBody.Builder()        .add("search", "Jurassic Park")        .build();    Request request = new Request.Builder()        .url("https://en.wikipedia.org/w/index.php")        .post(formBody)        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    System.out.println(response.body().string());  }

多部分请求

MultipartBody.Builder可以构造复杂的请求主体与HTML文件上传表单兼容。multipart请求主体的每部分本身就是一个请求主体,可以定义它自己的头。如果存在自己的头,那么这些头应该描述部分主体,例如它的Content-Disposition。Content-Length和Content-Type会在其可用时自动添加。

private static final String IMGUR_CLIENT_ID = "...";  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");  private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image    RequestBody requestBody = new MultipartBody.Builder()        .setType(MultipartBody.FORM)        .addFormDataPart("title", "Square Logo")        .addFormDataPart("image", "logo-square.png",            RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))        .build();    Request request = new Request.Builder()        .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)        .url("https://api.imgur.com/3/image")        .post(requestBody)        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    System.out.println(response.body().string());  }

缓存响应设置

要设置缓存响应,你需要一个进行读取和写入的缓存目录,以及一个缓存大小的限制。缓存目录应该是私有的,且不被信任的应用不能够读取它的内容。让多个缓存同时访问相同的混存目录是错误的。大多数应用应该只调用一次new OkHttpClient(),配置它们的缓存,并在所有地方使用相同的实例。否则两个缓存实例会相互进行干涉。

同时OkHttp还支持对缓存的时间和大小进行设置。如添加像Cache-Control:max-stale=3600设置请求头缓存大小,使用Cache-Control:max-age=9600来配置响应缓存时间。

网络超时配置

网络部分可能是由于连接问题,服务器可用性问题或者其他原因造成网络请求超时。所以在使用时,可以根据实际情况进行网络的超时设置。

private final OkHttpClient client;  public ConfigureTimeouts() throws Exception {    client = new OkHttpClient.Builder()        .connectTimeout(10, TimeUnit.SECONDS)        .writeTimeout(10, TimeUnit.SECONDS)        .readTimeout(30, TimeUnit.SECONDS)        .build();  }  public void run() throws Exception {    Request request = new Request.Builder()        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.        .build();    Response response = client.newCall(request).execute();    System.out.println("Response completed: " + response);  }

取消请求

OkHttp支持取消网络请求,使用Call.cancel()来立即停止一个正在进行的调用。如果一个线程正在写请求或读响应,它会接收到一个IOException,同步和异步调用都可以取消。

private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);  private final OkHttpClient client = new OkHttpClient();  public void run() throws Exception {    Request request = new Request.Builder()        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.        .build();    final long startNanos = System.nanoTime();    final Call call = client.newCall(request);    // Schedule a job to cancel the call in 1 second.    executor.schedule(new Runnable() {      @Override public void run() {        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);        call.cancel();        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);      }    }, 1, TimeUnit.SECONDS);    try {      System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);      Response response = call.execute();      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",          (System.nanoTime() - startNanos) / 1e9f, response);    } catch (IOException e) {      System.out.printf("%.2f Call failed as expected: %s%n",          (System.nanoTime() - startNanos) / 1e9f, e);    }  }

认证请求

如果网络请求涉及到认证机制,OkHttp也提供了Authenticator来进行应用证书认证,Authenticator的实现应该构建一个包含缺失证书的新请求,如果没有证书可用,返回null来跳过重试。

使用Response.challenges()来获取所有认证挑战的模式和领域。当完成一个Basic挑战时,使用Credentials.basic(username,password)来编码请求头。涉及的示例如下:

private final OkHttpClient client;  public Authenticate() {    client = new OkHttpClient.Builder()        .authenticator(new Authenticator() {          @Override public Request authenticate(Route route, Response response) throws IOException {            System.out.println("Authenticating for response: " + response);            System.out.println("Challenges: " + response.challenges());            String credential = Credentials.basic("jesse", "password1");            return response.request().newBuilder()                .header("Authorization", credential)                .build();          }        })        .build();  }  public void run() throws Exception {    Request request = new Request.Builder()        .url("http://publicobject.com/secrets/hellosecret.txt")        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    System.out.println(response.body().string());  }

OkHttp的Call

OkHttp支持重写,重定向,跟进和重试,OkHttp会使用Call来模化满足请求的任务,然而中间的请求和响应是必要的。OkHttp提供了两种方式的Call:

  • Synchronous:线程会阻塞直到响应可读;
  • Asynchronous:在一个线程中入队请求,当你的响应可读时在另外一个线程获取回调。

请求可以从任何线程取消,如果请求还没有执行完成,会使请求失败,请求失败会出现IOException异常错误。

OkHttp支持同步和异步方式请求,对于同步调用,使用的是自己的线程并对管理你同时创建多少请求负责。对于异步调用,Dispatcher实现了最大并发请求的策略,你可以设置每个服务器最大值(默认是5)和所有最大值(默认是64)。

OkHttp网络链接

在使用OkHttp进行请求的时候,我们只需要提供请求的url地址即可实现网络的访问,其实OkHttp在规划连接服务器的连接时提供了三种类型:URL,Address和Route。
下面就分别来说一下这三种链接的关系即使用场合。

URL

URL是HTTP和网络的最基本的联系方式,成为统一资源定位符,URL是一个抽象的概念。

  • 它们规定了调用可能是明文(http)或密文(https),但是没有规定应该使用哪个加密算法。也没有规定如何验证对等的证书(HostnameVerifier)或者哪个证书可被信任(SSLSocketFactory)。
  • 每一个URL确定一个特定路径,每个服务器包含很多的URL。

Addresses

在OkHttp中,Addresses规定了服务器和所有连接服务器需要的静态配置:端口号,HTTPS设置和优先网络协议(如HTTP/2或SPDY)。共享相同address的URLs也可能共享相同的下层TCP socket连接。
共享一个连接有巨大的性能好处:低延迟,高吞吐量(因为TCP启动慢)和节省电源。OkHttp使用ConnectionPool来自动复用HTTP/1.X连接和多路传输HTTP/2和SPDY连接。

在OkHttp中,address的一些字段来自URL(机制,主机名,端口),剩下的来自OkHttpClient。

Routes

Routes提供了真正连接到服务器所需要的动态信息,它会Routes明确的要尝试的IP地址以及代理服务器,以及什么版本的TLS来协商(针对HTTPS连接)。

对于一个地址有可能有很多路由,一个存在多个数据中心的网络服务器可能在它的DNS响应中产生多个IP地址。

OkHttp网络连接流程

当你使用OkHttp请求一个URL时,下面是它执行的流程:
1. 它使用URL和配置的OkHttpClient来创建一个address,这个address规定了如何连接到服务器。
2. OkHttp尝试使用这个address从连接池中获取一个连接。
3. 如果它没有在池中找到一个连接,它会选择一个route来尝试。这通常意味着创建一个DNS请求来获取服务器的IP地址。
4. 如果这是一个新route,它会通过构建一个直接的socket连接或一个TLS隧道或一个直接的TLS连接来进行连接。如果需要它会执行TLS握手。
5. 然后发送HTTP请求然后读取响应。

当连接出现问题时,OkHttp会选择另外一个route进行尝试。一旦接收到服务端的响应,连接就会返回到池中,这样它可以在之后的请求复用,连接空闲一段时间会从池中移除。

拦截器

看过OkHttp源码分析的同学对于拦截器肯定不会陌生,在OkHttp中拦截器是所有的网络请求的必经之地,拦截器主要有以下一些作用。

1、拦截器可以一次性对所有的请求和返回值进行修改;
2、拦截器可以一次性对请求的参数和返回的结果进行编码,比如统一设置为UTF-8;
3、拦截器可以对所有的请求做统一的日志记录,不需要在每个请求开始或者结束的位置都添加一个日志操作;
4、其他需要对请求和返回进行统一处理的需求….

下面是一个最简单的拦截器使用,用来打印OkHttp的请求和收到的响应。

class LoggingInterceptor implements Interceptor {  @Override public Response intercept(Interceptor.Chain chain) throws IOException {    Request request = chain.request();    long t1 = System.nanoTime();    logger.info(String.format("Sending request %s on %s%n%s",        request.url(), chain.connection(), request.headers()));    Response response = chain.proceed(request);    long t2 = System.nanoTime();    logger.info(String.format("Received response for %s in %.1fms%n%s",        response.request().url(), (t2 - t1) / 1e6d, response.headers()));    return response;  }}

OkHttp使用列表来跟踪拦截器,并且拦截器按顺序被调用。栖拦截的模型如下:
OkHttp基础概念解释_第1张图片

OkHttp中的拦截器分为两类:APP层面的拦截器(Application Interception)、网络请求层面的拦截器(Network Interception)。在OkHttp中,首先从App Interceptor开始,然后执行Network Interceptor,最后又回到App Interceptor。

应用拦截器

下面我们使用OkHttpCleint.Builder上调用addInterceptor()来注册一个应用拦截器。代码如下:

OkHttpClient client = new OkHttpClient.Builder()    .addInterceptor(new LoggingInterceptor())    .build();Request request = new Request.Builder()    .url("http://www.publicobject.com/helloworld.txt")    .header("User-Agent", "OkHttp Example")    .build();Response response = client.newCall(request).execute();response.body().close();

如果我们需要将http://www.publicobject.com/helloworld.txt这个URL重定向到https://publicobject.com/helloworld.txt,那么OkHttp会自动跟进这个重定向。下面是重定向的相关的执行信息:

INFO: Sending request http://www.publicobject.com/helloworld.txt on nullUser-Agent: OkHttp ExampleINFO: Received response for https://publicobject.com/helloworld.txt in 1179.7msServer: nginx/1.4.6 (Ubuntu)Content-Type: text/plainContent-Length: 1759Connection: keep-alive

通过日志,我们可以看到OkHttp已经重定向了,可以通过引文reponse.request().url()与request.url()不同来区分。我们发现,应用拦截器只会被调用一次,并且从chain.proceed()返回的响应是重定向后的响应。

网络拦截器

注册一个网络拦截器很相似,调用addNetworkInterceptor()替代addInterceptor()。同样是上面的实例:

OkHttpClient client = new OkHttpClient.Builder()    .addNetworkInterceptor(new LoggingInterceptor())    .build();Request request = new Request.Builder()    .url("http://www.publicobject.com/helloworld.txt")    .header("User-Agent", "OkHttp Example")    .build();Response response = client.newCall(request).execute();response.body().close();

当我们运行这个代码,拦截器会执行两次:一次是访问http://www.publicobject.com/helloworld.txt的初始请求,另外一个是重定向到https://publicobject.com/helloworld.txt。

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}User-Agent: OkHttp ExampleHost: www.publicobject.comConnection: Keep-AliveAccept-Encoding: gzipINFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6msServer: nginx/1.4.6 (Ubuntu)Content-Type: text/htmlContent-Length: 193Connection: keep-aliveLocation: https://publicobject.com/helloworld.txtINFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}User-Agent: OkHttp ExampleHost: publicobject.comConnection: Keep-AliveAccept-Encoding: gzipINFO: Received response for https://publicobject.com/helloworld.txt in 80.9msServer: nginx/1.4.6 (Ubuntu)Content-Type: text/plainContent-Length: 1759Connection: keep-alive

网络请求也包含更多数据,例如通过OkHttp添加的Accept-Encoding:gzip头来通知支持响应压缩。网络拦截器的Chain有一个非空Connection,可以用来访问IP地址和用来连接网络服务器的TLS配置。

应用拦截器VS网络拦截器

选择哪种拦截器需要根据实际情况,每种拦截器chain都有自己相对的优势。

应用拦截器

  • 不需要关心像重定向和重试这样的中间响应;
  • 总是调用一次,即使HTTP响应从缓存中获取服务;
  • 监视应用原始意图。不关心OkHttp注入的像If-None-Match头;
  • 允许短路并不调用Chain.proceed();
  • 允许重试并执行多个Chain.proceed()调用。

网络拦截器

  • 可以操作像重定向和重试这样的中间响应;
  • 对于短路网络的缓存响应不会调用;
  • 监视即将要通过网络传输的数据;
  • 访问运输请求的Connection。

重写请求

拦截器支持添加,移除或替换请求头,如果有请求主体,它们也可以改变。例如,如果你连接一个已知支持请求主体压缩的网络服务器,你还可以使用一个应用拦截器来添加请求主体压缩。

final class GzipRequestInterceptor implements Interceptor {  @Override public Response intercept(Interceptor.Chain chain) throws IOException {    Request originalRequest = chain.request();    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {      return chain.proceed(originalRequest);    }    Request compressedRequest = originalRequest.newBuilder()        .header("Content-Encoding", "gzip")        .method(originalRequest.method(), gzip(originalRequest.body()))        .build();    return chain.proceed(compressedRequest);  }  private RequestBody gzip(final RequestBody body) {    return new RequestBody() {      @Override public MediaType contentType() {        return body.contentType();      }      @Override public long contentLength() {        return -1; // We don't know the compressed length in advance!      }      @Override public void writeTo(BufferedSink sink) throws IOException {        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));        body.writeTo(gzipSink);        gzipSink.close();      }    };  }}

重写响应

当然,拦截器也可以重写响应头并且改变响应主体。如果你在一个棘手的环境下并准备处理结果,重写响应头是一个解决问题强大的方式。

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {  @Override public Response intercept(Interceptor.Chain chain) throws IOException {    Response originalResponse = chain.proceed(chain.request());    return originalResponse.newBuilder()        .header("Cache-Control", "max-age=60")        .build();  }};

OkHttp使用Https

关于Https及其工作的流程本文不做任何的介绍,本文主要介绍在OkHttp中如何使用Https进行网络校验即请求。在使用OkHttpClient初始化OkHttpClient对象时,有两个关键的地方需要注意:hostnameVerifier和sslSocketFactory。

OkHttpClient okHttpClient = new OkHttpClient.Builder()                .connectTimeout(20000L, TimeUnit.MILLISECONDS)                .readTimeout(20000L, TimeUnit.MILLISECONDS)                .addInterceptor(new LoggerInterceptor("TAG"))                .hostnameVerifier(new HostnameVerifier() {                    @Override                    public boolean verify(String hostname, SSLSession session) {                        return true;                    }                })                .sslSocketFactory(sslParams.sSLSocketFactory,sslParams.trustManager)                .build();

其中sslSocketFactory传入两个参数,一个是SSLSocketFactory,另一个是TrustManager,通常都是写一个HttpsUtils,里面持有这两个对象,读取本地的一个证书,进行相关初始化赋值动作。 hostnameVerifier则是对服务端返回的一些信息进行相关校验的地方, 用于客户端判断所连接的服务端是否可信,通常默认return true。

public boolean verify(String host, X509Certificate certificate) {    return verifyAsIpAddress(host)        ? verifyIpAddress(host, certificate)        : verifyHostname(host, certificate);  }

OkHttp的验证逻辑

对于一个android开发者来说,目前的网络请求框架大部分都是使用okhttp进行网络请求的,所以了解okhttp是如何具体工作的对于我们平时开发有很大的帮助的。当我们使用https进行网络请求的时候最终进行连接的类是RealConnection,该类的关键代码如下:

private void connectTls(int readTimeout, int writeTimeout,      ConnectionSpecSelector connectionSpecSelector) throws IOException {    Address address = route.address();    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();    boolean success = false;    SSLSocket sslSocket = null;    try {      // Create the wrapper over the connected socket.    //创建Socket      sslSocket = (SSLSocket) sslSocketFactory.createSocket(          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);      // Configure the socket's ciphers, TLS versions, and extensions.      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);      if (connectionSpec.supportsTlsExtensions()) {        Platform.get().configureTlsExtensions(            sslSocket, address.url().host(), address.protocols());      }      // Force handshake. This can throw!        //初次握手      sslSocket.startHandshake();      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());      // Verify that the socket's certificates are acceptable for the target host.    //校验,回调hostnameVerifier.verify方法      if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"            + "\n    certificate: " + CertificatePinner.pin(cert)            + "\n    DN: " + cert.getSubjectDN().getName()            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));      }      // Check that the certificate pinner is satisfied by the certificates presented.      address.certificatePinner().check(address.url().host(),          unverifiedHandshake.peerCertificates());

在该类中,我们主要关心的地方也是在初次握手建立连接和本地校验的那,正常情况下,我们在调用https地址的时候会先连接,就是调到上面代码的位置,之后执行初次握手,回调验证服务端是否可信,然后在进行正常的网络请求。如果在这个过程中出现异常,就会报一个证书信任的问题,出现这种情况有两方面,一是客户端验证服务端,二是服务端验证客户端。

证书获取

下面介绍下证书获取的相关内容,证书校验主要用到了hostnameVerifier.verify(),该方法的源码如下:

@Override public boolean verify(String hostname, SSLSession session) {       Certificate[] localCertificates = new Certificate[0];       try {     //获取证书链中的所有证书           localCertificates = session.getPeerCertificates();         } catch (SSLPeerUnverifiedException e) {                e.printStackTrace();           }    //打印所有证书内容        for (Certificate c : localCertificates) {          Log.d(TAG, "verify: "+c.toString());        }     try {    //将证书链中的第一个写到文件           createFileWithByte(localCertificates[0].getEncoded());            } catch (CertificateEncodingException e) {              e.printStackTrace();            }       return true;       }    //写到文件    private void createFileWithByte(byte[] bytes) {        // TODO Auto-generated method stub        /**         * 创建File对象,其中包含文件所在的目录以及文件的命名         */        File file = new File(Environment.getExternalStorageDirectory(),                "ca.cer");        // 创建FileOutputStream对象        FileOutputStream outputStream = null;        // 创建BufferedOutputStream对象        BufferedOutputStream bufferedOutputStream = null;        try {            // 如果文件存在则删除            if (file.exists()) {                file.delete();            }            // 在文件系统中根据路径创建一个新的空文件            file.createNewFile();            // 获取FileOutputStream对象            outputStream = new FileOutputStream(file);            // 获取BufferedOutputStream对象            bufferedOutputStream = new BufferedOutputStream(outputStream);            // 往文件所在的缓冲输出流中写byte数据            bufferedOutputStream.write(bytes);            // 刷出缓冲输出流,该步很关键,要是不执行flush()方法,那么文件的内容是空的。            bufferedOutputStream.flush();        } catch (Exception e) {            // 打印异常信息            e.printStackTrace();        } finally {            // 关闭创建的流对象            if (outputStream != null) {                try {                    outputStream.close();                } catch (IOException e) {                    e.printStackTrace();                }            }            if (bufferedOutputStream != null) {                try {                    bufferedOutputStream.close();                } catch (Exception e2) {                    e2.printStackTrace();                }            }        }    }

hostnameVerifier主要有两个参数,一个是hostname就是你请求地址的host,session则包括了从服务端返回的证书链。
证书链通常有三个,第一个是我们自己的,然后也能在本地看到证书文件。包含一些相关信息,包括公钥,颁发机构等,最为严苛的方式就是可以从本地读取一个证书,取公钥与服务器返回的证书公钥进行对比。

但是证书也不是完全安全的,CertificatePinner就是一个用来限制哪些证书和证书颁发机构可以被信任。证书锁定提升安全性,但是限制你的服务器团队更新他们的TLS证书的能力。例如:

public CertificatePinning() {    client = new OkHttpClient.Builder()        .certificatePinner(new CertificatePinner.Builder()            .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")            .build())        .build();  }  public void run() throws Exception {    Request request = new Request.Builder()        .url("https://publicobject.com/robots.txt")        .build();    Response response = client.newCall(request).execute();    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);    for (Certificate certificate : response.handshake().peerCertificates()) {      System.out.println(CertificatePinner.pin(certificate));    }  }

自定义可信任的证书

当然,也可以使用自定义的证书来替换主机的证书,然后使用sslSocketFactory函数进行设置。

private final OkHttpClient client;  public CustomTrust() {    SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream());    client = new OkHttpClient.Builder()        .sslSocketFactory(sslContext.getSocketFactory())        .build();  }  public void run() throws Exception {    Request request = new Request.Builder()        .url("https://publicobject.com/helloworld.txt")        .build();    Response response = client.newCall(request).execute();    System.out.println(response.body().string());  }  private InputStream trustedCertificatesInputStream() {    ... // Full source omitted. See sample.  }  public SSLContext sslContextForTrustedCertificates(InputStream in) {    ... // Full source omitted. See sample.  }

SSLSocketFactory

安全套接层工厂,用于创建SSLSocket,默认的SSLSocket是信任手机内置信任的证书列表,我们可以通过OKHttpClient.Builder的sslSocketFactory方法定义自己的信任策略。下面是加载SSLSocketFactory的相关代码:

public static SSLSocketFactory getSSLSocketFactory(InputStream... certificates) {        try {//用我们的证书创建一个keystore            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());            keyStore.load(null);            int index = 0;            for (InputStream certificate : certificates) {                String certificateAlias = "server"+Integer.toString(index++);                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));                try {                    if (certificate != null) {                        certificate.close();                    }                } catch (IOException e) {                    e.printStackTrace();                }            }//创建一个trustmanager,只信任我们创建的keystore            SSLContext sslContext = SSLContext.getInstance("TLS");            TrustManagerFactory trustManagerFactory =                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());            trustManagerFactory.init(keyStore);            sslContext.init(                    null,                    trustManagerFactory.getTrustManagers(),                    new SecureRandom()            );            return sslContext.getSocketFactory();        } catch (Exception e) {            e.printStackTrace();            return null;        }    }

X509TrustManager

public interface X509TrustManager extends TrustManager {    void checkClientTrusted(X509Certificate[] var1, String var2) throws CertificateException;    void checkServerTrusted(X509Certificate[] var1, String var2) throws CertificateException;    X509Certificate[] getAcceptedIssuers();}

HostnameVerifier

HostnameVerifier的接口定义如下:

public interface HostnameVerifier {    boolean verify(String var1, SSLSession var2);}

这个接口主要实现对于域名的校验,OKHTTP实现了一个OkHostnameVerifier,对于证书中的IP及Host做了各种正则匹配,默认情况下使用的是这个策略。相关代码如下:

OKHttpClient.Builder.hostnameVerifier(new HostnameVerifier() {                    @Override                    public boolean verify(String hostname, SSLSession session) {                        return true;                    }                })

在实际使用中可以将上面的东西封装起来,例如:

   public class SSLSocketClient{     //获取这个SSLSocketFactory      public static SSLSocketFactory getSSLSocketFactory(){         try{             SSLContext sslContext = SSLContext.getInstance("SSL");            sslContext.init(null, getTrustManager(), new SecureRandom());             return sslContext.getSocketFactory();        }         catch (Exception e){             throw new RuntimeException(e);        }     }   //获取TrustManager       private static TrustManager[] getTrustManager(){         TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager(){            @Override            public void checkClientTrusted(X509Certificate[] chain, String authType){            }            @Override            public void checkServerTrusted(X509Certificate[] chain, String authType){            }            @Override            public X509Certificate[] getAcceptedIssuers(){                 return new X509Certificate[]{};             }        }};         return trustAllCerts;    }    //获取HostnameVerifier       public static HostnameVerifier getHostnameVerifier(){        HostnameVerifier hostnameVerifier = new HostnameVerifier(){            @Override            public boolean verify(String s, SSLSession sslSession){                return true;            }        };         return hostnameVerifier;     } }

然后在需要使用的使用的地方

OkHttpClient.Builder builder=new OkHttpClient.Builder();...builder.sslSocketFactory(SSLSocketClient.getSSLSocketFactory();builder.hostnameVerifier(SSLSocketClient.getHostnameVerifier();

更多相关文章

  1. Android 实现从网络上异步加载图像
  2. Android 获取本机WIFI及3G网络IP
  3. Android ListView异步加载网络图片
  4. Android使用HttpURLConnection显示网络图片
  5. android 加载 网络图片
  6. Android使用URLConnection显示网络图片
  7. Android之针对webview的缓存
  8. android 监听网络连接状态的改变

随机推荐

  1. android:服务器和客户端通信2
  2. Android Studio中获取sha1证书指纹
  3. Unity调用Android录音
  4. Android设置文本框单行多行显示
  5. android 中的日历控件
  6. Android源码解析系列
  7. Android SMS(一) —— 读取短信
  8. android wifi子系统
  9. Android Wear
  10. Android Studio 2.0 to Android Studio 3