从0到1开发一款APP直播APP计算机杂谈

【从 0 开始开发一款直播 APP】4.2 网络封装之 OkHt

2017-04-13  本文已影响403人  菜鸟窝

本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类"腾讯直播"的商业化项目
视频地址:http://www.cniao5.com/course/10121


【从 0 开始开发一款直播 APP】4.1 网络封装之 Okhttp -- 基础回顾
【从 0 开始开发一款直播 APP】4.2 网络封装之 OkHttp -- GET,POST,前后端交互
【从 0 开始开发一款直播 APP】4.3 网络封装之 OkHttp -- 封装 GET,POST FORM,POST JSON
【从 0 开始开发一款直播 APP】4.4 网络封装之 OkHttp -- 网络请求实现直播登录


一、前言

对于OkHttp的基本介绍,上一章节已经讲得差不多了,这节来讲解 OkHttp 基本请求。主要包括以下内容:
GET 请求
POST 请求
文件上传
文件下载
Session 过期问题
追踪进度问题
缓存控制

OkHttp 官网
Okio 官网

对于 android studio 用户,需要添加

    compile 'com.squareup.okhttp:okhttp:2.7.5'

Eclipse的用户,可以下载最新的 jar 包 okhttp jar ,添加依赖就可以用了。

注意:okhttp 内部依赖 okio,别忘了同时导入 okio:

compile 'com.squareup.okio:okio:1.11.0'

二、服务器搭建

软件:MyEclipse
服务器:tomcat
架构:struts
myeclipse 和 tomcat 的配置这里不细讲,但是我会找到教程 windows 下 MyEclipse 和 Tomcat 配置mac 安装 tomcatMac 下 MyEclipse 和 tomcat 配置,会介绍一下如何集成 struts 架构。

2.1、集成struts。

先在 myeclipse 上创建一个 webproject。


在下载好的 struts-2.3.32 包中,找到 apps 包,解压 struts2-blank.war 包,找到 WEB-INF 下的 lib 包,全部拷贝到 myeclipse 创建的项目的 webRoot 下 的 lib 目录下。


找到 web.xml 文件,打开,将如下部分粘贴到 项目 web.xml 中。

找到 struts.xml 文件,将其复制到项目的 src 目录下。

将不需要的都删掉,如图。将 struts.enable.DynamicMethodInvocation 的 value 设置为 true,为什么为 true?。

运行服务,如下图可以看到服务已经启动。

2.2、代码编写

在 src 下创建一个类继承 ActionSupport,编写代码,这里为了简单演示一下,定义了两个成员变量和一个方法,获取服务端的用户名和密码。



在 struts.xml 文件中配置 login 方法。



启动服务器,将项目部署到服务端,然后在浏览器中访问地址,可以在控制台观察到打印的用户名和密码信息。

接下来我们在 android 中请求服务端信息。

三、OkHttp 基本使用

3.1、GET

Get 请求主要分为 4 个步骤:
1、获取 OkhttpClient 对象
2、构造 Request 对象
3、将 Request 封装成 Call
4、执行 Call

使用之前需要先添加网络访问权限:

<uses-permission android:name="android.permission.INTERNET"/>

get 方法请求上面的服务端信息,定义一个按钮,将下面的代码写在按钮点击事件中。

private final String TAG = "MainActivity";
//将浏览器上的 localhost 改为本机 ip 地址
private String mBaseUrl = "http://192.168.43.238:8080/okhttptest/";
private OkHttpClient mHttpClient = new OkHttpClient();
private Handler mHandler = new Handler();
private TextView tv;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    tv = (TextView) findViewById(R.id.tv);
    }

