Linux 高性能服务器编程- TCP 协议详解
TCP 协议是 更靠近应用层,因此在应用程序中具有更强可操作性,一些重要 socket 选项都和 TCP 协议相关。
- TCP 头部信息:TCP 头部信息出现在每个 TCP 报文段中,用于指定通信的源端端口号、目的端端口号、管理 TCP 连接、控制两个方向的数据流。
- TCP 状态转移信息:TCP 连接的任意一端都是一个状态机。在 TCP 连接从建立到断开的整个过程中,连接两端的状态机将经历不同的状态变迁。
- TCP 数据流:通过分析 TCP 数据流,我们可以从网络应用程序外部来了解应用层协议和通信双方交换的应用层数据。
- TCP 数据流的控制:为了保证可靠传输和提高网络通信质量,内核需要对 TCP 数据流进行控制。
3.1 TCP 服务的特点#
TCP 协议相对 UDP 协议主要:面向连接、数据流、可靠传输。
使用 TCP 协议的双方必须先建立连接,然后才能开始数据的读写。双方必须为连接分配必要的内核资源,来管理连接的状态和连接上传输的数据。TCP 是全双工的,即双方的读写可以通过一个连接进行。数据交换完毕后,双方必须断开连接以释放资源。
TCP 连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用 TCP 服务,而无连接的 UDP 则非常适合广播和多播。
当发送端应用程序连续执行多次写操作时,TCP 模块先将这些数据放入 TCP 发送缓存区中,当 TCP 模块真正开始发送数据时,发送缓冲区将这些等待的数据可能封装成一个或多个 TCP 报文段发出,因此 TCP 模块发出的 TCP 报文段的个数和应用程序的写操作之间没有固定的数量关系。
这就是字节流:应用程序对数据的发送和接收是没有边界限制的。
UDP 发送端每一次写操作, UDP 模块就将其封装成 UDP 数据报并发送。接收端必须及时针对每一个 UDP 数据报执行读取操作(通过 recvform 系统调用),否则会丢包。并且如果用户没有指定足够的应用程序缓冲区来读取 UDP 数据,则 UDP 数据将被截断。
TCP 协议是可靠的#
TCP 协议采用发送应答机制,即发送端发送的每个 TCP 报文段都必须得到接收方的应答,才能认为这个 TCP 报文段传输成功。TCP 协议采用超时重传机制,发送端在发送一个 TCP 报文段后启动定时器,如果在指定时间内未收到应答,它将重发该报文段。因为 TCP 报文段最终是以 IP 数据报发送的,而 IP 数据报达到接收端可能乱序、重复,所以 TCP 协议还会对接受到的 TCP 报文段重排、整理,在交付给应用层。
UDP 协议和 IP 协议一样,提供不可靠服务。他们都需要上层协议来处理数据确认和超时重传。
3.2 TCP 头部结构#
TCP 头部信息出现在每个 TCP 报文段中,用于指定通信的源端端口、目的端端口、管理 TCP 连接等。
3.2.1 TCP 固定头部结构#
- 16 位端口号:告知主机该报文段来自哪里(源端口号)以及传给哪个上层协议或应用程序(目的端口)。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号,而服务器使用知名服务器端口好。
- 32 位序号(sequence number): 一次 TCP 通信过程中某一个传输方向上的字节流的每个字节的编号。 假设主机 A 发送给 B 第一个 TCP 报文段,序号值被初始化为某个随机值 ISN ,那么在该传输方向上(从 A 到 B ),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移, 如某个 TCP 报文段传送的数据是字节流中的第 1025 - 2048 字节,那么该报文段的序号值就是 ISN + 1025 。
- 32 位确认号 (acknowlegegment number): 用作对另一方发送来的 TCP 报文段的响应,其值是收到的 TCP 报文段的序号 + 1. 假设 A 和 B 进行 TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号,反之,B 发送出的 TCP 报文段也同时携带自己的序号和对 A 发送来的报文段的确认号。
- 4 位头部长度:标识该 TCP 头部有多少个 32 bit 字(4 字节), 因为 4 字节最大能表示 15,所以 TCP 头部最长 60 字节。
- 6 位标志位包含:
- URG 标志: 表示紧急指针是否有效
- ACK 标志: 表示确认号是否有效。 携带 ACK 标志的 TCP 报文段为确认报文段
- PSH 标志: 表示接收端应用程序应该立即从 TCP 缓冲区中读取数据,为接受后续数据腾出空间(应用程序不将接受到的数据读走,他们会一直停留在 TCP 接收缓冲区中)
- RST 标志:表示要求对方重新建立连接。携带 RST 标志的 TCP 报文段为复位报文段。
- SYN 标志: 表示请求建立一个连接。 携带 SYN 标志的 TCP 报文段为结束报文段。
- 16 位窗口大小: TCP 控制流量的手段。这里的窗口是指通告窗口(Reveiver Window,RWND),它告诉对方本段的 TCP 接受缓冲区还能容纳多少字节的数据。
- 16 位校验和(TCP checksum): 由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验 TCP 报文段在传输过程中是否损坏。校验头部及数据部分。
是 TCP 可靠传输的一个重要保障
- 16 位紧急指针:偏移量。 它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。
3.2.2 TCP 头部选项#
TCP头部的最后一个选项字段是可变长的可选信息。这部分最多包含 40 字节,因为TCP 头部最长 60 字节(20 字节固定部分)。
kind : 选项的类型。 有的 TCP 选项没有后面两个字段。
length : 如果有,指定该选项的总长度。包括 kind 、length 占据的 2 字节
info : 如果有,是选项的具体信息
kind = 0 表示选项表结束选项。
kind = 1 是空操作(nop)选项,没有特殊含义,一般用于将 TCP 选项的总长度填充位 4 字节的整倍数。
kind = 2 是最大报文段长度选项。TCP 连接初始化时,通信双方使用该选项来协商最大报文长度(Max Segment Size).TCP 模块通常将 MSS 设置为 (MTU-40)字节(减掉 40 字节 为 20 字节的 TCP 头部 和 20 字节的 IP 头部)。这样携带 TCP 报文段的 IP 数据报长度就不会超过 MTU,从而避免本机发生 IP 分片。对以太网而言,MSS 的值是 1460 (1500-40)字节。
kind = 3 是窗口扩大因子选项。 TCP 连接初始化时,通信双方使用该选项来协商接受通告窗口的扩大因子。在 TCP 的头部中,接受通告窗口大小是用 16 位表示的,故最大位 65535 字节,但实际上 TCP 模块允许的接受通告窗口大小远不止这个数(为了提高 TCP 通信的吞吐量)。窗口扩大因子 解决了这个问题。假设 TCP 头部中的接受通告窗口大小是 N ,窗口扩大因子(位移数)是 M ,那么 TCP 报文段的实际接受通告窗口大小是 N 乘 2^M , 或者说 N 左移 M 位。 M 取值范围 0-14。窗口扩大因子只能出现在同步报文中,否则将被忽略。
3.3 TCP 连接的建立和关闭#
3.3.1#
TCP 连接的建立和关闭时序图
第 1 个 TCP 报文段包含 SYN 标志,因此为同步报文段,即客户端向服务器发起连接请求。该同步报文段包含一个 ISN 值为 535734930
的序号。
第 2 个 TCP 报文段也是同步报文段,表示服务器同意与客户端建立连接,同时发送自己的 ISN 值为 2159701207 的序号,并对第 1 个同步报文段进行确定,确认值是 535734931,即源值 +1,序号值是用来标识 TCP 数据流重的每一个字节的,但是同步报文段比较特殊,即使没有携带任何应用程序数据,它也要占用一个序号值
第 3 个 TCP 报文段是客户端对第 2 个同步报文段的确认,至此,TCP 连接建立起来了。
建立 TCP 连接的这 3 个步骤被成为 TCP 三次握手
第 4 个 TCP 报文段包换 FIN 标志,因此它是一个结束报文段,即客户端要求关闭连接。结束报文段和开始报文段一样,都要占用一个序号值。
服务器用 TCP 报文段 5 来确认该结束报文段,同时发送自己的结束报文段 6, 客户端则用 TCP 报文段 7 给予确认。
实际上 仅用确认的 报文段 5 是可以省略的,因为结束报文段 6 也携带了该确认信息,确认报文段 5 是否出现在连接断开的过程中,取决于 TCP 的延迟确认特性。
3.3.2 半关闭状态#
TCP 连接是全双工的,所以它允许两个方向的数据传输被独立关闭。
通信的一端可以发送结束报文段给对方,告诉它本段已经完成了数据的发送,但允许继续接受来自对方的数据,直到对方也发送结束报文段以关闭连接。 TCP 连接的这种状态成为半关闭 (half close) 状态。
服务器和客户端应用程序判断对方是否已经关闭连接的方法是: read 系统调用返回 0 (收到结束报文段).
3.3.3 连接超时#
对于提供可靠服务的 TCP 来说,它必然是先进行重连(可能执行多次),如果重连无效,通知应用程序连接超时。
3.4 TCP 状态转移#
TCP 连接的任意一端在任一时刻都处于某种状态。
粗虚线表示典型的服务器端连接的状态转移:粗实线表示典型的客户端连接的状态转移。
3.4.1 TCP 状态转移总图#
服务器通过 listen 系统调用进入 LISTEN 状态,被动等待客户端连接,因此执行的是所谓的被动打开。
服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中,并向客户端发送 SYN 标志的确认报文段。此时该连接处于 SYN_RCVD 状态。如果服务器成功地接受到客户端发回的确认报文段,则该连接转移到 ESTABLISHED 状态(连接双方都能够进行双向数据传输的状态)。
当客户端主动关闭连接时(通过 close 或 shutdown 系统调用向服务器发送结束报文段),服务器通过返回确认报文段使连接进入 CLOSE_WAIT 状态(等待服务器应用程序关闭连接)。通常服务器检测到客户端关闭连后,也会立即向客户端发送一个结束报文段来关闭连接。状态变为 LAST_ACK 状态,以等待客户端对结束报文段的最后一次确认。确认完成,连接彻底关闭。
客户端通过 connect 系统调用主动与服务器建立连接, connect 系统调用首先给服务器发送一个同步报文段,使连接转移到 SYN_SENT 状态。此后 connect 系统调用可能因为 如下两个原因失败返回:
1 如果 connect 连接的目标端口不存在(未被任何进程监听),或者该端口仍被处于 TIME_WAIT 状态的连接所占用,则服务器将给客户端发送 复位报文段, connect 调用失败
2 如果 connect 目标端口存在,在超时时间范围内服务器未 返回确认报文段,则 connect 调用失败
connect 调用失败,将使连接返回到初始的 CLOSE 状态。 如果客户端成功收到服务器的 同步报文段和确认,则 connect 调用成功,连接转移至 ESTABLISHED 状态。
当客户端执行主动关闭时,它向服务器发送一个 结束报文段,同时连接进入 FIN_WAIT_1 状态。如果客户端此时收到服务器专门用户确认的确认报文段,连接进入 FIN_WAIT_2 状态,服务器处于 CLONSE_WAIT 状态,这一对状态是可能发生半关闭的状态。此时如果服务器也发送结束报文段,则客户端予以确认并进入 TIME_WAIT 状态。
客户端直接从 FIN_WAIT_1 状态进入 TIME_WAIT 状态,是 FIN_WAIT_1 状态的服务器直接收到带确认信息的结束报文段(不是先收到确认报文段,在收到结束报文段)
处于 FIN_WAIT_2 状态的客户端需要等待服务器发送结束报文段才能转移至 TIME_WAIT 状态,否则将一直停留在此状态。如果不是为了在半关闭状态接受数据,连接长时间停留在 FIN_WAIT_2 毫无意义。 连接停留在 FIN_WAIT_2 原因:
- 客户端执行半连接后,未等待服务器关闭连接就强行退出了。(此时客户端连接由内核接管,可成为孤儿连接,Linux 为了防止孤儿连接长时间停留在内核中,定义了两个内核变量,指定内核能接管的孤儿连接数量和孤儿连接在内核中的生存时间)
3.4.2 TIME_WAIT 状态#
客户端连接在收到服务器的结束报文段后,并没有直接进入 CLOSE 状态,而是转移到了 TIME_WAIT 状态。
在这个状态客户端需要在 2 MSL (maximum segment life 报文段最大生存时间,2 min)的时间,才能完全关闭。
TIME_WAIT 状态存在的原因:
- 可靠的终止 TCP 连接
- 保证 让迟来的 TCP 报文端由足够的时间被识别并丢弃
1 假设用户确认服务器结束报文段 6 的 TCP 报文段 7 丢失, 那么服务器将重结束报文段,因为客户端需要停留在某个状态来处理服务器重发的结束报文段(即向服务器发送确认报文段),否则客户端将以复位报文段回复服务器,服务器认为这是一个错误,因为它期望的是 像报文段 7 那样的确认报文段。
2 在 LINUX 系统中,一个 TCP 端口不能被同时打开多次。如果不存在 TIME_WAIT 状态,则应用程序可能建立一个和刚关闭连接相似的连接,这个连接可能收到原来连接的 TCP 报文段。
3.5 复位报文段#
3.5.1 访问不存在的端口#
当客户端访问一个不存在的端口时,目标主机将给它发送一个复位报文段。
收到复位报文段的一端应该关闭连接或者重新连接,而不能回应这个复位报文段。实际上,当客户端向服务器的某个端口发起连接,而该端口处于 TIME_WAIT 状态的连接所占用时,客户端程序也将收到复位报文段。
3.5.2 异常终止连接#
TCP 提供了异常终止一个连接的方法,即给对方发送 一个复位报文段。一旦发送了复位报文段,发送端所有排队等待发送的数据将被丢弃。
3.5.3 处理半打开连接#
如下情况:
1 服务器或客户端关闭或异常终止了连接,而对方没有收到结束报文段,此时客户端或服务器还保持着原来的连接,而服务器或客户端即使重启,也没有该连接的任何信息了,这种状态被成为半打开连接。
如果往半打开的连接写入数据,则对方回应一个复位报文段。
3.6 TCP 交互数据流#
TCP 报文段所携带的应用程序数据根据长度分为:交互数据和成块数据。
交互数据仅包含较少的字节,使用交互数据的应用程序对实时性要求较高,如 telnet、ssh 等。成块数据一般为 TCP 报文段允许的最大长度,使用成块数据的对传输效率要求高,如 ftp。以下介绍交互数据:
广域网上的交互数据可能收到很大的延迟,并且携带交互数据的 微小 TCP 报文段特别多,这些因素可能导致阻塞。可使用 Nagle 算法。
Nagle 算法要起一个 TCP 连接的通信双方任意时刻只能发送一个未被确认的 TCP 报文段,在该 TCP 报文段未被确认前不能发送其他 TCP 报文段,另一方面,发送方在等待确认的同时搜集本端需要发送的微量数据,并在确认到来时以一个 TCP 报文段将他们全部发出,这样就极大减少了网络上的微小 TCP 报文段的数量,该算法的另一个优点:确认到达的越快,数据也就发送的越快。
3.7 TCP 成块数据流#
当传输大量大块数据的时候,发送方会连续发送多个TCP 报文段,接收方可以一次确认这些报文段。
服务器每发送 4 个 TCP 报文段就传输一个 PSH 标志给客户端,通知客户端的应用程序尽快读取数据,不过这对服务器不是必须的,因为它知道客户端的 TCP 接受缓冲区中还有空间(接受通告窗口不为 0)。
3.8 带外数据#
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: