Back to Posts

浅析TCP协议

Posted in Tech

谈到TCP连接,三次握手这个名词都会在大多数人的脑海里浮现。

那么三次握手的过程是什么样的?每个阶段的包都有什么样特征?

最近把《计算机网络》这本教材的”运输层”这一章重新复习一遍,整理成本文。

TCP连接的特点

面向连接

在使用TCP传输数据之前,必须先建立TCP连接,每一条连接只能连接两个端点,一般我们使用以下四元组来标识一条连接

{(端点1IP, 端点1Port), (端点2IP, 端点2Port)}

TCP连接只是一条虚拟的连接,他并不是固定的一条物理链路

可靠交付

TCP提供可靠的交付服务,数据无差错,不丢失,不重复,并且按序到达(对于应用层来说)

与之相对,UDP提供尽最大努力交付

全双工通信

所谓全双工,是指在同一时间,两个端点皆可发送数据和接收数据。

TCP在两端都设置有接收和发送缓冲区:

对于接收缓冲区,它是用来缓存接收到的数据,应用层可以在合适的时候读取缓冲区的内容;

对于发送缓冲区,应用层在发送数据时,只是将数据先放入到发送缓冲区,TCP会在合适的时间将这些数据发送出去。

面向字节流

TCP将应用层交下来的数据看成一连串无结构的字节流,应用层不必关心这些字节流被拆成多少个包来发送,接收方在应用层看到的是和发送端一样大小,一样顺序的字节流

TCP报文段的格式

虽然TCP是面向字节流的,但传输单元实际上是报文段,报文段可分为首部和数据两部分

以下是TCP报文段首部格式

TCP报文段首部格式

TCP首部的前20个字节是固定的,后续可以跟4N个可选的选项数据

  • 源端口和目的端口: 各占用2字节,所以端口号的范围是0~65535

  • 序号:占用4字节;TCP传输的数据每一个字节都是编号的,此序号表示本次发送的第一个字节编号;当序号增加到2的32次方减1(4G数据)后,又会从0开始

  • 确认号:占用4个字节,表示期望收到的下一个数据的编号,例如B正确收到了A发送过来的一个报文段,编号为501,数据长度为200,则B收到A的最后一个字节编码是700,期望收到的下一个数序序号为701,所以确认号为701

  • 数据偏移:占用4位,表示TCP数据部分距离起整个报文起始位置的偏移,此值的单位为4字节,所以选项的最大长度为

15 * 4字节 - 首部固定长度20 = 40字节

  • 保留:占用6位,一直没有使用,应置成0。但在Wireshark中可以看到保留位的后3位有特别的用途,分别是Nonce,Congestion Window Reduced(CWR),ECN-Echo。作用可能与滑动窗口、拥塞控制有关

  • 紧急URG(URGent):占用1位,表明紧急指针字段有效,做为传输高优先级数据使用。

  • 确认ACK(ACKnowlegment):占用1位,仅当ACK=1时,确认号才有效;连接建立后,所有的报文ACK必须置成1

  • 推送PSH(PuSH):占1位,可以让TCP尽快传送,接收到数据后尽快交付给应用程序,而不是等缓冲区满了再向上交付。一般地,如果用户调用一次Write后,都会在这次写入数据的最后一个包补上PSH=1

  • 复位RST(ReSeT):占1位,如果RST=1,表示此连接出现严重错误,必须释放连接。RST=1还可以用来拒绝一个非法的报文或拒绝打开一个连接

  • 同步SYN(SYNchronization):占1位,连接建立请求时用来同步初使编号

  • 终止FIN(Finish):占1位,用来释放一个连接,发送FIN=1表示发送方的数据已经发送完毕,并要求释放连接

  • 窗口:占2字节,表示发送此报文一方的接口收窗口,接收方依据此值设置自身的发送窗口

  • 校验和:占2字节,校验数据的正确性

  • 紧急指针:占2字节,只有当URG=1时才有效,表明此报文中紧急数据的字节数;紧急数据不受发送窗口限制

  • 选项:长度可变,最长为40字节

选项类别

每一种选项的格式都为:1字节类型(如2表示MSS,3表示扩大窗口,8表示时间戳等),1字节长度(包括之前1字节的类型和自己),实际数据(实际数据长度为长度减去2)

