Android OkHttp相关解析 实践篇

概述

OkHttp是一个处理网络请求的开源框架,由知名移动支付公司square开发,是android当下最热门的轻量级框架之一。相比较google自带的网络请求API(HttpURLConnetion、HttpClient不推荐),OkHttp能提交更简易、更方便的网络请求。
本篇博客想通过代码实践的方式来和大家一起学习okhttp的使用,再多的理论都不如直接编码,写些demo有用,只有通过不断的实践,才能更好地掌握理解理论。主要包括以下内容:

  • 如何使用okhttp
  • okhttp get请求
  • okhttp post请求
  • okhttp 提取响应头
  • okhttp 解析json
  • okhttp 提交表单
  • okhttp 提交文件
  • okhttp 下载文件
  • okhttp https请求相关

主要内容

如何使用okhttp

在工程对应的build.gradle文件中添加一句依赖,然后sync project即可:

compile 'com.squareup.okhttp3:okhttp:3.7.0'

那怎么知道当前okhttp的最新版本呢?
很简单,直接通过AS查找添加即可。
右键project->open module setting->Dependencies 点击右上角+号,添加library dependencies,搜索okhttp,找到square公司发布的最新版本okhttp添加。

okhttp get 请求

添加了okhttp依赖库后,就可以通过代码去使用http了,以下是通过get请求去访问一个网站:

 public static String doGet(Context context, String url, boolean needVerify) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps(), needVerify);
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

private static OkHttpClient httpClient = new OkHttpClient();

Get请求过程包括:

  1. 首先,通过Request.Builder实例化一个request对象
  2. 然后,实例化一个okhttpclient对象
  3. 最后,通过call.excute()方法发起一个get请求

TIP: 这里的请求过程是同步的,且只适用于普通的请求,大文件的下载和上传需要使用另一个异步请求接口call.enqueue()。

 //异步请求
                try {
                    OkHttpHelper.doAsyncGet(mContext, "http://publicobject.com/helloworld.txt",
                            new Callback() {
                                @Override
                                public void onFailure(Call call, IOException e) {
                                    e.printStackTrace();
                                }

                                @Override
                                public void onResponse(Call call, Response response) throws IOException {
                                    System.out.println("onResponse() 当前线程:" + Thread.currentThread().getName());
                                    if (response.isSuccessful()) {
                                        Headers respHeaders = response.headers();
                                        for (int i = 0; i < respHeaders.size(); i++) {
                                            System.out.println(respHeaders.name(i) + ": " + respHeaders.value(i));
                                        }
                                        String respContent = response.body().string();
                                        System.out.println(respContent);
                                        sendRespMessage(MSG_HTTP_RESPONCE, true, respContent);
                                    } else {
                                        throw new IOException("Unexpected code: " + response);
                                    }
                                }
                            });
                } catch (Exception e) {
                    e.printStackTrace();
                    sendRespMessage(MSG_HTTP_RESPONCE, false, "请求出错");
                }

 public static void doAsyncGet(Context context, String url, Callback callback) throws Exception {
        Request request = new Request.Builder()
                .url(url)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        client.newCall(request).enqueue(callback);
    }

TIP: 异步请求和同步的区别是网络请求是由okhttp新开一个线程来执行的,需要注意请求完成后的callback是在子线程上,不是主线程
因此,若回调后需要改变界面内容,则需要用handler来跳转到主线程进行操作。
为什么不直接回调到主线程上呢?我的理解是为了让框架更灵活,适应更多不同的需求,很多项目在做完网络请求后,可能还有很多的业务需要在子线程上处理,这时候直接回调主线程就不是明智的选择。

okhttp post请求

 public static String doPostString(Context context) throws IOException {
        String postBody = ""
                + "Releases\n"
                + "--------\n"
                + "\n"
                + " * _1.0_ May 6, 2013\n"
                + " * _1.1_ June 15, 2013\n"
                + " * _1.2_ August 11, 2013\n";
        RequestBody body = RequestBody.create(MARKDOWN, postBody);
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

    private static final MediaType MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");

这个例子是提交一串string到服务器上,和get 请求相比多了一个requestBody对象,用来存储要上传的body(也就是string)。

okhttp 提取响应头

在实际项目开发中,经常会碰到需要自定义请求头,或者获取自定义响应头,那在okhttp中该如何实现呢?

 public static String doGetHeader(Context context) throws IOException {
        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") //可添加自定义header
                .addHeader("Accept", "application/vnd.github.v3+json")
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            System.out.println("Server: " + response.header("Server")); //获取自定义响应头
            System.out.println("User-Agent: " + response.header("User-Agent"));
            System.out.println("Date: " + response.header("Date"));
            System.out.println("Vary: " + response.headers("Vary"));
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

只要通过Request.Builder().addHeader就可以添加自定义header;通过responce.header(“”)来获取响应头。

okhttp 使用gson解析json

现在的很多项目,对于只需要简单的数据交互时,这时候json格式的数据传输就是一个很好的选择。接下来介绍如何通过google的gson来解析json。
第一步:配置google gson依赖库,具体方法和配置okhttp一样,不再做具体介绍

 compile 'com.google.code.gson:gson:2.8.0'

第二步:安装GsonFormat插件(此步骤主要用于方便自动生成json实体类,可不做)

找到setting->Plugins 
搜索 gsonformat 安装
重启android studio

第三步:发起okhttp json请求

 public static String doGetJson(Context context) throws Exception {
        Request request = new Request.Builder()
                .url("https://api.github.com/gists/c2a7c39532239ff261be")
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
            StringBuffer buffer = new StringBuffer();
            for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
                System.out.println(entry.getKey());
                System.out.println(entry.getValue().content);
                buffer.append(entry.getKey() + ":");
                buffer.append(entry.getValue() + "\n");
            }
            return buffer.toString();
        } else {
            throw new Exception("Unexpected code: " + response);
        }
    }

    static class Gist {
        Map<String, GistFile> files;
    }

    static class GistFile {
        String content;
    }

