3、连接

可以带着这些问题去阅读:

  • 报文是如何通过传输控制协议 (Transmission Control Protocol,TCP)连接从一个地方搬移到另一个地方去?
  • HTTP 是如何使用 TCP 连接的?
  • TCP 连接的时延、瓶颈以及存在的障碍?
  • HTTP 的优化,包括并行连接、keep-alive(持久连接)和管道化连接?
  • 管理连接时应该以及不应该做的事情?

3.1、TCP/IP

  • HTTP:应用层协议
  • TCP:无差错的数据传输;按序传输; 未分段的数据流(可以在任意时刻以任意尺寸将数据发送出去)。
  • IP:网际协议,Internet Protocol。

HTTP和HTTPS网络协议栈(数据链路层的下面是物理层):

网络协议栈

3.1.1、HTTP 是如何使用 TCP 连接的

HTTP 要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的 TCP 连 接按序传输。

TCP 收到数据流之后,将数据流砍成被称作段的小数据块,并将段封装在 IP 分组中,每个 IP 分组中都包括一个IP分组首部(通常为20字节)、一个TCP段首部(通常为20字节)、一个TCP数据块(0个或多个字节)。

IP首部包含了源和目的IP地址、长度和其他标记。

TCP 段的首部包含了 TCP 端口号、TCP 控制标记,以及用于数据排序和完整性检查的一些数字值。

TCP 是通过端口号来保持 所有这些连接持续不断地运行。

IP 地址可以连接到正确的计算机,而端口号则可以连接到正确的应用程序上去。

**TCP 连接是通过 4 个值来识别的,唯一地定义了一条连接:

< 源 IP 地址、源端口号、目的 IP 地址、目的端口号 >**

TCP分段

3.1.2、用TCP套接字编程

操作系统提供了一些操纵其 TCP 连接的工具, 如套接字 API 允许用户创建 TCP 的端点数据结构,将这些端点与远程服务器的 TCP 端点进行连接,并对数据流进行读写。TCP API隐藏了所有底层网络协议的握手细 节,以及 TCP 数据流与 IP 分组之间的分段和重装细节。

常见套接字接口函数:

常见套接字接口函数

TCP 客户端和服务器是如何通过 TCP 套接字接口进行通信的:

如何通过 TCP 套接字接口进行通信

3.2、基本的浏览器连接处理

DNS:域名服务,Domain Name Service

默认端口号是80。

连接步骤如下: (1) 浏览器从 URL 中解析出服务器的主机名;

(2) 浏览器将服务器的主机名转换成服务器的 IP 地址;

(3) 浏览器将端口号(如果有的话)从 URL 中解析出来;

(4) 浏览器建立一条与 Web 服务器的 TCP 连接;

(5) 浏览器向服务器发送一条 HTTP 请求报文;

(6) 服务器向浏览器回送一条 HTTP 响应报文;

(7) 关闭连接,浏览器显示文档。

浏览器连接处理步骤

3.3、对TCP性能的考虑

HTTP 紧挨着 TCP,位于其上层,所以 HTTP 事务的性能在很大程度上取决于底层 TCP 通道的性能。

3.3.1、HTTP事务的时延

下图是串行HTTP事务的时间线,可以看到处理时间是比其他时间都短的,除非客户端或服务器超载,或正在处理复杂的动态资源, 否则 HTTP 时延就是由 TCP 网络时延构成的。

TCP时延

导致时延的主要原因:

  • DNS查询可能要花数十秒的时间。不过大部分HTTP 客户端都有一个小的 DNS 缓存,用来保存近期所访问站点的 IP 地址,这样子查询就可以立即完成。
  • 每条新的TCP连接时延,通常最多只有一两秒钟。但是有数百个HTTP事务则会增加时延了。
  • 传输报文、处理报文的时间。

TCP 网络时延的大小取决于硬件速度、网络和服务器的负载,请求和响应报文 的尺寸,以及客户端和服务器之间的距离,TCP 协议的技术复杂性。

3.3.2、性能聚焦

最常见的TCP相关时延:

  • TCP 连接建立握手;
  • 用于捎带确认的TCP延迟确认算法;
  • TCP慢启动拥塞控制;
  • 数据聚集的Nagle算法;
  • TIME_WAIT时延和端口耗尽。
(1) TCP连接的握手时延

小的HTTP事务可能会在TCP建立上花费 50%,或更多的时间。