public void onGet(View view) {
  //1、获取 OkHttpClient 对象
  private OkHttpClient mHttpClient = new OkHttpClient();
  //2、构造 Request
  final Request request = new Request
                  .Builder()
                  .get()
                  .url(mBaseUrl+"login?username=dali&password=1234")
                  .build();
  //3、将 Request 封装成 call
  final Call call = mHttpClient.newCall(request);
  //4、执行 call
  call.enqueue(new Callback() {
     @Override
     public void onFailure(Request request, IOException e) {
         Log.e(TAG, "onFailure");
         e.printStackTrace();
      }

      @Override
      public void onResponse(Response response) throws IOException {
          Log.e(TAG, "onResponse");
          if (response.isSuccessful()) {
              final String res = response.body().string();
              //onResponse 方法不能直接操作 UI 线程,利用 runOnUiThread 操作 ui
              runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
                     tv.setText(res);
                 }
               });
            }
      }
  });
}

在服务端继续完善代码,客户端请求之后,服务端应该响应客户端并返回信息。HttpServletRequest 文档

public String login() throws IOException {
        //HttpServletRequest对象代表客户端的请求,当客户端通过 HTTP 协议访问服务器时,HTTP 请求头中的所有信息都封装在这个对象中,通过这个对象提供的方法,可以获得客户端请求的所有信息。
        HttpServletRequest request = ServletActionContext.getRequest();
        System.out.println(username + "," + password);
        // 返回数据给客户端
        HttpServletResponse response = ServletActionContext.getResponse();
        PrintWriter writer = response.getWriter();
        writer.write("login success !");
        writer.flush();
        return null;
    }

接着运行 app,可以看到如下效果。点击 GET 按钮,textview 上显示服务端传递的信息,服务端显示客户端信息。好了,基本的流程知道了,接下来不要截这么多图了,好想哭😢。


3.2 POST

POST 请求的步骤:
1、初始化 OkHttpClient
2、构造 Request 对象

2.1、构造 RequeatBody 对象
2.2、包装 RequestBody 对象

3、将 Request 封装成 Call
4、执行 Call
上一章讲解了请求体传递信息到服务端需要构造基本信息,使用 FormEncodingBuilder 构造请求体。查看源码可以看到,FormEncodingBuilder 只有一个方法,就是传递键值对的。

/** Add new key-value pair. */
public FormEncodingBuilder addEncoded(String name, String value) {
  if (content.size() > 0) {
    content.writeByte('&');
  }
  HttpUrl.canonicalize(content, name, 0, name.length(),
      HttpUrl.FORM_ENCODE_SET, true, true, true);
  content.writeByte('=');
  HttpUrl.canonicalize(content, value, 0, value.length(),
      HttpUrl.FORM_ENCODE_SET, true, true, true);
  return this;
}

Post 请求

public void onPost(View view) {
    FormEncodingBuilder builder = new FormEncodingBuilder();
    //构造Request
    //2.1 构造RequestBody
    RequestBody requestBody = builder.add("username", "dali").add("password", "1234").build();
    final Request request = new Request
            .Builder()
            .post(requestBody)
            .url(mBaseUrl + "login")
            .build();
  
  //3、将 Request 封装成 call
  final Call call = mHttpClient.newCall(request);
  //4、执行 call
  call.enqueue(new Callback() {
     @Override
     public void onFailure(Request request, IOException e) {
         Log.e(TAG, "onFailure");
         e.printStackTrace();
      }

      @Override
      public void onResponse(Response response) throws IOException {
          Log.e(TAG, "onResponse");
          if (response.isSuccessful()) {
              final String res = response.body().string();
              //onResponse 方法不能直接操作 UI 线程,利用 runOnUiThread 操作 ui
              runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
                     tv.setText(res);
                 }
               });
            }
      }
  });
}

运行到结果和 GET 一样。


3.3 Post String,将 json 字符串传递到服务器。

和上述不同的就是 RequestBoby,提交字符串不需要使用到构造者模式,RequestBoby 提供了一个静态方法,可以提交 String、byte 数组、byteString、文件。



第一个参数是 MediaType,即是 Internet Media Type,互联网媒体类型,也叫做 MIME 类型,在 Http 协议消息头中,使用 Content-Type 来表示具体请求中的媒体类型信息。

