1. UDP:用户数据报协议
1.1. 细节概述
- UDP 是一个简单的面向数据报的运输层协议:进程的每个输出操作都正好产生一个 UDP 数据报,并组装成一份待发送的 IP 数据报。这与面向流字符的协议不同,如 TCP,应用程序产生的全体数据与真正发送的单个 IP 数据报可能没有什么联系。
- UDP 数据报封装成一份 IP 数据报的格式:
- UDP 不提供可靠性:它把应用程序传给 IP 层的数据发送出去,但是并不保证它们能到达目的地
- 应用程序必须关心 IP 数据报的长度,如果它超过网络的 MTU(,那么就要对 IP 数据报进行分片。源端到目的端之间的每个网络都要进行分片,并不只是发送端主机连接第一个网络才这样做
- UDP 首部(8 个字节):
- 4 字节(源、目标端口各两个字节)
- 2 字节 UDP 长度(UDP 首部+UDP 数据的字节长度)、2 字节 UDP 校验和
- IP 基于协议号对 IP 包分用,TCP 端口号与 UDP 端口号是相互独立的(基于约定应用应该选用不同,避免混淆)
- IP 首部的检验和仅校验 IP 首部,UDP、TCP 校验覆盖到数据(另外,UDP 校验是可选的,TCP 校验是必须的)
- UDP 校验:
- UDP 数据报的长度可以为奇数字节,但是检验和算法是把若干个 16 bit 字相加。解决方法是必要时在最后增加填充字节 0,这只是为了检验和的计算(也就是说,可能增加的填充字节不被传送)。
- UDP 数据报和 TCP 段都包含一个 12 字节长的伪首部,它是为了计算检验和而设置的。
- 尽管 UDP 检验和是可选的,但是它们应该总是在用,在单个局域网中这可能是可以接受的,但是在数据报通过路由器时,通过对链路层数据帧进行循环冗余检验(如以太网或令牌环数据帧)可以检测到大多数的差错,导致传输失败。路由器中也存在软件和硬件差错,以致于修改数据报中的数据。如果关闭端到端的 UDP 检验和功能,那么这些差错在 UDP 数据报中就不能被检测出来。
- Socket 编程中可以开启校验
- 因为太网和 IP 层还使用其他的协议。例如,不是所有的以太网数据帧都是 IP 数据报,至少以太网还要使用 ARP 协议。不是所有的 IP 数据报都是 UDP 或 TCP 数据,因为 ICMP 也用 IP 传送数据。不要完全相信数据链路(如以太网,令牌环等)的 CRC 检验。应该始终打开端到端的检验和功能。而且,如果你的数据很有价值,也不要完全相信 UDP 或 TCP 的检验和,因为这些都只是简单的检验和,不能检测出所有可能发生的差错。
- 链路层:CRC 循环冗余校验(16 bit 字的二进制反码求和)
- 传输层:CRC 循环冗余校验
- 首部检验和字段是根据 IP 首部计算的检验和码。它不对首部后面的数据进行计算。ICMP、IGMP、UDP 和 TCP 在它们各自的首部中均含有同时覆盖首部和数据检验和码。(3.2 章节)
- IP 分片:物理网络层一般要限制每次发送数据帧的最大长度。任何时候 IP 层接收到一份要发送的 IP 数据报时,它要判断向本地哪个接口发送数据(选路),并查询该接口获得其 MTU。IP 把 MTU 与数据报长度进行比较,如果需要则进行分片。分片可以发生在原始发送端主机上,也可以发生在中间路由器上。把一份 IP 数据报分片以后,只有到达目的地才进行重新组装,重新组装由目的端的 IP 层来完成,其目的是使分片和重新组装过程对运输层(TCP 和 UDP)是透明的,除了某些可能的越级操作外。已经分片过的数据报有可能会再次进行分片(可能不止一次)。IP 首部中包含的数据为分片和重新组装提供了足够的信息。
- IP 首部,标志字段用其中一个比特来表示“更多的片”,另外标志字段中有一个比特称作“不分片”位
- 当数据报被分片后,每个片的总长度值要改为该片的长度值。
- 当 IP 数据报被分片后,每一片都成为一个分组,具有自己的 IP 首部,并在选择路由时与其他分组独立。这样,当数据报的这些片到达目的端时有可能会失序,但是在 IP 首部中有足够的信息让接收端能正确组装这些数据报片。
- 即使只丢失一片数据也要重传整个数据报,因为 IP 层本身没有超时重传的机制——由更高层来负责超时和重传(TCP 有超时和重传机制,但 UDP 没有。一些 UDP 应用程序本身也执行超时和重传),就这个原因,经常要避免分片
- 在一个以太网上,数据帧的最大长度是 1500 字节,其中 1472 字节留给数据,假定 IP 首部为 20 字节,UDP 首部为 8 字节,可以基于
tcpdump
查看分片明细 - 术语:
- IP 数据报:是指 IP 层端到端的传输单元(在分片之前和重新组装之后),对上而言;
- 分组:是指在 IP 层和链路层之间传送的数据单元。一个分组可以是一个完整的 IP 数据报,也可以是 IP 数据报的一个分片,对下而言;
- 发生 ICMP 不可达差错的另一种情况是,当路由器收到一份需要分片的数据报,而在 IP 首部又设置了不分片(DF)的标志比特。
- 基于
traceroute
确定路径 MTU,要做的是发送分组,并设置“不分片”标志比特(需要自己改写 traceroute,类似于试探 MTU 大小) - 现在许多但不是所有的广域网都可以处理大于 512 字节的分组。利用路径 MTU 发现机制,应用程序就可以充分利用更大的 MTU 来发送报文。
- 最大 UDP 数据报长度:理论上,IP 数据报的最大长度是 65535 字节,这是由 IP 首部 16 比特总长度字段所限制的。去除 20 字节的 IP 首部和 8 个字节的 UDP 首部,UDP 数据报中用户数据的最长长度为 65507 字节。但是,大多数实现所提供的长度比这个最大值小,遇到两个限制因素:
- 系统调用库的限制:socket API 提供了一个可供应用程序调用的函数,以设置接收和发送缓存的长度。大部分系统都默认提供了可读写大于 8192 字节的 UDP 数据报
- TCP/IP 的内核限制:使 IP 数据报长度小于 65535 字节。
- 数据报截断:由于 IP 能够发送或接收特定长度的数据报并不意味着接收应用程序可以读取该长度的数据。因此,UDP 编程接口允许应用程序指定每次返回的最大字节数。如果接收到的数据报长度大于应用程序所能处理的长度,取决于编程接口和实现来处理超出的内容。
- UDP 服务器设计:通常一个客户启动后直接与单个服务器通信,然后就结束了。而对于服务器来说,它启动后处于休眠状态,等待客户请求的到来。对于 UDP 来说,当客户数据报到达时,服务器苏醒过来,数据报中可能包含来自客户的某种形式的请求消息。UDP 服务涉及内容:
- 源 IP 地址和端口号,方便给每个发送请求的客户发回应答
- 目的 IP 地址:一些应用程序需要知道数据报是发送给谁的,即目的 IP 地址,并非所有的 OS 实现都提供这个功能(这要求操作系统从接收到的 UDP 数据报中将目的 IP 地址交给应用程序)。
- UDP 输入队列:程序所使用的每个 UDP 端口都与一个有限大小的输入队列相联系,这意味着,来自不同客户的到达的请求将由 UDP 自动排队,接收到的 UDP 数据报以其接收顺序交给应用程序,排队溢出造成内核中的 UDP 模块丢弃数据报的可能性是存在的。关于 UDP 输入队列的几个要点:
- 应用程序并不知道其输入队列何时溢出。只是由 UDP 对超出数据报进行丢弃处理。
- 没有发回任何信息告诉客户其数据报被丢弃,这里不存在像 ICMP 源站抑制这样发回发送端的消息。
- 最后,看来 UDP 输出队列是 FIFO(先进先出)的,所看到的 ARP 输入却是 LIFO(后进先出)
- 限制本地 IP 地址:
- 比如限制服务器在 en0 接口(140.252.1.29)处接收数据报
- 有可能在相同的端口上启动不同的服务器,需要开启端口重用
- 限制远端 IP 地址
- 每个端口有多个接收者
- 指明 socket 选项
SO_REUSEADDR
,告诉系统应用程序重用相同的端口号(man 7 socket
)
- UDP 的 RFC 仅 3 页,
RFC 768
2. TCP:传输控制协议
2.1. 关键内容点
- 可靠性:分段、定时器-失败重发、接收 ack 确认、延迟确认、首部+数据数据校验、差错不确认丢弃、IP 失序重排、IP 包去重、发送,接收 buffer
- 字节流服务:对应用透明,数据可能被分多包、不对内容解释(二进制或 ASCII 字符或其他 UTF8 字符),应用层做协议解释(雷同 Linux 对文件系统交给应用程序处理)
- tcp 首部,20 字节(IP 首部也是 20 字节):
- 4 字节(源、目标端口)- 进程关联
- 4 字节(2^32-1)seq 序号 - ISN(Initial Sequence Number)、FIN,SYN 包都消耗一个 ISN
- 4 字节(2^32-1)ack 序号 - 期望下次收到的序号
- 2 字节(4bit 首长、6bit 保留、6bit 标志位-URG|ACK|PSH|RST|SYN|FIN),2 字节滑动窗口
- 2 字节校验和、2 字节 URG 指针
- 4 字节可选字段
- 全双工、没有选择确认或否认滑动窗口协议:
- 后续包先到,SEQ 序列跳过,TCP 仅发送之前确认接收到的段
- 校验出错,TCP 也 ACK 之前确认接收的段
- TCP 流控:滑动窗口、窗口大小(期望接收的字节大小)、16bit(合计 65535 字节)
- 校验和:发端计算和存储,收端验证
- URG 指针:仅在 URG 标志为 1 可用,ISN+URG 正向偏移=最后一个字段的序号
- 可选项:MSS(Maximum Segment Size),最长报文大小,首个 SYN 报文段中指明
- 数据项:可选,TCP 连接建立、终止、处理超时,不会发生任何报文段
- 报文段:TCP 对用户数据的打包
2.2. 细节概述
- 如何建立和终止一个 TCP 连接、数据传输过程,批量数据传送、TCP 超时及重传的技术细节、定时器、TCP 新的特性以及 TCP 的性能
- 尽管 TCP 和 UDP 都使用相同的网络层(IP),TCP 却向应用层提供与 UDP 完全不同的服务。TCP 提供一种面向连接的、可靠的字节流服务。
- 打电话:面向连接意味着两个使用 TCP 的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个 TCP 连接。
- 在一个 TCP 连接中,仅有两方进行彼此通信,广播和多播不能用于 TCP
- 可靠性保障通过:
- 应用数据被分割成 TCP 认为最适合发送的数据块(UDP 数据报长度保持不变),TCP 传递给 IP 的信息单位称为报文段或段(segment)
- 超时及重传策略(启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。)
- 发送接收确认(当 TCP 收到发自 TCP 连接另一端的数据,稍后它将发送一个确认)
- 校验和差错检测,出错 TCP 丢弃该包(首部和数据的检验和)
- 乱序重排(TCP 将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层)
- 丢弃重复 IP 数据包
- 流量控制,避免较慢主机缓冲区溢出
- 字节流服务:两个应用程序通过 TCP 连接交换字节构成的字节流,但 TCP 不在字节流中插入记录标识符,我们将这称为字节流服务(byte stream service),一端将字节流放到 TCP 连接上,同样的字节流将出现在 TCP 连接的另一端。TCP 对字节流的内容不作任何解释。TCP 不知道传输的数据字节流是二进制数据,还是 ASCII 字符、EBCDIC 字符或者其他类型数据。对字节流的解释由 TCP 连接双方的应用层解释。
- TCP 数据被封装在一个 IP 数据报中,TCP 首部的数据格式通常为 20 字节
- 每个 TCP 段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上 IP 首部中的源端 IP 地址和目的端 IP 地址唯一确定一个 TCP 连接。
- TCP 包首部结构:
- TCP 首部包结构说明:
- 伯克利编程接口:一个 IP 地址和一个端口号也称为一个插口(socket),插口对(socketpair)(包含客户 IP 地址、客户端口号、服务器 IP 地址和服务器端口号的四元组)可唯一确定互联网络中每个 TCP 连接的双方。(RFC793)
- 序号:用来标识从 TCP 发端向 TCP 收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。(序号是 32 bit 的无符号数,序号到达 232-1 后又从 0 开始。)
- 确认序号:包含发送确认的一端所期望收到的下一个序号,因此,确认序号应当是上次已成功收到数据字节序号加 1,且仅当 ACK 标识为 1 时候确认序号字段才有效
- 首部长度:指明 TCP 首部长度(任选字段的长度是可变,正常是 TCP 首部是 20 字节)
- 首部 6bit 标志:URG、ACK、PSH、RST、SYN、FIN
- 16bit 滑动窗口:用于流量控制,故最大可以 65535 字节
- 校验和
- 指针偏移量:紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。
- 可选字段:MSS(Maximum Segment Size),通常在 SYN 段中指定,指明本端所能接收的最大长度的报文段
- 建立一个新的连接时,SYN 标志变 1,初始序号 ISN(Initial Sequence Number),由于 SYN 标志消耗了一个序号,故发送数据的第一个字节序号为这个 ISN 加 1;同一样,断开连接 FIN 标志也要占用一个序号。
- TCP 为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。
- TCP 可以表述为一个既不确认也不否认的滑动窗口协议:
- 发端虽然在 TCP 首部包含了确认序号,但当前还无法对数据流中选定的部分进行确认。
- 收端无法对一个报文段进行否认,比如检验和错,TCP 接收端所能做的就是发回上一个确认序号的 ACK,让发端重传。
- TCP 提供了一种可靠的面向连接的字节流运输层服务,TCP 将用户数据打包构成报文段;它发送数据后启动一个定时器;另一端对收到的数据进行确认,对失序的数据重新排序,丢弃重复数据;TCP 提供端到端的流量控制,并计算和验证一个强制性的端到端检验和。
3. TCP:连接的建立和终止
3.1. 关键内容点
- TCP、面向连接的协议、必须建立连接,无连接协议 UDP 发送数据报时,无需握手
- 连接建立(3 次握手)
- tcpdump 输出说明,win4096:窗口大小、mss1024:由发端指明的最大报文段长度选项,避免分段
- ISN、初始化 SYN 报文、ACK 确认标识
- 发送 SYN 主动打开,接收 SYN 被动打开、同时执行主动打开
- ISN,32bit(TCP 首部),ISN 每 4ms 加 1
- 连接终止(4 次挥手):
- TCP 的半关闭(halfclose)、全双工
- FIN 报文也占一个 1ISN
- FIN 的 ACK 是由 TCP 软件自动产生的,同时向应用程序交付 EOF
- 连接建立超时:默认超时时间,75s, BSD 版的 TCP 软件采用一种
500 ms的定时器
- 最大报文长度 MSS 只能出现在 SYN 报文段中,期望接收的 MSS,不接收,则设置为默认值 536 字节
- MSS 越大,每个报文段传送的数据就越多,有更高的网络利用率
- MSS 值设置为外出接口上的 MTU 长度减去固定的 IP 首部(20)和 TCP 首部(20)长度。对于一个以太网 MTU(1500),MSS 值可达 1460 字节。
- MSS 默认值为 536 字节
- MSS 让主机限制另一端发送数据报的长度,方便以较小 MTU 连接到一个网络上的主机避免分段
- TCP 半关闭,全双工,仅关闭发送端,还可以接收数据
- 半关闭可以作为,客户通知服务器完成了它的数据传送的一种方式
- 状态变迁:主动打开、被动打开、主动关闭、被动关闭
- SYN,SYN_SENT
- SYNACK, SYN_RCVD
- ACK, ESTABLISHED
- FIN, FIN_WAIT_1, CLOSE_WAIT
- ACK, FIN_WAIT_2, LAST_ACK
- TIME_WAIT、CLOSING、LISTEN、CLOSED
- TIME_WAIT 状态也称 2MSL(Maximum Segment Lifetime)等待状态
- 2min、1min、30s
- 作用:可让被动断开一方的 FIN 失败重传时候,可以有机会再次发送最后的 ACK 以防这个 ACK 丢失,若重复的 FIN 会得到确认,2MSL 定时器重新开始
- 副作用:
- Socket 无法重用,需要过 2MSL 时间限制(或者开启 SO_REUSEADDR,不推荐)
- 服务器通常执行被动关闭,不会进入 TIME_WAIT 状态,客户使用本地端口,也并不关心这个端口号是什么
- 服务器使用熟知端口,重新启动服务器程序前,它需要在 1~4 分钟(MSL 为 30s~2min)。
- SO_REUSEADDR, 允许一个进程重新使用仍处于 2MSL 等待的端口,但 TCP 不能允许一个新的连接建立在相同的插口对上,因为定义这个连接的 Socket 对仍处于 2MSL 等待状态。
- 违背原则:通过修改 socket 四元组,允许一个新的连接请求到达仍处于 TIME_WAIT 状态的连接,只要新的序号大于该连接前一个替身的最后序号(比如更换主机)
- TIME_WAIT 状态时忽略 RST 段
- 平静时间:RFC 793 指出 TCP 在重启动后的 MSL 秒内不能建立任何连接
- 防止这种在 FIN_WAIT_2 状态的无限等待,连接空闲 10 分钟 75 秒,TCP 将进入 CLOSED 状态(违背协议规范)
- 基准的连接(socket 连接)出现错误,引发 TCP 发送 RST 复位报文段:
- 到不存在的端口的连接请求:UDP 收到 ICMP 端口不可达,TCP 收到 RST 的 ACK 响应
- 异常终止连接:非有序释放,异常释放(丢弃未发送数据,发送 RST 段,RST 接收区分是否正常,应用程序使用的 API 必须提供产生异常关闭而不是正常关闭的手段。)
- 半打开连接(Half-Open):
- 一方已经关闭或异常终止连接而另一方却还不知道,注意区别于半关闭连接(关闭方知情)(掉电、断网)
- 可以通过 keepalive 选项发现
- 同时打开(simultaneous open),双方都执行被动打开,仅开启一条通路(TCP 特意为止)
- 同时关闭(simultaneous close),双方都执行主动关闭,均从 ESTABLISHED 变为 FIN_WAIT_1、CLOSING、TIME_WAIT
- TCP 选项
- TCP 服务器的设计
- 并发设计:客户请求进入,调用一个新进程或线程来处理这个新的客户请求(fork 进程或线程处理)
- tcp 服务端口:TCP 使用由本地地址和远端地址组成的 4 元组:目的 IP 地址、目的端口号、源 IP 地址和源端口号来处理传入的多个连接请求
- 限定的本地 IP 地址:非本地主机请求,导致连接被内核中的 TCP 模块拒绝
- 限定的远端 IP 地址,大多数 API 都不支持这么做
- 呼入连接请求队列,服务进程始终准备处理下一个呼入的连接请求,当到达多个连接请求,当服务器正处于忙时,TCP 是如何处理这些呼入的连接请求?(伯克利的 TCP 原则:)
- 等待连接请求的一端,有固定长度的连接队列,队列存放 tcp 握手完成但应用层未接受连接(注意:TCP 接受,放入队列,应用层接受,移除队列)
- 应用层通过
backlog
积压值,指定队列最大长度(BSD 被设置成 backlogx3/2+1),积压值对系统所允许的最大连接数,或者并发服务器所能并发处理的客户数,并无影响 - 当连接请求 SYN 到达,TCP 根据算法是否接收该连接:
- 若队列未满,TCP 模块接收 SYN,完成握手,放入请求队列(此时若应用未处理该请求连接,客户端发送的数据被存储在缓冲队列中)
- 若队列已满,TCP 模块不处理 SYN(不接收,也不拒绝 - 软错误),可能被服务端处理(按 LIFO 的模式),也可能客户端主动打开 SYN 超时
- 通常队列已满是由于应用程序或操作系统忙造成的,这样可防止应用程序对传入的连接进行服务。
3.2. TCP 首部标识
- TCP 是一个面向连接的协议,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接,因此搞清楚 TCP 连接是如何建立的以及通信结束后是如何终止是必须知道事情。
- 使用 UDP 向另一端发送数据报时,无需任何预先的握手。
- TCP 首部标识,多个可能同时出现在同一个报文:
- S: SYN,同步序号
- F: FIN,发送方完成数据发送
- R: RST,复位连接
- P: PSH,尽可能快地将数据推送到接收进程
- .: SFRP 标志位皆置为 0
- A: ACK
- U: URG
3.3. 常规 TCP 连接建立和终止
-
- 源>目的:标志位
- 1415531521:1415531521(0):开始的序号、一个冒号、隐含的结尾序
- 结尾序显示(1、2、4、6报文),在这个例子中通信双方没有交换任何数据。
- 报文段中至少包含一个数据字节
- SYN、FIN或RST被设置为1时才显示
- 第2行中,字段ack1415531522表示确认序号(ACK标志比特被设置1时才显示)
- 每行显示的字段win4096表示发端通告的窗口大小(默认情况下的4096)
-
表示由发端指明的最大报文段长度选项,发端将不接收超过这个长度的TCP报文段,这通常是为了避免分段。 - 建立 TCP 连接(三次握手),三个报文段完成连接的建立,
- 请求端/客户端,发送 SYN 段,指明客户打算连接的服务器的端口,以及初始序号 ISN(报文段 1)
- 服务器发回包含服务器的初始序号的 SYN 报文段(报文段 2)作为应答,同时,将确认序号设置为客户的 ISN 加 1 以对客户的 SYN 报文段进行确认,一个 SYN 将占用一个序号。
- 客户必须将确认序号设置为服务器的 ISN 加 1 以对服务器的 SYN 报文段进行确认(报文段 3)
- 发送第一个 SYN 的一端将执行主动打开(active open),接收这个 SYN 并发回下一个 SYN 的另一端执行被动打开(passive open)
- 当一端为建立连接而发送它的 SYN 时,它为连接选择一个初始序号。ISN 随时间而变化,因此每个连接都将具有不同的 ISN。RFC 793[Postel 1981c]指出 ISN 可看作是一个 32 比特的计数器,每 4ms 加 1。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它作错误的解释。在 4.4BSD(和多数的伯克利的实现版)中,系统初始化时初始的发送序号被初始化为 1,每 0.5 秒增加 64000,并每隔 9.5 小时又回到 0(对应这个计数器每 8 ms 加 1,而不是每 4 ms 加 1),另外,每次建立一个连接后,这个变量将增加 64000。
- 断开 TCP 连接(四次挥手),这由 TCP 的半关闭(halfclose)造成的:
- 键入 quit 命令后发生,导致 TCP 客户端发送一个 FIN,用来关闭从客户到服务器的数据传送。(报文 4)
- 当服务器收到这个 FIN,它发回一个 ACK,确认序号为收到的序号加 1(报文段 5)(和 SYN 一样,一个 FIN 将占用一个序号),同时 TCP 服务器还向应用程序(即丢弃服务器)传送一个文件结束符。
- 接着这个服务器程序就关闭它的连接,导致它的 TCP 端发送一个 FIN(报文段 6)
- 客户必须发回一个确认,并将确认序号设置为收到序号加 1(报文段 7)
- TCP 连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接。当一端收到一个 FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。发送 FIN 通常是应用层进行关闭的结果。
- TCP 的一端收到一个 FIN 只意味着在这一方向上没有数据流动,一个 TCP 连接在收到一个 FIN 后仍能发送数据。
- 首先进行关闭的一方(即发送第一个 FIN)将执行主动关闭,而另一方(收到这个 FIN)执行被动关闭。
- 有很多情况导致无法建立 TCP 连接:
- 服务器主机没有处于正常状态,客户端将进行 SYN 连接重试,大多数伯克利系统将建立一个新连接的最长时间限制为 75 秒
3.4. TCP 的状态变迁图
- 说明:
- 粗的实线箭头表示正常的客户端状态变迁,用粗的虚线箭头表示正常的服务器状态变迁。
- 两个导致进入 ESTABLISHED 状态的变迁对应打开一个连接,而两个导致从 ESTABLISHED 状态离开的变迁主动打开对应关闭一个连接,ESTABLISHED 状态是连接双方能够进行双向数据传递的状态。
- 左下角 4 个状态放在一个虚线框内,并标为“主动关闭”
- 右下两个状态(CLOSE_WAIT 和 LAST_ACK)也用虚线框住,并标为“被动关闭”
- CLOSED 状态不是一个真正的状态,而是这个状态图的假想起点和终点,11 个状态的名称(CLOSED,LISTEN,SYN_SENT 等)是有意与 netstat 命令显示的状态名称一致。
- 从 LISTEN 到 SYN_SENT 的变迁是正确的,但伯克利版的 TCP 软件并不支持它。
- 当 SYN_RCVD 状态是从 LISTEN 状态(正常情况),而不是从 SYN_SENT 状态(同时打开)进入时,从 SYN_RCVD 回到 LISTEN 的状态变迁才是有效的。这意味着如果我们执行被动关闭(进入 LISTEN),收到一个 SYN,发送一个带 ACK 的 SYN(进入 SYN_RCVD),然后收到一个 RST,而不是一个 ACK,便又回到 LISTEN 状态并等待另一个连接请求的到来。
3.5. 同时主动打开 TCP 连接
两个应用程序同时彼此执行主动打开的情况是可能的,尽管发生的可能性极小。每一方必须发送一个 SYN,且这些 SYN 必须传递给对方,这需要每一方使用一个对方熟知的端口作为本地端口,这又称为同时打开(simultaneous open)。 一个同时打开的连接需要交换 4 个报文段,比正常的三次握手多一个。此外,要注意的是我们没有将任何一端称为客户或服务器,因为每一端既是客户又是服务器。
- 两端几乎在同时发送 SYN,并进入 SYN_SENT 状态
- 当每一端收到 SYN 时,状态变为 SYN_RCVD,同时它们都再发 SYN 并对收到的 SYN 进行确认
- 当双方都收到 SYN 及相应的 ACK 时,状态都变迁为 ESTABLISHED。
3.6. 同时主动关闭 TCP 连接
双方都执行主动关闭也是可能的,TCP 协议也允许这样的同时关闭(simultaneous close)。
- 当应用层发出关闭命令时,双方各发送一个 FIN,两个 FIN 经过网络传送后分别到达另一端,两端均从 ESTABLISHED 变为 FIN_WAIT_1
- 当每一端收到 FIN 后,状态由 FIN_WAIT_1 变迁到 CLOSING,并发送最后的 ACK,对 FIN 进行确认
- 当双方都收到最后的 ACK 时,状态变化为 TIME_WAIT
3.7. 最大报文段长度(MSS)
- 服务类型字段,IP 数据报内的服务类型(TOS)字段,BSD/386 中的 Telnet 客户进程将这个字段设置为最小时延。
- 最大报文段长度(MSS)
- MSS 表示 TCP 传往另一端的最大块数据的长度,当一个连接建立时,连接的双方都要通告各自的 MSS,MSS 选项只能出现在 SYN 报文段中,起到协商左右,但如果一方不接收来自另一方的 MSS 值,则 MSS 就定为默认值 536 字节(这个默认值允许 20 字节的 IP 首部和 20 字节的 TCP 首部以适合 576 字节 IP 数据报)。
- IP 数据报通常是 40 字节长:20 字节的 TCP 首部和 20 字节的 IP 首部。
- 当 TCP 发送一个 SYN 时,或者是因为一个本地应用进程想发起一个连接,或者是因为另一端的主机收到了一个连接请求,它能将 MSS 值设置为外出接口上的 MTU 长度(1500)减去固定的 IP 首部(20)和 TCP 首部长度(20)。对于一个以太网,MSS 值可达 1460 字节。使用 IEEE 802.3 的封装(参见 2.2 节),它的 MSS 可达 1452 字节。
- 许多 BSD 的实现版本需要 MSS 为 512 的倍数,其他的系统,如 SunOS 4.1.3、Solaris 2.2 和 AIX 3.2.2,当双方都在一个本地以太网上时都规定 MSS 为 1460。
3.8. TCP 的半关闭
- TCP 的半关闭:TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力
- 如果应用程序不调用 close 而调用 shutdown,且第 2 个参数值为 1,则插口的 API 支持半关闭。然而,大多数的应用程序通过调用 close 终止两个方向的连接。
- 执行排序命令:
sun % rsh bsdi sort < datafile
- 在主机 bsdi 上执行 sort 排序命令,rsh 命令的标准输入来自文件 datafile。rsh 将在它与在另一主机上执行的程序间建立一个 TCP 连接。rsh 的操作很简单:它将标准输入(datafile)复制给 TCP 连接,并将结果从 TCP 连接中复制给标准输出(我们的终端)。
- 在远端主机 bsdi 上,rshd 服务器将执行 sort 程序,它的标准输入和标准输出都是 TCP 连接。
- sort 程序只有读取到所有输入数据后才能产生输出。所有的原始数据通过 TCP 连接从 rsh 客户端传送到 sort 服务器进行排序。当输入(datafile)到达文件尾时,rsh 客户端执行这个 TCP 连接的半关闭。
- 接着 sort 服务器在它的标准输入(这个 TCP 连接)上收到一个文件结束符,对数据进行排序,并将结果写在它的标准输出上(TCP 连接)。
- rsh 客户端继续接收来自 TCP 连接另一端的数据,并将排序的文件复制到它的标准输出上。
- 没有半关闭,需要其他的一些技术让客户通知服务器,客户端已经完成了它的数据传送,但仍要接收来自服务器的数据。使用两个 TCP 连接也可作为一个选择,但使用半关闭的单连接更好。
3.9. 2MSL 等待状态
- TIME_WAIT 状态也称为 2MSL 等待状态,每个具体 TCP 实现必须选择一个报文段最大生存时间 MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。我们知道这个时间是有限的,因为 TCP 报文段以 IP 数据报在网络内传输,而 IP 数据报则有限制其生存时间的 TTL 字段。
- RFC 793 [Postel 1981c]指出 MSL 为 2 分钟。然而,实现中的常用值是 30 秒,1 分钟,或 2 分钟。
- 2MSL 处理的原则是:当 TCP 执行一个主动关闭,并发回最后一个 ACK,该连接必须在 TIME_WAIT 状态停留的时间为 2 倍的 MSL,这样可让 TCP 再次发送最后的 ACK 以防这个 ACK 丢失(若另一端超时并重发最后的 FIN)
- 弊端:在 2MSL 等待期间,定义这个连接的 Socket 地址(客户的 IP 地址和端口号,服务器的 IP 地址和端口号)不能再被使用。这个连接只能在 2MSL 结束后才能再被使用。大多数 TCP 实现(如伯克利版)强加了更为严格的限制。在 2MSL 等待期间,Socket 中使用的本地端口在默认情况下不能再被使用。
SO_REUSEADDR
选项可以配置地址重用(有风险,违反了 TCP 规范),当要建立一个有效的连接时,来自该连接的一个较早替身(incarnation)的迟到报文段作为新连接的一部分不可能被曲解。- 客户执行主动关闭并进入
TIME_WAIT
是正常的。服务器通常执行被动关闭,不会进入 TIME_WAIT 状态。这暗示如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。(如果很高频次,容易导致客户端的端口资源耗尽) - 平静时间:TCP 在重启动后的 MSL 秒内不能建立任何连接。这就称为平静时间(quiet time)。
3.10. FIN_WAIT_2 状态
在 FIN_WAIT_2
状态我们已经发出了FIN
,并且另一端也已对它进行确认。除非我们在实行半关闭,否则将等待另一端的应用层意识到它已收到一个文件结束符说明,并向我们发一个 FIN
来关闭另一方向的连接。
只有当另一端的进程完成这个关闭,我们这端才会从 FIN_WAIT_2
状态进入 TIME_WAIT
状态。这意味着我们这端可能永远保持这个状态。另一端也将处于 CLOSE_WAIT
状态,并一直保持这个状态直到应用层决定进行关闭。
许多伯克利实现采用如下方式来防止这种在 FIN_WAIT_2
状态的无限等待,设置了定时器,如果这个连接空闲 10 分钟 75 秒,TCP 将进入 CLOSED 状态(这个实现代码违背协议的规范)。
3.11. RST 复位报文段
- TCP 首部中的 RST 比特是用于“复位”的。
- 无论何时一个报文段发往基准的连接(referenced connection)出现错误,TCP 都会发出一个复位报文段(这里提到的“基准的连接”是指由目的 IP 地址和目的端口号以及源 IP 地址和源端口号指明的连接。这就是为什么 RFC 793 称之为插口)。
- 复位报文段产生场景:
- 当连接请求不存在的端口
- 异常释放,异常终止一个连接,
- 检测半打开连接(SYN 握手一半)
- 有序释放:终止一个连接的正常方式是一方发送 FIN。有时这也称为有序释放(orderly release),因为在所有排队数据都已发送之后才发送 FIN,正常情况下没有任何数据丢失。
- 异常释放:可能发送一个 RST 复位报文段而不是 FIN 来中途释放一个连接,这为异常释放(abortive release)
- 丢弃任何待发数据并立即发送复位报文段
- RST 的接收方会区分另一端执行的是异常关闭还是正常关闭
- RST 报文段不会导致另一端产生任何响应,另一端根本不进行确认,收到 RST 的一方将终止该连接,并通知应用层连接复位。
- 场景异常释放的错误:
read error: Connect reset by peer
- 任何一端的主机异常都可能导致发生半打开(
Half-Open
)的情况,只要不打算在半打开连接上传输数据,仍处于连接状态的一方就不会检测另一方已经出现异常。(比如客户主机突然掉电、网络断开,服务器将永远不知道客户程序已经消失了) - TCP 的
keepalive
选项能使 TCP 的一端发现另一端已经消失
3.12. TCP 选项
- 常规的选项表结束、无操作和 MSS 最大报文段长度
- 最新的 TCP 实现中才能见到新的 TCP 选项(RFC1323)
3.13. TCP 服务器的设计
- TCP 服务器进程是并发的,当一个新的连接请求到达服务器时,服务器接受这个请求,并调用一个新进程来处理这个新的客户请求(不同的操作系统使用不同的技术来调用新的服务器进程)。在 Unix 系统下,常用的技术是使用 fork 函数来创建新的进程,如果系统支持,也可使用轻型进程,即线程(thread)
- 当一个服务器进程接受一来自客户进程的服务请求时是如何处理端口的?如果多个连接请求几乎同时到达会发生什么情况?
- TCP 服务器端口号、本地地址、远端地址、
LISTEN
状态,等待连接请求的到达 - 当传入的连接请求到达并被接收时,处于
LISTEN
状态的服务器进程仍然存在,同时系统内核中的 TCP 模块就创建一个处于 ESTABLISHED 状态的进程。 - TCP 使用由本地地址和远端地址组成的 4 元组:目的 IP 地址、目的端口号、源 IP 地址和源端口号来处理传入的多个连接请求。
- TCP 服务器端口号、本地地址、远端地址、
- 限定的本地 IP 地址:程序指明一个 IP 地址(或主机名),并将它作为服务器,那么该 IP 地址就成为处于
LISTEN
服务器的本地 IP 地址。(错误的连接请求,将被拒绝) - 限定的远端 IP 地址:
RFC 793
中显示的接口函数允许一个服务器在执行被动打开时,可指明远端插口(等待一个特定的客户执行主动打开),也可不指明远端插口(等待任何客户);大多数 API 都不支持这么做,因为没有什么太多意义,服务器必须不指明远端插口,而等待连接请求的到来,然后检查客户端的 IP 地址和端口号。
3.14. 呼入连接请求队列
3.14.1. TCP 有半连接队列和全连接队列是如何转换的?
半连接队列用于保存尚未完成三次握手的连接,而全连接队列用于保存已经建立起连接的连接,以供服务器端进行处理。
- 半连接队列(SYN 队列) -
listen
- 当客户端发送 SYN(同步)报文段给服务器端时,服务器端会将该连接放入半连接队列。
- 半连接队列中保存了已经收到 SYN 报文段的连接,但尚未完成三次握手的连接。
- 服务器端会向客户端发送 SYN-ACK(同步-确认)报文段,等待客户端的确认。
- 全连接队列(ACCEPT 队列) -
accept
- 当服务器端收到客户端发送的 SYN-ACK 报文段的确认后,将连接从半连接队列移动到全连接队列。
- 全连接队列中保存了已经完成三次握手的连接,即已经建立起连接的连接。
- 服务器端在全连接队列中等待处理这些连接,执行应用层的操作。
转换过程如下:
- 当服务器端从半连接队列中将连接移动到全连接队列时,表示该连接已经完成三次握手,即建立了完整的TCP连接。
- 在半连接队列(SYC队列)中,连接的状态为
SYN_RECV
(收到SYN报文段)。 - 在全连接队列中,连接的状态为
ESTABLISHED
(已建立)。 - 一旦连接进入全连接队列,服务器端可以通过该连接进行数据的传输和应用层的处理。 当连接的应用层操作完成后,服务器端会将连接从全连接队列中移除,释放连接资源
3.14.2. 半连接队列和全连接队列在Socket网络编程中如何控制?是通过Linux操作系统配置,还是通过应用软件编程代码配置?
操作系统配置和应用软件编程代码两种方式可以结合使用,但它们的作用范围不同
- Linux操作系统(全局): 修改系统内核参数来控制半连接队列和全连接队列的大小,管理权限重新启动网络服务或重新加载内核模块才能生效
net.ipv4.tcp_max_syn_backlog
: 修改半连接队列net.core.somaxconn
: 全连接队列
- 应用软件(系统): Socket编程时,控制半连接队列和全连接队列
listen()
函数来指定监听套接字,全连接队列的长度(配置backlog
) - 已被 TCP 接受而等待应用层接受(等待accept的连接)
3.14.3. 伯克利的 TCP 实现中采用以下规则,解决连接请求队列问题
服务端正等待连接请求的一端有一个固定长度的全连接队列,该队列中的连接已被 TCP 接受(即三次握手已经完成),但还没有被应用层所接受。(TCP 接受一个连接是将其放入这个队列,而应用层接受连接是将其从该队列中移出。)。
应用层将指明请求队列的最大长度,这个值通常称为积压值(
backlog
),积压值说明的是 TCP 监听的端点已被 TCP 接受而等待应用层接受的最大连接数。当一个连接请求(即 SYN)到达时,TCP 使用一个算法,根据当前连接队列中的连接数来确定是否接收这个连接。
- 如果对于新的连接请求,该 TCP 监听的端点的全连接队列中还有空间(连接队列未满),TCP 模块将对 SYN 进行确认并完成连接的建立。但应用层只有在三次握手中的第三个报文段收到后才会知道这个新连接时。另外,当客户进程的主动打开成功但服务器的应用层还不知道这个新的连接时,它可能会认为服务器进程已经准备好接收数据了(如果发生这种情况,客户端可能在发送数据了,服务器的 TCP 仅将接收的数据放入缓冲队列)。
- 如果对于新的连接请求,连接队列中已没有空间,TCP 将不理会收到的 SYN。也不发回任何报文段(即不发回 RST)。如果应用层不能及时接受已被 TCP 接受的连接,这些连接可能占满整个连接队列,客户的主动打开最终将超时。
示例
- 1090、1091 都是正常的 TCP 成功
- 1092、1093 由于 TCP 请求队列已满,出现 SYN 重试
- 1093 率先重试 SYN 连接成功,后续 1092 也重试成功(我们期望接收连接队列按先进先出顺序传递给应用层,许多伯克利的 TCP 实现都出现按后进先出的传递顺序,这个错误已存在了多年)
backlog 连接队列说明:当队列已满时,TCP 将不理会传入的 SYN,也不发回 RST 作为应答,因为这是一个软错误,而不是一个硬错误。通常队列已满是由于应用程序或操作系统忙造成的,这样可防止应用程序对传入的连接进行服务。这个条件在一个很短的时间内可以改变。但如果服务器的 TCP 以系统复位作为响应,客户进程的主动打开将被废弃(如果服务器程序没有启动我们就会遇到)。由于不应答 SYN,服务器程序迫使客户 TCP 随后重传 SYN,以等待连接队列有空间接受新的连接。
3.15. 小结
- 两个进程在使用 TCP 交换数据之前,它们之间必须建立一条连接,完成后,要关闭这个连接,详解了该过程;
- 利用 tcpdump 程序显示了 TCP 首部中的各个字段,也了解了连接建立是如何超时,连接复位是如何发送,使用半打开连接发生的情况以及 TCP 是如何提供半关闭、同时打开和同时关闭。
- 弄清 TCP 操作的关键在于它的状态变迁图。我们跟踪了连接建立与关闭的步骤以及它们的状态变迁过程。还讨论了在设计 TCP 并发服务器时 TCP 连接建立的具体实现方法。
- 一个 TCP 连接由一个 4 元组唯一确定:本地 IP 地址、本地端口号、远端 IP 地址和远端端口号。
- 无论何时关闭一个连接,一端必须保持这个连接,我们看到 TIME_WAIT 状态将处理这个问题。处理的原则是执行主动打开的一端在进入这个状态时要保持的时间为 TCP 实现中规定的 MSL 值的两倍。