http

服务端开发离不开和 http 打交道,无论是 API 请求、RPC 请求,都有可能使用 http 协议。http 太常见了, spring 提供的注解开发可能让我们忽略了协议内不同内容是如何存放,header、body、表单都如何使用。参考极客时间-透视 http 协议,从头到尾梳理一遍 http 的详细知识。

http 是什么

http 全称超文本传输协议,拆分为三个名词详细理解:

  • 超文本:普通的文本指的是文字数据,超文本不但包含了文字,它也包括了语音、图片、视频等数据,是一切资源的泛称。同时它含有超链接,可以从一个超文本跳转到另外一个超文本,形成复杂的网状结构。
  • 传输:数据在双方的流动叫为传输,http 是一个双向传输协议,一般情况下把发起数据传输的称为请求方,接受数据的称为响应方。
  • 协议:首先协议可以分为两个部分,它需要最少两个参与者,参与者要想互相理解对方传输的内容,这离不开规范。简单来说协议就是规定了双方使用同样可以理解的语音进行交流

作为一种协议规范,它本身不是一种程序或软件,HTTP 通常跑在 TCP/IP 协议栈之上,依靠 IP 协议实现寻址和路由、TCP 协议实现可靠数据传输、DNS 协议实现域名查找、SSL/TLS 协议实现安全通信。

http 具有简单灵活、无状态、跨语言、明文传输的特点。

  • 简单灵活:使用 k:v 的文字键值对,通俗易懂。
  • 无状态:http 本身不会保存任何数据,即使和同一服务器进行多次通信,它也是无感知的。
  • 明文传输:协议里的报文(也就是 header 部分)不使用二进制数据,而是用简单可阅读的文本形式。
  • 没有身份认证能力、数据完整性校验机制,容易被冒充身份。

协议的内容

请求体

http 的请求和响应格式类似,都是由三部分组成:

  • 起始行:描述请求或响应的基本信息 GET /path HTTP/1.1
  • header 集合:使用 k-v 键值对描述报文
  • 消息正文(body):实际传输的数据,可以文本、图片、视频等二进制数据

起始行和 header 统称为请求头,正文称为请求体。http 协议规定报文必须有请求头,且请求头后必须要有一个“空行”,可以没有请求体

响应

响应报文由响应头加响应体数据组成,响应头又由状态行和头字段构成。

  • version:HTTP 协议的版本号,通常是 HTTP/1.1。
  • 状态码:是一个十进制数字,以代码的形式表示服务器对请求的处理结果。
    • 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
    • 2××:成功,报文已经收到并被正确处理;
    • 3××:重定向,资源位置发生变动,需要客户端重新发送请求;
    • 4××:客户端错误,请求报文有误,服务器无法处理;
    • 5××:服务器错误,服务器在处理请求时内部发生了错误。
  • 原因短语:是状态码的简短文字描述,例如 ok、Not Found。

1××

属于提示信息,是协议处理的中间状态,实际能够用到的时候很少,可以忽略。

2××

表示服务器收到并成功处理了客户端的请求。

200 OK”是最常见的成功状态码,表示一切正常,服务器如客户端所期望的那样返回了处理结果,如果是非 HEAD 请求,通常在响应头后都会有 body 数据。

204 No Content”是另一个很常见的成功状态码,它的含义与“200 OK”基本相同,但响应头后没有 body 数据。所以对于 Web 服务器来说,正确地区分 200 和 204 是很必要的。

206 Partial Content”是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。

状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节。

3××

重定向,即表示资源位置发生了变化,需要客户端使用新的URI重新进行请求。

301 Moved Permanently”俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用改用新的 URI 再次访问。

与它类似的是“302 Found”,曾经的描述短语是“Moved Temporarily”,俗称“临时重定向”,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。

301 和 302 都会在响应头里使用字段Location指明后续要跳转的 URI,最终的效果很相似,浏览器都会重定向到新的 URI。两者的根本区别在于语义,一个是“永久”,一个是“临时”,所以在场景、用法上差距很大。

304 Not Modified” 用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。

4××

4××类状态码表示客户端发送的请求报文有误,服务器无法处理,它就是真正的“错误码”含义了。

400 Bad Request”是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误,客户端看到 400 只会是“一头雾水”“不知所措”。所以,在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。

403 Forbidden”实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因,不过现实中通常都是直接给一个“闭门羹”。

404 Not Found”可能是我们最常看见也是最不愿意看到的一个状态码,它的原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被“用滥了”,只要服务器“不高兴”就可以给出个 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,某种程度上它比 403 还要令人讨厌。

4××里剩下的一些代码较明确地说明了错误的原因,都很好理解,开发中常用的有:

  • 405 Method Not Allowed:不允许使用某些方法操作资源,例如不允许 POST 只能 GET;
  • 406 Not Acceptable:资源无法满足客户端请求的条件,例如请求中文但只有英文;
  • 408 Request Timeout:请求超时,服务器等待了过长的时间;
  • 409 Conflict:多个请求发生了冲突,可以理解为多线程并发时的竞态;
  • 413 Request Entity Too Large:请求报文里的 body 太大;
  • 414 Request-URI Too Long:请求行里的 URI 太大;
  • 429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略;
  • 431 Request Header Fields Too Large:请求头某个字段或总体太大;

5××

5××类状态码表示客户端请求报文正确,但服务器在处理时内部发生了错误,无法返回应有的响应数据,是服务器端的“错误码”。

500 Internal Server Error”与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。

501 Not Implemented”表示客户端请求的功能还不支持,这个错误码比 500 要“温和”一些,和“即将开业,敬请期待”的意思差不多,不过具体什么时候“开业”就不好说了。