握手步骤:

  • 请求新TCP连接时,发送一个40~60字节的TCP分组,分组中的SYN标记说明是请求连接。如下图的(a)步骤。
  • 服务器接收连接,计算连接参数,回送TCP分组,分组中SYN和ACK说明已接受请求。如下图的(b)步骤。
  • 客户端回送确认信息,通知成功建立,此信息可发送数据。如下图的(c)步骤。

握手时延

(2) 延迟确认

每个 TCP 段都有一个序列号和数据完整性校验和。

每个段的接收者收到完好的段 时,都会向发送者回送小的确认报文,TCP 允许在发往相同方向的输出数据分组中对其进行“捎 带”确认报文,更有效利用网络。

如果发送者没有在指定的窗口时间内收到确 认信息,发送者就认为分组已被破坏或损毁,并重发数据。

为了增加确认报文找到同向传输数据分组的可能性,很多 TCP 栈都实现了一种 “延迟确认”算法。 延迟确认算法会在一个特定的窗口时间(通常是 100 ~ 200 毫 秒)内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组,寻找不了且超出时间则单独发送。

HTTP 具有双峰特征的请求 - 应答行为降低了捎带信息的可能,因此延迟确认算法会引入相当大的时 延。根据所使用操作系统的不同,可以调整或禁止延迟确认算法。

(3) TCP慢启动

TCP数据传输的性能还取决于 TCP 连接的使用期(age)。TCP 连接起初会限制连接的最大速度,如果数据成功传输,随着时间的推移提高传输的速度,这被称为TCP慢启动(slow start),用于防止因特网的突然过载和拥塞。

TCP 慢启动限制了一个 TCP 端点在任意时刻可以传输的分组数。

(4) Nagle算法与TCP_NODELAY

Nagle算法试图在发送一个分组之前,将大量 TCP数据绑定在一起,防止发送大量单字节分组的行为(发送端傻窗口综合症),以提高网络效率。 Nagle 算法鼓励发送全尺寸(LAN 上最大尺寸的分组大约是 1500 字节,因特网几百字节)的段。如果其他分组仍然在传输过程中,就将非全尺寸的数据缓存起来。只有当挂起分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会将缓存的数据发送出去。

Nagle 算法会引发几种 HTTP 性能问题:

  • 小的 HTTP 报文可能无法填满一个分组,可能会因为等待那些永远不会到来的额外数据而产生时延。
  • Nagle 算 法与延迟确认之间的交互存在问题:Nagle 算法会阻止数据的发送,直到有确认分组抵达为止,但确认分组自身会被延迟确认算法延迟 100 ~ 200 毫秒。

设置参数 TCP_NODELAY,禁用 Nagle 算法,提高性能,但是一定要确保向 TCP 写入大块的数据,这样才不会产生一堆小分组。

(5) TIME_WAIT累积与端口耗尽

当某个 TCP 端点关闭 TCP 连接时,会在内存中维护一个小的控制块,用来记录最 近所关闭连接的 IP 地址和端口号。这类信息维持的时间通常是最大分段使用期的两倍(称为 2MSL,通常为 2 分钟左右),防止在两分钟内 创建、关闭并重新创建两个具有相同 IP 地址和端口号的连接。

进行性能基准测试时,通常只有一台或几台用来产生流量的计算机连接到某 系统中去,这样就限制了连接到服务器的客户端 IP 地址数。用 TIME_W AIT 防止端口号重用时,这些 情况也限制了可用的连接值组合。

因为TCP 连接是由 4 个值来唯一定义一条连接的:< 源 IP 地址、源端口号、目的 IP 地址、目的端口号 > 源IP和端口号减少,其他不变的话,则导致TCP连接减少。

即使没有遇到端口耗尽问题,也要特别小心在有大量打开连接或控制块的情况 下,有些操作系统的速度会严重减缓

3.4、HTTP连接的处理

3.4.1、Connection首部

由逗号分隔的连接标签列表指定了一些不会传播到其他连接中去的选项。

Connection 首部可以承载 3 种不同类型的标签:

  • HTTP 首部字段名,列出了只与此连接有关的首部。
  • 任意标签值,用于描述此连接的非标准选项。
  • 值close,说明操作完成之后需关闭这条持久连接。

如果连接标签中包含了一个 HTTP 首部字段的名称,那么这个首部字段就包含了 与一些连接有关的信息,不能将其转发出去,如下:

Connection

