Denua 博客

OkHttp 总结, 常见问题以及简单封装

发布时间: 2018-10-31 14:31   分类 : Android    标签: Android 网络请求 浏览: 17268   

这篇文章总结了 OKHttp 的一些知识点, 以及使用过程中容易出现的问题, 我对 OKHttp 的封装.

OKHttp 的特点: 使用简单方便, 设计非常巧妙, 使用了很多设计模式. 支持 https, 支持 websocket, 管理cookie 方便, 缓存机制, 拦截器.

文章结构

一. 简单使用

1 构建一个 OKHttpClient

简单翻译即为 OKHttp 客户端, 在发送请求之前, 我们需要构建一个 OKHttpClient 对象, 该对象用于发送我们构建的请求, 并管理一些请求所需要的配置, 例如如何处理 Cookies, 统一 Headers, 如何配置 SSL/TSL 协议. 在一般情况下, 我们只在全局构建一个 OKHttpClient 对象, 因为这样我们不需要为每个请求都配置一些共有的参数.

构建该对象使用了构建者模式, OkHttp 中大量使用构建者模式以及其他一些设计模式. 这里我们构建了一个最简单的 client 对象.

OKHttpClient client = new OkHttpClient.Builder().build();

2 Request 类, 构建 HTTP 请求

OKHttp 将所有类型的请求都封装为一个 Request, 一个 Request 中主要的参数有 url, method, 请求参数, header, 支持的请求方法有 GET, HEAD, POST, DELETE, PUT, PATCH.

其中协议类型不能像浏览器一样省略.

2.1 构建 GET 请求

Request request = new Request.Builder().url("https://www.baidu.com").get().build();

这里我们构建了一个请求, 请求 url 为百度首页, 方法为 get, 其中 get 方法可以省略, 默认方法就是 get.

2.2 构建 POST 请求

FormBody formBody = new FormBody.Builder()
        .add("name1", "value1")
        .add("name2", "value2")
        .addEncoded("name3",new String("value3".getBytes(), Charset.forName("utf-8")))
        .build();
Request request = new Request.Builder()
        .url("http://example.com")
        .post(formBody)
        .build();

post 请求与 get 不同的是多了一个 FormBody 请求表单. 在一些特殊情况下可以用 addEncoded 对部分字段设置编码方式, 那么将不会再次对应字段进行编码.

3 发送请求

发送请求前需要先用之前创建的 client 以及 request 创建一个 call. Call 类似于在浏览器点击一个链接开启的一个新页面, 我们可以查看该页面的状态,是否请求完成, 或者关闭该页面取消这次请求.

这个步骤的意义在于我可以在他返回请求结果前取消他, 为什么要取消, 例如用户打开一个页面我们发起一个网络请求在结果没有返回之前用户又关闭了, 而请求结果返回到页面时页面已经销毁, 将引起一个 NullPointException.

Call call = client.newCall(request);

3.1 同步请求

Response response = call.execute();

调用 call 的 execute 方法即可执行该请求, 同步请求将阻塞当前线程, 在 Android 主线程中调用这个方法将引发 ANR(Application Not Response) 错误. 直到请求结果返回给 response. 所有的关于这次请求的内容都可以在该 response 中得到.

3.2 异步请求

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }
    @Override
    public void onResponse(Call call, Response response) throws IOException {

    });

调用 enqueue 方法并传入一个用于监听请求结果的 CallBack 接口实例即可异步执行该请求. CallBack 中 onFailure 表示服务器响应失败, 多半是网络问题, onResponse 表示收到服务器响应.

4 请求结果, Response

所有关于这次请求的内容, 都封装在这个对象中, 比如请求 Request, 响应 Header. 响应时长, 使用协议, 是否重定向, 等等.

4.1 响应主体 ResponseBody

在请求结果 response 中包含了我们需要的响应主体 responseBody, 在 responseBody 中我们可以获取更多我们直接想要的类型的结果, 比如 String, InputStream, bytes.

同时还可以通过该对象获取到响应主体的 MIME 类型, 响应主体大小. 其中应注意的是, string() 方法只能调用一次.

ResponseBody responseBody = response.boody()
responseBody.string();
responseBody.contentType();
responseBody.byteStream();

4.2 Response 比较重要的几个方法

  • isSuccessful() 请求是否返回 200 OK, 表示这次请求是否成功.
  • code() 响应 HTTP 状态码, 用于判断这次响应的状态.
  • headers() 包含了本次响应的所有 header.
  • request() 这个响应的请求对象
  • close() 关闭响应流并释放系统资源

二. 如何管理 Cookies 以及 Headers

Cookie 和 Header 是我们经常需要设置的, 而 OkHttp 对这方面提供的方法和接口非常方便使用.

1 Cookies

cookie 的属性介绍

  • name cookie 的名称
  • value 值
  • domain 该 cookie 的使用域名
  • path 路径
  • secure 是否只在使用 https 时传输该 cookie
  • expires/Max-Age 过期时间, 默认为直到浏览器关闭