MediaType 说明
text/html HTML格式
text/plain 纯文本格式
text/xml XML格式
image/gif gif图片格式
image/jpeg jpg图片格式
image/png png图片格式
application/xhtml+xml XHTML格式
application/xml XML数据格式
application/atom+xml Atom XML聚合格式
application/json JSON数据格式
application/pdf pdf格式
application/msword Word文档格式
application/octet-stream 二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded <form encType=””>中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
multipart/form-data 需要在表单中进行文件上传时,就需要使用该格式

传递一个纯文本数据类型,数据是 json 格式。将后面的重复代码进行了抽取。后面再出现就不贴出来了。

    //post提交String
    public void onPostString(View view) {
        RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{\"username\":\"dali\",\"password\":\"1234\"}");

        final Request request = new Request
                .Builder()
                .post(requestBody)
                .url(mBaseUrl + "postString")
                .build();
        executeRequest(request);
    }

    private void executeRequest(final Request request) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mHttpClient.newCall(request).enqueue(new Callback() {
                    @Override
                    public void onFailure(Request request, IOException e) {
                        Log.e(TAG, "onFailure");
                    }

                    @Override
                    public void onResponse(final Response response) throws  IOException {

                        Log.e(TAG, "onResponse");

                        if (response.isSuccessful()) {
                            final String res = response.body().string();
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    tv.setText(res);
                                }
                            });
                        }
                    }
                });
            }
        });
    }

当我们把 string 传递到服务端,服务端是通过 request 对象获取 客户端数据。

    public String postString() throws IOException {
        HttpServletRequest request = ServletActionContext.getRequest();
        //读取流,获取传递过来的 string 对象
        ServletInputStream is = request.getInputStream();
        StringBuilder sb = new StringBuilder();
        int len = 0;
        byte[] buf = new byte[1024];
        while ((len = is.read(buf)) != -1) {
            sb.append(new String(buf, 0, len));
        }
        System.out.println(sb.toString());
        return null;
    }

接着在 struts.xml 中配置。

<action name="postString" class="okhttp.UserAction" method="postString"></action>

重启服务器,运行代码。服务端已经收到传递的 json 字符串。


3.4、Post Form,提交表单

客户端代码

public void doPostForm(View view){
    RequestBody body = new FormEncodingBuilder()
            .add("username","dali")
            .add("password","1234").build();
    final Request request = new Request
            .Builder()
            .post(body)
            .url(mBaseUrl + "postForm")
            .build();
    executeRequest(request);
}

服务端代码
使用 Servlet 读取表单数据
Servlet 以自动解析的方式处理表单数据,根据不同的情况使用如下不同的方法:
getParameter():你可以调用 request.getParameter() 方法来获取表单参数的值。
getParameterValues():如果参数出现不止一次,那么调用该方法并返回多个值,例如复选框。
getParameterNames():如果你想要得到一个当前请求的所有参数的完整列表,那么调用该方法。

public String postForm() throws IOException {
        HttpServletRequest request = ServletActionContext.getRequest();
        String username = new String(request.getParameter("username")
                .getBytes());
        String password = new String(request.getParameter("password")
                .getBytes());
        System.out.println("postForm:" + username + "," + password);

        // 返回数据给客户端
        HttpServletResponse response = ServletActionContext.getResponse();
        PrintWriter writer = response.getWriter();
        writer.write("login success !");
        writer.flush();
        return null;
    }

接着在 struts.xml 中配置。

<action name="postForm" class="okhttp.UserAction" method="postForm"></action>

运行可以看到,打印出了传过来的数据


3.5、Post File,提交文件到服务端

文件是从手机上传的,需要添加读写权限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

android 端代码。

