OKhttp 就 Get 和 Post 這兩個基本請求,你還學(xué)不會?

微信截圖_20170808140218.png
本文為菜鳥窩作者蔣志碧的連載。“從 0 開始開發(fā)一款直播 APP ”系列來聊聊時下最火的直播 APP,如何完整的實(shí)現(xiàn)一個類"騰訊直播"的商業(yè)化項(xiàng)目
視頻地址:http://www.cniao5.com/course/10121

【從 0 開始開發(fā)一款直播 APP】4.1 網(wǎng)絡(luò)封裝之 Okhttp -- 基礎(chǔ)回顧
【從 0 開始開發(fā)一款直播 APP】4.2 網(wǎng)絡(luò)封裝之 OkHttp -- GET,POST,前后端交互
【從 0 開始開發(fā)一款直播 APP】4.3 網(wǎng)絡(luò)封裝之 OkHttp -- 封裝 GET,POST FORM,POST JSON
【從 0 開始開發(fā)一款直播 APP】4.4 網(wǎng)絡(luò)封裝之 OkHttp -- 網(wǎng)絡(luò)請求實(shí)現(xiàn)直播登錄


一、前言

對于OkHttp的基本介紹,上一章節(jié)已經(jīng)講得差不多了,這節(jié)來講解 OkHttp 基本請求。主要包括以下內(nèi)容:
GET 請求
POST 請求
文件上傳
文件下載
Session 過期問題
追蹤進(jìn)度問題
緩存控制

OkHttp 官網(wǎng)
Okio 官網(wǎng)

對于 android studio 用戶,需要添加

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

Eclipse的用戶,可以下載最新的 jar 包 okhttp jar ,添加依賴就可以用了。

注意:okhttp 內(nèi)部依賴 okio,別忘了同時導(dǎo)入 okio:

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

二、服務(wù)器搭建

軟件:MyEclipse
服務(wù)器:tomcat
架構(gòu):struts
myeclipse 和 tomcat 的配置這里不細(xì)講,但是我會找到教程 windows 下 MyEclipse 和 Tomcat 配置mac 安裝 tomcat、Mac 下 MyEclipse 和 tomcat 配置,會介紹一下如何集成 struts 架構(gòu)。

2.1、集成struts。

先在 myeclipse 上創(chuàng)建一個 webproject。


在下載好的 struts-2.3.32 包中,找到 apps 包,解壓 struts2-blank.war 包,找到 WEB-INF 下的 lib 包,全部拷貝到 myeclipse 創(chuàng)建的項(xiàng)目的 webRoot 下 的 lib 目錄下。


找到 web.xml 文件,打開,將如下部分粘貼到 項(xiàng)目 web.xml 中。

找到 struts.xml 文件,將其復(fù)制到項(xiàng)目的 src 目錄下。

將不需要的都刪掉,如圖。將 struts.enable.DynamicMethodInvocation 的 value 設(shè)置為 true,為什么為 true?。

運(yùn)行服務(wù),如下圖可以看到服務(wù)已經(jīng)啟動。

2.2、代碼編寫

在 src 下創(chuàng)建一個類繼承 ActionSupport,編寫代碼,這里為了簡單演示一下,定義了兩個成員變量和一個方法,獲取服務(wù)端的用戶名和密碼。



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



啟動服務(wù)器,將項(xiàng)目部署到服務(wù)端,然后在瀏覽器中訪問地址,可以在控制臺觀察到打印的用戶名和密碼信息。

接下來我們在 android 中請求服務(wù)端信息。

三、OkHttp 基本使用

3.1、GET

Get 請求主要分為 4 個步驟:
1、獲取 OkhttpClient 對象
2、構(gòu)造 Request 對象
3、將 Request 封裝成 Call
4、執(zhí)行 Call

使用之前需要先添加網(wǎng)絡(luò)訪問權(quán)限:

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

get 方法請求上面的服務(wù)端信息,定義一個按鈕,將下面的代碼寫在按鈕點(diǎn)擊事件中。

private final String TAG = "MainActivity";
//將瀏覽器上的 localhost 改為本機(jī) 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、構(gòu)造 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、執(zhí)行 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);
                 }
               });
            }
      }
  });
}

在服務(wù)端繼續(xù)完善代碼,客戶端請求之后,服務(wù)端應(yīng)該響應(yīng)客戶端并返回信息。HttpServletRequest 文檔

