Okhttp解析(3.10.0)
Okhttp(3.10.0)
目录
请求体数据结构
简介
-
SSL与TLS
SSL — Secure Sockets Layer(安全套接层)
TLS — Transport Layer Security(传输层安全协议)
SSL 协议,为了解决HTTP协议是明文,存在很多缺点——比如传输内容会被偷窥(嗅探)和篡改的问题。到了1999年,SSL 因为应用广泛,已经成为互联网上的事实标准。IETF 就在那年把 SSL 标准化。标准化之后的名称改为 TLS
-
URL
URL — uniform resource locator
主要类
类图关系:
-
OkhttpClient
Call的工厂类。应用中应该只使用一个OkhttpClient对象。
当使用单例时,由于每个client拥有独立的连接池和线程池,Okhttp可以重用所有的HTTP请求。
重用连接池和线程池可以减少延迟和节省内存。
可以使用构造器实例化
OkHttpClient client = new OkHttpClient()
,或者使用Builder方式构建OkHttpClient client = new OkHttpClient.Builder().build()
可以通过该类对象设置连接参数,如:读超时,写超时,重试,拦截器…
-
Request
请求封装实体,包含url,方法(post,get…),请求头,请求体等结构
-
Response
返回结果封装,包含请求,协议(http1.0,http1.1,http2,spdy3,h2c…),返回码(200,404…),返回体(字符串),握手信息(tls版本,证书信息),返回头,返回体(字节流),网络响应结果,缓存响应结果,预先响应结果,发送时间戳,接收时间戳
-
RequestBody
请求体封装抽象类,三个方法
public abstract @Nullable MediaType contentType(); public long contentLength() throws IOException { return -1; } //写入流中 public abstract void writeTo(BufferedSink sink) throws IOException;
其内置实现类有:
-
FormBody – 表单
MediaType为
application/x-www-form-urlencoded
,内部使用两个字符串集合记录表单的name和value:private final List<String> encodedNames; private final List<String> encodedValues;
写入的实现
private long writeOrCountBytes(@Nullable 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; }
以utf-8的编码形式写入,最后的结构为
name1=value1&name2=value2&name3=value3
-
MultipartBody – 文件
内部变量
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//请求的MediaType private final List<Part> parts;//内部Headers和RequestBody
写入的实现:
private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) throws IOException { long byteCount = 0L; Buffer byteCountBuffer = null; if (countBytes) { sink = byteCountBuffer = new Buffer(); } for (int p = 0, partCount = parts.size(); p < partCount; p++) { Part part = parts.get(p); Headers headers = part.headers; RequestBody body = part.body; sink.write(DASHDASH); sink.write(boundary); sink.write(CRLF); 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); } } MediaType contentType = body.contentType(); if (contentType != null) { sink.writeUtf8("Content-Type: ") .writeUtf8(contentType.toString()) .write(CRLF); } 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); 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; }
如果是表单+文件形式,最后结构为:
boundary的构建为UUID.randomUUID()
--`boundary` \r\n part1HeaderName1: part1HeaderValue1 \r\n part1HeaderName2: part1HeaderValue2 \r\n Content-type: part1Body.contentType() \r\n Content-Length: part1Body.contentLenth() \r\n name1=value1&name2=value2&name3=value3 --- 写入body,此处以表单为例 --`boundary` \r\n part2HeaderName1: part2HeaderValue1 \r\n part2HeaderName2: part2HeaderValue2 \r\n Content-type: part1Body.contentType() \r\n ---此处contentLenth为-1,省略 source = Okio.source(file);sink.writeAll(source); ---写入文件流 \r\n --`boundary`-- \r\n ---结束
-
内部通过静态方法使用byte[],File或ByteString创建的匿名类
将byte[],File,ByteString写入流中
-
-
ResponseBody
返回体封装,三个方法
public abstract @Nullable MediaType contentType(); public abstract long contentLength(); public abstract BufferedSource source();
实现类:
- CacheResponseBody
- RealResonseBody
- 内部通过静态方法使用BufferedSource创建的匿名类
-
Call — interface
请求操作,包括执行,加入队列,取消等
实现类:RealCall
-
Callback – interface
异步结果回调
// 由于取消,连接问题,超时等请求失败回调;由于网络原因,服务器可能接收了请求 void onFailure(Call call, IOException e); // Http请求成功应答返回。传输层的成功不代表应用层的成功,仍可能会是404或500返回码 void onResponse(Call call, Response response) throws IOException;
-
EventListener
Http请求事件监听,包括数量,大小和用时等
3.10中由于主体API的修改,关于这个类的实现没有完成,会在3.11或3.12完善
每个开始/连接/接收事件都会接收到匹配的事件回调方法,无论是成功或是失败,包含下列主要事件方法对
事件 方法(对) 说明 call callStart/End/Failed 每个Call只会回调一次start dns dnsStart/dnsEnd 多次回调(不同主机的重定向结果)或不回调(连接池复用的Call) connect connetctStart/End/Failed/Acquired/Released 连接事件 secureConnect secureConnectStart/End 使用TLS时调用 requestHeader requestHeaderStart/End 发送请求头前/后 requestBody requestBodyStart/End 发送请求体前/后 responseHeader responseHeaderStart/End 接收返回头前/后 responseBody responseBodyStart/End 接收返回体前/后 总的事件流程:
call —> (dns –> connect –> secure connect) –> request
请求事件流程:
requestHeader —> requestBody —> responseHeaders —> responseBody
-
Handshake
记录TLS握手信息。
内部成员:
private final TlsVersion tlsVersion; // 加密方式 private final CipherSuite cipherSuite; // 远程服务端证书 private final List<Certificate> peerCertificates; // 本地客户端证书 private final List<Certificate> localCertificates;
枚举类TlsVersion的值
版本 值(java name) 年份 TLS_1_3 TLSv1.3 2016 TLS_1_2 TLSv1.2 2008 TLS_1_1 TLSv1.1 2006 TLS_1_0 TLSv1.0 1999 SSL_3_0 SSLv3 1996 -
Dispatcher
异步执行策略,内部包括一个线程池,几个请求队列(准备执行,正在执行-同步,正在执行-异步)
其中:默认线程池大小为5,最大请求数64(同步执行队列大于该值时,会将请求放入准备队列中)
每个OkHttpClient有独立的Dispatcher,每个Dispatcher有独立的线程池
-
Headers
Http请求和返回的头部信息,String[]数组结构
{“headerName1”,”headerValue1”,”headerName2”,”headerValue2”}
-
HttpUrl
Url封装类,用于构建,分解网络地址
google搜索
HttpUrl url = new HttpUrl.Builder() .scheme("https") .host("www.google.com") .addPathSegment("search") .addQueryParameter("q", "polar bears") .build(); // 打印 // https://www.google.com/search?q=polar%20bears
URL组成
final String scheme; private final String username; private final String password; final String host; final int port; private final List<String> pathSegments; private final @Nullable List<String> queryNamesAndValues; private final @Nullable String fragment; private final String url;
名称 含义 Scheme 取决于协议,决定如何接收数据,多种mailto,file,ftp,此类只支持http/https username/password 成对出现或省略,http使用其他机制实现认证 Host 如:square.com,localhost,IPv4 192.168.0.1,IPv6 ::1 Port 默认http - 80,https - 443 Path 资源位置,“/square/okhttp/issues/1486”会被解构成[“square”, “okhttp”,”issues”, “1486”] Query 可选,通常会是name-value参数集合 Fragment 可选,并且不会发送给服务器,仅客户端私有 Encoding 每个部分在放入URL之前要完成编码(UrlEndcode),如:cute #puppies –> cute%20%23puppies Percent encoding 使用UTF-8 16进制字节替换字符,适用于空格,Control,无ASCII字符和在上下文中其他含义的字符 IDNA Mapping and Punycode encoding hostname使用不同的scheme编码 注意:
- web服务使用多个特征:IP地址,域名,或localhost。每个服务的名字是不同的,无法交换的。
-
Percent encoding会应用于除hostname以外的所有地方,但是每个部分对应的编码字符集不一样,如path部分需要去除所有的
?
字符来表示query的开始,但是在query和Fragment中的?
需要保留HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder() .addPathSegment("_Who?_") .query("_Who?_") .fragment("_Who?_") .build(); // 打印 // http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_
- 为了避免混淆和钓鱼攻击,”http://www.unicode.org/reports/tr46/#ToASCII“使用IDNA mapping变换名称来避免混淆字符
- 由于Java中的URL类使用IP地址判断两个url是否相同,如
http://square.github.io/
和http://google.github.io/
会被认为相同,使用equals方法会得到true;由此会引发一个问题:触发DNS过程降低效率,不同的Url由于托管方式而造成的相同引发的错误 - java中Uri对于
http://host:80/
和http://host
的结果是不相等的,
-
Interceptor – interface
请求拦截器。操作请求和结果,如添加,移除,变换请求/返回头
每个请求,OkHttp会预制(在自定义之前)几个拦截器,包括缓存,重试,连接,解析的实现
如:CacheInterceptor,ConnectInterceptor,CallServerInterceptor,RetryAndFollowUpInterceptor等
-
ConnectionPool
管理重用HTTP,HTTP/2中的连接来减少网络延时。使用相同地址的HTTP也会使用相同的Connection。该类决定了保留哪些连接并用于将来重用的策略
-
Connection – interface
HTTP,HTTP/2或者HTTP + HTTP/2 连接中的Socket和流。可用于多个HTTP的请求/相应。连接可以直接连接服务器或者通过代理
方法:
Route route(); // HTTP -- Socket,HTTPS -- SSLSocket,如果是HTTP/2连接,可能会多个Call共享 Socket socket(); @Nullable Handshake handshake(); Protocol protocol();
实现类:RealConnection
-
Protocol – enum
支持一下协议:
- HTTP_1_0(“http/1.0”)
- HTTP_1_1(“http/1.1”)
- SPDY_3(“spdy/3.1”)
- HTTP_2(“h2”)
- H2C(“h2c”)
- QUIC(“quic”)
-
WebSocket – interface
非阻塞式web socket,通过WebSocket.Factory创建实例
声明周期:
-
Connection
每个web socket的初始状态。可以添加消息,但是直到
Open
时才会发送 -
Open
web socket已被远程接受,可以执行所有操作。将会把消息加入队列
-
Closing
web socket任何一方终止了连接。web socket会继续提交队列中的消息,但会拒绝向队列中添加新的消息
-
Closed
web socket已经提交所有已存在消息,并接收所有消息
-
Canceld
web socket连接失败。加入队列中的消息可能不会被对方收到。
可能由于HTTP升级问题,连接问题等
注意:
连接状态依赖于连接的双方。如果是优雅的关闭状态,标明发送了所以存在信息和接收了所有到来的信息,但不保证另一方接收了信息
几个方法:
// 初始化该web socket的请求实例 Request request(); long queueSize(); // UTF-8编码 type 0x1 boolean send(String text); // bytes type 0x2 boolean send(ByteString bytes); // code --- http://tools.ietf.org/html/rfc6455#section-7.4 boolean close(int code, @Nullable String reason); // 无视消息,直接断开 void cancel();
-
流程分析
构建
OkHttpClient的构建可以通过无参构建器(默认配置参数)或者使用OkHttpClient.Builder构建。
内部包括如下参数:
-
协议
默认支持HTTP_2,HTTP_1_1,具体参见
-
对应的Dispatcher
请求队列,线程池,并发数,线程数
-
连接配置 - ConnectionSpec
默认为标准Tls和不加密、不认证模式,分别对应ConnectionSpec.MODERN_TLS和ConnectionSpec.CLEARTEXT
- 事件回调EventListener
- 代理设置
- Cookie设置
- SocketFactory
- 证书
- 连接池
- dns设置
- 重定向与重连
- 超时,心跳等
通过上述配置后,即可通过newCall()方法获取Call对应,并通过Call的execute()/enqueue()方法执行请求
请求
上面创建的Call对象实际是RealCall类型,使用RealCall的静态方法newRealCall()创建。
这里以同步方法execute方法为例,进行分析,下面是RealCall的execute方法
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
这里可以看到EventListener的相关回调,不过这里的关键代码在于client.dispatcher().executed(this)
和Response result = getResponseWithInterceptorChain()
,分别是将call加入到同步队列中,通过预制和用户自定的Intercepltor获取请求结果。重点看下Interceptor是怎么获取结果的,也就是getResponseWithInterceptorChain这个方法
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
在拦截器这个集合中,除开用户添加的(client.interceptors()),OkHttp如添加了几项:
- RetryAndFollowUpInterceptor – 重试
- BridgeInterceptor – 对OkHttp中的Request和Response与网络代码的转换
- CacheInterceptor – 缓存读取和相应的写入(DiskLruCache)
- ConnectInterceptor – 创建连接
- networkInterceptors –
- CallServerInterceptor – 与服务端交互
通过流程,一一进行解析。首先进入RealInterceptroChain的proceed()方法中,内部直接调用重载的proceed()方法,直接看重载的方法
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {
throw new IllegalStateException(
"interceptor " + interceptor + " returned a response with no body");
}
return response;
}
其实上述代码中开头和结尾是一些校验工作,可以直接看中间部分代码:
- 使用同一套参数创建了新的RealInterceptorChain对象,为其指定了新的Interceptor
- 每个Interceptor的intercept方法传入的是下一个Chain对象,并限制其调用proceed()方法,完成整个链式调用
共用的参数为RealInterceptorChain的所有成员变量(不包括calls,用于判断是否调用proceed方法),介绍几个:
-
HttpCodec
用于HTTP请求的编码和返回的解码,实现类有Http1Codec和HTTP2Codec
-
RealConnection
TODO
用于连接和数据接收,内部包含socket(应用层,可能为SSLSocket或rawScoket),rawSocket(tcp),握手信息,输入输出流
-
StreamAllocation
该类协调了三个类型的关系
- Connections:连接远程服务的物理Socket。由于建立过程比较慢,需要能够断开已经建立连接的连接
- Streams:HTTP 请求/响应应该成对出现在连接中。对于每个连接可携有的并发流,每个连接有独立的分配限制。HTTP/1.x只能携带一个流,而HTTP/2可以携带多个
- Calls:流的顺序由请求决定。希望将相同连接中的所有流放在同一个Call中。
代表call,在一个或多个连接上使用一个或多个流,并提供API来释放资源
支持同步取消。如果HTTP/2流存活,会取消对应的流而不会取消共享这个连接的其他流。但是如果正在进行TLS握手,取消会破坏整个连接
整理下RealConnection和Interceptor的关系
每个RealConnection对应一个Interceptor,方法调用顺序为 RealConnection.proceed() --> Interceptor.intercept(nextRealConnection) --> nextRealConnection.proceed()
,最后一个CallServerInterceptor为调用proceed()方法,并返回结果
拦截器
-
RetryAndFollowUpInterceptor – 重试与跟进
使用client的connectionPool初始化StreamAllocation,并传递给后续的Interceptor
-
BridgeInterceptor – 请求解析
- 补充请求头信息,如Host,Accept-Encoding,Cookie等;
- 保存cookie信息
- 处理
gzip
模式的返回头
-
ConnectInterceptor – 连接
建立连接,并将流交由HttpCodec,实现后续的数据传输与解析
@Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); }
-
通过StreamAllocation创建(复用)RealConnection – 并完成连接状态
- 如果复用,直接返回connection
- 如果创建,需先进行TCP + TLS握手(阻塞)
-
通过StreamAllocation创建HttpCodec
这里分为Http1Codec和Http2Codec
- Http1Codec – 通过输入输出流创建
- Http2Codec – 通过RealConnection创建
- 将创建好的RealConnection,HttpCodec传递给后续的Interceptor
-
-
CallServerInterceptor – 解析
由ConnectInterceptor建立连接后,开始http请求的写入与结果读取,截取部分代码
// 写入请求头信息 httpCodec.writeRequestHeaders(request); // 如果不是GET,或者HEAD,写入请求体 CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); // 完成请求写入 httpCodec.finishRequest(); // 读取相应头信息,相应码,协议,响应体(message) responseBuilder = httpCodec.readResponseHeaders(false); // 赋值body属性 response = response.newBuilder() .body(httpCodec.openResponseBody(response)) .build(); // 返回结果 return response;
上一篇: 【考研每日一题35】点菜问题(C++)
下一篇: Python之文件处理