前言

对于写应用程序的同学来说,平时关注tcp可能不多,但其实我们每天都有在用,可靠信息的传递离不开tcp,比如我们通过浏览器访问一个页面,传输层便是使用的tcp,不同的http版本使用的方式略有不同。今天会比较基础的先引入tcp协议,包括协议首部格式,tcp常见状态机及三次握手四次挥手,最后介绍一些有关TCP常见的问题。

TCP协议首部格式

image-1701008871008

图片来源传送门
1字节=8bit

如图,前5行为固定大小,4字节*5=20字节;在 TCP Options下面是是数据部分,未画出。
TCP头部的头部需要满足32位(4字节)的整数倍,而数据部分没有限制

格式组成说明如下:

  • Source Port(源端口):发送端端口号,占16bit为2字节,可表示2^16(65535)个端口。不过端口0是被保留的,1-1023系统保留用于常见服务;1024-49151可能被某些应用所使用;49152-65535操作系统动态分配端口,一般用于客户端场景。

  • Dest Port(目标端口):接收端端口号,占16bit为2字节。

  • Sequence Number(序列号):序列号也叫序号,用于解决网络包乱序问题。它的初始值不是固定0或1,是在建立连接时通过虚拟时钟计算所得,每发送一次数据,就加上发送数据(不包括头部)字节数的大小。单位字节。

  • Acknowledgement Number(确认应答号):长度32位为4字节,指下一次应该收到的数据的序列号。即该应答号前一位及之前的数据已接收到。用来解决不丢包的问题。单位字节。

  • Offset(数据偏移):4位长度,最大表示15,单位4字节,最大表示60字节。该字段表示TCP所传输的数据应该从TCP包的哪个位开始计算。最小值为5,表示20字节,是没有Options部分的固定TCP长度。

  • Reserved(保留):用于以后扩展,可忽略。

  • TCP Flags(控制位):共8位,每一位有0,1两种状态,1表示打开,0表示关闭;从左到右分别是CWR、ECE、URG、ACK、PSH、RST、SYN、FIN。
    CWR(Congestion Window Reduced):设置1表示发送端响应接收端已将拥塞窗口减小,这样接收端就不同一直标志ECE字段了。
    ECE(ECN-Echo):接收端IP包如果检测到拥塞,会设置IP的ECN标识,接收端会在随后的TCP中设置ECE,响应发送方遭遇了拥塞。发送方在收到设置了ECE标志的TCP报文后,应该启动其拥塞控制机制(如减少其拥塞窗口大小),即使没有丢包发生。
    URG(Urgent Flag):设置1时表示有紧急需要处理的数据。结合“紧急指针”(指向紧急字段结尾的下一个数据)字段一起使用,对于紧急处理的数据不用顺序放到缓冲区,可以更快传给应用程序,例如中断指令等。
    ACK(Acknowledgement Flag):为1表示一个确认响应,意味着Acknowledgement Number(确认应答号)是有效的。
    PSH(Push Flag):加急处理,让接收端立即将数据送至应用层,减少延迟。
    RST(Reset Flag):为1表示原来的tcp连接不可用了,强制进行断开连接。
    SYN(Synchronize Flag):用于建立连接,为1表示希望建立连接,并完成Sequence Number初始化。
    FIN(Fin Flag):用于断开连接,为1表示不再发送数据,希望断开连接。

  • Window(窗口大小):又叫Advertised-Window,著名的滑动窗口,表示作为接收方能接收的窗口大小,用于解决流控;长度16位,单位字节,可表示65535个字节大小。

  • Checksum(校验和):2字节16位长度;出现位错误时,能够校验出数据是否被破坏,主要针对路由器和程序导致的破坏情况。(通信途中的出错链路层FCS也会校验一次);关于校验和下面会扩展讲一点。

  • Urgent Pointer(紧急指针):在控制位URG为1时有效,从数据部分的首位到指向的位置表示为紧急数据。

  • Options(选项):根据Offset偏移量字段最大可以表示60字节,Options之前固定20字节,可得,Options最大是40字节。且长度是4字节的整数倍,毕竟Offset单位是4字节。Options由3部分组成:1字节表示类型 + 1字节表示长度(可无) + n字节表示信息(可无),其中信息的字节数等于长度字段的“值” - 2字节(类型和长度)。常见的选项类型有:0、1、2、3、4、8。
    类型为0,长度为-:End of Option List(选项列表结束),只有1字节的type,无length和info信息。
    类型为1,长度为-:No-Operation(无操作)
    类型为2,长度为4:MSS(Maxinum Segment Size),通常用于在建立连接时,决定最大段长度,一般为1460字节(MTU-40字节)(减去的40是20字节TCP头和20字节IP头)。
    类型为3,长度为3:窗口扩大因子,用于将原16位的窗口大小左移指定的位数,而改善CPU吞吐量(吞吐量T = 窗口小W / 往返时间RTT);理论上扩大因子的值最大位数14,所以65535字节 * 2^14 约= 1GB。
    类型为4,长度为2:表示支持SACK,可以确认多个到达的数据序列号;SACK可以有效提高传输效率,后续再单独介绍。
    类型为8,长度为10:表示时间戳选项,用于发送方记录往返时间;及序列号回绕问题也叫“两个小时问题”。
    如下所示:使用的Wireshark抓包SYN建连的Options内容,从上到下相加共24字节长度。

