HTTP全称为HyperText Transfer Protocol,从名字不难看出这是一种基于文本的网络协议,对于初学者来说比较友好,容易上手。各平台上的一些第三方库都对HTTP做了进一步的封装,让HTTP变得更加亲民,但往往拿来就用的技术,很容易忽视其背后隐藏的细节。今天一起来扒一扒HTTP到底是如何构成的。 初窥全貌HTTP第一眼看上去非常简单,先来看看Request部分: 上图主要分为三部分:request line,header和body,中间的CRLF为换行符。如果能将我们平常发送的http请求对应到上述三个部分,就能形成初步的印象了。 我们以一个实际的http request例子,抓包来看一看详细的内部构造。假设我们的请求URL为:
后续的分析都是以此请求为基础。 Request LineRequest Line的结构为:
Method也就是我们平常谈论最多的POST和GET所处的部分(除了POST和GET,还有其他类型的Method)。 SP是个分隔符,我用Wireshark抓包看了下,就一个字节大小,值为0x20,对应ASCII码中的空格。 Request-URI我们就更熟悉了,上述请求对应为:/res/static/thirdparty/connect.jpg?t=1480992153.564331。这里值得注意的一点是:实际传输的时候Request-URI有两种可能的形式,一种是完整的absoluteURI,包含Schema和Host,另一种是abs_path,并没有包含Schema(http)和Host(mrpeak.cn)部分,Host部分被移交到了Header当中。所以平时我们抓包,有时看到的是完整的URI,有时则只有路径信息。 HTTP-Version也很直观,文本展示形式为:HTTP/1.1,代表我们当前使用的版本。 CRLF由两个字节组成。CR值为16进制的0x0D,对应ASCII中的回车键,LF值为0x0A,对应ASCII中的换行键,CRLF合起来就是我们平常所说的\r\n。 所以上述请求的Request-Line的文本展示:
Headerheader其本质上是一些文本键值对,一个典型的例子如下图所示: 每个键值对的形式为:Key:空格 Value CRLF。 上面讲述Request-URI的时候,缺失的Host就以键值对的形式存在于header中,比如,Host: pan.baidu.com。 将若干个上述格式的键值对组合起来,就成了我们HTTP请求的完整header。最后一个键值对之后再跟一个CRLF,就表示我们的header结束了。 HTTP本身定义了一些header key,另外也允许开发者添加自己的key,自定义的key一般以X开头,比如可以定义X-APP-VERSION来记录客户端的版本号。 Bodybody里面包含请求的实际数据。 对于Method=GET的请求来说,body体是为空的,或者说不存在body体,Header最后的两个CRLF就标识着请求的结尾。我们一般调用请求的业务参数是通过Request Line当中的Request-URI来传递的,比如上述请求中的?t=1480992153.564331,也就是URI的query string部分。这部分同样是以键值对的形式存在,不过是位于Request Line当中。 对于Method=POST的请求来说,body体一般不为空,我们实际的业务数据都存放于body当中,数据在body体中是以何种形式存在,其实大有门道,后面再细说。至于Request-URI当中的query string部分,我们依然可以选择放置一部分数据在其中,但更普遍的做法是使用body体。 HTTP Responseresponse的结构和request结构大致相同,可以用下图表示: 不过是将Request Line换成了Status Line。 Status Line的结构如下:
这里关键在于Status-Code的记忆,记住常见的Status-Code值,对于我们平时分析网络错误十分有帮助,不需要记住每个值的含义,只需理解每个类别的含义即可:
可以用来携带数据的部分分析至此,我们可以总结一个http请求,哪些地方是可以用来携带业务数据的。 Request Line当中的Request-URI是一个选择,也是标准的GET请求用来传递数据的位置,一般以query string的格式存在于URI当中。一些浏览器或者Framework对于query string的长度会有一定的限制,所以此处不适宜于传递较大的数据。 Header也是一个选择,我们可以选择协议中的一些标准header key,比如Host,User-Agent等,将我们的业务数据存放其Value中。或者我们通过自定义key,比如上面提到的X-APP-VERSION,使用X-开头是业界默认的习惯,虽然RFC 6648当中建议大家不要再使用X-作为Prefix,但这一习惯今天依旧还在持续。 Body体是我们的第三个选择,POST请求可以根据Header中的Content-Type值,以不同的形式将数据保存在body体中。 一些隐藏的细节可以看出http是一种基于文本解析的协议,上面提到的空格(0x20),换行(0x0D0A)都是HTTP用来做文本解析的辅助符号。 解析HTTP的text流程,其实也比较好理解。一个简化的流程大致是这样:当我们从TCP层拿到应用层的buffer之后,以CLRF(\r\n)为分割符,将整个buffer分成若干行,第一行自然是我们的Request Line,之后每一行代表一个Header,如果连续读到两个CLRF,则表示header结束,如果是Method=POST,读取Header中的Content-Length值,最后根据这个值读取固定长度的body体。这样就完成了我们上述三个主要部分的读取。当然,上述是个简化的流程,实际解析场景会更多一些。 我们再深入看下Request Line的解析 我们从TCP层拿到的实际上是一个字节流,要将字节流解析成我们能够阅读交流的形式,我们需要将字节码进行编码和解码。Request Line使用的编解码格式是US-ASCII,也就是我们平时接触的ASCII码中的一种。 Request Line通过ASCII码做还原之后,我们得到的是类似这样的结果:
URI的解析也自有一套规范,我们需要特别注意的是query string部分。我们平时编写业务代码的时候,可能会在query string当中塞入自己的数据,这些数据可能是任意形式的字节流,而Request Line和URI的解析都依赖于一些特殊字符来做分割,比如空格,/,?等等,所以为了能正确,安全的解析整个Request Line和URI,我们需要对query string中的字节流做进一步的编码约束,只允许其中出现安全的ASCII码,这也是我们为什么需要UrlEncode的原因。 UrlEncode的过程也比较简单,它将字节流中的所有字节,对照ASCII码表分为,安全的ASCII码和不安全的ASCII码。安全的ASCII码不用做任何处理,不安全的ASCII码(比如空格0x20)则做进一步的编码处理,编码的思路也简单:用安全的ASCII码来代替不安全的ASCII码。比如空格(0x20)被编码成%20,由一个ASCII码(空格)变成了三个ASCII码(%,2,0)。对于原本就不是ASCII码的内容来说,比如中文,则先以UTF-8编码成字节流,再对照ASCII码做编码。比如中文字「高」,其UTF-8的表现形式为:\xE9\xAB\x98,再进一步做ASCII编码,最后UrlEncode的结果就为:%E9%AB%98。 由此可见,UrlEncode是出于URL安全解析的需要,Encode的结果是由%和一部分安全的ASCII码所组成。UrlEncode的缺点也比较明显,Encode非ASCII码的时候(比如中文),一个字节会被encode成3个字节,长度整整是原先的3倍,造成流量的浪费。 我见过有人使用base64来对query string做encode,这是把概念搞混淆了,至少base64 encode之后的=就不是一个URL安全的字符,=在UrlEncode之后对应%3d。 Header的解析 对于Header的解析可以先按CRLF分割成一个个的键值对,键值对里面的值,也就是我们所说的field content其实也有编码要求。RFC 7230中有阐述:
简单来说,我们在实际使用当中使用ASCII码来限制field content。我们常用几个Field,诸如Host,User-Agent等,使用ASCII码字符也已绰绰有余,一般不会对值做进一步的encode处理。 Body的解析 body的解析是我们平时打交道最多的部分,不是说我们需要知道如何去解析body,而是要了解body体里的数据格式。 body的解析本身比较简单,从header中知道Content-Length之后,读取固定长度的字节流即完成了body的获取,关键的环节是获取之后,如何读取其中的数据并递交给应用层,所以HTTP协议本身并没有对Body中的内容编码做约束,而是把它交给协议的使用者去决定,我们甚至可以在body体里存放二进制流,对应的Content-Type为application/octet-stream。 我们来看看平时发送HTTP请求时,以AFNetworking为例,使用最频繁的几种Content-Type:
当我们向Server发送数据的时候,需要和Server约定好所使用的Content-Type,客户端在发送Request的时候也要注意API的差别,以AFNetworking为例,发送json则使用:
发送multipart/form-data:
发送x-www-form-urlencoded:
json不用多说,大家都非常熟悉的数据交换格式。multipart/form-data和x-www-form-urlencoded比较容易引起混淆。 在AFNetworking中有这样一段代码:
最新评论最新文章
版权与免责声明 © 2006-2024 Gizwits IoT Technology Co., Ltd. ( 粤ICP备11090211号 ) |