首先http的请求报文是由:htpp首部+空行+请求数据。 我们的post就是往服务器中insert信息,所以我们必须要带上我们的请求数据-requestBody。 requestBody有两个继承类:FormBody和MultipartBody 数据类型大概:
- 单纯的上传一个String、byte[]、File:可以使用Reqeust.create()方法。
- 上传一些有键值对的数据:使用FormBody
- 上传键值对和文件(.txt,png,mp3,ma4):使用MultipartBody
一:requestBody.create():
//上传String
public static RequestBody create(@Nullable MediaType contentType, String content) {
...
//将String转换成二进制传输
byte[] bytes = content.getBytes(charset);
return create(contentType, bytes);
}
/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final @Nullable MediaType contentType, final byte[] content) {
return create(contentType, content, 0, content.length);
}
/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final @Nullable MediaType contentType, final byte[] content,
final int offset, final int byteCount) {
if (content == null) throw new NullPointerException("content == null");
Util.checkOffsetAndCount(content.length, offset, byteCount);
return new RequestBody() {
@Override public @Nullable MediaType contentType() {
//返回类型
return contentType;
}
@Override public long contentLength() {
//返回长度
return byteCount;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
//发送数据,开头和结束的长度
sink.write(content, offset, byteCount);
}
};
}
/** Returns a new request body that transmits the content of {@code file}. */
public static RequestBody create(final @Nullable MediaType contentType, final File file) {
if (file == null) throw new NullPointerException("file == null");
return new RequestBody() {
@Override public @Nullable MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return file.length();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(file)) {
sink.writeAll(source);
}
}
};
}
例子不写了,就是往post(requestBody)就可以了。
二:FormBody提交键值对表单 键值对的信息可以用HashMap存储起来,也可以用javaBean存储进List<>。
//这里的大小最好是size*0.75+1,免得扩容
HashMap<String,String> date= new HashMap<>(16);
date.put("useName","daming");
date.put("password","12345");
FormBody.Builder builder = new FormBody.Builder();
for (Map.Entry<String,String> map:date.entrySet()){
builder.add(map.getKey(),map.getValue());
}
FormBody formBody = builder.build();
Request request =new Request.Builder()
.url(path)
.post(formBody)
.build();
三:MultipartBody上传文件 我们有时会把要上传的图片、视频的路径生成File上传给服务器。 先看看MultipartBody的添加方法addFormDataPart()
public Builder addFormDataPart(String name, String value) {
return addPart(Part.createFormData(name, value));
}
//第三个参数我们用第一个方法创建
public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) {
return addPart(Part.createFormData(name, filename, body));
}
模拟上传一个视频
File file = new File(Environment.getExternalStorageDirectory(),"text.png");
if(file.exists()) {
OkHttpClient okHttpClient = new OkHttpClient();
//构建请求body
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("platform", "android")
.addFormDataPart("flie", file.getName(),
RequestBody.create(MediaType.parse(guessMimeType(file.getAbsolutePath())),
MultipartBody multipartBody = builder.build();
//构建一个请求
final Request request = new Request.Builder()
.url("path")
.post(multipartBody )
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
private String guessMimeType(String filePath) {
FileNameMap fileNameMap = URLConnection.getFileNameMap();
String mimType = fileNameMap.getContentTypeFor(filePath);
if(TextUtils.isEmpty(mimType)){
return "application/octet-stream";
}
return mimType;
}
text/html:HTML格式
text/pain:纯文本格式
image/jpeg:jpg图片格式
application/json:JSON数据格式
application/octet-stream:二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded:form表单encType属性的默认格式,表单数据将以key/value的形式发送到服务端
multipart/form-data:表单上传文件的格式
##文件上传/下载进度监听
显示文件下载进度:我们可以在接口的onResponse方法中拿到response的输入流,并拿到文件的总大小
//这是retry拦截器返回给我们response
@Override
public void onResponse(Call call, Response response) throws IOException {
//拿到response的输入流
InputStream is = response.body().byteStream();
long sum = 0L;
//文件总大小
final long total = response.body().contentLength();
int len = 0;
//生成一个新文件
File file = new File(Environment.getExternalStorageDirectory(), "n.png");
FileOutputStream fos = new FileOutputStream(file);
byte[] buf = new byte[128];
while ((len = is.read(buf)) != -1){
fos.write(buf, 0, len);
//每次递增
sum += len;
final long finalSum = sum;
Log.d("pyh1", "onResponse: " + finalSum + "/" + total);
runOnUiThread(new Runnable() {
@Override
public void run() {
//将进度设置到TextView中
contentTv.setText(finalSum + "/" + total);
}
});
}
fos.flush();
fos.close();
is.close();
}
文件上传监听 **问题分析:**如果我们要上传比较大的文件,我们就要监听它的下载进度。 首先,我们要获得File的总大小:我们在桥接拦截器的设置请求首部就会设置contentLength。我们看一下代码。
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
//调用RequestBody.contentType()拿到方法类型
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
//调用RequestBody.contentLength()拿到文件大小
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
}
}
所以,我们可以在MultipartBody的contentLength()方法中拿到文件的大小。那么怎么拿到文件上传的大小呢?在CallService拦截是专门进行数据通信的,我们看看它是如何传输数据,就可以在传输数据的方法中拿到我们传输的大小。
public final class CallServerInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Exchange exchange = realChain.exchange();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest();
responseHeadersStarted = true;
exchange.responseHeadersStart();
responseBuilder = exchange.readResponseHeaders(true);
}
if (responseBuilder == null) {
if (request.body().isDuplex()) {
// Prepare a duplex body so that the application can send a request body later.
exchange.flushRequest();
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, true));
//看见没有,它是调用requestBody.writeTo方法中发送数据的。
request.body().writeTo(bufferedRequestBody);
} else {
// Write the request body if the "Expect: 100-continue" expectation was met.
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, false));
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
}
所以,我们只要在MultipartBody的contentLength()方法和writeTo方法里面就可以拿到其总大小和上传大小。 可是MultipartBody修饰为final并不能继承类,重写方法。那么我们就用静态代理,通过构造参数把MultipartBody传进来。
public class ExMultipatyRequestBody extends RequestBody {
private long total; //总长度
private long current; //当前进度
private RequestBody requestBody; //MultipartBody
private UpdateListener updateListener; //通过接口实例把参数传出去
public ExMultipatyRequestBody(RequestBody requestBody){
this.requestBody=requestBody;
}
public ExMultipatyRequestBody(RequestBody build, UpdateListener updateListener) {
this.requestBody= build;
this.updateListener=updateListener;
}
@Override
public MediaType contentType() {
return requestBody.contentType();
}
@Override
public long contentLength() throws IOException {
return requestBody.contentLength();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
total=contentLength();
// writeTo方法里最终还是调用Sink.write方法传输数据,所以我们实现继承Sink的ForwardingSink
ForwardingSink forwardingSink = new ForwardingSink(sink) {
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
concurrent+=byteCount;
updateListener.update(total,concurrent);
}
};
BufferedSink bufferedSink=(BufferedSink)forwardingSink;
requestBody.writeTo(bufferedSink); //操作细节还是留给MultipartBody完成
bufferedSink.flush();
}
}
接口:
public interface UpdateListener {
void update(long total,long current);
}
客户端:
private void initOkhttp() {
File file = new File(Environment.getExternalStorageDirectory(),"text.png");
if(file.exists()) {
OkHttpClient okHttpClient = new OkHttpClient();
//构建请求body
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("platform", "android")
.addFormDataPart("flie", file.getName(),
RequestBody.create(MediaType.parse(guessMimeType(file.getAbsolutePath())), file));
MultipartBody multipartBody = builder.build();
ExMultipatyRequestBody exMultipatyRequestBody = new ExMultipatyRequestBody(multipartBody,
new UpdateListener() {
@Override
public void update(long total, long current) {
showDate(total, current);
}
});
//构建一个请求
final Request request = new Request.Builder()
.url("path")
.post(exMultipatyRequestBody)
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
}
private void showDate(final long total, final long current){
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity7.this,"当前/总数="+current+"/"+total,Toast.LENGTH_LONG).show();
}
});
}
###okHttp缓存 问题:对30s内同一个请求直接拿本地缓存。 分析:我们可以对第一次返回的response的缓存策略的cache-Control:max_age=30进行控制,所以我们要在CacheInterceptor得到response之前把reponse的缓存策略改了,okHttpClien添加拦截器有两个位置addInterceptor()在最前,addNetworkInterceptor()在Connect之后。 1.新增一个拦截器,在拿到后面传过来的response中设置缓存时间为30s,传给cache拦截器让它缓存数据
public class CacheResponseInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response=chain.proceed(chain.request());
response=response.newBuilder()
.removeHeader("cache-Control")
.addHeader("cache-Control","max_age=30")
.build();
return response;
}
}
第二加载新建的拦截器
File file = new File(Environment.getExternalStorageDirectory(),"cache");
Cache cache=new Cache(file,10*1024*1024);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor(new CacheResponseInterceptor())
.build();
//构建一个请求
final Request request = new Request.Builder()
.url("path")
.post(exMultipatyRequestBody)
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e)
}
@Override
public void onResponse(Call call, Response respo
Log.d("cache=",""+response.cacheResponse());
Log.d("response=",""+response.body().string()
}
});
这就完成了在规定时间对同一个请求多次请求用本地缓存。
问题:在没网的情况下,直接使用本地缓存 分析:这个需要在我们构建request请求的时候,就判断有没有网络,如果没有网络,我们要设置只从本地拿取缓存,这个要在cache拦截器之前完成。
第一:新建一个拦截器判断是否有网络需要拿本地缓存
public class CacheRequestInterceptor implements Interceptor {
private Context context;
public CacheRequestInterceptor(Context context){
this.context=context;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request =chain.request();
//没有网络
if(!isNetWork()){
//只从本地拿缓存
request=request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
return chain.proceed(request);
}
private boolean isNetWork() {
//判断是否有网络
return false;
}
}
第二加载新建的拦截器
File file = new File(Environment.getExternalStorageDirectory(),"cache");
Cache cache=new Cache(file,10*1024*1024);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(new CacheRequestInterceptor(this))
.addNetworkInterceptor(new CacheResponseInterceptor())
.build();
//构建一个请求
final Request request = new Request.Builder()
.url("path")
.post(exMultipatyRequestBody)
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e)
}
@Override
public void onResponse(Call call, Response respo
Log.d("cache=",""+response.cacheResponse());
Log.d("response=",""+response.body().string()
}
});
###OkHttpClient的单线程/多线程断点下载 区别:
单线程下载 在上面已经有过单线程下载监听的代码,这里再写一遍:
OkHttpClient okHttpClient = new OkHttpClient();
Request request= new Request.Builder()
.url("path")
.build();
Call call=okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
InputStream inputStream= response.body().byteStream();
final long total = response.body().contentLength();
File file =new File(Environment.getDataDirectory(),"xx.apk");
OutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024*10];
int len=0;
long sum=0;
while ((len=inputStream.read(buffer))!= -1){
outputStream.write(buffer,0,len);
sum+=len;
final long finalSum= sum;
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d("总比",""+finalSum+"/"+total);
}
});
}
outputStream.close();
inputStream.close();
}
});