关于TCP首部数值字段:
表示大小的数值字段,除了offset单位为4字节,其它单位为字节。

关于协议

1、校验和计算
RFC793解释如下:

The checksum field is the 16 bit one's complement of the one's complement sum of 
all 16 bit words in the header and text.  
If a segment contains an odd number of header and text octets to be checksummed, 
the last octet is padded on the right with zeros to form a 16 bit word for checksum purposes.  
The pad is not transmitted as part of the segment.  
While computing the checksum, the checksum field itself is replaced with zeros.

1)、校验和的计算包括了 伪首部、TCP首部、TCP数据 三个部分,将它们分成16位的字段,如果不是16bit的倍数,在末尾加0补充(补充的数不进行实际传输);
2)、每16位进行二进制相加(TCP头部校验和字段全为0),如果溢出,将高位补充到低位;
3)、将结果进行取反,得到最后的校验和,填充到校验和字段进行传输。

4)、接收方同样的方式计算(包括收到的校验和参与计算),计算新的校验和值如果全部为0,则表示数据校验通过,否则校验失败。

举例如下:
发送方:取反前:1100 1001 1010 0001 -> 校验和:0011 0110 0101 1110
接收方:取反前:1100 1001 1010 0001 + 0011 0110 0101 1110 = 1111 1111 1111 1111 1111 -> 校验和:0000 0000 0000 0000

其中伪首部如下:

Address:32位长的ip地址
PTCL:传输协议号,TCP固定为6,UDP固定17。
TCP Length:包括了TCP的首部加数据长度。

2、MTU和MSS
MTU:Maximum Transmission Unit(最大传输单元),指的是数据链路层的最大payload,由硬件网卡决定,是一个硬件限制,以太网通常是1500字节,1500字节可以有一个比较好的传输效率。如果超过了会通过IP层进行分片传输,进而导致传输效率的降低

值得注意的是,在发送时只能知道两端的最大传输限制,在数据通过一个个路由器传输时,如果有小于MTU的包的限制,那么经过该路由器前会被分片传输。TCP/IP协议栈通常使用一种称为Path MTU Discovery(PMTU)的技术来确定在两个通信节点之间传输的最大MTU大小,以避免分片。

MSS:Maximum Segment Size(最大段长度),TCP头部的选项字段,三次握手时协商,通过MTU计算所得(MSS = MTU - 20TCP头 - 20IP头)。为了数据分段传输,防止数据在网络层发生分片,影响传输效率

这里减去TCP和IP头部包括对应选项长度,所以这里计算的是Max Size。

为什么有了IP分片,TCP还要分段呢?
1、因为如果不分段,传输一大份数据,中途丢了重试需要再发送一大份数据;而分段丢哪块传哪块。
2、分段后,IP正常也不用进行分片了,提高传输。