//post提交文件
public void onPostFile(View view) {
    File file = new File(Environment.getExternalStorageDirectory(), "/DCIM/Camera/dali.jpg");
    Log.e(TAG, "path:    " + file.getAbsolutePath());
    if (!file.exists()) {
        Log.e(TAG, file.getAbsolutePath() + " is not exits !");
        return;
    }
    RequestBody requestBody =  RequestBody.create(MediaType.parse("application/octet-stream"), file);
    Request request = new Request.Builder()
            .post(requestBody)
            .url(mBaseUrl + "postFile")
            .build();
    executeRequest(request);
}

服务端获取图片,并存储在电脑上。

public String postFile() throws IOException {
        HttpServletRequest request = ServletActionContext.getRequest();
        ServletInputStream is = request.getInputStream();
        String dir = ServletActionContext.getServletContext().getRealPath("files");
        File file = new File(dir,"dali.jpg");
        System.out.println("path: "+file.getAbsolutePath());
        FileOutputStream fos = new FileOutputStream(file);
        int len = 0;
        byte[] buf = new byte[1024];
        while ((len = is.read(buf)) != -1) {
            fos.write(buf,0,len);
        }
        fos.flush();
        fos.close();
        return null;
    }

在 struts.xml 中配置。

<action name="postFile" class="okhttp.UserAction" method="postFile"></action>

运行效果,路径是默认的,也可以自己更改路径,直接打开该路径便可以看到多了一张图。


3.6、上传文件

post 提交文件,web 开发有个属性叫 multipart,用于上传文件,okhttp 也提供了上传文件的构造者 MultipartBuilder。只有这几个方法,使用键值对将要添加的信息传递进去。


public void doUpload(View view) {
    File file = new File(Environment.getExternalStorageDirectory(), "/DCIM/Camera/dali.jpg");
    Log.e(TAG, "path:    " + file.getAbsolutePath());
    if (!file.exists()) {
        Log.e(TAG, file.getAbsolutePath() + " is not exits !");
        return;
    }
    MultipartBuilder multipartBuilder = new MultipartBuilder();
    //                name:表单域代表了一个key,服务端通过key找到对应的文件
    //addFormDataPart(String name,String filename,RequestBody body)
    //type: MediaType.parse("multipart/form-data"),上传文件时需要传递此参数
    RequestBody requestBody = multipartBuilder
            .type(MultipartBuilder.FORM)
            .addFormDataPart("username", "dali")
            .addFormDataPart("password", "1234")
            .addFormDataPart("mPic", "dali2.jpg", RequestBody.create(MediaType.parse("application/octet-stream"), file)).build();
  
    Request request = new Request.Builder()
            .post(requestBody)
            //和服务端方法名一致
            .url(mBaseUrl + "uploadFile")
            .build();
    executeRequest(request);
}

服务端代码

public File mPic;//key 和客户端一样
public String mPicFileName;
public String uploadFile() throws IOException {
        System.out.println(username+","+password);
        if (mPic == null) {
            System.out.println(mPicFileName + " is null");
        }
        String dir = ServletActionContext.getServletContext().getRealPath("files");
        File  file = new File(dir,mPicFileName);
        FileUtils.copyFile(mPic, file);
        return null;
    }

在 struts.xml 中配置。

<action name="uploadFile" class="okhttp.UserAction" method="uploadFile"></action>

运行效果,在服务端可以看到上传的图片,名字和客户端一样。


3.7、下载文件

将服务端文件下载,就是在 onResponse 方法中读取流。

