HTTP/1.1应对方案
在HTTP/1.0的基础上,HTTP/1.1又做了一些演化。
CONNECT请求的2xx response不许带body
HTTP/1.1新引入了一种method: CONNECT,用于建立HTTP隧道代理(HTTP tunnel)。
可能有用小知识:
HTTP隧道代理将client和自己之间的TCP连接上的字节流,不做任何修改地转发给自己和server之间的TCP连接;以及同样地,将自己和server之间的连接上的字节流,转发给client和自己之间的连接。
隧道代理一旦建立,就相当于client和server之间建立了TCP连接,只是client的真实ip和端口被代理隐藏了。client和server之后进行通信,可以使用任何基于TCP的协议,比如SSH、FTP、TLS,以及我们正在讨论的HTTP。
如何建立HTTP隧道代理呢?[参见:RFC7231 section-4.3.6] :
- client发送HTTP CONNECT request给代理,request示例:
- 代理收到该request后,解析出client的目标host以及目标port(在本例中,目标host是server.example.com,目标port是80),然后建立自己和目标host:目标port之间的连接。连接建立成功后,向client回复状态码为2XX的response,提示其隧道代理建立成功;
- 之后client和server就可以像代理并不存在一样进行通信了。
整个过程如下图所示(图改自《HTTP权威指南》):
除HTTP/1.0规定的那些不许带body的response外,HTTP/1.1规定CONNECT的状态码为2XX的response,也一定没有body。
让我们看看okhttp是如何implement这一点的。
首先,okhttp仅在如下情况下,会先建立HTTP隧道,然后再进行用户想要的网络请求(代码参见Route#requiresTunnel()):首先,该请求在client和server之间的传输会经过HTTP代理,且
-
该网络请求的目标资源的scheme为
https,即client与server之间需进行由TLS保障的加密通信。这种情况下,通过建立HTTP隧道,在隧道桥接下完成client和server的TLS handshake,最后再进行数据传输,就可以保障client到代理、代理到server之间的数据都是加密传输的。否则的话,client到HTTP代理之间的数据就是明文(cleartext)传输的了(因为client和HTTP代理之间不进行TLS handshake);还有一种方案可以让client到代理、代理到server之间的数据均加密传输,即client和代理之间进行TLS handshake、建立TLS连接,此时代理不再起一个单纯桥接两个TCP连接的作用,而是执行一个普通代理的职能:解析client发来的请求,代替client向server发起请求,并将server的响应转发给client。关于这种方案可行性,在HTTPs proxy server only works in SwitchOmega 这个stackoverflow里有相关讨论。这个讨论里提到的参考的非英语的文章是这篇:HTTP代理原理及实现(一)(发现这篇文章关于代理讲得很好,其实大家就看这篇就行了,似乎完全不用看我写的。。。)无论如何,目前okhttp是不支持设置这种HTTPs代理的。
-
或者,该网络请求将按照HTTP/2 cleartext协议(明文HTTP/2)进行传输。这种情况说明用户知道目标资源所在server支持HTTP/2 cleartext,因此可以放心按照HTTP/2传输请求数据给server,并基于HTTP/2解析server发来的数据。但是HTTP代理是否支持明文HTTP/2,我们却并不知道,很大可能是并不支持的。所以我们选择通过一个HTTP/1.1的CONNECT请求创建隧道代理,之后就可以如HTTP代理不存在一样,使用明文HTTP/2与server进行通信了。
难道每个请求要用啥HTTP协议版本,都要用户提前知道server会使用啥协议版本处理数据吗?并不是如此,这涉及到ALPN。之后应该会有一篇文章分享okhttp是如何为请求选择适合的HTTP协议版本的。
确定需要先通过CONNECT请求建立隧道后,okhttp会发起CONNECT请求,请求中不带body。对于response,okhttp并不根据其状态码来决定如何处理它的body,仅根据response中Content-Length的值,确定body长度,从TCP连接中读完body。
// ConnectPlan.kt
private fun createTunnel(): Request? {
...
val response =
tunnelCodec.readResponseHeaders(false)!!
.request(nextRequest)
.build()
// 根据response里Content-Length,读完response剩余的body部分
tunnelCodec.skipConnectBody(response)
...
}
tunnelCodec.skipConnectBody(response: Response):
// Http1ExchangeCodec.kt
* The response body from a CONNECT should be empty, but if it is not then we should consume it
* before proceeding.
*/
fun skipConnectBody(response: Response) {
val contentLength = response.headersContentLength()
if (contentLength == -1L) return
val body = newFixedLengthSource(contentLength)
body.skipAll(Int.MAX_VALUE, MILLISECONDS)
body.close()
}
这种处理,在HTTP代理发送的是非2XX的response,且有body,同时又选择使用chunked(关于chunked,在Transfer-Encoding章节进行详细介绍)来进行body传输时,会因为reponse没有Content-Length字段,而误判其不带body,使body残存在TCP连接里。
不过我们不必担心这种情形,因为okhttp只会在response状态码是200/407(Proxy Authentication Required)的时候保留TCP连接,用于后续的网络请求/带上Proxy-Authorization头部重试CONNECT请求。而这两种response,都不带body。其余场景下,连接都会被关闭,即使有body残存,也无所谓了。
// ConnectPlan.kt
private fun createTunnel(): Request? {
...
while(true) {
...
when (response.code) {
// 200,隧道代理成功建立
HttpURLConnection.HTTP_OK -> return null
// 407,如果可以得到Proxy-Authoirzation、生成nextRequest(里面主要有一些CONNECT请求要用的header信息,
// 就在当前TCP连接上重试其对应的CONNECT请求
//(但如果发现代理在response中使用Connection header指明了将要关闭连接,就直接返回nextRequest,
// 后面会在新的TCP连接上进行其对应的CONNECT请求),否则抛出异常
HttpURLConnection.HTTP_PROXY_AUTH -> {
nextRequest = route.address.proxyAuthenticator.authenticate(route, response)
?: throw IOException("Failed to authenticate with proxy")
if ("close".equals(response.header("Connection"), ignoreCase = true)) {
return nextRequest
}
}
// 其余状态码,抛出异常
else -> throw IOException("Unexpected response code for CONNECT: ${response.code}")
}
}
// 方法调用方在捕获到异常后,会关闭当前TCP连接
}
状态码1XX被启用
在HTTP/1.0中,就已经提到状态码为1XX的response禁止带body,但是当时并没有定义这类状态码,只是起到占位作用。
在HTTP/1.1中,正式定义了100和101两种状态码。
状态码100与Expect: 100-continue - 减少不必要body传输
HTTP/1.1引入了状态码100(Continue),server用该状态码表明自己根据已经收到的request信息(比如一些request header)确认自己可以接收全部的request并进行处理。client在收到这种response后,就可以继续发送完剩下的request,并等待server发来对request的最终response。
该状态码是与request中的header - Expect结合使用的,client可以在Expect中列出想要提前告知server的信息,供server判断自己是否可以处理整个request。
HTTP/1.1仅定义了一种header值:100-continue(其实到目前为止,也仅这一种)。其含义是告知server,该request会有一个body(一般来说,这个body比较大),如果server:
- 根据当前收到的请求行和header信息,已经可以给出最终的代表成功的2XX response,那可以直接发送该2XX response;
- 根据当前收到的请求行和header信息,确认该次request自己无法正确处理,已经可以给出3XX,4XX,5XX的response(比如根据header中的content-length,发现body太大,自己不愿接收,就可以直接给一个4XX的response),那可以直接发送该response;
- 否则的话,发送状态码为100的临时响应(interim response)。client在收到这个response后可以继续发送当前request剩下的body。
这种机制,可以减少不必要的对body的传输。在这套机制下,client应该完成的工作有:
- 先发送request的headers;
- 读取response的headers:
1)如果发现状态码为100,则继续发送request的body。发送完后,由于根据HTTP/1.1规定,状态码为1XX的response禁止携带body,因此不用担心之前状态码为100的response还有body残存在连接中,可以直接开始从连接中读取server发来的对request的最终response;
2)如果发现状态码为其它(2XX,3XX,4XX,5XX),则不用再发送request的body,当前收到的response就是最终response,直接处理当前response即可。
okhttp implement了这套机制,它支持用户在request的header中添加Expect:100-continue,以保证在必要条件下才发送request的body。okhttp已经做好了对1XX的response的处理,用户侧得到的response就是最终的response。
// CallServerInterceptor.kt
override fun intercept(chain: Interceptor.Chain): Response {
....
try {
//发送request的headers
exchange.writeRequestHeaders(request)
if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
// 用户想使用100-continue机制,在header中带了"Expect: 100-continue",则开始读response
if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
exchange.flushRequest()
// 通过response,判断是否可以继续发request body。若server回复了状态码100,即需继续发送request body的情况,此处会返回一个null;否则,即server回复的状态码为非100的其它1XX,2XX,3XX,4XX,或5XX,此处会返回一个已经填充好了headers的responseBuilder
responseBuilder = exchange.readResponseHeaders(expectContinue = true)
....
}
if (responseBuilder == null) {
// header里不带Expect:100-continue,
// 或者header中有Expect: 100-continue, 且server回复了100,请client继续发送request body,
// 则要继续发送request body
if (requestBody.isDuplex()) {
...
} else {
...
}
} else {
// 否则,说明对于当前的Expect: 100-continue request,server不需要request继续传输body过去
exchange.noRequestBody()
if (!exchange.connection.isMultiplexed) {
// 由于这个TCP连接上有本应发而未发的request body,
// 之后再复用这个连接,有状态管理混乱的风险,
// 因此干脆指定该连接不能再被复用。
// 等到当前response被读取完,连接会进入idle态,达到用户设定的连接复用的keep-alive时间后,连接就会被连接池的cleanup task回收关闭
exchange.noNewExchangesOnConnection()
}
}
} else {
exchange.noRequestBody()
}
...
} catch (e: IOException) {
...
}
try {
if (responseBuilder == null) {
// 对于无request body的,
// 或者有request body但没有Expect: 100-continue的,
// 或者有Expect: 100-continue,同时又收到了状态码100的response
// 此时已经发送完了request,开始读取"最终"response
// 最终加引号是因为此时只能保证不会收到针对Expect: 100-continue的临时响应,但这个response仍有状态码为1XX、是一个临时响应的可能性。
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
...
}
...
// 根据读到的“最终”response的状态行和headers,判断它是否是1XX临时响应
if (shouldIgnoreAndWaitForRealResponse(code, exchange)) {
// 若当前response是1XX临时响应,则下一个response才是真正的最终响应。
// 根据HTTP规范,1XX的response肯定没有body,所以不用读取response body,可以直接开始读取下一个response的header
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
...
}
// 现在response终于是正经最终response了
exchange.responseHeadersEnd(response)
...
}
状态码101与websocket
Transfer-Encoding
stay tuned 写文速度真的太慢了,写半天,完全没进入HTTP/1.1关于message定界的重点。 6月11日补充了100-continue的部分,希望6月14日可以写完全部。