收藏官网首页
机智云 门户 新闻频道 查看内容

扒一扒HTTP的构成

2016-12-15 15:27| 发布者: Genius| 查看: 1470| 评论: 0|来自: http://mrpeak.cn/blog/http-constitution/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

摘要: HTTP全称为HyperText Transfer Protocol,从名字不难看出这是一种基于文本的网络协议,对于初学者来说比较友好,容易上手。各平台上的一些第三方库都对HTTP做了进一步的封装,让HTTP变得更加亲民,但往往拿来就用的 ...

HTTP全称为HyperText Transfer Protocol,从名字不难看出这是一种基于文本的网络协议,对于初学者来说比较友好,容易上手。各平台上的一些第三方库都对HTTP做了进一步的封装,让HTTP变得更加亲民,但往往拿来就用的技术,很容易忽视其背后隐藏的细节。今天一起来扒一扒HTTP到底是如何构成的。

初窥全貌

HTTP第一眼看上去非常简单,先来看看Request部分:

上图主要分为三部分:request line,header和body,中间的CRLF为换行符。如果能将我们平常发送的http请求对应到上述三个部分,就能形成初步的印象了。

我们以一个实际的http request例子,抓包来看一看详细的内部构造。假设我们的请求URL为:

http://www.baidu.com/res/static/thirdparty/connect.jpg?t=1480992153433

后续的分析都是以此请求为基础。

Request Line

Request Line的结构为:

Request-Line   = Method SP Request-URI SP HTTP-Version CRLF

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的文本展示:

GET 空格 /res/static/thirdparty/connect.jpg?t=1480992153.564331 空格 HTTP/1.1 CRLF

header其本质上是一些文本键值对,一个典型的例子如下图所示:

每个键值对的形式为:Key:空格 Value CRLF

上面讲述Request-URI的时候,缺失的Host就以键值对的形式存在于header中,比如,Host: pan.baidu.com。

将若干个上述格式的键值对组合起来,就成了我们HTTP请求的完整header。最后一个键值对之后再跟一个CRLF,就表示我们的header结束了。

HTTP本身定义了一些header key,另外也允许开发者添加自己的key,自定义的key一般以X开头,比如可以定义X-APP-VERSION来记录客户端的版本号。

Body

body里面包含请求的实际数据。

对于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 Response

response的结构和request结构大致相同,可以用下图表示:

不过是将Request Line换成了Status Line。

Status Line的结构如下:

Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

这里关键在于Status-Code的记忆,记住常见的Status-Code值,对于我们平时分析网络错误十分有帮助,不需要记住每个值的含义,只需理解每个类别的含义即可:

1xx: Informational - Request received, continuing process。 2xx: Success - The action was successfully received, understood, and accepted。 3xx: Redirection - Further action must be taken in order to complete the request。 4xx: Client Error - The request contains bad syntax or cannot be fulfilled。 5xx: Server Error - The server failed to fulfill an apparently valid request。

可以用来携带数据的部分

分析至此,我们可以总结一个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码做还原之后,我们得到的是类似这样的结果:

GET /res/static/thirdparty/connect.jpg?a=1&b=2 HTTP/1.1

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中有阐述:

Historically, HTTP has allowed field content with text in the ISO-8859-1 charset [ISO-8859-1], supporting other charsets only through use of [RFC2047] encoding. In practice, most HTTP header field values use only a subset of the US-ASCII charset [USASCII]. Newly defined header fields SHOULD limit their field values to US-ASCII octets. A recipient SHOULD treat other octets in field content (obs-text) as opaque data.

简单来说,我们在实际使用当中使用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:

  • multipart/form-data
  • application/x-www-form-urlencoded
  • application/json

当我们向Server发送数据的时候,需要和Server约定好所使用的Content-Type,客户端在发送Request的时候也要注意API的差别,以AFNetworking为例,发送json则使用:

AFJSONRequestSerializer* jsonSerializer = [AFJSONRequestSerializer serializer];
request = [jsonSerializer requestWithMethod:@"POST" URLString:requestUrl parameters:requestParams error:nil];

发送multipart/form-data:

request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:requestUrl parameters:requestParams constructingBodyWithBlock:nil error:nil];

发送x-www-form-urlencoded:

request = [self.requestSerializer requestWithMethod:@"POST" URLString:requestUrl parameters:requestParams error:nil];

json不用多说,大家都非常熟悉的数据交换格式。multipart/form-data和x-www-form-urlencoded比较容易引起混淆。

在AFNetworking中有这样一段代码:

//AFURLRequestSerialization
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
    

鲜花

握手

雷人

路过

鸡蛋

最新评论

最新文章
一场狂欢 共享单车正引领物联网第一波爆发
一场狂欢 共享单车正引领物联网第一波爆发
2014年共享单车首次出现在北大校园,一群北大青年成立了一家叫OFO的公司,当时在北大
东芝确认7月分拆能源以外业务 防止核电危机扩散
东芝确认7月分拆能源以外业务 防止核电危机
4月25日,东芝发布消息称,7月以后将陆续拆分社会基础设施等4项主要业务。
机智云智能家电运营管理服务平台获艾普兰智能创新奖
机智云智能家电运营管理服务平台获艾普兰智
机智云智能家电运营管理服务平台获艾普兰智能创新奖
国产芯片这里找:2016年中国半导体行业设计/制造/封测十强名单公布! ...
国产芯片这里找:2016年中国半导体行业设计
根据中国半导体行业协会统计,2016年中国集成电路产业销售额达到4335.5亿元,同比增长
谷歌Hangouts推出两项新功能
谷歌Hangouts推出两项新功能
唯一比会议更加讨厌的就是会议筹备。为了简化会议筹备流程,3月10日谷歌在其通讯聊天