502 Bad Gateway”通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。

503 Service Unavailable”表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。

503 是一个“临时”的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求。

安全与幂等

安全有两个含义:分别是传输数据的安全、服务器数据的安全。其中传输过程的安全可以通过 https 协议来完成,请求方法中只有 GET 和 HEAD 方法是“安全”的,因为它们是“只读”操作,只要服务器不故意曲解请求方法的处理方式,无论 GET 和 HEAD 操作多少次,服务器上的数据都是“安全的”。

幂等指的是多次执行某个操作结果都是相同的。在这个定义下,GET 和 HEAD 既是安全的也是幂等的,DELETE 可以多次删除同一个资源,效果都是“资源不存在”,所以也是幂等的。而 POST、PUT 操作会修改数据状态,多次重复操作后数据会有不同的表现形式,因此是不幂等的。

https

http 协议天生就是不安全的,可以给通信过程中定义以下四个条件,只有满足这些条件我们才认为此次网络通信是安全的。

  • 完整性:数据在传输前后没有经过修改。
  • 机密性:传输过程中的数据对第三方不可见。
  • 身份认证能力:确保发送的目的地是自己熟知的。
  • 不可否认:发送过的行为无法否认。

https 协议的默认端口号是443,其余的请求应答等(除去安全部分)都和 http 相同。https 可以实现安全传输的核心原因在于下层的通信协议发生了改变(SSL/TSL 属于传输层协议),收发报文不再使用 Socket API,而是调用专门的安全接口。

机密性

对称加密

加密和解密使用同一套密钥,加解密速度快,难点在于对密钥的传输,如果密钥被窃取,那么加密也就失去了意义。

非对称加密

分为公钥和私钥,公钥加密的数据可以使用私钥解开,但是加解密性能不好,实际传输过程使用会对拖垮服务。

TLS 里使用的混合加密方式,使用的就是把对称、非对称加密混合的办法:

  1. 在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE,首先解决密钥交换的问题。
  2. 然后用随机数产生对称算法使用的“会话密钥”(session key),再用公钥加密。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。
  3. 对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。

完整性

黑客可以伪造身份发布公钥。如果使用假的公钥,混合加密就完全失效了。所以,在机密性的基础上还必须加上完整性、身份认证等特性,才能实现真正的安全。

实现完整性的手段主要是摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数。它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,并且细微的不同都会造成摘要结果的剧烈变化。

目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2。

摘要算法保证了“数字摘要”和原文是完全等价的。因此只要在原文后附上它的摘要,就能够保证数据的完整性。

身份认证

CA(Certificate Authority,证书认证机构)。它具有极高的可信度,由它来给各个公钥签名,用自身的信誉来保证公钥无法伪造,是可信的。

公钥的分发需要使用数字证书,必须由 CA 的信任链来验证,否则就是不可信的。

URI

统一资源标识符(URI)是一个字符串,用来唯一的标识一个资源,这个资源可以是互联网上的资源、也可以是磁盘上的文件、甚至可以是一封邮件,它通常由 schema、host、port、path、query param 组成,有些部分可以省略。

  • scheme:协议名,表示资源使用哪种协议来访问,包括:http、https、ftp、file。
  • authority:表示资源所在的主机名,表现形式为主机加端口号,其中主机名不可以省略,端口号可以省略。
  • path:表示资源所在的位置,以 / 开头。
  • 查询参数:以 ?开始,多个k=v以 & 连接。

编码

uri 内除了英文和数字以外也存在包含中文、&等特殊字符的场景,由于在 URI 里只能使用 ASCII 码,因此这种场景就需要进行编码操作。编码对于 ASCII 码以外的字符集和特殊字符做一个特殊的操作,把它们转换成与 URI 语义不冲突的形式,直接**把非 ASCII 码或特殊字符转换成十六进制字节值,然后前面再加上一个%**。有了编码操作后,uri可以支持任意的字符集用任何语言来标记资源。

连接管理

http1.1 以前使用的都是短连接进行管理,所谓短连接就是每次请求-响应结束后就关闭 TCP 连接,而 TCP 连接的建立需要三次握手、关闭需要四次挥手,这就导致可能建立连接的花费比传输数据还要高。

后续 http 改进了这一点,默认使用长连接:Connection: keep-alive,所谓长连接即通信结束后服务器不会立刻释放掉此次连接,在连接空闲超过指定时间,或请求次数达到指定数目后再关闭连接。这也会引发出一个新的问题,服务器维护一个建立的连接需要耗费一定的资源,如果每个请求都是空闲请求,那么资源很快会被打满,无法给真正需要通信的对象服务。

web 服务器

web 服务器的瓶颈不在 CPU 资源,作为一种 IO 密集型服务,处理能力的关键在于网络收发,而网络 I/O 会因为各式各样的原因不得不等待,比如数据还没到达、对端没有响应、缓冲区满发不出去等等。普通的多线程处理模型此时会切换线程,多个并发看下来阻塞情况不会太严重,但是由于需要频繁切换进程、以及需要保护临界区资源,多数资源就耗费在了非业务处理上。

nginx 使用多路复用技术(epoll),把多个 HTTP 请求处理打散成碎片,都“复用”到一个单线程里,不按照先来后到的顺序处理,而是只当连接上真正可读、可写的时候才处理,如果可能发生阻塞就立刻切换出去,处理其他的请求。这种方式可以消除掉 IO 阻塞,把资源都用来做核心业务,每个请求处理单独来看是分散、阻塞的,但因为都复用到了一个线程里,所以资源的利用率非常高。