前面的请求和get一样,获得响应后,通过response.body.charStream()方法获取body的char stream,然后再用gson.fromJson()方法解析stream,获取json实体类。json实体类的构造要和返回的json内容相对应,不同请求得到的json实体是不一样的。

okhttp 提交表单

public static String doPostForm(Context context) throws IOException {
        RequestBody body = new FormBody.Builder()
                .add("search", "China")
                .build();
        Request request = new Request.Builder()
                .url("https://en.wikipedia.org/w/index.php")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

通过FormBody.buider() 去构造一个request body,然后post(body)即可。

okhttp 提交文件

这个和post string是类似的,只是从文件当中去获取要post的body内容

 public static String doPostFile(Context context) throws IOException {
        File file = new File("assets/readme.txt");
        RequestBody body = RequestBody.create(MARKDOWN, file);
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

TIP: 本例子中的文件上传是有问题的,因为assets下的文件不能这么读取,会提示找不到。
当文件存储在assets目录中,可以通过post stream方式来上传文件。

public static String doPostStream(final Context context) throws IOException {
        RequestBody body = new RequestBody() {
            @Override
            public MediaType contentType() {
                return MARKDOWN;
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                InputStream is = context.getAssets().open("readme.txt");
                byte[] buffer = new byte[1024];
                int byteCount=0;
                while((byteCount = is.read(buffer)) != -1) {//循环从输入流读取 buffer字节
                    sink.write(buffer, 0, byteCount);//将读取的输入流写入到输出流
                }
                is.close();
            }
        };
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

    private static final MediaType MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");

这里的buffersink可以理解为内存和网络请求之间的管道,当上传文件readme.txt时,首先通过读取文件内容到内存中;接着通过buffersink把文件内容上传到网络中;最后服务器接收,返回请求结果。

okhttp 文件下载

这里对文件下载功能做了一个简单的扩展,增加下载过程进度的展示。
1、定义一个接口,当正在下载和下载完成时,都会触发回调

  public interface DownloadStatusListener {
        void onProgress(long totalSize, long currSize);
        void onFinish(boolean success, String msg);
    }

2、通过responce.body.byteStream流读取文件,并保存到指定位置

 /**
     * 下载文件
     * @param context
     * @param url
     * @param destPath
     */
    public static void downloadFile(Context context, String url, String destPath, final DownloadStatusListener listener) {
        String fileName = url.substring(url.lastIndexOf("/"));
        File dirPath = new File(destPath);
        if (!dirPath.exists()) {
            dirPath.mkdirs();
        }
        final File file = new File(dirPath, fileName);
        Request request = new Request.Builder().url(url).build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "onFailure()...");
                listener.onFinish(false, e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "onResponse() ...");
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                try {
                    long total = response.body().contentLength();
                    Log.e(TAG, "total------>" + total);
                    long current = 0;
                    is = response.body().byteStream();
                    fos = new FileOutputStream(file);
                    while ((len = is.read(buf)) != -1) {
                        current += len;
                        fos.write(buf, 0, len);
                        Log.e(TAG, "current------>" + current);
                        listener.onProgress(total, current);
                    }
                    fos.flush();
                    listener.onFinish(true, file.getAbsolutePath());
                } catch (IOException e) {
                    Log.e(TAG, e.toString());
                    listener.onFinish(false, "下载失败");
                } finally {
                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

3、调用downloadFile方法,实现回调,更新主界面。

mTvRespContent.setText("开始文件下载...");
                try {
                    OkHttpHelper.downloadFile(mContext,
                            "https://hhap.bjyada.com:443/downloads_sit/2/000011/FJCT/K9/BSA/com.boc.spos.client/1.apk",
                            DEST_PATH,
                            new OkHttpHelper.DownloadStatusListener() {
                                @Override
                                public void onProgress(long totalSize, long currSize) {
                                    String msg = "总大小:" + formatFileSize(totalSize) + "   已下载:" + formatFileSize(currSize);
                                    sendRespMessage(MSG_DOWNLOAD_FILE, true, msg);
                                }

                                @Override
                                public void onFinish(boolean success, String msg) {
                                    sendRespMessage(MSG_DOWNLOAD_FILE, success, msg);
                                }
                            });
                } catch (Exception e) {
                    e.printStackTrace();
                }

 private void sendRespMessage(int msgWhat, boolean success, String content) {
        String status = (success ? "成功:\n" : "失败:\n");
        Message msg = Message.obtain();
        msg.what = msgWhat;
        msg.obj = status + content;
        mHandler.sendMessage(msg);
    }

 private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            String content = (String) msg.obj;
            switch (msg.what) {
                case MSG_HTTP_RESPONCE:
                    mTvRespContent.setText(Html.fromHtml(content));
                    break;
                case MSG_DOWNLOAD_FILE:
                    mTvRespContent.setTextSize(16);
                    mTvRespContent.setText(content);
                    break;
            }
        }
    };

https相关请求

okhttp默认是支持https请求的,比如访问https://baidu.com等,但需要注意的是,okhttp支持的网站是由CA机构颁发的证书,默认支持。但对于使用自签名的证书的网站比如12306网站https://kyfw.12306.cn/otn/会直接报错:

javax.net.ssl.SSLHandshakeException: 
    java.security.cert.CertPathValidatorException: 
        Trust anchor for certification path not found.

那为什么会报这个错误,https请求和http有什么区别呢?

https相关知识

https是个安全版的http,它是在http的基础加一层ssl/tsl
这里写图片描述

SSL (Secure Sockets Layer) 是一种在客户端跟服务器端建立一个加密连接的安全标准. 一般用来加密网络服务器跟浏览器, 或者是邮件服务器跟邮件客户端(如: Outlook)之间传输的数据。
它能够:

  • 认证用户和服务器,确保数据发送到正确的客户机和服务器;(验证证书)
  • 加密数据以防止数据中途被窃取;(加密)
  • 维护数据的完整性,确保数据在传输过程中不被改变。(摘要算法)

下面我们简单描述下HTTPS的工作原理:

  1. 客户端向服务器端索要并验证公钥。
  2. 双方协商生成“对话密钥”。
  3. 双方采用“对话密钥”进行加密通信。

上面过程的前两步,又称为“握手阶段”。
这里写图片描述
TTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。握手过程的简单描述如下:

  1. 浏览器将自己支持的一套加密算法、HASH算法发送给网站。
  2. 网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
  3. 浏览器获得网站证书之后,开始验证证书的合法性,如果证书信任,则生成一串随机数字作为通讯过程中对称加密的秘钥。然后取出证书中的公钥,将这串数字以及HASH的结果进行加密,然后发给网站。
  4. 网站接收浏览器发来的数据之后,通过私钥进行解密,然后HASH校验,如果一致,则使用浏览器发来的数字串使加密一段握手消息发给浏览器。
  5. 浏览器解密,并HASH校验,没有问题,则握手结束。接下来的传输过程将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用“会话密钥”加密内容。

访问自签名的网站

了解了https基础知识后,我们来看看如何访问自签名的网站。

第一种方式:直接忽略证书,不进行认证。

 /**
     * 忽略Https证书
     * @return
     */
    public static OkHttpClient getUnSafedOkHttpClient() {
        // Create a trust manager that does not validate certificate chains
        TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] chain,
                                           String authType) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
                                           String authType) throws CertificateException {
            }

            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                X509Certificate[] x509Certificates = new X509Certificate[0];
                return x509Certificates;
            }
        }};

        // Install the all-trusting trust manager
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
            // Create an ssl socket factory with our all-trusting manager
            javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .sslSocketFactory(sslSocketFactory)
                    .hostnameVerifier(new HostnameVerifier() {

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

                        }
                    })
                    .build();
            return okHttpClient;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