public String login() throws IOException {
        //HttpServletRequest對象代表客戶端的請求,當(dāng)客戶端通過 HTTP 協(xié)議訪問服務(wù)器時,HTTP 請求頭中的所有信息都封裝在這個對象中,通過這個對象提供的方法,可以獲得客戶端請求的所有信息。
        HttpServletRequest request = ServletActionContext.getRequest();
        System.out.println(username + "," + password);
        // 返回數(shù)據(jù)給客戶端
        HttpServletResponse response = ServletActionContext.getResponse();
        PrintWriter writer = response.getWriter();
        writer.write("login success !");
        writer.flush();
        return null;
    }

接著運(yùn)行 app,可以看到如下效果。點(diǎn)擊 GET 按鈕,textview 上顯示服務(wù)端傳遞的信息,服務(wù)端顯示客戶端信息。好了,基本的流程知道了,接下來不要截這么多圖了,好想哭??。


3.2 POST

POST 請求的步驟:
1、初始化 OkHttpClient
2、構(gòu)造 Request 對象

2.1、構(gòu)造 RequeatBody 對象
2.2、包裝 RequestBody 對象

3、將 Request 封裝成 Call
4、執(zhí)行 Call
上一章講解了請求體傳遞信息到服務(wù)端需要構(gòu)造基本信息,使用 FormEncodingBuilder 構(gòu)造請求體。查看源碼可以看到,F(xiàn)ormEncodingBuilder 只有一個方法,就是傳遞鍵值對的。