例如扩大窗口选项占3字节,第一字节表示类型,值为3,第二字节表示长度,值为3,第三字节为实际数据

  • 最大报文段长度选项MSS(Maximum Segment Size)

类型为2,占用4字节,数据段为2字节,即最大MSS为2的16次方减1
这里指的是数据段(不包括首部)的最大长度;若MSS过小,则信道利用率不高,若MSS过大,一但丢失就要整包重传,所以都会浪费资源
一般将MSS设置成IP层不被分片的值,本选项只能在SYN包里添加

  • 扩大窗口选项(Window Scale)

类型为3,占用3字节
用来扩展窗口字段,固定首部的窗口字段只有2字节,最大窗口大小为64k,在时延大的情况下不够用
数据字段为1字节,表示移位值S,即将原窗口值左移S位,得到新的窗口大小,最大允许的值为14

  • 时间戳选项(Time Stamp Option)

类型为8,点10字节 数据字段的4字节为时间戳,另外4字节为时间戳回送回答字段,主要做用:
a) 用来计算RTT值,发送方将发送时间写入时间戳字段,接收方确认时,将这个时间值复制进时间戳回送回答字段,发送方可通过这两个值算出RTT
b) 用来防止序号绕回(PAWS/Protect Against Wrapped Sequence numbers),在序号重复利用场景下,判断过期的包

其它选项类型

除了以上的选项类型,还有一些其它的类型,比如类型为0时表示选项结束;类型为1时表示无操作,一般用来做字节对齐(但不是必须的)

TCP的三次握手和四次挥手

TCP三次握手

连接的建立(三次握手)

在网络上,服务端需要监听某一个端口才能接受来自客户端的连接,此时服务端处理LISTEN状态

连接发起方(Client)首先发起连接请求报文,报文的SYN=1,同时选择一个初始的序号seq=x。此报文不包含任何数据,但需要消耗一个序号,此时Client进入SYN-SEND状态

服务端(Server)收到连接请求报文后,如果同意建立连接,则向Client发送确认报文,此报文包含SYN=1,ACK=1,确认号ack=x+1,并选择自己的起始序号seq=y,此报文同样消耗一个序号,Server进入SYN-RCVD状态

Client收到确认包后,再次发送确认报文,ACK=1,确认号ack=y+1,进入ESTABLISHED状态,即连接建立完成。此时发送的确认包可以包含数据,如果没有包含数据,纯ACK包是不消耗序号的

Server收到确认包后,也进入ESTABLISHED状态,双方建立连接完成

连接的释放(四次挥手)

数据传输的任意一方都可以主动要求释放连接

在需要释放连接时,发起方(A)发送连接释放报文FIN=1,seq=u,此处的u紧跟之前传送过的数据序号,进入FIN-WAIT-1状态,FIN包需要消耗一个序号

接收到FIN包的另一方(B)需要发送确认报文ACK=1,ack=u+1,表明确认收到了FIN包,进入CLOSE-WAIT状态,此时TCP连接处于半关闭状态,B依然可以发送数据

A收到确认包后,进入FIN-WAIT-2状态,等待B发完数据后发出FIN包

B发送完数据后,发送FIN=1,ACK=1,ack=u+1,seq=w,进入LAST-ACK状态

A收到B的FIN包后,发送FIN包的确认包ACK=1,ack=w+1,seq=u+1,进入TIME-WAIT状态,等待2MSL(Maximum Segment Lifetime)后才真正进入CLOSED状态,释放连接资源

对TIME-WAIT的理解

TIME-WAIT的时间一般是4分钟(但在linux上实际测试为1分钟),主要有以下目的

  1. 当A最后发送的确认ACK丢失后,B会超时重传最后一个FIN包,如果A没有等待2MSL,则无法收到重传的FIN包,也不会再次发送确认包,从而导致B无法正确进入CLOSED状态

  2. 等待2MSL后,本次连接中所有数据包基本都从网络中消失,避免了新的连接收到已经失效的报文

TCP的Keepalive机制

服务端会在一个保活周期(一般两个小时)内没有收到任何客户端数据的情况下,主动发送探测报文,以后每隔75分钟再发送一次,如果连续10次未来收到应答,则会主动关闭此连接

Wireshark抓包界面

TCP层数据汇总 整个连接过程

参考

RFC793 RFC1323

Read Next

住顺义