这个系列大概就是想学习一下HTTP相关文档,然后看看okhttp是怎么implement的。至于起源,可能是看文档过程中,激发了自己想去看看okhttp的对应实现的心,也可能是okhttp代码看着看着,就想去看看HTTP文档具体咋说的,两者相互激发。okhttp版本为5.0.0-alpha14。
TCP拆包、粘包
虽然HTTP仅要求传输层可以可靠、有序地传输数据,并非和TCP强绑定,但是其implementation基本都基于TCP。
没用小知识:
根据RFC 7230 section 2.7.1的说法,统一资源定位符(URI)体系里,“http”这个scheme是在语义上就和TCP绑定的,因为这个scheme包含依赖TCP connection的建立来进行client和server的认证的意思。
TCP作为流式(STREAM)传输协议,它并不以应用层委托其传输的消息(message)为单位进行数据传输。具体来说,TCP发送数据时,将应用层委托其传输的放在发送缓冲区的各message对应的字节(octet)们,按协议自己的规则(比如等到要传输的字节数达到一定数量)进行分组,搞成一个个数据分组(segment)进行传输;传到另一端、应用层从TCP读取数据时,不能保证以发送的segment为单位读到数据,更不能保证以发送的message为单位读到数据。因此,我们说,TCP有拆包、粘包现象。
拆包是指接收方应用只读到部分message:
粘包则是指接收方应用把几个message并在一起读出来了:
无论拆包、粘包,对应用来说,它要解决的问题其实只有一个 - message定界(delimit)问题(或者说data framing),即:
应用从TCP读取字节流时,如何判断哪里是一个message的开头,哪里是一个message的结尾。事实上,在RFC文档中,并没有提出拆包、粘包的概念,但是经常可以看到data framing、delimit message之类的描述。
HTTP/1.0的应对方案
参考文档:RFC1945
HTTP message(message是对request和response的统称)有其规定格式:
对于request来说,start-line指请求行(request-line);对于response来说,start-line指状态行(status-line)。
应用对字节流进行解析时,根据换行符(CRLF)确定出message的start-line,以及各个header,接下来的问题就是判断是否有message body,以及如果有的话,那么message body的长度是多少。应用一旦掌握这些,就可以知道message啥时候结束了。
根据RFC 1945 section 7.2 Entity Body,HTTP/1.0对request和response是否有message body分别作了如下规定:
-
对于request, 如果有
Content-Length这个header,那么就有message body, 反之则无。因此,如果一个request有body, 就必须带上Content-Length这个header,指明body的长度; -
对于response,其是否有message body,由它所对应的request的方法(method)以及自身status code决定:
- HEAD方法对应的response一定没有body;
- status code为1xx,204(No Content),304(Not Modified),一定没有body;
- 其余情况,都认为有body,即使body长度可能是0。
Content-Length 这个header的值是以十进制表示的body所包含的字节数。
接下来,我们分析一下,如果遵循这种规定进行implement,那么message的接收方是否都能成功地判断出已经读完了一个message:
- request:client发送request时,若携带了body,必须带上
Content-Length指明body长度,否则就不加入这个header。server接收这个request时,如果发现读取的header里有Content-Length,就读取其指明长度的字节数,作为request的body,否则就认为没有body,读完请求行、header以及最后的空行后就认为完成了对request的读取。可以看到,对于request来说,这些规定让message接收方完成了对message的正确定界; - response: server一定不能在协议规定的没有body的response类型中携带body,对于其余response,最好带上
Content-Length指明body的长度(如果没有body,Content-Length的值就写0)。client接收这个response的时候,会先判断它是不是协议规定的没有body的response,是的话就认为没有body,读完状态行、header以及最后的空行后就认为完成了对response的读取;否则,如果有Content-Length,就读取其指明的字节数作为body;若没有Content-Length这个header,就一直读取直到连接(connection)关闭(连接关闭后,试图读取的一方可以检测到EOF,得知连接已关闭)。由于HTTP/1.0的连接最初设计为一次性的,server在发送完response后会关闭连接,因此可以看到,对response来说,这些规定也可以让message接收方完成对message的正确定界。
不过后来HTTP/1.0在被各方implement的过程中,又引入了Connection: keep-alive机制:
- client可以在request中加入这个header,表明想让server发完response后仍然保留当前连接;
- server收到这个request后,如果支持保留当前连接,则在response中也加入
Connection: keep-alive这个header,并在发送完response后不关闭连接; - client收到response后,如果发现有
Connection: keep-alive这个header,则后续仍然可用这个连接发送request;否则就关闭连接,和server一起完成对这个连接的彻底关闭。
可以看到,在连接被保留的情况下,对于协议规定应该有body的response,如果server再不加入Content-Length指明body长度,那么client就会一直等待(hung request),无法对response正确定界。因此,如果server要决定保留连接,就一定要在response中加入Content-Length。
接下来,让我们看看okhttp是咋implement HTTP/1.0的,很不幸(Lucky~),okhttp不支持HTTP/1.0。为啥这么说呢?
首先,我们看到,okhttp的默认支持协议为HTTP/2和HTTP/1.1.
// OkHttpClient.kt
companion object {
internal val DEFAULT_PROTOCOLS = immutableListOf(HTTP_2, HTTP_1_1)
...
}
另一方面,okhttp在其提供的用户可自定义支持协议的方法OkHttpClinet.Builder#protocols中对用户设置的协议进行了检测,如果发现其中包含HTTP/1.0,就会报错:
// OkHttpClient.kt
fun protocols(protocols: List<Protocol>) =
apply {
// Create a private copy of the list.
val protocolsCopy = protocols.toMutableList()
...
require(Protocol.HTTP_1_0 !in protocolsCopy) {
"protocols must not contain http/1.0: $protocolsCopy"
}
...
}