在将报文转发出去之前,必须删除 Connection 首部列出的所有首部字段。由于 Connection 首部可以防止无意中对 本地首部的转发,因此将逐跳首部名放入 Connection 首部被称为“对首部的保 护”;可能还会有少量没有作为 Connection 首部值列出,但是一定不能被代理转发的逐跳首部,其中包括 Prxoy-Authenticate、Proxy-Connection、Transfer-Encoding 和 Upgrade。

3.4.2、串行事务处理时延以及解决办法

串行加载引入的缺点:

  • 实际连接时延和慢启动时延。
  • 在加载了足够多的对象之前,无法在屏幕上显示任何内容,比如污染确定对象的尺寸和位置。

解决方法:并行连接、持久连接、管道化连接、复用的连接。

对比:

连接优化对比

并行连接

HTTP/1.1(以及 HTTP/1.0 的各种增强版本)允许客户端打开多条连接,并行地执行多个 HTTP 事务,比如并行加载四幅图片。

并行连接可能会提高页面的加载速度:克服了单条连接的空载时间和带宽限制;每个连接之间都会有一小段软件时延,不过连接请求和传输时间基本都是重叠起来的。

当客户端的网络带宽不足时,并行连接会导致有限带宽的竞争,每个连接以较慢速度按比例加载,并行连接不一定更快

打开大量连接会消耗很多内存资源,造成服务器或代理性能的严重下降,一般限制并行连接数量为4个

并行连接一般让页面有多个组件对象同时出现在屏幕上,给用户一种错觉,页面加载得很快,即使并行连接没有加快页面的传输速度。

持久连接

在事 务处理结束之后仍然保持在打开状态的 TCP 连接被称为持久连接。持久连接可以避免缓慢的连接建立阶段以及慢启动的拥塞适应阶段。

相比并行连接,持久连接的好处:降低时延和连接建立的开销,后续无慢启动时延,减少打开连接的潜在数量。 缺点:可能累积大量的空闲连接。

持久连接与并行连接配合使用:打开少量的并行连接,每个并行连接为持久连接。

HTTP/1.0+ keep-alive连接

早期的实验性持久连接,已经不再使用,当前的 HTTP/1.1 规范无说明,但是对keep-alive握手的使用还是很广泛。

(1) 实现方法: 请求头包含Connection: Keep-Alive,若服务器支持则返回该首部,若不返回则说明服务器不支持而且关闭连接:

keep-alive

用 Keep-Alive 通用首部中指定的、由逗号分隔的选项来调节 keep-alive 的 行为,如下例子说明服务器最多还会为另外 5 个事务保持连接的打开状态,或者将打开状态保持到连接空闲了 2 分钟之后:

Connection: Keep-Alive
Keep-Alive: max=5, timeout=120

(2) 注意事项:

  • HTTP/1.0 中,客户端必须发送一个 Connection: Keep-Alive 请求首部来激活 keep-alive 连接。
  • Connection: Keep-Alive 首部必须随所有希望保持持久连接的报文一起发送。
  • 不应该与无法确定是否支持Connection首部的代理服务器建立 keep-alive 连接。
  • 从技术上来讲,应该忽略所有来自HTTP/1.0设备Connection首部字段,因为可能由老代理误转发的(老代理不知道要删除Connection首部再转发),但是有些客户端和服务器不忽略。

(3) Keep-Alive和哑代理

很多老的或简单的代理都是盲中继(blind relay),它们只是将字节从一个连接转发到另一个连接中去,不对 Connection 首部进行特殊的处理,将其作为一个扩展首部对待。

以下错误的通信方式会使浏览器一直处于挂起状态,直到客户端或服务器将连接 超时,并将其关闭为止:

哑代理

逐跳首部只与一条特定的连接有关,不能被转发。

为避免此类代理通信问题的发生,现代的代理都绝不能转发 Connection 首部 和所有名字出现在 Connection 值中的首部:Keep-Alive、Proxy-Authenticate、Proxy-Connection、 Transfer-Encoding 和 Upgrade。

(4) 插入Proxy-Connection

对盲中继问题的变通做法:引入了一个名为 Proxy-Connection 的新首部,解决了在客户端后面紧跟着一个盲中继所带来的问 题。对有多层次代理的情况,Proxy-Connection 仍然无法解决问题

实现方法:

