一楼肪,HTTP請(qǐng)求、響應(yīng)報(bào)文格式
要弄明白網(wǎng)絡(luò)框架汉柒,首先需要先掌握Http請(qǐng)求的误褪,響應(yīng)的報(bào)文格式。
HTTP請(qǐng)求報(bào)文格式:
HTTP請(qǐng)求報(bào)文主要由請(qǐng)求行碾褂、請(qǐng)求頭部兽间、請(qǐng)求正文3部分組成.
-
請(qǐng)求行:由請(qǐng)求方法,URL正塌,協(xié)議版本三部分構(gòu)成嘀略,之間用空格隔開
請(qǐng)求方法包括:POST、GET乓诽、HEAD帜羊、PUT、POST鸠天、TRACE讼育、OPTIONS、DELETE等
協(xié)議版本:HTTP/主版本號(hào).次版本號(hào)稠集,常用的有HTTP/1.0和HTTP/1.1
請(qǐng)求方法.png -
請(qǐng)求頭部:
請(qǐng)求頭部為請(qǐng)求報(bào)文添加了一些附加信息奶段,由“名/值”對(duì)組成,每行一對(duì)剥纷,名和值之間使用冒號(hào)分隔
常見請(qǐng)求頭如下:
Host ----接受請(qǐng)求的服務(wù)器地址痹籍,可以是IP:端口號(hào),也可以是域名
User-Agent ----發(fā)送請(qǐng)求的應(yīng)用程序名稱
Connection ---- 指定與連接相關(guān)的屬性晦鞋,如Connection:Keep-Alive
Accept-Charset ---- 通知服務(wù)端可以發(fā)送的編碼格式
Accept-Encoding ---- 通知服務(wù)端可以發(fā)送的數(shù)據(jù)壓縮格式
Accept-Language ---- 通知服務(wù)端可以發(fā)送的語言 -
請(qǐng)求正文
可選部分词裤,比如GET請(qǐng)求就沒有請(qǐng)求正文 -
請(qǐng)求示例:
image.png
HTTP響應(yīng)報(bào)文格式:
HTTP響應(yīng)報(bào)文主要由狀態(tài)行刺洒、響應(yīng)頭部、響應(yīng)正文3部分組成
-
狀態(tài)行:
由3部分組成吼砂,分別為:協(xié)議版本逆航,狀態(tài)碼,狀態(tài)碼描述渔肩,之間由空格分隔
狀態(tài)碼:為3位數(shù)字因俐,200-299的狀態(tài)碼表示成功,300-399的狀態(tài)碼指資源重定向周偎,400-499的狀態(tài)碼指客戶端請(qǐng)求出錯(cuò)抹剩,500-599的狀態(tài)碼指服務(wù)端出錯(cuò)(HTTP/1.1向協(xié)議中引入了信息性狀態(tài)碼,范圍為100-199)
常見的:
200:響應(yīng)成功
302:重定向跳轉(zhuǎn)蓉坎,跳轉(zhuǎn)地址通過響應(yīng)頭中的Location屬性指定
400:客戶端請(qǐng)求有語法錯(cuò)誤澳眷,參數(shù)錯(cuò)誤,不能被服務(wù)器識(shí)別
403:服務(wù)器接收到請(qǐng)求蛉艾,但是拒絕提供服務(wù)(認(rèn)證失斍弧)
404:請(qǐng)求資源不存在
500:服務(wù)器內(nèi)部錯(cuò)誤
image.png 響應(yīng)頭部 :
與請(qǐng)求頭部類似,為響應(yīng)報(bào)文添加了一些附加信息
Server - 服務(wù)器應(yīng)用程序軟件的名稱和版本
Content-Type - 響應(yīng)正文的類型(是圖片還是二進(jìn)制字符串)
Content-Length - 響應(yīng)正文長度
Content-Charset - 響應(yīng)正文使用的編碼
Content-Encoding - 響應(yīng)正文使用的數(shù)據(jù)壓縮格式
Content-Language - 響應(yīng)正文使用的語言
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
-
響應(yīng)正文
是請(qǐng)求響應(yīng)的最終結(jié)果勿侯,都在響應(yīng)體里拓瞪。
報(bào)文可以承載很多類型的數(shù)字?jǐn)?shù)據(jù):圖片、視頻助琐、HTML文檔祭埂、軟件應(yīng)用程序等 -
響應(yīng)示例
image.png
二,HTTP請(qǐng)求和響應(yīng)的基本使用
主要包含:
- 一般的get請(qǐng)求
- 一般的post請(qǐng)求
- 基于Http的文件上傳
- 文件下載
- 加載圖片
- 支持請(qǐng)求回調(diào)兵钮,直接返回對(duì)象蛆橡、對(duì)象集合
- 支持session的保持
- 添加網(wǎng)絡(luò)訪問權(quán)限并添加庫依賴
<uses-permission android:name="android.permission.INTERNET" />
api 'com.squareup.okhttp3:okhttp:3.9.0'
- HTTP的GET請(qǐng)求
//1,創(chuàng)建okHttpClient對(duì)象
OkHttpClient mOkHttpClient = new OkHttpClient();
//2,創(chuàng)建一個(gè)Request
final Request request = new Request.Builder()
.url("https://www.baidu.com")
.build();
//3,新建一個(gè)call對(duì)象
Call call = mOkHttpClient.newCall(request);
//4,請(qǐng)求加入調(diào)度掘譬,這里是異步Get請(qǐng)求回調(diào)
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();
}
});
對(duì)以上的簡單請(qǐng)求的構(gòu)成:
- 發(fā)送一個(gè)GET請(qǐng)求的步驟泰演,首先構(gòu)造一個(gè)Request對(duì)象,參數(shù)最起碼有個(gè)URL屁药,當(dāng)然也可以通過Request.Builder設(shè)置更多的參數(shù)比如:header粥血、method等柏锄。
//URL帶的參數(shù)
HashMap<String,String> params = new HashMap<>();
//GET 請(qǐng)求帶的Header
HashMap<String,String> headers= new HashMap<>();
//HttpUrl.Builder構(gòu)造帶參數(shù)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的對(duì)象去構(gòu)造得到一個(gè)Call對(duì)象酿箭,類似于將你的請(qǐng)求封裝成了任務(wù),既然是任務(wù)趾娃,就會(huì)有execute(),enqueue()和cancel()等方法缭嫡。
execute():同步GET請(qǐng)求
//同步
Response response = call.execute()
if(response.isSuccessful()){
//響應(yīng)成功
}
enqueue():異步GET請(qǐng)求,將call加入調(diào)度隊(duì)列抬闷,然后等待任務(wù)執(zhí)行完成妇蛀,我們?cè)贑allback中即可得到結(jié)果耕突。
cancel():Call請(qǐng)求的取消,okHttp支持請(qǐng)求取消功能评架,當(dāng)調(diào)用請(qǐng)求的cancel()時(shí)眷茁,請(qǐng)求就會(huì)被取消,拋出異常纵诞。又是需要監(jiān)控許多Http請(qǐng)求的執(zhí)行情況上祈,可以把這些請(qǐng)求的Call搜集起來,執(zhí)行完畢自動(dòng)剔除浙芙,如果在請(qǐng)求執(zhí)行過程中(如下載)登刺,想取消執(zhí)行,可使用call.cancel()取消嗡呼。
-
請(qǐng)求的響應(yīng)Response
對(duì)于同步GET請(qǐng)求纸俭,Response對(duì)象是直接返回的。異步GET請(qǐng)求南窗,通過onResponse回調(diào)方法傳參數(shù)揍很,需要注意的是這個(gè)onResponse回調(diào)方法不是在主線程回調(diào),可以使用runInUIThread(new Runnable(){})矾瘾。
我們希望獲得返回的字符串女轿,可以通過response.body().string()
獲取壕翩;
如果希望獲得返回的二進(jìn)制字節(jié)數(shù)組蛉迹,則調(diào)用response.body().bytes()
;
如果你想拿到返回的inputStream放妈,則調(diào)用response.body().byteStream()
3. HTTP的POST請(qǐng)求
看來上面的簡單的get請(qǐng)求北救,基本上整個(gè)的用法也就掌握了,比如post攜帶參數(shù)芜抒,也僅僅是Request的構(gòu)造的不同珍策。
//POST參數(shù)構(gòu)造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));
}
}
// 構(gòu)造Request->call->執(zhí)行
Request request = new Request.Builder()
.headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//extraHeaders 是用戶添加頭
.url(url)
.post(urlBuilder.build())//參數(shù)放在body體里
.build();
Call call = httpClient.newCall(request);
try (Response response = call.execute()) {
if (response.isSuccessful()){
//響應(yīng)成功
}
}
Post的時(shí)候宅倒,參數(shù)是包含在請(qǐng)求體中的攘宙,所以我們通過MultipartBody.Builder 添加多個(gè)String鍵值對(duì),然后去構(gòu)造RequestBody拐迁,最后完成我們Request的構(gòu)造蹭劈。
4. OKHTTP的上傳文件
上傳文件本身也是一個(gè)POST請(qǐng)求。在上面的POST請(qǐng)求中可以知道线召,POST請(qǐng)求的所有參數(shù)都是在BODY體中的铺韧,我們看看請(qǐng)求體的源碼RequestBody:請(qǐng)求體=contentType + BufferedSink
RequestBody
//抽象類請(qǐng)求體,**請(qǐng)求體=contentType + BufferedSink**
public abstract class RequestBody {
/** Returns the Content-Type header for this body. */
//返回Body體的內(nèi)容類型
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的字節(jié)長度
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.
*/
//創(chuàng)建一個(gè)請(qǐng)求體缓淹,如果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");
}
}
//按字符集變成字節(jié)
byte[] bytes = content.getBytes(charset);
return create(contentType, bytes);
}
/** Returns a new request body that transmits {@code content}. */
//創(chuàng)建新的請(qǐng)求體塔逃,傳輸字節(jié)
public static RequestBody create(
final @Nullable MediaType contentType, final ByteString content) {
return new RequestBody() {
@Override public @Nullable MediaType contentType() {
//請(qǐng)求體需要的內(nèi)容類型
return contentType;
}
@Override public long contentLength() throws IOException {
//寫入BufferedSink 的長度
return content.size();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
//將需要傳輸?shù)淖止?jié),寫入緩存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}. */
//創(chuàng)建一個(gè)請(qǐng)求體料仗,傳輸文件file內(nèi)容湾盗,其實(shí)就是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請(qǐng)求中Content-Type
客戶端在進(jìn)行http請(qǐng)求服務(wù)器的時(shí)候,需要告訴服務(wù)器請(qǐng)求的類型立轧,服務(wù)器在返回給客戶端的數(shù)據(jù)的時(shí)候淹仑,也需要告訴客戶端返回?cái)?shù)據(jù)的類型
默認(rèn)的ContentType為 text/html 也就是網(wǎng)頁格式. 常用的內(nèi)容類型
- 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數(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ù)(如常見的文件下載)
MultipartBody.Builder 添加多個(gè)String鍵值對(duì)
//MultipartBody源碼,MultipartBody其實(shí)也是RequestBody 肺孵,需要在此RequestBody 體內(nèi)匀借,添加多個(gè)Part
/** An <a >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".
*/
//混合的內(nèi)容類型
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;
//請(qǐng)求體的內(nèi)容類型
private final MediaType contentType;
//MultiPartBody需要添加多個(gè)Part對(duì)象,一起請(qǐng)求
private final List<Part> parts;
private long contentLength = -1L;
//構(gòu)造函數(shù)
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 的數(shù)量
public int size() {
return parts.size();
}
//多個(gè)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的內(nèi)容類型
@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);
}
//將每個(gè)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.
*/
//將每個(gè)Part的內(nèi)容都寫入,MultiPartBody的BufferedSink 中
private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) throws IOException {
long byteCount = 0L;
Buffer byteCountBuffer = null;
if (countBytes) {
sink = byteCountBuffer = new Buffer();
}
//寫每個(gè)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符號(hào)是鬼,引號(hào)
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);
}
//創(chuàng)建key-value的Part紫新,name其實(shí)就是key
public static Part createFormData(String name, String value) {
return createFormData(name, null, RequestBody.create(null, value));
}
//創(chuàng)建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);//對(duì)name中的特殊符號(hào)轉(zhuǎn)換
if (filename != null) {
disposition.append("; filename=");
// disposition = form-data; name=name; filename=filename;
appendQuotedString(disposition, filename);//對(duì)filename中的特殊符號(hào)轉(zhuǎn)換
}
//創(chuàng)建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. */
//添加表單數(shù)據(jù)Part
public Builder addFormDataPart(String name, String value) {
return addPart(Part.createFormData(name, value));
}
/** Add a form data part to the body. */
//添加表單數(shù)據(jù)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.");
}
//構(gòu)建MultipartBody對(duì)象
return new MultipartBody(boundary, type, parts);
}
}
}
總結(jié)一下MultipartBody:
- MultipartBody本質(zhì)一個(gè)是一個(gè)RequestBody,具有自己的contentType+BufferedSink芒率,是POST請(qǐng)求的最外層封裝囤耳,需要添加多個(gè)Part
- Part對(duì)象組成:Headers+RequestBody。是MultipartBody的成員變量偶芍,需要寫入MultipartBody的BufferedSink中充择。
HTTP真正的上傳文件
- 最基本的上傳文件:
重點(diǎn):RequestBody create(MediaType contentType, final File file)構(gòu)造文件請(qǐng)求體RequestBody ,并且添加到MultiPartBody中
OkHttpClient client = new OkHttpClient();
// form 表單形式上傳,MultipartBody的內(nèi)容類型是表單格式,multipart/form-data
MultipartBody.Builder urlBuilder= new MultipartBody.Builder().setType(MultipartBody.FORM);
//參數(shù)
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()) {
//重點(diǎn):RequestBody create(MediaType contentType, final File file)構(gòu)造文件請(qǐng)求體RequestBody
urlBuilder.addFormDataPart(key, files.get(key).getName(), RequestBody.create(MediaType.parse("multipart/form-data"), files.get(key)));
}
}
//構(gòu)造請(qǐng)求request
Request request = new Request.Builder()
.headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))
.url(url)
.post(urlBuilder.build())
.build();
//異步執(zhí)行請(qǐng)求
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中構(gòu)造MultiPartBody所需要的Part對(duì)象(Headers+body),RequestBody是個(gè)抽象類材彪,里面的所有create方法如下:
可以看出观挎,基本都是重寫了抽象類的RequestBody的三種方法,所以我們也可以繼承實(shí)現(xiàn)自己的Body體:
EG:已上傳相機(jī)圖片(5M)為例段化,分塊多線程異步同時(shí)上傳嘁捷,但是這種方法需要服務(wù)端接口才行。
//文件路徑
String path = "xxx.jpg";
1,文件塊對(duì)象
public static final int FILE_BLOCK_SIZE = 500 * 1024;//500k
/*文件塊描述*/
public static class FileBlock {
public long start;//起始字節(jié)位置
public long end;//結(jié)束字節(jié)位置
public int index;//文件分塊索引
}
2,文件切塊
//計(jì)算切塊,存儲(chǔ)在數(shù)組
final SparseArray<FileBlock> blockArray = splitFile(path, FILE_BLOCK_SIZE);
/**
* 文件分塊
*
* @param filePath 文件路徑
* @param blockSize 塊大小
*
* @return 分塊描述集合 文件不存在時(shí)返回空
*/
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,對(duì)文件塊分塊多線程異步上傳
服務(wù)端的接口:
url:domain/sync/img/upload
method: POST
//請(qǐng)求參數(shù)
data = {
'img_md5': 'dddddsds',
'total': 10, #總的分片數(shù)
'index': 5, #該分片所在的位置, start by 1
}
請(qǐng)求返回值json:
{
'status': 206/205/400/409/500,
'msg': '分片上傳成功/上傳圖片成功/參數(shù)錯(cuò)誤/上傳數(shù)據(jù)重復(fù)/上傳失敗'
'data': { # 205時(shí)有此字段
'img_url': 'https://foo.jpg',
}
}
只需要圖片的md5,總的分片數(shù)穗泵,該分片的位置普气,當(dāng)一塊傳輸成功時(shí)返回206谜疤,當(dāng)全部塊傳完成是返回206佃延,并返回該圖片在服務(wù)器的url
服務(wù)端接口返回解析類:
/**
* 分片上傳部分的接口返回
*
* @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();
//上傳函數(shù)
/**
* 上傳原圖,異步上傳
*
* @param httpCallback 回調(diào)接口
* @param md5 文件md5
* @param path 圖片路徑
* @param total 總塊數(shù)
* @param index 分塊索引
* @param start 分塊開始位置
* @param end 分塊結(jié)束位置
*/
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));//總的分片數(shù)
params.put("index", String.valueOf(index));//當(dāng)前分片位置现诀,從1開始
//全局單例OKHttpClient
OkHttpClient httpClient = DataProvider.getInstance().inkApi.getLongWaitHttpClient();
Runnable httpUploadRunnable = HttpRunnableFactory.newPostFileBlockRunnable(
httpClient,
upload_url,//上傳url,自定義
null,
params,//上傳參數(shù)
"image",
new File(path),//圖片文件
start,//index塊開始的位置
end,//index塊結(jié)束的位置
ChuckUploadData.class,
httpCallback);//回調(diào)函數(shù)
threadManager.submit httpUploadRunnable );
}
/**
* 異步post請(qǐng)求 表單方式拆塊上傳大型文件用履肃,構(gòu)造Runnable
*
* @param httpClient okhttp客戶端
* @param url 請(qǐng)求地址
* @param headers 額外添加的header(通用header由中斷器統(tǒng)一添加)
* @param params 請(qǐng)求參數(shù)
* @param fileKey 文件的接收用key
* @param file 大型文件對(duì)象
* @param seekStart 起始字節(jié)
* @param seekEnd 結(jié)束字節(jié)
* @param cls 返回結(jié)果需要序列化的類型
* @param listener 異步回調(diào)
* @param <T> 返回結(jié)果需要序列化的類型聲明
*
* @return 異步post請(qǐng)求用的默認(rèn)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");
}
//構(gòu)造path文件的index塊的seekStart到seekEnd的請(qǐng)求體requestBody 仔沿,添加到MultiPartBody中
RequestBody requestBody = new RequestBody() {
@Override
public MediaType contentType() {
//請(qǐng)求體的內(nèi)容類型
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;
}
}
//將該塊的字節(jié)內(nèi)容寫入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);
}
};
//組裝其它參數(shù)
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));
}
}
}
//把文件塊的請(qǐng)求體添加到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);
//雖說是同步調(diào)用call.execute(),但是此Http請(qǐng)求過程是在線程池中的尺棋,相當(dāng)于異步調(diào)用
try (Response response = call.execute()) {
if (!response.isSuccessful()){
throw new IOException("Unexpected code " + response.code());
}
/*打印json串封锉,json樣式的*/
String json = response.body().string();
//解析返回的響應(yīng)json
T result = JsonUtils.getObjFromStr(cls, json);
if (listener != null) {
//防止回調(diào)內(nèi)的業(yè)務(wù)邏輯引起二次onFailure回調(diào)
try {
listener.onResponse(call, result);
} catch (Exception e) {
e.printStackTrace();
}
}
} finally {
}
} catch (Exception e) {
if (listener != null) {
//中途取消導(dǎo)致的中斷
if (call != null && call.isCanceled()) {
listener.onCancel(call);
} else {
//其它意義上的請(qǐng)求失敗
listener.onFailure(call, e);
}
}
} finally {
if (listener != null) {
listener.onEnd(call);
}
}
}
};
}
//循環(huán)遍歷所有的文章塊,多線程上傳
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下載文件成福,并通知進(jìn)度
下載文件的原理其實(shí)很簡單,下載過程其實(shí)就是一個(gè)GET過程(上傳文件是POST過程相對(duì)應(yīng))荆残,下載文件需要在異步線程中執(zhí)行(方法有二奴艾,1,使用okhttp的call.enquene()方法異步執(zhí)行内斯,2蕴潦,使用call.excute()同步方法,但是在線程次中執(zhí)行整個(gè)請(qǐng)求過程)俘闯,在成功響應(yīng)之后潭苞,獲得網(wǎng)絡(luò)文件輸入流InputStream,然后循環(huán)讀取輸入流上的文件真朗,寫入文件輸出流此疹。
/**
* @param url 下載連接
* @param saveDir 儲(chǔ)存下載文件的SDCard目錄
* @param params url攜帶參數(shù)
* @param extraHeaders 請(qǐng)求攜帶其他的要求的headers
* @param listener 下載監(jiān)聽
*/
public void download(final String url, final String saveDir,HashMap<String,String> params, HashMap<String,String> extraHeaders,final OnDownloadListener listener) {
//構(gòu)造請(qǐng)求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));//非必須
}
}
}
//構(gòu)造請(qǐng)求request
Request request = new Request.Builder()
.url(urlBuilder.build())
.headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//headers非必須
.get()
.build();
//異步執(zhí)行請(qǐng)求
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;
// 儲(chǔ)存下載文件的目錄
String savePath = isExistDir(saveDir);
try {
//獲取響應(yīng)的字節(jié)流
is = response.body().byteStream();
//文件的總大小
long total = response.body().contentLength();
File file = new File(savePath);
fos = new FileOutputStream(file);
long sum = 0;
//循環(huán)讀取輸入流
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的基本網(wǎng)絡(luò)請(qǐng)求訪問秀菱,發(fā)送GET請(qǐng)求,發(fā)送POST請(qǐng)求蹭睡,基本上傳文件衍菱,切塊多線程異步上傳文件,下載文件就到這里了肩豁,其實(shí)下載文件還可以做成斷點(diǎn)續(xù)傳脊串,獲取每次的seek點(diǎn)