在构建 OkHttpClient 对象的时候, 其中有一个 cookieJar 方法, 这个方法中接受一个 CookieJar 接口, 我们只需传入一个实现了该接口的对象即可.

从该接口方法名即可看出. saveFromResponse 方法即但响应中有 cookie 时调用, 用于保存从响应中返回的 cookie. 而 loadForRequest 则是每次请求都将调用, 我们可以根据传入的 url 返回相应的 cookie.

这里我实现了一个简单管理 cookie 的 CookieManager.

OkHttpClient client = new OkHttpClient.Builder()
        .cookieJar(new CookieManager())
        .build();
...
class CookieManager implements CookieJar{

    private Map小于String, ConcurrentHashMap小于String, Cookie大于大于 cookies = new HashMap小于大于();

    @Override
    public void saveFromResponse(@NonNull HttpUrl httpUrl, @NonNull List小于Cookie大于 list) {
        if (list.size() 大于 0) {
            for (Cookie item : list) {
                add(httpUrl, item);
            }
        }
    }
    @Override
    public List小于Cookie大于 loadForRequest(@NonNull HttpUrl url) {

        ArrayList小于Cookie大于 ret = new ArrayList小于大于();
        if (cookies.containsKey(url.host()))
            ret.addAll(cookies.get(url.host()).values());
        return ret;
    }
    private void add(HttpUrl url, Cookie cookie){

        String name = cookie.name() + "@" + cookie.domain();
        if (cookie.persistent()) {
            if (!cookies.containsKey(url.host())) {
                cookies.put(url.host(), new ConcurrentHashMap小于大于());
            }
            cookies.get(url.host()).put(name, cookie);
        } else {
            if (cookies.containsKey(url.host())) {
                cookies.get(url.host()).remove(name);
            }
        }
    }
}

我在这里遇到一个问题, 在 saveFromResponse 中的 cookie 如果没有设置 expired 属性, cookie 的过期时间值则 persistent 会为 false, expiredAt() 返回一个错误的时间.

在这一点需要注意一下.

2. Headers

比较低级的方法就是直接给 Request 设置 headers, 比较好的方法是在构建 OkHttpClient 对象时通过 addInterceptor 方法添加一个拦截器给所有请求添加header. 拦截器将在后面介绍, 这里先用第一种方法.

    Headers headers = new Headers.Builder()
            .add("User-Agent","okhttpclient")
            .add("Host","baidu.com")
            .build();
    Request request = new Request.Builder().url("https://baidu.com").headers(headers).build();

非常简单.但是如果每个请求都要这样设置就不简单了.

三. Multipart 类型表单, 上传文件

上传文件是非常常用的, 使用也与普通请求差不了多少, 只是 RequestBody 变成了 MultipartBody, 这与 html 中的表单一致了, 可以添加 普通的字段, 也可以添加文件;

例如:

File file = new File("/a.txt");
MultipartBody.Builder builder =new MultipartBody.Builder()
        .addFormDataPart("field","value")
        .addFormDataPart("file", file.getName(), RequestBody.create(MediaType.parse("text/plain"), file));
Request request = new Request().url("http://example.com").post(builder.build()).build();

这里上传了一个文件, 和一个字段. 其中 RequestBody.create() 可以传入多种参数, 比如 byte[], ByteString;

四. Interceptor, 拦截器

拦截器用于拦截所有请求并统一做出处理, 比如添加一个请求头, 在请求响应后作出预先处理, 比如简单的判断请求是否成功, 从请求头中获取 cookies 并保存, 或者记录每次请求, 花费时间, 以及相关信息;

灵活地运用拦截器可以更好的管理项目中的请求, 使代码更加精炼无冗余;

Interceptor 是一个接口, 只需实现该接口并传入构建 okHttpClient 的 Builder.addInterceptor(Interceptor) 方法中即可添加一个拦截器;

class HeaderInterceptor implements Interceptor{
    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        request.newBuilder().addHeader("Host", "example.com");
        Response response = chain.proceed(request);
        String contentType = response.header("contentType");
        // do something else
        return response;
    }
}
// ...
OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new HeaderInterceptor())
                //...
                .build();

这个拦截器给所有请求进行前添加了一个 Header 请求头 Host, 并且在所有响应返回前获取了响应的类型, 当然, 这里可以做更多巧妙的事情.

比如如果不用 OkHttp 自带的 CookieJar, 则可以在这做出相应的处理, 获取所有的 Cookie 并管理;

五. 使用 HTTPS, 设置 X509TrustManager 和 HostNameVerifier

现在已经2018 年了, 基本都使用 HTTPS 协议进行传输, 因为他才能保证安全;

OkHttp 支持 CA 认证 HTTPS 请求, 非 CA 证书需要自行配置, 网站证书可以在浏览器中导出, 导出格式需为 .cer, 然后将证书放在 app/src/main/assets/ 文件夹中, 该文件夹不存在则自行创建, 这个例子文件名为 cert.cer 并且协议为 TLS;