第二种方式: 证书认证
首先,导出自签名网站的证书,以12306网站为例。
这里写图片描述

然后,代码中添加证书

  /**
     * 添加https证书(多个)
     * @param certificates
     */
    public static OkHttpClient getOkHttpClientByCertificate(InputStream... certificates) {
        try {
            //构造CertificateFactory对象,通过它的generateCertificate(is)方法得到Certificate
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates) {
                String certificateAlias = Integer.toString(index++);
                //将得到的Certificate放入到keyStore中
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
                try {
                    if (certificate != null) {
                        certificate.close();
                    }
                } catch (IOException e) {
                }
            }
            SSLContext sslContext = SSLContext.getInstance("TLS");
            final TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            //通过keyStore初始化TrustManagerFactory
            trustManagerFactory.init(keyStore);
            //由trustManagerFactory.getTrustManagers获得TrustManager[]初始化SSLContext
            sslContext.init(null,
                    trustManagerFactory.getTrustManagers(),
                    new SecureRandom());
            javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

            //设置sslSocketFactory
            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .sslSocketFactory(sslSocketFactory)
                    .hostnameVerifier(new HostnameVerifier() {

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

                        }
                    })
                    .build();
            return okHttpClient;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

最后,用得到的okhttpclient 实例发起https请求。

 Request request = new Request.Builder()
                .url(url)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps(), needVerify);
        Response response = client.newCall(request).execute();

参考资料

完整代码请戳这里

本篇博客参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值