3、RFC
RFC:请求意见稿(英语:Request for Comments,缩写:RFC),最终演变为用来记录互联网规范、协议、过程等的标准文件。
不同编号的RFC文档大多是某个特定模块的说明,也可能是替代之前的某个RFC文档。

一些关于TCP的主要RFC文档:
1、RFC793 - Transmission Control Protocol: 定义TCP的原始标准文档,理解TCP的基础。
2、RFC1222 - Requirements for Internet Hosts - Communication Layers: 文档提供了更详细的关于主机如何实现TCP协议的要求。
3、RFC5681 - TCP Congestion Control: 文档描述了标准的TCP拥塞控制算法。
4、RFC6298 - Computing TCP’s Retransmission Timer: 文档更新了关于TCP重传计时器的计算方法。
5、其他相关RFCs: TCP协议及其不同方面被多个RFC文档描述,包括但不限于流量控制、时间戳、窗口缩放等。

TCP状态

TCP的长连接并不是物理上的长连接,而是借助TCP两端的连接状态维持的一种长连接。TCP连接中,包括的状态有:CLOSED、LISTEN、SYN-SENT、SYN-RECEIVED、ESTABLISHED、CLOSE-WAIT、LAST-ACK、FIN-WAIT-1、FIN-WAIT-2、CLOSING、TIME-WAIT。

相关文档参考1参考2

控制状态之间发生变化的三类消息,以控制位的方式出现在TCP消息首部,它们分别是:SYN、FIN、ACK

  • SYN:A Synchronize message,用于初始化和建立连接,同时用于两端之间同步初始化序列号。
  • FIN:A finish message,某端设备想要去结束这个连接。
  • ACK:An acknowledgement,作为接收消息后的回复,例如SYN或者FIN。

状态的定义如下:
CLOSED:这是一种虚拟的状态,表示两端未建立连接,此时没有TCB(Transmission Control Block,记录了一些变量有关发送和接收的序列号)。
LISTEN:通常是服务器打开端口监听,在等待 SYN消息。
SYN-SENT:通常是客户端发送SYN消息后,在等待匹配的连接回复。
SYN-RECEIVED:接收到了SYN消息,同时发出了ACK和自己的SYN消息,在等待自己SYN的ACK。
ESTABLISHED完成TCP连接状态,数据传输阶段的正常状态

CLOSE-WAIT:收到了FIN请求,并回复了ACK后的状态,表示有请求欲关闭连接;在等待本地应用程序主动发起关闭连接。
LAST-ACK:处于CLOSE-WAIT的TCP连接对应的应用程序主动发起了FIN消息;在等待自己FIN消息的ACK。

FIN-WAIT-1:处于连接状态的TCP主动发起了FIN消息,进入FIN-WAIT-1状态;在等待ACK或者对方的FIN。
FIN-WAIT-2:收到了发出SYN的ACK消息;在等待对端的FIN消息。
CLOSING:主动发出FIN关闭但没有收到ACK;收到了对端的FIN并回复了ACK;在等待主动FIN的ACK。
TIME-WAIT:设备主动发起了FIN并收到了ACK,也收到了对端的FIN并回复了ACK;等待足够的时间(2MSL)后再关闭。

不同状态之间的流转如下:

服务器的状态流转

CLOSED -> LISTEN -> SYN-RECEIVED -> ESTABLISHED -> 
CLOSE-WAIT -> LAST-ACK -> CLOSED。

客户端的状态流转

CLOSED -> SYN-SENT -> ESTABLISHED -> 
FIN-WAIT-1 -> FIN-WAIT-2 -> TIME-WAIT -> CLOSE。

特殊的状态流转
1、SYN-SENT -> SYN-RECEIVED:准备建立连接时,两端同时发出SYN消息时,进入SYN-SENT状态,然后收到了对端的FIN的并进行了回复,但自己发出的SYN消息未收到回复,此时状态变为SYN-RECEIVED。

正常的场景不太会有,但在如不分客户端和服务器端的点对点通信或者一些鲁棒性测试场景会出现。

2、FIN-WAIT-1 -> CLOSING:准备关闭连接时,已经发出FIN在等待ACK,进入FIN-WAIT-1后,因为两端同时FIN,所以先收到对端的FIN消息并进行了ACK,而后进入CLOSING状态。