public void doDownloadImage(View view){
    final Request request = new Request
            .Builder()
            .get()
            .url(mBaseUrl + "files/dali.jpg")
            .build();
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            mHttpClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Request request, IOException e) {
                    Log.e(TAG, "onFailure");
                }
                @Override
                public void onResponse(final Response response) throws IOException {
                    Log.e(TAG, "onResponse");
                    if (response.isSuccessful()) {
                        InputStream is = response.body().byteStream();
                        //将图片显示在 ImageView 上
                            final Bitmap bitmap = BitmapFactory.decodeStream(is);
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    iv.setImageBitmap(bitmap);
                                }
                            });
                            //将图片存储在模拟器上,读取字节流
                            File file = new File(Environment.getExternalStorageDirectory(),"dalidali.jpg");
                            FileOutputStream fos = new FileOutputStream(file);
                            int len = 0;
                            byte[] buf = new byte[1024];
                            while ((len = is.read(buf)) != -1){
                                fos.write(buf,0,len);
                            }
                            fos.flush();
                            fos.close();
                            is.close();
                            Log.e(TAG,"download success !");
                    }
                }
            });
        }
    });
}

运行效果。



在模拟器中查找到图片。


3.8、Session 的保持

会话(session)是一种持久网络协议,在用户(或用户代理)端和服务器端之间创建关联,从而起到交换数据包的作用机制,session 在网络协议(例如 telnet 或 FTP)中是非常重要的部分。
服务器端会话和客户端的协作
在动态页面完成解析的时候,储存在会话 Session 中的变量会被压缩后传输给客户端的 Cookie。此时完全依靠客户端的文件系统来保存这些数据(或者内存)。
在每一个成功的请求中,Cookie 中都保存有服务器端用户所具有的身份证明(PHP 中的 Ssession id )或者更为完整的数据。
虽然这样的机制可以保存数据的前后关联,但是必须要保障数据的完整性和安全性。
分别在服务端的 login()、postString()、postFile() 方法中获取 SessionId.

System.out.println("SessionId: "+request.getSession().getId());

运行之后,看到控制台的打印信息如下。



可以看到 SessionId 不一样,就是没做 Session 保持。OkHttp 里面定义了 cookie 保持的方法。

mHttpClient.setCookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL));

CookieManager 是 CookieHandler 的一个子类。管理 cookie 的存储和 cookie 策略。

/**
 * Create a new cookie manager with specified cookie store and cookie policy.
 *
 * @param store     a <tt>CookieStore</tt> to be used by cookie manager.
 *                  if <tt>null</tt>, cookie manager will use a default one,
 *                  which is an in-memory CookieStore implmentation.
 * @param cookiePolicy      a <tt>CookiePolicy</tt> instance
 *                          to be used by cookie manager as policy callback.
 *                          if <tt>null</tt>, ACCEPT_ORIGINAL_SERVER will
 *                          be used.
 */
public CookieManager(CookieStore store,
                     CookiePolicy cookiePolicy)
{
    // use default cookie policy if not specify one
    policyCallback = (cookiePolicy == null) ? CookiePolicy.ACCEPT_ORIGINAL_SERVER
                                            : cookiePolicy;

    // if not specify CookieStore to use, use default one
    if (store == null) {
        cookieJar = new InMemoryCookieStore();
    } else {
        cookieJar = store;
    }
}

接受所有 cookie 的策略

/**
 * One pre-defined policy which accepts all cookies.
 */
public static final CookiePolicy ACCEPT_ALL = new CookiePolicy(){
    public boolean shouldAccept(URI uri, HttpCookie cookie) {
        return true;
    }
};

再次运行,可以看到服务端 session 一致。


3.9、下载进度

在下载图片方法里面经过改写,通过 response.body().contentLength() 获取文件总长度,读文件的时候,每次读取 1024 字节,将其存储到 sum 临时变量中,通过 TextView 显示出来,并在控制台打印。

