我们知道,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 这个意思是不用缓存,每次都用最新的。