连接管理
HTTP 中的短连接与长连接
短连接
在 HTTP/1.1 之前的版本中,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。 因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”(short-lived connections)。
短连接的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP 建立连接要有“三次握手”,发送 3 个数据包,需要 1.5 个 RTT;关闭连接是“四次挥手”,4 个数据包需要 2 个 RTT。
而 HTTP 的一次简单“请求 - 响应”通常只需要 4 个包,如果不算服务器内部的处理时间,最多是 2 个 RTT。这么算下来,浪费的时间就是“3.5÷5.5=64%”,有约三分之二的时间被浪费掉了,传输效率低得惊人。
RTT 指的是“往返时延”(Round-Trip Time),即从发送方发送数据开始,到发送方接收到来自接收方的确认消息所经过的时间。RTT 时延通常由三部分决定:链路的传播时间、末端系统的处理时间、路由器等网络中间节点的缓存和排队时间。正常情况下报文的传输时间和在应用处理时间相对固定,在网络拥堵情况下会出现 RTT 时延的波动。
长连接
短连接有两个比较大的问题:创建新连接耗费的时间尤为明显,另外 TCP 连接的性能只有在该连接被使用一段时间后(热连接)才能得到改善。为了缓解这些问题,长连接的概念便被设计出来了。
一个长连接会保持一段时间,重复用于发送一系列请求,节省了新建 TCP 连接握手的时间,还可以利用 TCP 的性能增强能力。当然这个连接也不会一直保留着:连接在空闲一段时间后会被关闭(服务器可以使用 Keep-Alive 协议头来指定一个最小的连接保持时间)。
长连接也是有缺点的;就算是在空闲状态,它还是会消耗服务器资源,而且在重负载时,还有可能遭受 DoS 攻击。这种场景下,可以使用非长连接,即尽快关闭那些空闲的连接,也能对性能有所提升。
在 HTTP/1.1 及之后的版本中,长连接默认是启用的。既然长连接有缺点,那么我们就可能需要在某些场景下手动关闭长连接,这就需要借助于 HTTP 请求头中的 Connection{:yaml}
字段。
在客户端请求时在请求头里加上 Connection: close{:yaml}
字段,告诉服务器需要在本次请求后关闭长连接,服务器接受到这个请求后在响应报文里也加上这个字段,发送后就调用 Socket API 关闭 TCP 连接。
客户端通常不会去关闭连接,服务器通常也不会主动关闭连接,那在实际的开发中就不去处理这块的优化吗?也不是的,虽然服务器不会主动关闭连接,但是像 Nginx 这类型的服务器都提供了策略去优化连接。
在 Nginx 中提供了两种方式处理主动断开连接:
- 使用
keepalive_timeout{:yaml}
指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。(默认 75s) - 使用
keepalive_requests{:yaml}
指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。(默认 100)
域名分片
作为 HTTP/1.x 的连接,请求是序列化的,哪怕本来是无序的,在没有足够庞大可用的带宽时,也无从优化。一个解决方案是,浏览器为每个域名建立多个连接,以实现并发请求。曾经默认的连接数量为 2 到 3 个,现在比较常用的并发连接数已经增加到 6 条。如果尝试大于这个数字,就有触发服务器 DoS 保护的风险。
如果服务器端想要更快速的响应网站或应用程序的应答,它可以迫使客户端建立更多的连接。例如,不要在同一个域名下获取所有资源,假设有个域名是 www.example.com
,我们可以把它拆分成好几个域名:www1.example.com、www2.example.com、www3.example.com。所有这些域名都指向同一台服务器,浏览器会同时为每个域名建立 6 条连接。这一技术被称作域名分片。
除非你有紧急而迫切的需求,不要使用这一过时的技术;而是升级到 HTTP/2。在 HTTP/2 里,做域名分片就没必要了:HTTP/2 的连接可以很好的处理并发的无优先级的请求。域名分片甚至会影响性能。大多数 HTTP/2 的实现还会使用一种称作连接聚合的技术去尝试合并被分片的域名。
队头阻塞
队头阻塞与短连接和长连接无关,而是由 HTTP 基本的“请求-应答”模型所导致的。
因为 HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。 如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。
HTTP 的重定向与跳转
跳转
在网页中用户由一个页面进入到另一个页面有两种方式:
- 用户主动点击网页中的链接进行跳转,这种方式叫“主动跳转”;
- 当用户发送一个请求时,服务器控制进入另一个页面,浏览器无法控制,这种方式叫“被动跳转”,也称为“重定向”。
重定向
重定向按照时间的长短分为永久重定向和临时重定向,在 HTTP 的状态码中: 301{:bash}
是永久重定向, 302{:bash}
是临时重定向,浏览器收到这两个状态码就会跳转到新的 URI。重定向的 URI 在 HTTP 请求的响应头中以 Location{:yaml}
来表示。
Location{:yaml}
字段必须配合3xx{:bash}
状态码才会生效。
在 Location{:yaml}
里的 URI 既可以使用绝对 URI,也可以使用相对 URI。所谓“绝对 URI”,就是完整形式的 URI,包括 scheme、host:port、path 等。所谓“相对 URI”,就是省略了 scheme 和 host:port,只有 path 和 query 部分,是不完整的,但可以从请求上下文里计算得到。
# 相对URI
Location: /index.html
# 绝对URI
Location: https://www.baidu.com/index.html
重定向的应用
重定向的应用主要是区分好“永久”和“临时”,比如我们有一个网站上线了一个活动页面,活动结束后如果用户再次通过链接访问了这个页面,我们需要将用户重定向到主页,从需求来看如果这个活动页面下架之后再也不会上线,那么我们就需要永久重定向,如果过一段时间还会再次上线的话我们需要使用临时重定向。
重定向的问题
- 性能损耗:重定向会在前一个请求成功后在会进行重定向,会比正常的跳转多一次请求;
- 循环跳转:如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈,不过现代浏览器都提供了循环跳转的检测功能,只要发现有循环跳转的情况都会中止并给出错误提示。
HTTP 中的身份识别
HTTP 是无状态的,这既是优点也是缺点。优点是服务器没有状态差异,可以很容易地组成集群,而缺点就是无法支持需要记录状态的事务操作。
在实际的网页开发中我们有很多的页面都需要判断用户的身份,只有通过授权的用户才能访问,可是 HTTP 是无状态的,难道我们每次请求的时候都需要手动带上身份标识吗?为了简化这个操作,HTTP 提出了 Cookies 的概念,在请求的时候可以自动带上 Cookies 中的内容。
什么是 Cookies?
HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
Cookies 是由服务端设置值,然后发送到浏览器器由浏览器进行存储的。服务端在 HTTP 请求的响应头中通过 Set-Cookie{:yaml}
属性设置 Cookie 的值,值的类型为 key=value{:yaml}
,可以同时设置多个键值对,多个键值对之间以 ;{:bash}
进行分割。浏览器发现请求的响应头中有 Set-Cookie{:yaml}
字段后会自动存储 Cookie 并且在下一次请求时将刚刚存储的 Cookie 放入 HTTP 请求头中的 Cookie{:yaml}
字段中,服务器在接收到请求时解析请求头中的 Cookie{:yaml}
字段就可以判别这个请求对应的用户身份了。
Cookie 的属性
属性 | 描述 |
---|---|
Expires{:yaml} | 过期时间 |
Max-Age{:yaml} | 有效期 |
Secure{:yaml} | 标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端 |
HttpOnly{:yaml} | JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie;此类 Cookie 仅作用于服务器。 |
Domain{:yaml} | 指定了哪些主机可以接受 Cookie |
Path{:yaml} | Path 属性指定了一个 URL 路径,该 URL 路径必须存在于请求的 URL 中,以便发送 Cookie 标头。以字符 %x2F (“/”) 作为路径分隔符,并且子路径也会被匹配。 |
SameSite{:yaml} | SameSite 属性允许服务器指定是否/何时通过跨站点请求发送(其中站点由注册的域和方案定义:http 或 https)。这提供了一些针对跨站点请求伪造攻击(CSRF)的保护。它采用三个可能的值:Strict 、Lax 和 None |