public void doDownload(View view) {
    final Request request = new Request
            .Builder()
            .get()
            .url(mBaseUrl + "files/dali.jpg")
            .build();
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            mHttpClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Request request, IOException e) {
                    Log.e(TAG, "onFailure");
                }
                @Override
                public void onResponse(final Response response) throws IOException {
                    Log.e(TAG, "onResponse");
                    if (response.isSuccessful()) {
                        //下载进度
                        final long total = response.body().contentLength();
                        long sum = 0;
                        InputStream is = response.body().byteStream();
                        File file = new File(Environment.getExternalStorageDirectory(), "dalidali.jpg");
                        FileOutputStream fos = new FileOutputStream(file);
                        int len = 0;
                        byte[] buf = new byte[1024];
                        while ((len = is.read(buf)) != -1) {
                            fos.write(buf, 0, len);
                            sum += len;
                            Log.e(TAG, sum + " / " + total);
                            final long finalSum = sum;
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    tv.setText(finalSum + " / " + total);
                                }
                            });
                        }
                        fos.flush();
                        fos.close();
                        is.close();
                        Log.e(TAG, "download success !");
                    }
                }
            });
        }
    });
}

运行效果,可以看到进度打印出来了,可以设置进度条显示等。


3.10、上传进度

上文提到 OkHttp 需要依赖 Okio,一直未提起,这里便要用到 Okio。
Okio 是一款新的类库,可以使 java.io.* 和 java.nio.* 更加方便的被使用以及处理数据。
示例代码:

public class Main {
    public static void main(String[] args) throws Exception {
        //创建buffer
        BufferedSource source = Okio.buffer(Okio.source(new File("data/file1")));
        BufferedSink sink = Okio.buffer(Okio.sink(new File("data/file" + System.currentTimeMillis())));
        //copy数据
        sink.writeAll(source);
        //关闭资源
        sink.close();
        source.close();
    }
}

可以发现 Okio 可以非常方便的处理 io 数据。
在 Okio 中通过 byteString 和 buffer 这两只类型,提供了高性能和简单的 api。
ByteString 和 Buffer
1、ByteString 是一种不可变的 byte 序列,提供了一种基于 String,采用 char 访问的二进制模式。通过 ByteString 可以像 value 一样处理二进制数据,并且提供了对 encode/decode 中 HEX,base64 以及 utf-8 支持。
2、Buffer 是一种可变的 byte 序列,就像 ArrayList 一样,不需要知道 buffer 的大小。在处理 buffer 的 read/write 的时候,就像 queue 一样。
Source 和 Sink
这两个类在 InputStream 以及 OutputStream 上进行抽象而成的。
1、Timeout:可以提供超时处理机制。
2、Source 仅仅声明了 read,close,timeout 方法。实现起来非常方便。
3、通过实现/使用 BufferedSource 和 BufferedSink 接口,可以更加方便的操作二进制数据。
4、可以非常方便的将二进制数据处理为 utf-8 字符串,int 等类型数据。
Source 和 Sink 实现了 InputStream 以及 OutputStream。你可以将Source看成InputStream,将 Sink 看成 OutputStream。而通过 BufferedSource 和 BufferedSink 可以非常方便的进行数据处理。
拆 Okio
Android 善用 Okio 简化处理 I/O 操作
上传进度不好处理,下载的时候是我们自己将下载的流 write,因此,框架内部一定有 一个write 方法供上传使用,那么在哪里呢,可以看到提交数据是在 RequestBody 里面进行,打开 RequestBpdy 源码可以看到内部封装了一个 writeTo 方法,但是又看到参数是 BufferedSink,而不是我们想要的 byteLength,即已经传递的长度。

/** Writes the content of this request to {@code out}. */
public abstract void writeTo(BufferedSink sink) throws IOException;

再次打开 BufferedSink 源码,BufferedSink 是一个接口,需要用户去实现,里面的所有方法全是 write…,这里只贴了部分。

BufferedSink write(ByteString byteString) throws IOException;
BufferedSink write(byte[] source) throws IOException;
BufferedSink write(byte[] source, int offset, int byteCount) throws IOException;
long writeAll(Source source) throws IOException;

我们想要的进度如何而来,就是

final long total = response.body().contentLength();
int progress = total / byteCount;

byteCount 如何而来,看到这个方法了吧。
BufferedSink write(byte[] source, int offset, int byteCount)

这里给出一个方案,对原有的 RequestBody 进行封装,看解析。