class HttpsUtil {

    public static SSLSocketFactory getSslSocketFactory(X509TrustManager trustManager){

        SSLSocketFactory sslSocketFactory = null;

        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{trustManager}, null);
            sslSocketFactory = sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            e.printStackTrace();
        }
        return sslSocketFactory;
    }

    public static X509TrustManager getX509TrustManager(Context context){

        X509TrustManager x509TrustManager = null;
        InputStream inputStream = null;

        try {
            inputStream = context.getAssets().open("cert.cer");
            x509TrustManager = trustManagerForCertificates(inputStream);
        } catch (IOException | GeneralSecurityException e) {
            e.printStackTrace();
        }finally {
            if (null != inputStream){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return x509TrustManager;
    }

    private static X509TrustManager trustManagerForCertificates(InputStream in)
            throws GeneralSecurityException {

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Collection小于? extends Certificate大于 certificates = certificateFactory.generateCertificates(in);
        if (certificates.isEmpty()) {
            throw new IllegalArgumentException("expected non-empty set of trusted certificates");
        }
        char[] password = "password".toCharArray();
        KeyStore keyStore = newEmptyKeyStore(password);
        int index = 0;
        for (Certificate certificate : certificates) {
            String certificateAlias = Integer.toString(index++);
            keyStore.setCertificateEntry(certificateAlias, certificate);
        }

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
                KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, password);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
            throw new IllegalStateException("Unexpected default trust managers:"
                    + Arrays.toString(trustManagers));
        }
        return (X509TrustManager) trustManagers[0];
    }

    private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            InputStream in = null; // By convention, 'null' creates an empty key store.
            keyStore.load(in, password);
            return keyStore;
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
}

这是一个工具类, 可以方便获取 X509TrustManager 和 SslSocketFactory 关于 X509 是啥, 这是传输协议相关的东西.

如何设置, SslSocketFactory 和 HostnameVerifier

List小于String大于 hosts = new ArrayList小于String大于(){{
        add("example.com}");
        add("www.example.com");
        add("static.example.com");
 }};
X509TrustManager trustManager = HttpsUtil.getX509TrustManager(context);
OkHttpClient client = new OkHttpClient.Builder()
        .hostnameVerifier(verifier)
        .hostnameVerifier(new HostnameVerifier() {
                                @Override
                                public boolean verify(String hostname, SSLSession session) {
                                    return hosts.contains(hostname);
                                }
                            })
        .sslSocketFactory(HttpsUtil.getSslSocketFactory(trustManager), trustManager)
        .build();

HostnameVerifier 是在每次需要验证远程主机名的时候调用, 返回true 则表示验证通过, 返回 false 则这次请求会报错. 验证的域名则是需要用到的域名, 但需与证书颁发的域名匹配;

六. 简单的封装

为啥要封装, 为了可复用, 解耦合, 封装以后用起来特别爽, 例如

对 Request 进行封装

public final class MRequest {

    private Request.Builder requestBuilder;

    private MRequest(Request.Builder requestBuilder){
        this.requestBuilder = requestBuilder;
    }

    public Request getRequest() {
        return requestBuilder.build();
    }

    public Request.Builder getRequestBuilder(){
        return requestBuilder;
    }

    public MRequest addHeader(String key, String value){
        requestBuilder.addHeader(key, value);
        return this;
    }

    public MRequest addHeaders(Map小于String, String大于 headers){

        for (Map.Entry小于String, String大于 entry:
                headers.entrySet()){
            requestBuilder.addHeader(entry.getKey(), entry.getValue());
        }
        return this;
    }

    public static MRequest get(String url){

        Request.Builder mRequestBuilder = new Request.Builder().get().url(url);
        return new MRequest(mRequestBuilder);
    }

    public static MRequest get(String url, Map小于String, String大于 params){

        if (( null == params) || (params.size() == 0)){
            return get(url);
        }
        Request.Builder mRequestBuilder = new Request.Builder();
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(url)
                    .append("?");

        for (Map.Entry小于String, String大于 entry:
                params.entrySet()){
            stringBuilder.append(entry.getKey())
                        .append("=")
                        .append(entry.getValue())
                        .append("&");
        }
        mRequestBuilder.url(stringBuilder.substring(0, stringBuilder.length()-1));
        return new MRequest(mRequestBuilder);
    }

    public static MRequest post(String url, Map小于String, String大于 params){

        Request.Builder mRequestBuilder = new Request.Builder();
        FormBody.Builder formBodyBuilder = new FormBody.Builder();

        for (Map.Entry小于String,String大于 entry:
             params.entrySet()) {
            formBodyBuilder.add(entry.getKey(), entry.getValue());
        }

        mRequestBuilder
                .url(url)
                .post(formBodyBuilder.build());

        return new MRequest(mRequestBuilder);
    }
}

(等待更新....)

评论    

Copyright denua denua.cn