本文為菜鳥窩作者蔣志碧的連載。“從 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)度問題
緩存控制
對于 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,備注“菜鳥直播交流群”可加入菜鳥直播交流群
關(guān)注菜鳥窩官網(wǎng),免費(fèi)領(lǐng)取140套開源項(xiàng)目