public class CountingRequestBody extends RequestBody{

    //RequestBody 代理类,用于调用里面的方法
    private RequestBody mDelegate;
    private Listener mListener;
    private CountingSink mCountingSink;

    public CountingRequestBody(RequestBody delegate, Listener listener)     {
        this.mDelegate = delegate;
        this.mListener = listener;
    }

    //监听进度
    public interface Listener{
        //                   已写字节数          总共字节数
        void onRequestProgress(long byteWrited,long contentLength);

    }

    //传递 MediaType。类型
    @Override
    public MediaType contentType() {
        return mDelegate.contentType();
    }

    //writeTo 主要是实现这个方法,上文已经讲过,这里没有 byteLength(已经写入的字节长度),是一个 BufferedSink,在 onRequestProgress 监听中回调获取 byteWrited。封装 ForwardingSink 通过监听回调获取 bytesWritten,使用ForwardingSink 封装RequestBody 的 Sink。
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        //Sink 成为 CountingSink 的代理,再将 CountingSink 包装成 BufferedSink
        //初始化CountingSink
        mCountingSink = new CountingSink(sink);
        BufferedSink bufferedSink = Okio.buffer(mCountingSink);
        //BufferedSink 做一个转换再传进去
        // mDelegate 每次调用 writeTo 方法的时候就会调用 CountingSink 的 write 方法,根据监听回调获取 bytesWritten
        mDelegate.writeTo(bufferedSink);
        //刷新
        bufferedSink.flush();
    }

    protected final class CountingSink extends ForwardingSink{
        //已写字节
        private long bytesWritten;

        public CountingSink(Sink delegate) {
            super(delegate);
        }
        
        //重写 write 方法
        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            super.write(source, byteCount);
            //存储已写字节长度
            bytesWritten += byteCount;
            //监听回调获取 bytesWritten
            mListener.onRequestProgress(bytesWritten,contentLength());
        }
    }

    //获取总长度
    @Override
    public long contentLength() {
        try {
            return mDelegate.contentLength();
        } catch (IOException e) {
            return -1;
        }
    }
}

在 doUpload 方法中添加以下修改

public void doUpload(View view) {
    File file = new File(Environment.getExternalStorageDirectory(), "/DCIM/Camera/dali.jpg");
    Log.e(TAG, "path:    " + file.getAbsolutePath());
    if (!file.exists()) {
        Log.e(TAG, file.getAbsolutePath() + " is not exits !");
        return;
    }

    MultipartBuilder multipartBuilder = new MultipartBuilder();
    
    RequestBody requestBody = multipartBuilder
            .type(MultipartBuilder.FORM)
            .addFormDataPart("username", "dali")
            .addFormDataPart("password", "1234")
            .addFormDataPart("mPic", "dali2.jpg", RequestBody.create(MediaType.parse("application/octet-stream"), file))
            .build();
    //将 requestBody 封装成 CountingRequestBody
    CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody, new CountingRequestBody.Listener() {
        @Override
        public void onRequestProgress(long byteWrited, long contentLength) {
            //打印上传进度
            Log.e(TAG, byteWrited + " / " + contentLength);
        }
    });

    Request request = new Request.Builder()
            .post(countingRequestBody)
            .url(mBaseUrl + "uploadFile")
            .build();

    executeRequest(request);
}

运行结果,可以看到默认缓存区是 2048 个字节。


3.11、缓存控制

