本文为菜鸟窝作者蒋志碧的连载。“从 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 过期问题
追踪进度问题
缓存控制
对于 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 安装 tomcat、Mac 下 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 封装。
添加菜鸟窝运营微信:yrioyou,备注“菜鸟直播交流群”可加入菜鸟直播交流群
关注菜鸟窝官网,免费领取140套开源项目