OkHttp3簡單使用:請求和響應,post,get
一,HTTP請求、響應報文格式
要弄明白網路框架,首先需要先掌握Http請求的,響應的報文格式。
HTTP請求報文格式:
HTTP請求報文主要由請求行、請求頭部、請求正文3部分組成.
request.png
- 請求行:由請求方法,URL,協議版本三部分構成,之間用空格隔開
請求方法包括:POST、GET、HEAD、PUT、POST、TRACE、OPTIONS、DELETE等
協議版本:HTTP/主版本號.次版本號,常用的有HTTP/1.0和HTTP/1.1請求方法.png
- 請求頭部:
請求頭部為請求報文添加了一些附加資訊,由“名/值”對組成,每行一對,名和值之間使用冒號分隔
常見請求頭如下:
Host ----接受請求的伺服器地址,可以是IP:埠號,也可以是域名
User-Agent ----傳送請求的應用程式名稱
Connection ---- 指定與連線相關的屬性,如Connection:Keep-Alive
Accept-Charset ---- 通知服務端可以傳送的編碼格式
Accept-Encoding ---- 通知服務端可以傳送的資料壓縮格式
Accept-Language ---- 通知服務端可以傳送的語言 - 請求正文
可選部分,比如GET請求就沒有請求正文 - 請求示例:
image.png
HTTP響應報文格式:
HTTP響應報文主要由狀態行、響應頭部、響應正文3部分組成
響應報文.png
-
狀態行:
由3部分組成,分別為:協議版本,狀態碼,狀態碼描述,之間由空格分隔
狀態碼:為3位數字,200-299的狀態碼錶示成功,300-399的狀態碼指資源重定向,400-499的狀態碼指客戶端請求出錯,500-599的狀態碼指服務端出錯(HTTP/1.1向協議中引入了資訊性狀態碼,範圍為100-199)
常見的:
200:響應成功
302:重定向跳轉,跳轉地址通過響應頭中的Location屬性指定
400:客戶端請求有語法錯誤,引數錯誤,不能被伺服器識別
403
404:請求資源不存在
500:伺服器內部錯誤image.png
-
響應頭部 :
與請求頭部類似,為響應報文添加了一些附加資訊
Server - 伺服器應用程式軟體的名稱和版本
Content-Type - 響應正文的型別(是圖片還是二進位制字串)
Content-Length - 響應正文長度
Content-Charset - 響應正文使用的編碼
Content-Encoding - 響應正文使用的資料壓縮格式
Content-Language - 響應正文使用的語言
Server: bfe/1.0.8.1 Date: Sat, 04 Apr 2015 02:49:41 GMT Content-Type: text/html; charset=utf-8 Vary: Accept-Encoding Cache-Control: private cxy_all: baidu+8ee3da625d74d1aa1ac9a7c34a2191dc Expires: Sat, 04 Apr 2015 02:49:38 GMT X-Powered-By: HPHP bdpagetype: 1 bdqid: 0xb4eababa0002db6e bduserid: 0 Set-Cookie: BDSVRTM=0; path=/ BD_HOME=0; path=/ H_PS_PSSID=13165_12942_1430_13075_12867_13322_12691_13348_12723_12797_13309_13325_13203_13161_13256_8498; path=/; domain=.baidu.com __bsi=18221750326646863206_31_0_I_R_2_0303_C02F_N_I_I; expires=Sat, 04-Apr-15 02:49:46 GMT; domain=www.baidu.com; path=/ Content-Encoding: gzip X-Firefox-Spdy: 3.1
- 響應正文
是請求響應的最終結果,都在響應體裡。
報文可以承載很多型別的數字資料:圖片、視訊、HTML文件、軟體應用程式等 - 響應示例
image.png
二,HTTP請求和響應的基本使用
主要包含:
- 一般的get請求
- 一般的post請求
- 基於Http的檔案上傳
- 檔案下載
- 載入圖片
- 支援請求回撥,直接返回物件、物件集合
- 支援session的保持
- 新增網路訪問許可權並新增庫依賴
<uses-permission android:name="android.permission.INTERNET" />
api 'com.squareup.okhttp3:okhttp:3.9.0'
- HTTP的GET請求
//1,建立okHttpClient物件
OkHttpClient mOkHttpClient = new OkHttpClient();
//2,建立一個Request
final Request request = new Request.Builder()
.url("https://www.baidu.com")
.build();
//3,新建一個call物件
Call call = mOkHttpClient.newCall(request);
//4,請求加入排程,這裡是非同步Get請求回撥
call.enqueue(new Callback()
{
@Override
public void onFailure(Request request, IOException e)
{
}
@Override
public void onResponse(final Response response) throws IOException
{
//String htmlStr = response.body().string();
}
});
對以上的簡單請求的構成:
- 傳送一個GET請求的步驟,首先構造一個Request物件,引數最起碼有個URL,當然也可以通過Request.Builder設定更多的引數比如:header、method等。
//URL帶的引數
HashMap<String,String> params = new HashMap<>();
//GET 請求帶的Header
HashMap<String,String> headers= new HashMap<>();
//HttpUrl.Builder構造帶引數url
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
if (params != null) {
for (String key : params.keySet()) {
urlBuilder.setQueryParameter(key, params.get(key));
}
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.headers(headers == null ? new Headers.Builder().build() : Headers.of(headers))
.get()
.build();
- 通過Request的物件去構造得到一個Call物件,類似於將你的請求封裝成了任務,既然是任務,就會有execute(),enqueue()和cancel()等方法。
execute():同步GET請求
//同步
Response response = call.execute()
if(response.isSuccessful()){
//響應成功
}
enqueue():非同步GET請求,將call加入排程佇列,然後等待任務執行完成,我們在Callback中即可得到結果。
cancel():Call請求的取消,okHttp支援請求取消功能,當呼叫請求的cancel()時,請求就會被取消,丟擲異常。又是需要監控許多Http請求的執行情況,可以把這些請求的Call蒐集起來,執行完畢自動剔除,如果在請求執行過程中(如下載),想取消執行,可使用call.cancel()取消。
- 請求的響應Response
對於同步GET請求,Response物件是直接返回的。非同步GET請求,通過onResponse回撥方法傳引數,需要注意的是這個onResponse回撥方法不是在主執行緒回撥,可以使用runInUIThread(new Runnable(){})。
我們希望獲得返回的字串,可以通過response.body().string()
獲取;
如果希望獲得返回的二進位制位元組陣列,則呼叫response.body().bytes()
;
如果你想拿到返回的inputStream,則呼叫response.body().byteStream()
3. HTTP的POST請求
看來上面的簡單的get請求,基本上整個的用法也就掌握了,比如post攜帶引數,也僅僅是Request的構造的不同。
//POST引數構造MultipartBody.Builder,表單提交
HashMap<String,String> params = new HashMap<>();
MultipartBody.Builder urlBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if (params != null) {
for (String key : params.keySet()) {
if (params.get(key)!=null){
urlBuilder.addFormDataPart(key, params.get(key));
}
//urlBuilder.addFormDataPart(key, params.get(key));
}
}
// 構造Request->call->執行
Request request = new Request.Builder()
.headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//extraHeaders 是使用者新增頭
.url(url)
.post(urlBuilder.build())//引數放在body體裡
.build();
Call call = httpClient.newCall(request);
try (Response response = call.execute()) {
if (response.isSuccessful()){
//響應成功
}
}
Post的時候,引數是包含在請求體中的,所以我們通過MultipartBody.Builder 新增多個String鍵值對,然後去構造RequestBody,最後完成我們Request的構造。
4. OKHTTP的上傳檔案
上傳檔案本身也是一個POST請求。在上面的POST請求中可以知道,POST請求的所有引數都是在BODY體中的,我們看看請求體的原始碼RequestBody:請求體=contentType + BufferedSink
RequestBody
//抽象類請求體,**請求體=contentType + BufferedSink**
public abstract class RequestBody {
/** Returns the Content-Type header for this body. */
//返回Body體的內容型別
public abstract @Nullable MediaType contentType();
/**
* Returns the number of bytes that will be written to {@code sink} in a call to {@link #writeTo},
* or -1 if that count is unknown.
*/
//返回寫入sink的位元組長度
public long contentLength() throws IOException {
return -1;
}
/** Writes the content of this request to {@code sink}. */
//寫入快取sink
public abstract void writeTo(BufferedSink sink) throws IOException;
/**
* Returns a new request body that transmits {@code content}. If {@code contentType} is non-null
* and lacks a charset, this will use UTF-8.
*/
//建立一個請求體,如果contentType不等於null且缺少字符集,將使用UTF-8
public static RequestBody create(@Nullable MediaType contentType, String content) {
Charset charset = Util.UTF_8;
if (contentType != null) {
//contentType裡面的字符集
charset = contentType.charset();
if (charset == null) {
charset = Util.UTF_8;
//contentType 裡面加入字符集
contentType = MediaType.parse(contentType + "; charset=utf-8");
}
}
//按字符集變成位元組
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 ByteString content) {
return new RequestBody() {
@Override public @Nullable MediaType contentType() {
//請求體需要的內容型別
return contentType;
}
@Override public long contentLength() throws IOException {
//寫入BufferedSink 的長度
return content.size();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
//將需要傳輸的位元組,寫入快取BufferedSink 中
sink.write(content);
}
};
}
/** 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}. */
//建立一個請求體,傳輸檔案file內容,其實就是file寫入bufferedSink
public static RequestBody create(final @Nullable MediaType contentType, final File file) {
if (file == null) throw new NullPointerException("content == 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 {
Source source = null;
try {
//檔案寫入BufferedSink
source = Okio.source(file);
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
};
}
}
Http請求中Content-Type
客戶端在進行http請求伺服器的時候,需要告訴伺服器請求的型別,伺服器在返回給客戶端的資料的時候,也需要告訴客戶端返回資料的型別
預設的ContentType為 text/html 也就是網頁格式. 常用的內容型別
- text/plain :純文字格式 .txt
- text/xml : XML格式 .xml
- image/gif :gif圖片格式 .gif
- image/jpeg :jpg圖片格式 .jpg
- image/png:png圖片格式 .png
- audio/mp3 : 音訊mp3格式 .mp3
- audio/rn-mpeg :音訊mpga格式 .mpga
- video/mpeg4 : 視訊mp4格式 .mp4
- video/x-mpg : 視訊mpa格式 .mpg
- video/x-mpeg :視訊mpeg格式 .mpeg
- video/mpg : 視訊mpg格式 .mpg
以application開頭的媒體格式型別: - application/xhtml+xml :XHTML格式
- application/xml : XML資料格式
- application/atom+xml :Atom XML聚合格式
- application/json : JSON資料格式
- application/pdf :pdf格式
- application/msword : Word文件格式
- application/octet-stream : 二進位制流資料(如常見的檔案下載)
MultipartBody.Builder 新增多個String鍵值對
//MultipartBody原始碼,MultipartBody其實也是RequestBody ,需要在此RequestBody 體內,新增多個Part
/** An <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC 2387</a>-compliant request body. */
public final class MultipartBody extends RequestBody {
/**
* The "mixed" subtype of "multipart" is intended for use when the body parts are independent and
* need to be bundled in a particular order. Any "multipart" subtypes that an implementation does
* not recognize must be treated as being of subtype "mixed".
*/
//混合的內容型別
public static final MediaType MIXED = MediaType.parse("multipart/mixed");
/**
* The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the
* semantics are different. In particular, each of the body parts is an "alternative" version of
* the same information.
*/
public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a digest, the default {@code Content-Type} value for a body part is changed from
* "text/plain" to "message/rfc822".
*/
public static final MediaType DIGEST = MediaType.parse("multipart/digest");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a parallel entity, the order of body parts is not significant.
*/
public static final MediaType PARALLEL = MediaType.parse("multipart/parallel");
/**
* The media-type multipart/form-data follows the rules of all multipart MIME data streams as
* outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who
* fills out the form. Each field has a name. Within a given form, the names are unique.
*/
public static final MediaType FORM = MediaType.parse("multipart/form-data");
private static final byte[] COLONSPACE = {':', ' '};
private static final byte[] CRLF = {'\r', '\n'};
private static final byte[] DASHDASH = {'-', '-'};
private final ByteString boundary;
private final MediaType originalType;
//請求體的內容型別
private final MediaType contentType;
//MultiPartBody需要新增多個Part物件,一起請求
private final List<Part> parts;
private long contentLength = -1L;
//建構函式
MultipartBody(ByteString boundary, MediaType type, List<Part> parts) {
this.boundary = boundary;
this.originalType = type;
this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8());
this.parts = Util.immutableList(parts);
}
public MediaType type() {
return originalType;
}
public String boundary() {
return boundary.utf8();
}
/** The number of parts in this multipart body. */
//multipart 的數量
public int size() {
return parts.size();
}
//多個parts
public List<Part> parts() {
return parts;
}
public Part part(int index) {
return parts.get(index);
}
/** A combination of {@link #type()} and {@link #boundary()}. */
//MultiPart的內容型別
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() throws IOException {
long result = contentLength;
if (result != -1L) return result;
return contentLength = writeOrCountBytes(null, true);
}
//將每個part寫入BufferedSink中,傳輸
@Override public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);
}
/**
* Either writes this request to {@code sink} or measures its content length. We have one method
* do double-duty to make sure the counting and content are consistent, particularly when it comes
* to awkward operations like measuring the encoded length of header strings, or the
* length-in-digits of an encoded integer.
*/
//將每個Part的內容都寫入,MultiPartBody的BufferedSink 中
private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) throws IOException {
long byteCount = 0L;
Buffer byteCountBuffer = null;
if (countBytes) {
sink = byteCountBuffer = new Buffer();
}
//寫每個part
for (int p = 0, partCount = parts.size(); p < partCount; p++) {
Part part = parts.get(p);
//Part的Headers和RequestBody
Headers headers = part.headers;
RequestBody body = part.body;
sink.write(DASHDASH);
sink.write(boundary);
sink.write(CRLF);
//Part的Headers寫入sink
if (headers != null) {
for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
sink.writeUtf8(headers.name(h))
.write(COLONSPACE)
.writeUtf8(headers.value(h))
.write(CRLF);
}
}
//Part的RequestBody寫入Part
//1,寫contentType
MediaType contentType = body.contentType();
if (contentType != null) {
sink.writeUtf8("Content-Type: ")
.writeUtf8(contentType.toString())
.write(CRLF);
}
//2,寫contentLength
long contentLength = body.contentLength();
if (contentLength != -1) {
sink.writeUtf8("Content-Length: ")
.writeDecimalLong(contentLength)
.write(CRLF);
} else if (countBytes) {
// We can't measure the body's size without the sizes of its components.
byteCountBuffer.clear();
return -1L;
}
sink.write(CRLF);
//3,寫body體
if (countBytes) {
byteCount += contentLength;
} else {
body.writeTo(sink);
}
sink.write(CRLF);
}
sink.write(DASHDASH);
sink.write(boundary);
sink.write(DASHDASH);
sink.write(CRLF);
if (countBytes) {
byteCount += byteCountBuffer.size();
byteCountBuffer.clear();
}
return byteCount;
}
/**
* Appends a quoted-string to a StringBuilder.
*
* <p>RFC 2388 is rather vague about how one should escape special characters in form-data
* parameters, and as it turns out Firefox and Chrome actually do rather different things, and
* both say in their comments that they're not really sure what the right approach is. We go with
* Chrome's behavior (which also experimentally seems to match what IE does), but if you actually
* want to have a good chance of things working, please avoid double-quotes, newlines, percent
* signs, and the like in your field names.
*/
//裝換換行符,tab符號,引號
static StringBuilder appendQuotedString(StringBuilder target, String key) {
target.append('"');
for (int i = 0, len = key.length(); i < len; i++) {
char ch = key.charAt(i);
switch (ch) {
case '\n':
target.append("%0A");
break;
case '\r':
target.append("%0D");
break;
case '"':
target.append("%22");
break;
default:
target.append(ch);
break;
}
}
target.append('"');
return target;
}
//Part 的定義,Part 是由Headers+RequestBody組成
public static final class Part {
public static Part create(RequestBody body) {
return create(null, body);
}
public static Part create(@Nullable Headers headers, RequestBody body) {
if (body == null) {
throw new NullPointerException("body == null");
}
//Part的headers不能存在Content-Type和Content-Length欄位
if (headers != null && headers.get("Content-Type") != null) {
throw new IllegalArgumentException("Unexpected header: Content-Type");
}
if (headers != null && headers.get("Content-Length") != null) {
throw new IllegalArgumentException("Unexpected header: Content-Length");
}
return new Part(headers, body);
}
//建立key-value的Part,name其實就是key
public static Part createFormData(String name, String value) {
return createFormData(name, null, RequestBody.create(null, value));
}
//建立key-value的Part
public static Part createFormData(String name, @Nullable String filename, RequestBody body) {
if (name == null) {
throw new NullPointerException("name == null");
}
StringBuilder disposition = new StringBuilder("form-data; name=");
// disposition = form-data; name=name;
appendQuotedString(disposition, name);//對name中的特殊符號轉換
if (filename != null) {
disposition.append("; filename=");
// disposition = form-data; name=name; filename=filename;
appendQuotedString(disposition, filename);//對filename中的特殊符號轉換
}
//建立Part 體,Headers(Content-Disposition- form-data; name=name; filename=filename)+body
return create(Headers.of("Content-Disposition", disposition.toString()), body);
}
//headers
final @Nullable Headers headers;
//body
final RequestBody body;
private Part(@Nullable Headers headers, RequestBody body) {
this.headers = headers;
this.body = body;
}
//Part的headers
public @Nullable Headers headers() {
return headers;
}
//Part的body體
public RequestBody body() {
return body;
}
}
public static final class Builder {
private final ByteString boundary;
private MediaType type = MIXED;
private final List<Part> parts = new ArrayList<>();
public Builder() {
this(UUID.randomUUID().toString());
}
public Builder(String boundary) {
this.boundary = ByteString.encodeUtf8(boundary);
}
/**
* Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the default), {@link
* #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and {@link #FORM}.
*/
public Builder setType(MediaType type) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (!type.type().equals("multipart")) {
throw new IllegalArgumentException("multipart != " + type);
}
this.type = type;
return this;
}
/** Add a part to the body. */
//新增Part
public Builder addPart(RequestBody body) {
return addPart(Part.create(body));
}
/** Add a part to the body. */
//新增Part
public Builder addPart(@Nullable Headers headers, RequestBody body) {
return addPart(Part.create(headers, body));
}
/** Add a form data part to the body. */
//新增表單資料Part
public Builder addFormDataPart(String name, String value) {
return addPart(Part.createFormData(name, value));
}
/** Add a form data part to the body. */
//新增表單資料Part
public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) {
return addPart(Part.createFormData(name, filename, body));
}
/** Add a part to the body. */
public Builder addPart(Part part) {
if (part == null) throw new NullPointerException("part == null");
parts.add(part);
return this;
}
/** Assemble the specified parts into a request body. */
public MultipartBody build() {
if (parts.isEmpty()) {
throw new IllegalStateException("Multipart body must have at least one part.");
}
//構建MultipartBody物件
return new MultipartBody(boundary, type, parts);
}
}
}
總結一下MultipartBody:
- MultipartBody本質一個是一個RequestBody,具有自己的contentType+BufferedSink,是POST請求的最外層封裝,需要新增多個Part
- Part物件組成:Headers+RequestBody。是MultipartBody的成員變數,需要寫入MultipartBody的BufferedSink中。
HTTP真正的上傳檔案
- 最基本的上傳檔案:
重點:RequestBody create(MediaType contentType, final File file)構造檔案請求體RequestBody ,並且新增到MultiPartBody中
OkHttpClient client = new OkHttpClient();
// form 表單形式上傳,MultipartBody的內容型別是表單格式,multipart/form-data
MultipartBody.Builder urlBuilder= new MultipartBody.Builder().setType(MultipartBody.FORM);
//引數
HashMap<String,String> params = new HashMap<>();
if (params != null) {
for (String key : params.keySet()) {
if (params.get(key)!=null){
urlBuilder.addFormDataPart(key, params.get(key));
}
}
}
//需要上傳的檔案,需要攜帶上傳的檔案(小型檔案 不建議超過500K)
HashMap<String,String> files= new HashMap<>();
if (files != null) {
for (String key : files.keySet()) {
//重點:RequestBody create(MediaType contentType, final File file)構造檔案請求體RequestBody
urlBuilder.addFormDataPart(key, files.get(key).getName(), RequestBody.create(MediaType.parse("multipart/form-data"), files.get(key)));
}
}
//構造請求request
Request request = new Request.Builder()
.headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))
.url(url)
.post(urlBuilder.build())
.build();
//非同步執行請求
newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i("lfq" ,"onFailure");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//非主執行緒
if (response.isSuccessful()) {
String str = response.body().string();
Log.i("tk", response.message() + " , body " + str);
} else {
Log.i("tk" ,response.message() + " error : body " + response.body().string());
}
}
});
2. 大檔案分塊非同步上傳
我們知道Post上傳檔案,簡單的說就是將檔案file封裝成RequestBody體,然後新增到MultiPartBody的addPart中構造MultiPartBody所需要的Part物件(Headers+body),RequestBody是個抽象類,裡面的所有create方法如下:
image.png
filebody.png
可以看出,基本都是重寫了抽象類的RequestBody的三種方法,所以我們也可以繼承實現自己的Body體:
image.png
EG:已上傳相機圖片(5M)為例,分塊多執行緒非同步同時上傳,但是這種方法需要服務端接口才行。
//檔案路徑
String path = "xxx.jpg";
1,檔案塊物件
public static final int FILE_BLOCK_SIZE = 500 * 1024;//500k
/*檔案塊描述*/
public static class FileBlock {
public long start;//起始位元組位置
public long end;//結束位元組位置
public int index;//檔案分塊索引
}
2,檔案切塊
//計算切塊,儲存在陣列
final SparseArray<FileBlock> blockArray = splitFile(path, FILE_BLOCK_SIZE);
/**
* 檔案分塊
*
* @param filePath 檔案路徑
* @param blockSize 塊大小
*
* @return 分塊描述集合 檔案不存在時返回空
*/
public static SparseArray<FileBlock> splitFile(String filePath, long blockSize) {
File file = new File(filePath);
if (!file.exists()) {
return null;
}
SparseArray<FileBlock> blockArray = new SparseArray<>();
int i = 0;
int start = 0;
while (start < file.length()) {
i++;
FileBlock fileBlock = new FileBlock();
fileBlock.index = i;
fileBlock.start = start;
start += blockSize;
fileBlock.end = start;
blockArray.put(i, fileBlock);
}
blockArray.get(i).end = file.length();
return blockArray;
}
3,對檔案塊分塊多執行緒非同步上傳
服務端的介面:
url:domain/sync/img/upload
method: POST
//請求引數
data = {
'img_md5': 'dddddsds',
'total': 10, #總的分片數
'index': 5, #該分片所在的位置, start by 1
}
請求返回值json:
{
'status': 206/205/400/409/500,
'msg': '分片上傳成功/上傳圖片成功/引數錯誤/上傳資料重複/上傳失敗'
'data': { # 205時有此欄位
'img_url': 'https://foo.jpg',
}
}
只需要圖片的md5,總的分片數,該分片的位置,當一塊傳輸成功時返回206,當全部塊傳完成是返回206,並返回該圖片在伺服器的url
服務端介面返回解析類:
/**
* 分片上傳部分的介面返回
*
* @link {http://10.16.69.11:5000/iSync/iSync%E6%9C%8D%E5%8A%A1%E7%AB%AFv4%E6%96%87%E6%A1%A3/index.html#4_1}
*/
public static class ChuckUploadData implements Serializable {
public ChuckUploadBean data;
public static class ChuckUploadBean implements Serializable{
public String img_url;
}
/** 此塊是否上傳成功 */
public boolean isPicSuccess() {
return status == 206 || status == 409;
}
/** 全部原圖是否上傳成功 */
public boolean isAllPicSuccess() {
return status == 205;
}
public boolean isRepitition(){
return status == 409;
}
}
//上傳圖片的執行緒池
ExcutorService threadPool = Executors.newCachedThreadPool();
//上傳函式
/**
* 上傳原圖,非同步上傳
*
* @param httpCallback 回撥介面
* @param md5 檔案md5
* @param path 圖片路徑
* @param total 總塊數
* @param index 分塊索引
* @param start 分塊開始位置
* @param end 分塊結束位置
*/
public static void uploadBigImage(String userId, final HttpListenerAdapter<ChuckUploadData> httpCallback, String md5, String path, int total, int index, long start, long end) {
HashMap<String, String> params = new HashMap<String, String>();
params.put("img_uuid", uuid);//完整檔案的md5
params.put("total", String.valueOf(total));//總的分片數
params.put("index", String.valueOf(index));//當前分片位置,從1開始
//全域性單例OKHttpClient
OkHttpClient httpClient = DataProvider.getInstance().inkApi.getLongWaitHttpClient();
Runnable httpUploadRunnable = HttpRunnableFactory.newPostFileBlockRunnable(
httpClient,
upload_url,//上傳url,自定義
null,
params,//上傳引數
"image",
new File(path),//圖片檔案
start,//index塊開始的位置
end,//index塊結束的位置
ChuckUploadData.class,
httpCallback);//回撥函式
threadManager.submit httpUploadRunnable );
}
/**
* 非同步post請求 表單方式拆塊上傳大型檔案用,構造Runnable
*
* @param httpClient okhttp客戶端
* @param url 請求地址
* @param headers 額外新增的header(通用header由中斷器統一新增)
* @param params 請求引數
* @param fileKey 檔案的接收用key
* @param file 大型檔案物件
* @param seekStart 起始位元組
* @param seekEnd 結束位元組
* @param cls 返回結果需要序列化的型別
* @param listener 非同步回撥
* @param <T> 返回結果需要序列化的型別宣告
*
* @return 非同步post請求用的預設Runnable
*/
public static <T> Runnable newPostFileBlockRunnable(final OkHttpClient httpClient, final String url, final Map<String, String> headers, final Map<String, String> params, final String fileKey, final File file, final long seekStart, final long seekEnd, final Class<T> cls, final HttpListenerAdapter<T> listener) {
return new Runnable () {
@Override
public void run() {
Log.e("http", "---postfile---");
Log.e("http", "url: " + url);
Log.e("http", "extraHeaders: " + headers);
Log.e("http", "params: " + params);
Log.e("http", "filepath: " + file.getPath());
Log.e("http", "seekStart: " + seekStart);
Log.e("http", "seekEnd: " + seekEnd);
Call call = null;
if (listener != null) {
listener.onStart(call);
}
try {
if (TextUtils.isEmpty(url)) {
throw new InterruptedException("url is null exception");
}
//構造path檔案的index塊的seekStart到seekEnd的請求體requestBody ,新增到MultiPartBody中
RequestBody requestBody = new RequestBody() {
@Override
public MediaType contentType() {
//請求體的內容型別
return MediaType.parse("multipart/form-data");
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
//切塊上傳
long nowSeek = seekStart;
long seekEndWrite = seekEnd;
if (seekEndWrite == 0) {
seekEndWrite = file.length();
}
//跳到開始位置
FileInputStream in = new FileInputStream(file);
if (seekStart > 0) {
long amt = in.skip(seekStart);
if (amt == -1) {
nowSeek = 0;
}
}
//將該塊的位元組內容寫入body的BufferedSink 中
int len;
byte[] buf = new byte[BUFFER_SIZE_DEFAULT];
while ((len = in.read(buf)) >= 0 && nowSeek < seekEndWrite) {
sink.write(buf, 0, len);
nowSeek += len;
if (nowSeek + BUFFER_SIZE_DEFAULT > seekEndWrite) {
buf = new byte[Integer.valueOf((seekEndWrite - nowSeek) + "")];
}
}
closeStream(in);
}
};
//組裝其它引數
MultipartBody.Builder urlBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if (params != null) {
for (String key : params.keySet()) {
//urlBuilder.addFormDataPart(key, params.get(key));
if (params.get(key)!=null){
urlBuilder.addFormDataPart(key, params.get(key));
}
}
}
//把檔案塊的請求體新增到MultiPartBody中
urlBuilder.addFormDataPart(fileKey, file.getName(), requestBody);
Request request = new Request.Builder()
.headers(headers == null ? new Headers.Builder().build() : Headers.of(headers))
.url(url)
.post(urlBuilder.build())
.build();
call = httpClient.newCall(request);
//雖說是同步呼叫call.execute(),但是此Http請求過程是線上程池中的,相當於非同步呼叫
try (Response response = call.execute()) {
if (!response.isSuccessful()){
throw new IOException("Unexpected code " + response.code());
}
/*列印json串,json樣式的*/
String json = response.body().string();
//解析返回的響應json
T result = JsonUtils.getObjFromStr(cls, json);
if (listener != null) {
//防止回撥內的業務邏輯引起二次onFailure回撥
try {
listener.onResponse(call, result);
} catch (Exception e) {
e.printStackTrace();
}
}
} finally {
}
} catch (Exception e) {
if (listener != null) {
//中途取消導致的中斷
if (call != null && call.isCanceled()) {
listener.onCancel(call);
} else {
//其它意義上的請求失敗
listener.onFailure(call, e);
}
}
} finally {
if (listener != null) {
listener.onEnd(call);
}
}
}
};
}
//迴圈遍歷所有的文章塊,多執行緒上傳
for (int i = 0; i < blockArray.size(); i++) {
//非同步分塊上傳
final FileUtil.FileBlock block = blockArray.get(i + 1);
//提交執行緒池,非同步上傳單塊
uploadBigImage(userId, new HttpListenerAdapter<ChuckUploadData>() {
@Override
public void onResponse(Call call, SyncBeans.ChuckUploadData bean) {
try {
//單塊上傳
if (bean != null ) {
if (bean.isPicSuccess()) {
//205,單塊成功不做處理
} else if (bean.isAllPicSuccess()) {
//206,全部成功
}
}
}catch(Exception e){}
},uuid, mediaBean.imageNativeUrl, blockArray.size(), block.index, block.start, block.end);
}
5. OKHttp下載檔案,並通知進度
下載檔案的原理其實很簡單,下載過程其實就是一個GET過程(上傳檔案是POST過程相對應),下載檔案需要在非同步執行緒中執行(方法有二,1,使用okhttp的call.enquene()方法非同步執行,2,使用call.excute()同步方法,但是線上程次中執行整個請求過程),在成功響應之後,獲得網路檔案輸入流InputStream,然後迴圈讀取輸入流上的檔案,寫入檔案輸出流。
/**
* @param url 下載連線
* @param saveDir 儲存下載檔案的SDCard目錄
* @param params url攜帶引數
* @param extraHeaders 請求攜帶其他的要求的headers
* @param listener 下載監聽
*/
public void download(final String url, final String saveDir,HashMap<String,String> params, HashMap<String,String> extraHeaders,final OnDownloadListener listener) {
//構造請求Url
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
if (params != null) {
for (String key : params.keySet()) {
if (params.get(key)!=null){
urlBuilder.setQueryParameter(key, params.get(key));//非必須
}
}
}
//構造請求request
Request request = new Request.Builder()
.url(urlBuilder.build())
.headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//headers非必須
.get()
.build();
//非同步執行請求
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 下載失敗
listener.onDownloadFailed();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//非主執行緒
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
// 儲存下載檔案的目錄
String savePath = isExistDir(saveDir);
try {
//獲取響應的位元組流
is = response.body().byteStream();
//檔案的總大小
long total = response.body().contentLength();
File file = new File(savePath);
fos = new FileOutputStream(file);
long sum = 0;
//迴圈讀取輸入流
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
int progress = (int) (sum * 1.0f / total * 100);
// 下載中
if(listener != null){
listener.onDownloading(progress);
}
}
fos.flush();
// 下載完成
if(listener != null){
listener.onDownloadSuccess();
}
} catch (Exception e) {
if(listener != null){
listener.onDownloadFailed();
}
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
}
}
}
});
}
至此,OKHTTP3的基本網路請求訪問,傳送GET請求,傳送POST請求,基本上傳檔案,切塊多執行緒非同步上傳檔案,下載檔案就到這裡了,其實下載檔案還可以做成斷點續傳,獲取每次的seek點