浅谈Http
简介
Http(HyperText transfer protocol 超文本传输协议),是一种用于分布式、协作式和超媒体信息系统的应用层协议。
超文本
Tim Berners-Lee 在1989年发明Http的时候,就是为传输HTML文档的,而HTML最初就是带有超链接的文本文件,所以叫做超文本。后面又慢慢发展支持了更多的文件类型,如音视频、图片等。
传输协议
虽然Http名字中带有“传输”两个字,但是它并不是传输层协议,而是应用层协议,它的底层可以是任意的可靠传输层协议,目前一般都是TCP。
既然是一种协议,就会规定数据的传输格式,这些格式或规则的定义可以在IETF(Internet Engineering Task Force 互联网工程任务组)官网找到。其中比较新的版本为1999年公布的Http1.1 和2015年公布 Http2.0
分布式
Http协议的服务端可以部署到任意多台主机上,来提供无差别服务。客户端在发起请求时,不需要也不关心当前提供服务的是哪一台主机。
协作式
在Http协议中,服务端的资源通过URI(统一资源定位符)来标识,各个客户端均可以通过Http请求来获取或修改这些资源,从而体现出了资源可交互使用的协作性质。
无状态
Http协议本身不会对请求和响应之间的通信状态做保存,简单来说两次请求是毫无关系的。为了实现客户端的状态保存,才出现了Cookie技术。
无连接
在一次请求响应过程结束后,会将传输层的连接断开。等下次发送请求时再重新建立连接。
Http报文格式
Http的通讯标准是基于请求和应答的。发出请求的一端可以是网页浏览器、爬虫或其他工具,叫做用户代理程序(user agent)。应答的服务端上存储着一些资源,如HTML文件或图片,叫做源服务器(orgin server)。
下面对请求和应答的数据格式做简单说明。
请求
Http请求的数据包格式:
<method> <request url> <version>\r\n <---起始行
<header>\r\n
... <---请求头
<header>\r\n
\r\n
<body> <---请求体
报文的起始行:说明请求类型、资源地址、客户端期望使用的Http版本
报文的请求头:包含零个或多个首都字段,每个首部字段包含一个名字和一个值,例如“Host:www.baidu.com”,两者之间用冒号分割。
报文的请求体:承载具体传输内容,该部分可选。
注:每一行均以回车换行(\r\n)结尾,请求首部与请求体之间用回车换行分割
示例:
POST https://x.xxx.com/login http/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 37
Host: x.xxx.com
Connection: Keep-Alive
username=13456778997&password=123456
前面说到报文的请求头和请求体都是可选的,但是Http1.1版本中,Host字段是必须的。
请求方法(动作)
Http的请求方法除了常用的Get和Post之外,还有其他6种。详情见下表:
请求方法 | 说明 |
---|---|
GET | 向指定资源发出“显示”请求,即读取数据。 Get请求应当是幂等性的,原因是一个支持Get请求的资源可能会被网络蜘蛛等随意访问。 |
POST | 向指定资源提交数据,数据被包含在请求体中,即新增或修改数据。 |
PUT | 向指定资源位置上传最新数据,即新增数据。 |
DELETE | 请求删除指定URI位置的资源。 |
HEAD | 与Get方法一样,都是请求“显示”指定资源,只不过服务端不返回资源本身, 而是数据的元信息。 |
TRACE | 回显服务器收到的请求,用于测试或诊断 |
OPTIONS | 使服务器回传该资源所支持的所有Http方式。 可用*来代替资源名称,可以测试服务器功能是否正常工作。 |
CONNECT | Http1.1协议中预留给能够将连接改为隧道方式的代理服务器。 通常用于SSL加密通信经由非加密的Http代理服务器时。 |
一个Http服务器应当至少实现GET和HEAD方法,其他方法都是可选的。
除了上述方法,Http服务器还可以自定义方法,例如PATCH,用于局域修改服务器资源。
应答
<version> <status> <reason-phrase>\r\n <---起始行
<header>\r\n
... <---响应头
<header>\r\n
\r\n
<body> <---响应体
起始行:服务器期望使用的Http版本,响应状态码,原因短语
响应头和响应体与Http请求中的作用一致。
示例:
200 OK https://x.xxx.com/login
Content-Type: application/json; charset=UTF-8
Date: Thu, 29 Nov 2018 08:17:10 GMT
{ok}
状态响应码是由三个数字组成的,第一位代表当前的响应分类,后两位代表具体的响应类型。下面具体说明状态响应码的分类。
应答码
大类型 | 子类型 | 说明 |
---|---|---|
1xx消息 | 临时响应,表明请求已被服务器接收,继续处理。1xx应答码在Http1.1定义,所以不得向Http1.0客户端发送。 | |
100 | 客户端应继续发送请求的剩余部分 | |
101 | 协议升级。服务器通过升级消息头字段了解并愿意遵守客户端的请求,以更改此连接上使用的应用程序协议。如Http升级为WebSocket。 | |
2xx成功 | 请求已被服务器成功接收、理解并处理 | |
200 | 请求成功。响应返回的消息取决于请求中使用的方法 | |
201 | 请求已完成,并导致创建新资源。新创建的资源可由响应实体中返回的URI引用。 | |
202 | 请求已被接收,但仍未完成 | |
203 | 非权威信息。表示响应内容经过代理的修改,与原服务器的内容不一致。 | |
204 | 无内容,服务器已完成请求但不需要返回实体 | |
205 | 重置内容。服务器已完成请求,用户代理程序应重置输入表单,方便用户下次的输入。 | |
206 | 服务器已完成对资源的部分Get请求,通过Content-Range响应头标识内容的起始位置。 | |
3xx重定向 | 需要后续操作才能完成这一请求 | |
300 | 多种选择。客户端的请求指向了一组资源,服务器通过该应答码返回资源选项列表供客户端选择。 | |
301 | 永久移动。客户端请求的资源被移动了,并生成了新的URI,服务器通过该应答码将新的URI返回给客户端。 | |
302 | 找到。与301类似,只不过资源是被临时移动了,服务器通过Location响应头告诉客户端新地址。 | |
303 | 参见其他。类似302 | |
304 | 未修改。表明客户端请求的资源未被修改过,可以使用客户端的缓存。 | |
305 | 使用代理。必须使用代理访问资源,代理的地址在Location中 | |
306 | 未使用。此状态码用于规范先前版本,不再使用,代码保留 | |
307 | 临时重定向。与302类似 | |
4xx客户端请求错误 | 请求含有此法错误或者无法被执行 | |
400 | 错误请求。客户端请求语法格式错误,服务器无法理解。 | |
401 | 未经授权。要求客户端提供授权信息或因授权信息有误而拒绝授权。响应必须包含WWW-Authenticate响应头。 | |
402 | 付款要求。代码保留将来使用 | |
403 | 服务器理解请求,但拒绝执行。授权无效,请求不应重复。 | |
404 | 未找到。服务器未找到与Request-URI匹配的任何内容。 | |
405 | 不允许的方法。请求行中指定的方法不允许,响应中必须包含一个Allow响应头,其中包含所请求资源的有效列表。 | |
406 | 不可接受。 | |
407 | 需要代理身份验证。类似于401,但表示客户端必须首先使用代理进行身份验证。 | |
408 | 请求超时。客户端在服务器准备等待的时间内没有产生请求。 | |
409 | 冲突。与当前资源状态冲突,无法完成请求 | |
410 | 消失了。请求的资源已经不可用,并且不知道转发地址。 | |
411 | 需要指定长度。服务器拒绝接受没有定义Content-Length的请求。 | |
412 | 先决条件失败。 | |
413 | 请求实体太大。因为请求实体大于服务器愿意或能够处理的长度,而拒绝处理请求。 | |
414 | Request-URI太长。请求的URI长度太长。 | |
415 | 不支持的媒体类型 | |
416 | 请求的范围不满足。如果请求包含Range请求头,但其范围超出了资源的实际大小。 | |
417 | 无法满足期望。此服务器无法满足Expect请求头中给出的期望。 | |
5xx服务器错误 | 服务器在处理请求时发生错误 | |
500 | 服务器内部错误。服务器遇到意外情况,无法满足请求。 | |
501 | 未实现。服务器暂未实现客户端请求的方法。 | |
502 | 错误网关。代理使用的服务器遇到了上游的无效响应 | |
503 | 服务不可用。由于服务器临时过载或维护,无法处理当前请求。如果预置故障时间,可在响应头中添加Retry-After字段。 | |
504 | 网关超时。与状态吗408类似, 但是响应来自网关或代理,此网关或代理在等待另一台服务器的响应时出现了超时 | |
505 | 不支持的Http版本。服务器不支持或拒绝支持请求消息中使用的Http协议版本。 |
比较常用的请求头和响应头
Http定义了40多个请求头字段,包含请求专用、响应专用和共用三个类别。下面列出比较常用的头部字段。
请求头部专用
首部名称 | 含义 | 说明 |
---|---|---|
User-Agent | 包含了用户代理程序发起请求的信息。 用于统计、协议违规追踪、用户自动识别。 此字段是建议被添加到请求头中的。 | User-Agent中的内容一般为标识应用程序的软件名称和版本,如Apache/0.8.4。多个标识按照优先级排列,以空格分隔。 浏览器客户端的User-Agent样式如下: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 移动客户端的User-Agent样式如下: Near/4.1.0 (channel; language; uniqueid; build/buildcode) Dalvik/2.1.0 (Linux; U; Android 8.1.0; OE106 Build/OPM1.171019.026) 后半部分由System.getProperty("http.agent”)获取。 |
Host | 指明所请求资源所在主机名和端口号, 如果未指明端口号,则为Http服务默认80端口。 | 在Http1.1版本,此首部必须添加,因为同一个主机地址可能会绑定多个域名。 同时,Host中的值也必须TLS证书中的服务器域名列表匹配,否则在TLS握手会发生域名验证错误,导致建立安全连接失败。 |
Range | 仅请求某个实体的一部分。字节偏移以0开始。 | Range: bytes=500-999 |
Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: Basic <Base64编码的(Acount:passwd)> |
Accept | 能够接受的回应内容类型(Content-Types) | Accept: text/plain |
响应头部专用
首部名称 | 含义 | 说明 |
---|---|---|
Server | 服务器的名字 | Server: Apache/2.4.1 (Unix) |
Transfer-Encoding | 用来将实体安全地传输给用户的编码形式。 当前定义的方法包括:分块(chunked)、compress、deflate、gzip和identity。 | Transfer-Encoding: chunked |
Content-Range | 这条部分消息是属于某条完整消息的哪个部分 | Content-Range: bytes 21010-47021/47022 |
Location | 用来 进行重定向,或者在创建了某个新资源时使用。 | Location: http://www.w3.org/pub/WWW/People.html |
Refresh | 用于设定可定时的重定向跳转。 右边例子设定了5秒后跳转至“http://www.w3.org/pub/WWW/People.html”。 | Refresh: 5; url=http://www.w3.org/pub/WWW/People.html |
WWW-Authenticate | 表明在请求获取这个实体时应当使用的认证模式。 | WWW-Authenticate: Basic |
通用头部
首部名称 | 含义 | 说明 |
---|---|---|
Connection | 表示发送者想要指定特定的连接类型 | 该头部是在Http1.1中添加的,其取值可以是keep-alive;close。 在Http1.1版本中持久连接默认是开启的,如果要关闭该特性需显示添加请求头Connection:close。 而在Http1.0版本中默认是短连接,如果要开启该特性,需要客户端和服务端使用Connection:keep-alive头部进行协商。 |
Content-Type | 指明消息体的内容类型 | 格式为:<类型>/<子类型>; name=value… 例如:application/json; charset=UTF-8 |
Content-Length | 指明消息体长度,8字节长度 | Content-Length: 348 |
Content-Encoding | 指明消息体的编码方式 | 其取值可以为gzip/compress/deflate |
Upgrade | 要求服务器/客户端升级到另一个协议。 | Upgrade: HTTP/2.0, WebSocket |
非标准头部
首部名称 | 含义 | 说明 |
---|---|---|
Set-Cookie | 在响应头中,服务端用来将Cookie信息下发到客户端 | 每个Cookie由一个键值对开始,后面跟0个或多个属性值对。 例如:Set-Cookie: session=e7501e9f-a0a0-4077-9d1d-99c23608fdf2; Domain=.xxx.com; expires=Sun, 04 Jan 1970 00:00:00 GMT; Max-Age=259200; Path=/ |
Cookie | 在请求头中,客户端用来上送服务端下发的Cookie信息。 | Cookie/Set-Cookie头部并不是标准头部,而是由网景公司1993年在标准上扩展来的。 |
Origin | 发起一个针对 跨来源资源共享 的请求(要求服务器在回应中加入一个‘访问控制-允许来源’(‘Access-Control-Allow-Origin’)字段)。 | Origin: http://www.example-social-network.com |
Access-Control-Allow-Origin | 指定哪些网站可参与到跨来源资源共享过程中 | Access-Control-Allow-Origin: * |
Http历史以及版本差异
Http0.9
1991年发布的第一个版本,目前已过时。只接受GET一种请求方法,没有在通讯中指定版本号,且不支持请求头。由于该版本不支持POST方法,因此客户端无法向服务器传递太多信息,只能是简单的HTML文本类型。
GET http://www.xxx.com/test.html
Http1.0
1996年5月发布,这是第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用,特别是在代理服务器中。
相对于0.9版本,已经基本完善,主要新增一下内容:
- 请求方法除GET外,新增HEAD、POST方法。
- 新增请求/响应行及定义5类状态码
- 新增请求/响应首部
- 支持账户密码基本认证
- 支持缓存相关首部
- 支持多种文件类型
- 支持长连接,默认关闭,需要使用Connection首部协商
Http1.1
1999年发布,在Http1.0版本基础有很大进步,主要新增内容如下:
- 默认打开长连接属性(connect),支持在同一个连接上传输多个请求和响应
- 新增Http管道机制,允许客户端在响应返回前发送下一个请求,但服务端的响应必须是和发送顺序一致
- 新增PUT、DELETE、TRACE、CONNECT方法
- 支持对服务端资源部分请求(range),从而实现端点下载和上传
- 支持虚拟主机功能(host)
- 新增多个错误状态响应码(409/410等)
- 更为强大的缓存管理(e-tags/cache-control)
长连接的优势
在长连接之前,需要为每一个URL请求单独创建一个TCP连接。这样不仅增加了Http服务器的负载,并导致网络拥挤。一个很常见的场景,客户端请求一个HTML页面,当客户端请求HTML页面结束后,会依据其关联的js文件、css文件、图片文件等,针对同一服务器发出多个请求。而使用长连接则很好的解决这个问题,针对同一个域名服务器的请求只会建立一个TCP连接,大家来共享。
- 减少了TCP连接数,节省服务端连接数
- 降低TCP连接握手次数,数据包数量减小,降低网络拥塞
- Http在同一TCP连接上连续发送,客户端可以不用等待每个响应的情况下,发出多个请求
但是Http1.1中的管道技术有一个弊端:如果多个请求中的一个耗时严重,那么后面的请求也将被阻塞。
Connect 和 隧道
隧道技术的出现,是为了解决代理TLS流量问题的。
众所周知,Http代理的角色既是服务端,又是客户端。代理服务器可以查看或修改其代理的所有请求和响应,而且都是明文传输的。那么在建立TLS连接时,代理服务器不可能与客户端进行TLS握手,因为代理服务器没有服务端的证书。
而隧道技术的解决了这个问题,代理服务器不再改写客户端的请求,而是把客户端和服务端的请求和响应原样透传。
那么怎样建立隧道(tunnel)呢?
-
向代理服务器发送CONNECT请求
CONNECT http://www.xxxx.com HTTP/1.1 Host: www.xxx.com Proxy-Connection: Keep-Alive User-Agent: <some user agent info>
-
若代理服务器返回407,则客户端再次发送身份认证的请求
在请求头中添加认证首部Proxy-Authorization: <Basic 认证>
-
若代理服务器返回200,说明建立隧道成功。之后再向代理服务器发送的Http报文均会被原文转发。
Http2.0
2015年发布,为了追求更高效的数据通信,加入了跟多新技术。
- 首部字段压缩和复用,减小传输量。
- 多路复用,在同一个连接上可以并发多个具有优先级的请求,而不需要服务端响应的顺序返回
- 支持服务端主动向客户端推送数据
上述这些特性的实现要归功于Http2中提出的二进制帧技术,新的二进制分帧机制改变了客户端与服务器之间的交换数据方式,为了理解这个过程,需要先了解三个概念:
流 在已建立的TCP连接内,抽象出来的双向字节流,每条流都有唯一的流标识以及可自定义的优先级,在一条流通道内可以传输多条消息。
消息 消息对应一个逻辑请求或响应,包含了完整的一系列帧。
帧 Http2最小的通信单位,每个帧都包含帧头和载荷数据,至少也会标识出当前帧所属的流。来自不同数据流的帧可以交错发送,然后再根据流标识符重新组装。
二进制分帧
为了实现高效的数据通信和其他高级功能,Http2提供了一种优化的Http传输协议格式。在Http2中,最基本的传输协议单元叫做“帧”。每一帧都提供了不同的功能,例如HEADERS和DATA帧构成了基本的Http请求和响应。其他类型的帧,如SETTING帧、WINDOW_UPDATE帧、PUSH_PROMISE帧用来支持其他高级特性。相对于Http1.x中以回车和换行作为纯文本的分隔符,这里的“帧”格式是基于二进制编码。
帧格式
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
每一帧都有固定9字节(8位)的头部数据,以及不定长度的载荷数据。
帧长度
前24位(无符号)代表一帧中有效载荷部分的长度,其值最大为2^14 (16382),超出此长度不被发送,除非通过SETTING_MAX_FRAME_SIZE设置了更大的值。SETTING_MAX_FRAME_SIZE的取值范围是2^14 (16384)~2^24-1 (16777215)。超大帧的传输会导致时效性较高的帧(RST_STREAM/WINDOW_UPDATE/PRIORITY)发生延迟。另外,如果超大帧发生阻塞,则会影响性能。
帧类型
8位长度。该字段决定了后面的帧格式和含义。
帧类型 | 作用 | 说明 |
---|---|---|
DATA(0x0)数据帧 | 在指定流上传送任意个,可变长度的字节序列。 例如http请求和响应 | 数据帧的数据部分出除包含真实数据外, 还会在末尾包含一些填充数据,用于隐藏真实的帧长度 |
HEADERS(0x1)头部帧 | 用于开启一个流和携带请求头数据块 | |
PRIORITY(0x2)优先级帧 | 为指定流设置成发送者建议的优先级 | |
RST_STREAM(0x3)终止流帧 | 用于立即终止当前流 | 当需要取消请求或发生错误时,可发送此帧来终止。 该帧包含了一个指示错误类型的错误码。 |
SETTINGS(0x4)控制帧 | 用来向终端发送配置参数 (首选项、约束),或确认这些参数 | 前面说到的SETTING_MAX_FRAME_SIZE参数就是通过此帧设置的 |
PUSH_PROMISE(0x5) 推送承诺帧 | 在流初始化前发送,指定接下来的流可由服务端发起数据推送 | |
PING(0x6)Ping帧 | 用于发送端测量最小往返时间,从而判断一个空闲连接是否有效 | 除去帧头部,此帧只包含8字节不透明数据。 Pint帧的接收方要回复一个包含ACK标识的ping帧 |
GOAWAY(0x7)再见帧 | 用于关闭连接或发送严重错误信号 | 发送此帧后,发送端不再接受新的流, 但仍会继续处理先前建立的流。通过流标识判断新旧流。 |
WINDOW_UPDATE(0x8) 窗口更新帧 | 发送端发送剩余可发送字节数来实现流控,包含两个级别:独立的流;整个连接。 | 流控只适用于文档中定义的数据帧,其他类型的帧必须被接受和处理。 |
CONTINUATION(0x9)延续帧 | 用来发送多个请求头区块 | 该类型的帧可以跟在报头帧、推送帧、不带有END_HEADERS标记的延续帧 |
标志
8位长度。配合帧类型实现特定语义。如果未使用需设置为0x0。
标志类型 | 含义 | 说明 |
---|---|---|
NONE(0x0) | ||
ACK(0x1) | 确认标识 | 用于设置帧和ping帧的确认回复 |
END_STREAM(0x1) | 流结束,当前帧是最后一帧 | 用于头帧和数据帧 |
END_HEADERS(0x4) | 请求区块数据发送完毕 | 用于头帧和延续帧 |
END_PUSH_PROMISE(0x4) | ||
PADDED(0x8) | 帧中载荷数据部分填充 | 用于头帧和数据帧 |
PRIORITY(0x20) | 设置流的优先级 | 用于头帧 |
COMPRESSED(0x20) | 用于数据帧 |
R
1位的保留字段。
流标识
31位(无符号)长度。用来标识一个流,其中0x0值保留给与整个连接相关的帧,而不能在单个流中使用。
帧最后的载荷数据部分的内容和格式完全取决于帧类型。
首部字段压缩
Http2中采用一种叫做HPACK的压缩技术,来对首部进行压缩后传输。经过压缩后传输,可以减少几百到上千字节的节省。
HPACK压缩格式采用两种简单但是强大的技术:
- 通过静态Huffman代码对传输的首部字段进行编码,从而减小了各个传输的大小。
- 客户端和服务器同时维护和更新一个包含之前见过的首部字段的索引列表,之后就可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的首部键值对。
服务端推送
Http2打破了严格的请求-响应语义,支持一对多和服务器发起的推送工作流。
上图可以看出,客户端通过流1请求page.html,而page.html文档中内联了一些js/css文件,服务器可以选择直接将js/css文件通过流2、流4向客户端主动推送过去,从而避免了客户端的主动请求。
流的优先级
既然为每个请求-响应单独分配到一条流中来传输和处理,那么就可以通过设置流的优先级来实现控制请求优先级的目的。
Http2中优先级的计算由两方面组成:数据流依赖关系和权重
- 可以向每个数据流分配一个介于1至256之间的整数
- 每个数据流与其他数据流之间可以存在显示依赖关系
另外,数据流的优先级可以开启流时设定,并可以动态修改。
HEADERS帧设置优先级
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
在头帧中的Stream Dependency字段指定新流依赖的父流,默认为0。
通过Weight字段指定新流的权重。
PRIORITY帧修改优先级
+-+-------------------------------------------------------------+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+