三次握手

一般客户端-服务器从连接到断连,如下图所示:

Q:那为什么需要三次握手呢?

A:连接在建立的时候两端会初始化自己端的Sequence Number(叫 ISN:Inital Sequence Number),然后在建立连接的时候互换ISN,如图中的X、Y便是序号。后续通讯会使用该序号,序号保证了后续传输的消息顺序性,同时防止旧的延迟消息的干扰。

其实两端互传一次SYN(Synchronize Sequence Number)消息和接收ACK消息需要四次过程,但服务器回复ACK和发送SYN可以合并,便成为了著名的三次握手

四次挥手

如上图所示,客户端与服务器经历了四次挥手关闭连接。

Q:那为什么需要四次挥手完成两端的关闭呢?

A:TCP是“全双工”的通讯方式(两端客户同时发送和接收消息),那么一种简单的关闭方式,一方发起FIN消息申请关闭连接,表示它不会再发送消息,此时进入FIN-WAIT-1状态;但是可以接收对端消息,等待对端也不发送消息了也会发送FIN消息并等待ACK。所以需要四次挥手

另一种情况是,如果双端同时发起关闭,收到对方的FIN并ACK后会进入CLOSING状态,再进入到TIME-WAIT状态,最后关闭,如下图所示:

图片参考1参考2

常见问题

1、Inital Sequence Number的初始化与序号作用
ISN在连接创建的时候进行初始化,即TCP首部的序列号,长32bit。

ISN生成是通过一个虚拟的时钟初始化的,这个时钟每4微妙做加1操作(1秒25000),直到2^32,然后重新从0开始,所以大概一个周期是4.77小时(RFC793上说是4.55小时)。

序列号作用
首先序号的初始化是在连接建立时创建,且由虚拟的时钟计算;那么如果每次建连的初始化序列号都是从固定的值1开始,假如客户端发送seq=20的包,网络断了,重建连接,序列号仍从1开始,原seq=20的包此时到了,会当成新包处理,消息就错误了。所以序列号解决了这个问题,只要网络中的包最大存活时间(Maximum Segment Lifetime)小于4.77时,我们就可以认为序列号是唯一的。

2、确认应答号Ack与序列号Seq的关系
这里说的是值即内容大小的关系,任何TCP首部都有Seq字段,然后ACK是对Seq包的确认。我们分两种场景来看:

  • 连接建立和断开的时候
  • 连接建立后正常的传输的时候

对于连接建立后正常传输的时候:Ack = Seq + Len(数据),表示期望下次收到的Seq。

Len是指Tcp的payload大小,不包括TCP首部,即Length of the data

而连接建立和断开的过程中,SYN/FIN Seq=x,回复的ACK=x+1可得:Ack=Seq + 1。过程是没有TCP payload的,即Len = 0。

为什么出现这种情况呢?这是建/断连和数据传输阶段ACK计算方式的不同(一句废话),在建/断连的时候,SYN/FIN也占1个字节的序列号空间,对端收到该类型包时,序列号加1。Ack值加1是为了表示下一个期望接收的字节序列号。这是一种协议约定,用来表示对前一个包的确认,并且为后续的数据传输同步序列号。

阅读参考

3、关于SYN Flood攻击
在TCP连接建立的过程中,如果服务器收到SYN后回复了SYN-ACK,客户端掉线了,那么Server端会一直处于等待中,此时连接尚未建立。Server端会进行重试SYN-ACK。在Linux下,重试的次数由参数tcp_synack_retries控制,时间是指数退避的,从1s开始,依次是1s、2s、4s、8s、16s,共计5次,第5次后还要等待32s,所以共计63s,Server端的TCP堆栈才会断开这个半开的连接。

命令sysctl net.ipv4.tcp_synack_retries 输出 net.ipv4.tcp_synack_retries = 5

无疑如果上面的情况大量出现,会消耗系统资源,一些恶意的人会以此制造SYN Flood攻击,也叫SYN洪泛、DDoS攻击。如制造不同的端口发送SYN然后下线,于是服务器63秒后断开连接,以此耗尽服务器连接队列,正常请求无法处理。

