What happens when (二)TCP
一、 TCP/IP 协议栈
TCP/IP 协议栈,我们都很熟悉。应用层、传输层、网络层、接口层(数据链路层),如此多的协议,在访问网页时这些协议间是怎样相互配合的?
如前文所述,当用户输入 URL 按下回车键后,主机首先要完成 DNS 解析,使用应用层的 DNS 协议。同时,解析过程中可能会用到网络层的 ARP 协议,以寻找 DNS 服务器。解析完成,得到目标 IP 地址,计算机会用套接字(Socket)与对方建立 TCP 连接。
网络层通过 IP 协议寻找到目标主机,三次握手后,主机与服务器建立端到端的 TCP 通信通道。主机会发送 HTTP 请求,它通过 TCP 通道发送到服务器的对应端口。在此过程中,应用层生成一个 HTTP 报文(Message),向下发送到传输层。传输层为请求添加 TCP 首部,成为 TCP 数据段(TCP segment)。之后网络层再添加 IP 首部,成为 IP 数据包(IP packet)。数据通过封装与路由转发,到达目标地址,再由目标主机的数据链路层、网络层、传输层、应用层依次解析并传递上去。
IP 首部以源地址与目的地址开头,TCP 首部以源端口与目的端口开头,分别承担网络层与传输层的职责。
二、TCP 首部
TCP 首部内容
根据 RFC793 对 TCP 的定义,TCP 的首部内容主要有:
- 源端口、目的端口(Source Port, Destination Port):发送方与接收方的端口号,各 16 位 2 字节;
- 序列号(Sequence Number):32 位。当 SYN 标志位为 1 时,序列号为随机算法生成的初始序号(ISN)。序列号减去 ISN 得到相对序列号。
- 确认号(Acknowledgment Number):32位,当 ACK 标志位为 1 时,表示发送方希望接受到的下一个序列号。确认号一般是最后接收到的 TCP 数据段的 序列号+数据长度(字节),可以表示该序列号及之前的数据段已收到。当连接建立后,TCP 数据段总包含确认号。
- 数据偏移(Data Offset):4位,指数据起始处距离 TCP 报文段起始处长度,以 32 位字( 4 字节)为计算单位。
- 保留与标志位(Reserved, Control Bits):共 12 位,在 RFC793 中原本有 6 个保留位,6 个标志位。 RFC3168 将后两个保留位替换为 CWR 与 ECE 标志,用于 ECN(显式拥塞通知)。之后在 RFC3540 将倒数第三个保留位替换为 NS 标志,同样用于 ECN。当前共有 3 个保留位,9 个标志位。标志位分别是:
- NS(Nonce Sum)
- CWR(Congestion Window Reduced)
- ECE(ECN-Echo)
- URG(Urgent):紧急指针标志
- ACK(Acknowledgment):确认标志
- PSH(Push):推送标志,尽快将数据转由应用处理
- RST(Reset):重置连接标志
- SYN(Synchronize):同步序号标志
- FIN(Finish):结束标志
- 窗口(Window):窗口大小(详见滑动窗口)
- 检验和(Checksum):校验报文
- 紧急指针(Urgent Pointer):指向紧急数据后的字节
- 选项(Options):如规定最大报文段长度(MSS)
- 填充:凑整,使长度为4字节的整数倍
Wireshark 抓包
Wireshark 是常用的抓包工具,使用这个工具查看 TCP 首部。官方网站:www.wireshark.org
启动Wireshark,双击连接名称,可以进行抓包。左上角控制开始停止,下一栏是过滤器,可以过滤报文,如输入 “dns” ,”ip.addr == 192.168.0.1”.
右击记录,可以追踪流,获取与目标的全部会话。会话记录可以保存。单击一条记录,会有对这条记录的分析。再下面是数据包内容,用 16 进制表示。数据包封装有数据链路层协议、IP 协议、TCP 协议等内容,TCP 协议一般在最后。
以访问 www.baidu.com 为例,观察建立 TCP 连接过程。访问前开始捕获,待页面加载完成后停止。筛选条件为 “ip.addr == 61.135.169.125” 的记录,按编号排序,最上方是三次握手建立连接的过程。
选择一项,右键追踪流->TCP,列出该 TCP 连接的全部记录。首先是本机 2567 端口向目标 443 端口发送 SYN = 1 ,其序号为随机生成的初始序号ISN 1562792475:
主机收到 SYN=1 ACK=1 的回应后,发送 ACK=1 的回应,三次握手,连接建立。
建立连接后的每个 TCP 数据段,都会有确认号。两边的确认号分别计算,如:
- A 向 B 发送序号 22 确认号 36 长度 5 的 TCP 数据段
- B 向 A 发送序号 36 确认号 27 长度 11 的 TCP 数据段
- A 向 B 发送序号 27 确认号 47 长度 6 的 TCP 数据段
”三次握手“的前两次,不携带任何数据,长度为零,但都需要消耗一个序号。第三次可以携带数据内容。
三、三次握手、四次挥手
三次握手
TCP 提供面向连接的、可靠的数据传输服务,在建立连接时采用三次握手的方式:
第一次:客户端 发送 SYN 标志的数据段
第二次:服务端 发送 SYN 与 ACK 标志的数据段
第三次:客户端 发送 ACK 标志的数据段
设第一次发送序号是 M,则服务端返回的确认号是 M+1;
设服务端返回的序号是 N, 则第三次握手的确认号是 N+1,序号是 M+1(与对方返回的确认号相同)。
第一次握手,服务端确认发送端的发送能力;第二次,发送端确认服务端的发送和接收能力;第三次,服务端确认发送端的接收能力。经过了三次握手,这条连接才是可靠的。
四次挥手
TCP 连接建立后会占用端口,故在连接结束时需要及时关闭。关闭的过程也被称作“四次挥手”。
当客户端主动关闭时:
第一次:客户端发送 FIN 标志数据段
第二次:服务端返回 ACK 标志数据段
第三次:服务端发送 FIN 标志数据段
第四次:客户端返回 ACK 标志数据段
- 客户端主动发送 FIN,表示数据发送结束,进入 FIN_WAIT_1(终止等待1)状态。
- 服务端接收到 FIN,返回 ACK 确认,进入 CLOSE_WAIT(关闭等待)状态。
- 客户端接收到 ACK,进入 FIN_WAIT_2(终止等待2)状态。此时,连接半关闭,即客户端没有数据要发送,但如果服务端发送数据,客户端仍要接受。
- 服务端将数据发送完毕后,发送 FIN,进入 LAST_ACK(最后确认)状态。
- 客户端接收到 FIN,返回确认 ACK,进入 TIME_WAIT(时间等待)状态,等待两个最长报文寿命时间(MSL)后关闭连接。服务端收到最后的 ACK,立即关闭。
在客户端最后的等待两个 MSL 时间中,如果服务端没有收到 ACK,则会重传 FIN,客户端再重新发送 ACK,避免客户端已关闭而服务端持续等待 ACK 的情况。
四、TCP 如何确保可靠
传输确认
累计确认:TCP 连接接受到的常常是乱序的报文,接收方会在接受一定数量的 TCP 数据段后,才会发送一次确认,用确认号告诉对方自己接收到了的最后字节序号。发送方不用等待前一个报文段确认就可以发送下一个报文,这被称作连续ARQ协议。
重复累计确认:接收方收到中断的一段后面的段后,会发送中断处的确认号。发送方在收到三次相同的确认号便会重传。
超时重传:发送方会在发送时设置一个超时重传计数器(RTO,Retransmission Time Out),超时未收到确认则会重新发送。重传后若还是没有收到确认,则将超时计时器时间设为原来的两倍。
选择确认(SACK):在 TCP 连接采取累计确认时,如果丢失了一个分组的数据,即使接收方接收到这个分组之后的数据,这些数据也可能会被发送方重传,造成时间和资源的浪费。**SACK ** 允许接收方确认收到的不连续分组, 使接收方能告诉发送方哪些数据丢失,哪些数据重发了,哪些数据已经提前收到。SACK 选项不是强制的,需要在创建连接时在 TCP 首部选项声明。
最大分段大小:当建立 TCP 连接时,双方会在 TCP 首部的选项中声明各自的最大分段大小(MSS)。在发送时,发送方会把数据分为适当大小的段来发送。
校验和:计算报文头部与数据的和,再求反码,得到校验和。
流量控制与滑动窗口
TCP 使用滑动窗口实现流量控制,接收方在发送确认的 TCP 首部窗口处声明还可以接收的字节数量,发送方最多只能发送接收窗口大小的字节。这个数量受保存数据的缓冲区大小限制。接收方可以修改接收窗口的值,在确认包告知发送方。
根据连续ARQ协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。当发送方收到第一个分组的确认,就把发送窗口向前移动一个分组的位置。如果原来已经发送了前5个分组,则现在可以发送第6个分组。
如果接收窗口值为 0,则需要应用来接收、清空缓存区,才可以继续接受数据。当接收窗口为 0 时,发送方不发送数据,开启一个定时器。一段时间后,恢复发送一个 ZWP 包,期待对方回复一个带有新的接收窗口的确认包。
如果接收方总在出现较小字节的空闲窗口时,就发送确认,会让每次发送的数据量很小,甚至小于 TCP 首部大小,这被叫做 “糊涂窗口综合症” ( Silly Window Syndrome 奇葩名字)。故发送方和接收方都会通过算法(如 Nagle算法、延迟ACK)来修正窗口大小,发送方默认延迟发送,接收方等待窗口大小足够时再确认。
Nagle 算法: TCP 的发送方使用 Nagle 算法,限制传输过程中的小分组(小于 MSS最大报文段长度)的数量,确保只能有一个未收到确认的小分组,并期望将小分组合并为较大的分组一起发送。
拥塞控制
流量控制针对端到端的通信量,而拥塞控制涉及到整个的网络环境。 发送方与接收方根据确认包或者包丢失的情况,以及定时器,估计网络拥塞情况,从而修改数据流的行为,这称为拥塞控制。
TCP 的拥塞控制采用了四种算法,即慢启动,拥塞避免,快重传和快恢复。
慢启动 (slow start) 与 拥塞避免 (congestion avoidance):慢启动的目的是在传输开始时确定网络带宽大小。双方都拥有一个拥塞窗口参数(cwnd),从一个低初始值开始以指数增长,达到慢启动阈值(ssthresh),切换至拥塞避免算法,以线性增长。发送数据的流量最大值是接收窗口与拥塞窗口中较小的数值。
一旦发生拥塞,将慢启动阈值降低为窗口的一半,如果是超时重传,拥塞窗口被设置为1个报文段,重新进入慢启动。
快速重传 (Fast Retransmit) 与快速恢复 (Fast Recovery):快重传,即在收到大于三个重复 ACK 时即开始重传,之后进入快恢复,慢启动阈值降为窗口的一半,拥塞窗口为慢启动阈值+收到重复 ACK 的数量。
粘包现象
发送方使用 Nagle 算法将多个小分组合并为一个 TCP 数据段发送,接收方接受并存储在缓冲区的数据被应用从中间断开取走,都会造成不同包的数据粘连在一起,称作粘包现象。
解决方法:
- 使用 TCP_NODELAY 选项关闭 Nagle 算法
- 格式化数据,让每条数据都有相同的开头和结尾,便于读取
- 发送数据的长度,便于完整读取每条数据
五、TCP 与 UDP 应用
UDP(User Datagram Protocol):
相对于 TCP 协议,UDP 协议看起来更简单,只是给数据添加了源端口和目的端口。UDP 首部的长度与校验和是可选项,这样的一个 UDP 包被称为 UDP 数据报(UDP Datagram)。
UDP 有以下特征:
- 不保证消息交付:不确认,不重传,无超时;
- 不保证交付顺序:不设置包序号,不重排,不发生队首阻塞;
- 不跟踪连接状态:不必建立连接或重启状态机;
- 不需要拥塞控制:不内置客户端或网络反馈机。
TCP 提供面向连接、可靠的服务,UDP 提供面向消息、无连接的服务,尽最大努力交付而不保证可靠。因为这样的特性,TCP 是点到点的连接,UDP 可以一对一、一对多、多对一、多对多。UDP 不存在拥塞控制,资源消耗较小,传输速度相对较快,但应用场景较少,多用于实时应用,如音视频通话。腾讯QQ 使用 UDP 协议传输消息,这就是为什么会碰到能上 QQ 但打不开网页的问题。(DNS 配置不正确)
此外, HTTP、HTTPS、FTP、SMTP(简单邮件传输协议) 等,都是 TCP 的应用。
接下来第三篇是关于 HTTP 的,不知道又会写多久…
参考资料
更多阅读: