我們知道,http 請求分爲三個部分, 請求行、請求頭和請求體;對應的消息也分爲三個部分:響應行、響應頭和響應體。以前使用 HttpURLConnection 時,我們很容易設置消息頭及參數,它內部是封裝了 Socket 供我們使用。補充一點,我們知道網絡運輸層是由 TCP 和 UDP 構成的,TCP 建立連接,安全可靠,以流傳輸數據,沒有大小限制,速度慢;UDP 是不建立連接,每次傳遞數據限制在64k內,數據容易丟失,但是速度快。TCP 和 UDP 都是依據Socket來生效的,而 http 則是建立在 TCP 基礎上產生的。 舉個 HttpURLConnection 的 get 和 pos 請求的例子
private void get(){
try {
//子線程中執行請求
String age = "20", address = "SH";
URL url = new URL("http://baidu.com" + "?age=" + age + "&address=" + address);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
if (connection.getResponseCode() == 200) {
InputStream inputStream = connection.getInputStream();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void post() {
try {
//子線程中執行請求
String age = "20", address = "SH";
URL url = new URL("http://baidu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
String content = "age=" + URLEncoder.encode(age) + "&address=" + URLEncoder.encode(address);//數據編解碼
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");//設置請求頭
connection.setRequestProperty("Content-Length", content.length() + "");
connection.setDoOutput(true);
OutputStream outputStream = connection.getOutputStream();
outputStream.write(content.getBytes());
if (connection.getResponseCode() == 200) {
InputStream inputStream = connection.getInputStream();
}
} catch (Exception e) {
e.printStackTrace();
}
}
通過對比,明顯可以看出get和post請求的設置方式不一樣,由於 HttpURLConnection 封裝的比較好,我們直接設置就行了,接下來看看 OkHttp,OkHttp 是個網絡請求框架,支持異步和同步請求,也支持 get 、post 及壓縮上傳等,舉個栗子
private void okhttp() {
OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String string = response.body().string();
Log.e("onResponse", string);
}
});
}
private void okhttpPost() {
OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
.build();
RequestBody body = new FormBody.Builder()
.add("useName", "老老")
.add("usePwd", "321")
.build();
Request request = new Request.Builder()
.url("https://kp.dftoutiao.com/announcement")
.header("User-Agent", "OkHttp Example")
.addHeader("Accept", "application/json; q=0.5")
.post(body)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
if(response.body() != null){
String responseBody = response.body().string();
Log.i("onResponse"," onResponse " + responseBody);
}
}
}
});
}
private void okhttpPostGizp(String content) {
OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.header("Content-Encoding","gzip")
.build();
return chain.proceed(request);
}
})
.build();
RequestBody requestBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8");
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
gzipSink.writeUtf8(content);
gzipSink.flush();
gzipSink.close();
}
};
Request request = new Request.Builder()
.url("https://test.upstring.cn")
.post(requestBody)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
今天着重看看請求相關的代碼,先看看 Request 這個類
public final class Request {
private final HttpUrl url;
private final String method;
private final Headers headers;
private final RequestBody body;
private final Object tag;
private volatile CacheControl cacheControl; // Lazily initialized.
...
}
用到了 Builder 模式,這個模式適合有大量參數需要設置的bean,提高寫作效率及觀賞性。 HttpUrl 對應的是請求行,即所謂的url;method 對應的是請求格式,比如是 get 還是 post;headers 是消息頭,比如 User-Agent 等;body 是請求體,get方式沒有,它是post的,裏面包含一些請求參數,get方式的請求參數是拼接在url後面;tag 是用來做標識的,根據標識來找到請求的 request,可以取消請求等;cacheControl 是告訴服務端緩存模式,no-cache表示不使用緩存。
先看看 HttpUrl 這個類,它說白了就是對 url 做了詳細的拆分的工具類,對外提供各種細節,具體操作都是java代碼,我直接舉個例子
private static void testHttpUrl() {
HttpUrl parsed = HttpUrl.parse("https://translate.google.cn/?view=home&op=translate&sl=auto&tl=zh-CN&text=老大");
System.out.println("scheme: " + parsed.scheme() + "\n" +
"query: " + parsed.query() + "\n" +
"encodedQuery: " + parsed.encodedQuery() + "\n" +
"host: " + parsed.host() + "\n" +
"port: " + parsed.port() + "\n"
);
}
打印結果是
scheme: https
query: view=home&op=translate&sl=auto&tl=zh-CN&text=老大
encodedQuery: view=home&op=translate&sl=auto&tl=zh-CN&text=%E8%80%81%E5%A4%A7
host: translate.google.cn
port: 443
這裏基本就是我們需要的各種值了。如果我們想在外部添加參數,可以使用 Builder 模式中的 addQueryParameter() 、addEncodedQueryParameter() 方法,區別就是傳入的參數是否已經轉碼,默認會用 URLEncoder.encode(value, "utf-8" ) 來轉碼,防範中文出錯。
請求方式 method 默認是 "GET",如果有傳入 RequestBody 則變爲 "POST",也可以通過暴露的方法設置它的值;
Headers 這個也比較簡單,裏面是個字符串數組對象,用來存儲頭部信息的 key 和 value,它只能是字符串,不能是漢字,如果需要則先 URLEncoder 轉碼。
最後看看 RequestBody 這個類,它是個抽象類,裏面有抽象方法和靜態方法
public abstract class RequestBody {
public abstract MediaType contentType();
public long contentLength() throws IOException {
return -1;
}
public abstract void writeTo(BufferedSink sink) throws IOException;
...
}
這裏面有兩個比較關鍵的類,一個是 MediaType, 一個是 Okio 中的 Sink,Okio 前面幾章講過了,這裏就不多說了,不動Okio的話,OkHttp 基本就很難弄懂了;看看 MediaType 這個類
public final class MediaType {
private final String mediaType;
private final String type;
private final String subtype;
private final String charset;
private MediaType(String mediaType, String type, String subtype, String charset) {
this.mediaType = mediaType;
this.type = type;
this.subtype = subtype;
this.charset = charset;
}
public static MediaType parse(String string) {
Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
if (!typeSubtype.lookingAt()) return null;
String type = typeSubtype.group(1).toLowerCase(Locale.US);
String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
String charset = null;
Matcher parameter = PARAMETER.matcher(string);
for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
parameter.region(s, string.length());
if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
String name = parameter.group(1);
if (name == null || !name.equalsIgnoreCase("charset")) continue;
String charsetParameter = parameter.group(2) != null
? parameter.group(2) // Value is a token.
: parameter.group(3); // Value is a quoted string.
if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
throw new IllegalArgumentException("Multiple different charsets: " + string);
}
charset = charsetParameter;
}
return new MediaType(string, type, subtype, charset);
}
public static MediaType parse(String string) {
Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
if (!typeSubtype.lookingAt()) return null;
String type = typeSubtype.group(1).toLowerCase(Locale.US);
String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
String charset = null;
Matcher parameter = PARAMETER.matcher(string);
for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
parameter.region(s, string.length());
if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
String name = parameter.group(1);
if (name == null || !name.equalsIgnoreCase("charset")) continue;
String charsetParameter = parameter.group(2) != null
? parameter.group(2) // Value is a token.
: parameter.group(3); // Value is a quoted string.
if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
throw new IllegalArgumentException("Multiple different charsets: " + string);
}
charset = charsetParameter;
}
return new MediaType(string, type, subtype, charset);
}
...
}
這個類中有幾個屬性,比較核心的就是 parse() 方法,這裏會把傳進去的值按照正則去切分,分別賦值給幾個屬性,例如
MediaType type = MediaType.parse("application/x-www-form-urlencoded;charset=utf-8"); 其中,它 type : application; subtype : x-www-form-urlencoded; charset : UTF-8; mediaType : application/x-www-form-urlencoded;charset=utf-8。
重新回到 RequestBody 中,發現最終會執行
public static RequestBody create(final 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 MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return byteCount;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.write(content, offset, byteCount);
}
};
}
方法,這個方法中對應也是需要 MediaType 和 Okio 才能理解,我們看看上文中的例子 FormBody ,看看它有什麼特別的。FormBody 中有個內部類 Builder,對外提供的添加參數的方法,也是兩個,一個是直接添加,另一個添加已經編碼過的值,然後通過 Builer 模式創建 FormBody 對象,看看 FormBody 中的三個抽象方法:
private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");
@Override public MediaType contentType() {
return CONTENT_TYPE;
}
@Override public long contentLength() {
return writeOrCountBytes(null, true);
}
@Override public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);
}
contentType() 方法中返回的是靜態對象 CONTENT_TYPE,這個是固定的;另外兩個方法都調用了同一個方法,區別就是參數不一樣
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
long byteCount = 0L;
Buffer buffer;
if (countBytes) {
buffer = new Buffer();
} else {
buffer = sink.buffer();
}
for (int i = 0, size = encodedNames.size(); i < size; i++) {
if (i > 0) buffer.writeByte('&');
buffer.writeUtf8(encodedNames.get(i));
buffer.writeByte('=');
buffer.writeUtf8(encodedValues.get(i));
}
if (countBytes) {
byteCount = buffer.size();
buffer.clear();
}
return byteCount;
}
這裏不得不感慨,OkHttp 的作者真是把 Okio 用到了極致,writeOrCountBytes(null, true) 時,此時創建了 Buffer 對象,然後把參數都添加了進去,重點是它通過 buffer.size() 算出參數的長度,此時是以字節作爲個數的,然後把 Buffer 清空; writeOrCountBytes(sink, false) 中是傳入一個 sink 對象,獲取它內部的 Buffer 對象,把參數添加到 Buffer 中。這裏真的是很巧妙。RequestBody 還有個子類,是 MultipartBody,這個暫不分析。
Cache-Control: no-cache 這個意思是不用緩存,每次都用最新的。