应对SYN Flood的方式
1)、使用Cookie
配置tcp_syncookies参数,如果tcp连接队列满了,是否启用源端口、目标地址端口和时间戳来生成一个特别的Sequence Number(Cookie)发出,如果是攻击者不会响应,而正常客户端会响应,然后完成连接建立。

命令sysctl net.ipv4.tcp_syncookies 输出 net.ipv4.tcp_syncookies = 1(开启)

2)、参数调节

  • tcp_synack_retries:控制重试次数(默认5)
  • tcp_max_syn_backlog:收到SYN回复ACK后处于半连状态的半连接队列最大长度,也叫SYN队列(默认512);同时增大somaxconn参数(somaxconn 定义了系统级别的全连接队列最大长度)。
  • tcp_abort_on_overflow:超过全连接队列长度的连接请求处理方式(默认0静默忽略、1表直接拒绝)

命令 ss -lnt 可以查看全连接队列的情况,Send-Q在LISTEN状态下表最大全连接队列长度。

3)、其它一些手段预防

  • 增加防火墙通过安全策略识别并拒绝洪泛攻击,此方式不可能穷举所有安全策略。
  • 使用源认证服务器如Anti-DDos系统进行先应答,如果客户端回复,则将其加入白名单中。此方式可能Anti-DDos系统是连接瓶颈。
  • 利用如Anti-DDos系统进行首包丢弃。通常此方式结合源认证一起预防。

Anti-DDos:防御分布式拒绝服务(Distributed Denial of Service,简称DDoS)攻击的系统或服务

4、TIME-WAIT状态后时间

首先TIME-WAIT是个等待状态,发生在四次挥手的过程中。当主动关闭连接的一端(大多客户端)进入TIME-WAIT状态后,会等待2MSL时间再关闭连接。

Q:什么是MSL?
A:Maximum Segment Lifetime,在网络中一个TCP段最大存活时间。按RFC793规定,MSL为2分钟。
当网络中的包发出时有对应Seq,等待ACK时,如果出现网络延迟导致重发新包,过一段时间的Seq和之前的可能相等(序列号回绕),便会出现消息混乱的情况。MSL能保证序列号的有效回绕。
Seq可存储32位bit长度空间,可最大表示2^32次方,是有限的。每次发送Seq = 上次Seq + 数据Len,如果按10MB/秒的传输速率,要达到2^32大概需要4.5小时,如果是100MB/秒,大概需要4.5分钟,仍在控制范围内。

Q:为什么是2MSL?
A:作用有多个,如下:

  • 确保最后1个FIN消息的ACK能正常到达对端。
  • 网络中可能有延迟的报文段,如果在网络中漂泊超过MSL时间,便会丢弃该包,不至于影响新的连接(相同的ip和端口)。
  • 根据RFC 793(TCP规范),TIME-WAIT状态的持续时间建议为2MSL。这是一种保守的做法,确保在关闭连接之前所有的数据报文都已经在网络中消失。

5、TIME-WAIT太多怎么办
在大并发的短连接下,就会有比较多的TIME-WAIT,就看哪端先发起断连了。

Q:TIME-WAIT太多怎么办?

  • 有些控制参数可以使用
    • tcp_max-tw-bucket参数:控制并发的TIME-WAIT的数量,默认值是180000,超过则会销毁对应连接,并在日志中打印出(如,time wait bucket table overflow)。
    • tcp_fin_timeout参数:减少MSL的时间,默认Linux服务器是60s,这是一种权衡。
  • 服务器尽量使用长连接,现在一些应用服务器、中间件默认都是长连接实现。
  • 提高端口可用范围,支持更多的连接,参考参数/proc/sys/net/ipv4/ip_local_port_range

总结

TCP真是个复杂的协议,这还是基础内容,包括了协议首部定义、TCP维护连接的状态,状态的流转,以及为什么需要三次握手和四次挥手,最后QA了一些TCP的问题,可用更好的理解TCP协议。除了基础内容,还有重传机制、拥塞控制算法等等接下来再一一分析。