/** 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();
    //構(gòu)造Request
    //2.1 構(gòu)造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、執(zhí)行 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);
                 }
               });
            }
      }
  });
}

運(yùn)行到結(jié)果和 GET 一樣。


3.3 Post String,將 json 字符串傳遞到服務(wù)器。

和上述不同的就是 RequestBoby,提交字符串不需要使用到構(gòu)造者模式,RequestBoby 提供了一個靜態(tài)方法,可以提交 String、byte 數(shù)組、byteString、文件。



第一個參數(shù)是 MediaType,即是 Internet Media Type,互聯(lián)網(wǎng)媒體類型,也叫做 MIME 類型,在 Http 協(xié)議消息頭中,使用 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數(shù)據(jù)格式
application/atom+xml Atom XML聚合格式
application/json JSON數(shù)據(jù)格式
application/pdf pdf格式
application/msword Word文檔格式
application/octet-stream 二進(jìn)制流數(shù)據(jù)(如常見的文件下載)
application/x-www-form-urlencoded <form encType=””>中默認(rèn)的encType,form表單數(shù)據(jù)被編碼為key/value格式發(fā)送到服務(wù)器(表單默認(rèn)的提交數(shù)據(jù)的格式)
multipart/form-data 需要在表單中進(jìn)行文件上傳時,就需要使用該格式

傳遞一個純文本數(shù)據(jù)類型,數(shù)據(jù)是 json 格式。將后面的重復(fù)代碼進(jìn)行了抽取。后面再出現(xiàn)就不貼出來了。

    //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);
                                }
                            });
                        }
                    }
                });
            }
        });
    }

當(dāng)我們把 string 傳遞到服務(wù)端,服務(wù)端是通過 request 對象獲取 客戶端數(shù)據(jù)。

    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>

重啟服務(wù)器,運(yùn)行代碼。服務(wù)端已經(jīng)收到傳遞的 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);
}

服務(wù)端代碼
使用 Servlet 讀取表單數(shù)據(jù)
Servlet 以自動解析的方式處理表單數(shù)據(jù),根據(jù)不同的情況使用如下不同的方法:
getParameter():你可以調(diào)用 request.getParameter() 方法來獲取表單參數(shù)的值。
getParameterValues():如果參數(shù)出現(xiàn)不止一次,那么調(diào)用該方法并返回多個值,例如復(fù)選框。
getParameterNames():如果你想要得到一個當(dāng)前請求的所有參數(shù)的完整列表,那么調(diào)用該方法。

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);

        // 返回數(shù)據(jù)給客戶端
        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>

運(yùn)行可以看到,打印出了傳過來的數(shù)據(jù)


3.5、Post File,提交文件到服務(wù)端

文件是從手機(jī)上傳的,需要添加讀寫權(quán)限。

<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);
}

服務(wù)端獲取圖片,并存儲在電腦上。

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>

運(yùn)行效果,路徑是默認(rèn)的,也可以自己更改路徑,直接打開該路徑便可以看到多了一張圖。


3.6、上傳文件

post 提交文件,web 開發(fā)有個屬性叫 multipart,用于上傳文件,okhttp 也提供了上傳文件的構(gòu)造者 MultipartBuilder。只有這幾個方法,使用鍵值對將要添加的信息傳遞進(jìn)去。


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,服務(wù)端通過key找到對應(yīng)的文件
    //addFormDataPart(String name,String filename,RequestBody body)
    //type: MediaType.parse("multipart/form-data"),上傳文件時需要傳遞此參數(shù)
    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)
            //和服務(wù)端方法名一致
            .url(mBaseUrl + "uploadFile")
            .build();
    executeRequest(request);
}

服務(wù)端代碼

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>

運(yùn)行效果,在服務(wù)端可以看到上傳的圖片,名字和客戶端一樣。


3.7、下載文件

將服務(wù)端文件下載,就是在 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);
                                }
                            });
                            //將圖片存儲在模擬器上,讀取字節(jié)流
                            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 !");
                    }
                }
            });
        }
    });
}

運(yùn)行效果。



在模擬器中查找到圖片。


3.8、Session 的保持

會話(session)是一種持久網(wǎng)絡(luò)協(xié)議,在用戶(或用戶代理)端和服務(wù)器端之間創(chuàng)建關(guān)聯(lián),從而起到交換數(shù)據(jù)包的作用機(jī)制,session 在網(wǎng)絡(luò)協(xié)議(例如 telnet 或 FTP)中是非常重要的部分。
服務(wù)器端會話和客戶端的協(xié)作
在動態(tài)頁面完成解析的時候,儲存在會話 Session 中的變量會被壓縮后傳輸給客戶端的 Cookie。此時完全依靠客戶端的文件系統(tǒng)來保存這些數(shù)據(jù)(或者內(nèi)存)。
在每一個成功的請求中,Cookie 中都保存有服務(wù)器端用戶所具有的身份證明(PHP 中的 Ssession id )或者更為完整的數(shù)據(jù)。
雖然這樣的機(jī)制可以保存數(shù)據(jù)的前后關(guān)聯(lián),但是必須要保障數(shù)據(jù)的完整性和安全性。
分別在服務(wù)端的 login()、postString()、postFile() 方法中獲取 SessionId.

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

運(yùn)行之后,看到控制臺的打印信息如下。



可以看到 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;
    }
};

再次運(yùn)行,可以看到服務(wù)端 session 一致。


3.9、下載進(jìn)度

在下載圖片方法里面經(jīng)過改寫,通過 response.body().contentLength() 獲取文件總長度,讀文件的時候,每次讀取 1024 字節(jié),將其存儲到 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()) {
                        //下載進(jìn)度
                        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 !");
                    }
                }
            });
        }
    });
}

運(yùn)行效果,可以看到進(jìn)度打印出來了,可以設(shè)置進(jìn)度條顯示等。


3.10、上傳進(jìn)度

上文提到 OkHttp 需要依賴 Okio,一直未提起,這里便要用到 Okio。
Okio 是一款新的類庫,可以使 java.io.* 和 java.nio.* 更加方便的被使用以及處理數(shù)據(jù)。
示例代碼:

public class Main {
    public static void main(String[] args) throws Exception {
        //創(chuàng)建buffer
        BufferedSource source = Okio.buffer(Okio.source(new File("data/file1")));
        BufferedSink sink = Okio.buffer(Okio.sink(new File("data/file" + System.currentTimeMillis())));
        //copy數(shù)據(jù)
        sink.writeAll(source);
        //關(guān)閉資源
        sink.close();
        source.close();
    }
}

可以發(fā)現(xiàn) Okio 可以非常方便的處理 io 數(shù)據(jù)。
在 Okio 中通過 byteString 和 buffer 這兩只類型,提供了高性能和簡單的 api。
ByteString 和 Buffer
1、ByteString 是一種不可變的 byte 序列,提供了一種基于 String,采用 char 訪問的二進(jìn)制模式。通過 ByteString 可以像 value 一樣處理二進(jìn)制數(shù)據(jù),并且提供了對 encode/decode 中 HEX,base64 以及 utf-8 支持。
2、Buffer 是一種可變的 byte 序列,就像 ArrayList 一樣,不需要知道 buffer 的大小。在處理 buffer 的 read/write 的時候,就像 queue 一樣。
Source 和 Sink
這兩個類在 InputStream 以及 OutputStream 上進(jìn)行抽象而成的。
1、Timeout:可以提供超時處理機(jī)制。
2、Source 僅僅聲明了 read,close,timeout 方法。實(shí)現(xiàn)起來非常方便。
3、通過實(shí)現(xiàn)/使用 BufferedSource 和 BufferedSink 接口,可以更加方便的操作二進(jìn)制數(shù)據(jù)。
4、可以非常方便的將二進(jìn)制數(shù)據(jù)處理為 utf-8 字符串,int 等類型數(shù)據(jù)。
Source 和 Sink 實(shí)現(xiàn)了 InputStream 以及 OutputStream。你可以將Source看成InputStream,將 Sink 看成 OutputStream。而通過 BufferedSource 和 BufferedSink 可以非常方便的進(jìn)行數(shù)據(jù)處理。
拆 Okio
Android 善用 Okio 簡化處理 I/O 操作
上傳進(jìn)度不好處理,下載的時候是我們自己將下載的流 write,因此,框架內(nèi)部一定有 一個write 方法供上傳使用,那么在哪里呢,可以看到提交數(shù)據(jù)是在 RequestBody 里面進(jìn)行,打開 RequestBpdy 源碼可以看到內(nèi)部封裝了一個 writeTo 方法,但是又看到參數(shù)是 BufferedSink,而不是我們想要的 byteLength,即已經(jīng)傳遞的長度。

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

再次打開 BufferedSink 源碼,BufferedSink 是一個接口,需要用戶去實(shí)現(xiàn),里面的所有方法全是 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;

我們想要的進(jìn)度如何而來,就是

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

byteCount 如何而來,看到這個方法了吧。
BufferedSink write(byte[] source, int offset, int byteCount)

這里給出一個方案,對原有的 RequestBody 進(jìn)行封裝,看解析。

public class CountingRequestBody extends RequestBody{

    //RequestBody 代理類,用于調(diào)用里面的方法
    private RequestBody mDelegate;
    private Listener mListener;
    private CountingSink mCountingSink;

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

    //監(jiān)聽進(jìn)度
    public interface Listener{
        //                   已寫字節(jié)數(shù)          總共字節(jié)數(shù)
        void onRequestProgress(long byteWrited,long contentLength);

    }

    //傳遞 MediaType。類型
    @Override
    public MediaType contentType() {
        return mDelegate.contentType();
    }

    //writeTo 主要是實(shí)現(xiàn)這個方法,上文已經(jīng)講過,這里沒有 byteLength(已經(jīng)寫入的字節(jié)長度),是一個 BufferedSink,在 onRequestProgress 監(jiān)聽中回調(diào)獲取 byteWrited。封裝 ForwardingSink 通過監(jiān)聽回調(diào)獲取 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 做一個轉(zhuǎn)換再傳進(jìn)去
        // mDelegate 每次調(diào)用 writeTo 方法的時候就會調(diào)用 CountingSink 的 write 方法,根據(jù)監(jiān)聽回調(diào)獲取 bytesWritten
        mDelegate.writeTo(bufferedSink);
        //刷新
        bufferedSink.flush();
    }

    protected final class CountingSink extends ForwardingSink{
        //已寫字節(jié)
        private long bytesWritten;

        public CountingSink(Sink delegate) {
            super(delegate);
        }
        
        //重寫 write 方法
        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            super.write(source, byteCount);
            //存儲已寫字節(jié)長度
            bytesWritten += byteCount;
            //監(jiān)聽回調(diào)獲取 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) {
            //打印上傳進(jìn)度
            Log.e(TAG, byteWrited + " / " + contentLength);
        }
    });

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

    executeRequest(request);
}

運(yùn)行結(jié)果,可以看到默認(rèn)緩存區(qū)是 2048 個字節(jié)。


3.11、緩存控制

項(xiàng)目中有時候會用到緩存,當(dāng)沒有網(wǎng)絡(luò)時優(yōu)先加載本地緩存,基于這個需求,我們來學(xué)習(xí)下 OkHttp 的 Cache-Control。
Cache-Control
Cache-Control 指定請求和響應(yīng)遵循的緩存機(jī)制。在請求消息或響應(yīng)消息中設(shè)置Cache-Control 并不會修改另一個消息處理過程中的緩存處理過程。請求時的緩存指令有下幾種:
Public 指示響應(yīng)可被任何緩存區(qū)緩存。
Private 指示對于單個用戶的整個或部分響應(yīng)消息,不能被共享緩存處理。這允許服務(wù)器僅僅描述當(dāng)用戶的部分響應(yīng)消息,此響應(yīng)消息對于其他用戶的請求無效。
no-cache 指示請求或響應(yīng)消息不能緩存
**no-store **用于防止重要的信息被無意的發(fā)布。在請求消息中發(fā)送將使得請求和響應(yīng)消息都不使用緩存。
max-age 指示客戶機(jī)可以接收生存期不大于指定時間(以秒為單位)的響應(yīng)。
min-fresh 指示客戶機(jī)可以接收響應(yīng)時間小于當(dāng)前時間加上指定時間的響應(yīng)。
max-stale 指示客戶機(jī)可以接收超出超時期間的響應(yīng)消息。如果指定 max-stale 消息的值,那么客戶機(jī)可以接收超出超時期指定值之內(nèi)的響應(yīng)消息。
1. Expires策略
HTTP1.0使用的過期策略,這個策略用使用時間戳來標(biāo)識緩存是否過期。這個方式缺陷很明顯,客戶端和服務(wù)端的時間不同步,導(dǎo)致過期判斷經(jīng)常不準(zhǔn)確。現(xiàn)在HTTP請求基本都使用HTTP1.1以上了,這個字段基本沒用了。
2. Cache-control策略
Cache-Control與Expires的作用一致,區(qū)別在于前者使用過期時間長度來標(biāo)識是否過期;例如前者使用過期為30天,后者使用過期時間為2016年10月30日。因此使用Cache-Control能夠較為準(zhǔn)確的判斷緩存是否過期,現(xiàn)在基本上都是使用這個參數(shù)。基本格式如下:

CacheControl.java 類,和 Request 類一樣采用構(gòu)造者模式進(jìn)行構(gòu)造

CacheControl.Builder builder = new CacheControl.Builder();
builder.noCache();//不用緩存,全部走網(wǎng)絡(luò)
builder.noStore();//不用緩存,也不用存儲緩存
builder.onlyIfCached();//只使用緩存
builder.noTransform();//禁止轉(zhuǎn)碼
builder.maxAge(10, TimeUnit.MILLISECONDS);//指示客戶機(jī)可以接收生存期不大于指定時間響應(yīng)
builder.maxStale(10, TimeUnit.SECONDS);//指示客戶機(jī)可以接收超出時期間的響應(yīng)消息
builder.minFresh(10, TimeUnit.SECONDS);//指示客戶機(jī)可以接收響應(yīng)時間小于當(dāng)前時間加上指定時間的響應(yīng)
CacheControl cache = builder.build();//構(gòu)造 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.
 * 僅僅使用網(wǎng)絡(luò)
 */
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) {
    //創(chuàng)建緩存對象
    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();
}

可以在控制臺看到打印的數(shù)據(jù),第一次請求網(wǎng)絡(luò) cacheResponse 為 null,networkResponse 請求成功。第二次 cacheResponse 可以看到是從緩存獲取的數(shù)據(jù)。在 networkResponse 中顯示的是 304,這一層由 Last-Modified/Etag 控制,當(dāng)用戶請求服務(wù)器時,如果服務(wù)端沒有發(fā)生變化,則返回 304.




在手機(jī)文件中找到緩存文件。


對于 OkHttp 的基本使用就講到這里啦,下一章講 OkHttp 封裝。

添加菜鳥窩運(yùn)營微信:yrioyou,備注“菜鳥直播交流群”可加入菜鳥直播交流群

微信圖片_20170803172638.jpg

關(guān)注菜鳥窩官網(wǎng),免費(fèi)領(lǐng)取140套開源項(xiàng)目

菜鳥窩官網(wǎng)公號二維碼.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容