浏览器会向代理发送非标准的 Proxy-Connection 扩展首 部,而不是官方支持的著名的 Connection 首部。如果代理是盲中继,它会将无意 义的 Proxy-Connection 首部转发给 W eb 服务器,服务器会忽略此首部,不会带 来任何问题。但如果代理是个聪明的代理(能够理解持久连接的握手动作),就用一 个 Connection 首部取代无意义的 Proxy-Connection 首部,然后将其发送给服 务器,以收到预期的效果。

如下图:

Proxy-Connection

HTTP/1.1 持久连接

与 HTTP/1.0+ 的 keep-alive 连接不同,HTTP/1.1 持久连接在默认情况下是激活 的。

要在事务处理结束 之后将连接关闭,HTTP/1.1 应用程序必须向报文中显式地添加一个 Connection: close 首部。

限制与规则:

  • 只有当连接上所有的报文都有正确的、自定义报文长度时——也就是说,实体主 体部分的长度都和相应的 Content-Length 一致,或者是用分块传输编码方式 编码的——连接才能持久保持。错误的 Content-Length 是很糟糕的事,因为事务处理的另一端无法精确地检测出一条报文的结束和另一条报文的开始了(keep-alive也是)。
  • HTTP/1.1 的代理必须能够分别管理与客户端和服务器的持久连接——每个持久 连接都只适用于一跳传输。
  • 一个用户客户端对任何服务器或代理最多只能维护两条持久连接,以防服务器过 载。代理可能需要更多到服务器的连接来支持并发用户的通信,所以,如果有 N 个用户试图访问服务器的话,代理最多要维持 2N 条到任意服务器或父代理的 连接。
管道化连接

HTTP/1.1 允许在持久连接上可选地使用请求管道。这是相对于 keep-alive 连接的又 一性能优化。

在响应到达之前,可以将多条请求放入队列,当第一条的请求通过网络流向服务器时,第二条和第三条请求也可以开始发送了,从而降低网络的环回时间,提高性能。

限制:

  • 如果HTTP客户端无法确认连接是持久的,就不应该使用管道。
  • 必须按照与请求相同的顺序回送HTTP响应,因为无序列号标签。
  • HTTP 客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完 成的管道化请求。
  • HTTP 客户端不应该用管道化的方式发送会产生副作用的请求(比如 POST)。

3.5、关闭连接

所有 HTTP 客户端、服务器或代理都可以在任意时刻关闭一条 TCP 传输连接。

每条 HTTP 响应都应该有精确的 Content-Length 首部,用以描述响应主体的尺 寸。若实际传输的实体长度与 Content-Length 并不匹配(或没有 Content-Length)时,接收端就应该质疑长度的正确性。

如果一个事务,不管是执行一次还是很多次,得到的结果都相同,这个事务就是幂 等的。客户端不应该以管道化方式传送非幂等请求(比如 POST)。 否则,传输连接的过早终止就会造成一些不确定的后果。要发送一条非幂等请求, 就需要等待来自前一条请求的响应状态。

3.5.1、正常关闭连接

TCP连接是双向的,TCP 连接的每一端都有一个输入队列和一个 输出队列,用于数据的读或写。

输入输出信道

完全关闭与半关闭

完全关闭:TCP 连接的输入和输出信道都关闭,套接字调用 close()。

半关闭:TCP 连接的输入或输出信道关闭,套接字调用 shutdown()。

关闭连接的输出信道总是很安全的。

问题:

关闭连接的输入信道比较危险,除非你知道另一端不打算再发送其他数据了。比如你已经在一条持久连接上发送了10条管道式请求了,响应也已经收到了,正在操作系统的缓冲区中存着(但应用程序还未将其读走)。现在,假设你发送了第11条请求,但服务器认为你使用这条连接的时间已经够长了,决定将其关闭。那么你的第11条请求就会被发送到一条已关闭的连接上去,并会向你回送一条重置信 息。这个重置信息会清空你的输入缓冲区。当最终要去读取数据的时候,会得到一个连接被对端重置的错误,已缓存的未读 响应数据都丢失了。

解决: 想要正常关闭连接的应用程序应该先半关闭其输出信道,然后周期性地检查其输入信道的状 态(查找数据,或流的末尾)。如果在一定的时间区间内对端没有关闭输入信道,应 用程序可以强制关闭连接,以节省资源。

3.6、Telnet

Telnet 程序可以将键盘连接到某个目标 TCP 端口,并将此 TCP 端口的输出回送到 显示屏上。Telnet 常用于远程终端会话,但它几乎可以连接所有的 TCP 服务器,包 括 HTTP 服务器。