项目中有时候会用到缓存,当没有网络时优先加载本地缓存,基于这个需求,我们来学习下 OkHttp 的 Cache-Control。
Cache-Control
Cache-Control 指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置Cache-Control 并不会修改另一个消息处理过程中的缓存处理过程。请求时的缓存指令有下几种:
Public 指示响应可被任何缓存区缓存。
Private 指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。
no-cache 指示请求或响应消息不能缓存
**no-store **用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
max-age 指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
min-fresh 指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
max-stale 指示客户机可以接收超出超时期间的响应消息。如果指定 max-stale 消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。
1. Expires策略
HTTP1.0使用的过期策略,这个策略用使用时间戳来标识缓存是否过期。这个方式缺陷很明显,客户端和服务端的时间不同步,导致过期判断经常不准确。现在HTTP请求基本都使用HTTP1.1以上了,这个字段基本没用了。
2. Cache-control策略
Cache-Control与Expires的作用一致,区别在于前者使用过期时间长度来标识是否过期;例如前者使用过期为30天,后者使用过期时间为2016年10月30日。因此使用Cache-Control能够较为准确的判断缓存是否过期,现在基本上都是使用这个参数。基本格式如下:

CacheControl.java 类,和 Request 类一样采用构造者模式进行构造

CacheControl.Builder builder = new CacheControl.Builder();
builder.noCache();//不用缓存,全部走网络
builder.noStore();//不用缓存,也不用存储缓存
builder.onlyIfCached();//只使用缓存
builder.noTransform();//禁止转码
builder.maxAge(10, TimeUnit.MILLISECONDS);//指示客户机可以接收生存期不大于指定时间响应
builder.maxStale(10, TimeUnit.SECONDS);//指示客户机可以接收超出时期间的响应消息
builder.minFresh(10, TimeUnit.SECONDS);//指示客户机可以接收响应时间小于当前时间加上指定时间的响应
CacheControl cache = builder.build();//构造 CacheControl

常用常量

/**
 * Cache control request directives that require network validation of
 * responses. Note that such requests may be assisted by the cache via
 * conditional GET requests.
 * 仅仅使用网络
 */
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

/**
 * Cache control request directives that uses the cache only, even if the
 * cached response is stale. If the response isn't available in the cache or
 * requires server validation, the call will fail with a {@code 504
 * Unsatisfiable Request}.
 * 仅仅使用缓存
 */
public static final CacheControl FORCE_CACHE = new Builder()
    .onlyIfCached()
    .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
    .build();

请求时如何使用

public void doCacheControl(View view) {
    //创建缓存对象
    CacheControl.Builder builder = new CacheControl.Builder();
    builder.maxAge(10, TimeUnit.MILLISECONDS);
    CacheControl cacheControl = builder.build();
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    File cacheDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/cache");
    Cache cache = new Cache(cacheDirectory, cacheSize);
    System.out.println("cache: "+cacheDirectory.getAbsolutePath());
    final Request request = new Request
            .Builder()
            .get()
            .cacheControl(cacheControl)
            .url("http://publicobject.com/helloworld.txt")
            .build();

    mHttpClient.setCache(cache);

    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Call call1 = mHttpClient.newCall(request);
                Response response1 = call1.execute();
                String s = response1.body().string();
                System.out.println(s);
                System.out.println("response1.cacheResponse()" + response1.cacheResponse());
                System.out.println("response1.networkResponse()" + response1.networkResponse());

                Call call2 = mHttpClient.newCall(request);
                Response response2 = call2.execute();
                String s1 = response2.body().string();
                System.out.println(s1);
                System.out.println("response2.cacheResponse()" + response2.cacheResponse());
                System.out.println("response2.networkResponse()" + response2.networkResponse());

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

可以在控制台看到打印的数据,第一次请求网络 cacheResponse 为 null,networkResponse 请求成功。第二次 cacheResponse 可以看到是从缓存获取的数据。在 networkResponse 中显示的是 304,这一层由 Last-Modified/Etag 控制,当用户请求服务器时,如果服务端没有发生变化,则返回 304.




在手机文件中找到缓存文件。


对于 OkHttp 的基本使用就讲到这里啦,下一章讲 OkHttp 封装。

140套Android优秀开源项目源码,领取地址:http://mp.weixin.qq.com/s/afPGHqfdiApALZqHsXbw-A

上一篇下一篇

猜你喜欢

热点阅读