TCP(Transmission Control Protocol,传输控制协议)是互联网协议套件中一种核心协议。它提供面向连接的、可靠的字节流传输服务,确保数据从一端正确无误地传输到另一端。TCP的主要特点包括:
- 可靠性:TCP使用确认、重传、序列号等机制,确保数据包的可靠传输。
- 顺序传输:TCP保证数据按发送顺序到达接收方。
- 流量控制:TCP采用滑动窗口机制进行流量控制,防止发送方过快发送数据,导致接收方来不及处理。
- 拥塞控制:TCP检测网络拥塞情况,并通过调整发送速率来避免拥塞。
一.连接建立(三次握手)
1.1三次握手建立连接
TCP三次握手执行过程:
1)首先,服务端和客户端都是处于CLOSED
状态的,然后服务端启动,监听端口,状态变为LISTEN
(监听)状态;
2)客户端为了请求资源,发送连接,发送同步序列号SYN,此时客户端就变成了SYN-SEND
状态
3)服务端接收到客户端请求之后,发送SYN和ACK,然后服务端状态就变成SYN-RCVD
状态
4) 客户端接收到信息之后,再次发送ACK,然后变成ESTABLISHED
(已确认)状态,服务端接收到返回信息后,状态也变成ESTABLISHED
(已确认)状态
1.2TCP协议为什么需要三次握手?
知道了TCP的三次握手的基本工作原理之后,就可以解释为什么TCP需要三次握手?为什么不设计成两次握手就可以?
原因:避免重复链接
其实在RFC 793 Transmission Control Protocol里就有指出为什么要三次握手的原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
翻译为中文大致意思是主要原因是为了防止旧的重复连接引起连接混乱问题。
比如在网络环境比较复杂的情况,客户端可能会连续发送多次请求。如果只设计成两次握手的情况,服务端只能一直接收请求,然后返回请求信息,也不知道客户端是否请求成功。这些过期请求的话就会造成网络连接的混乱。
所以设计成三次握手的情况,客户端在接收到服务端SEQ+1
的返回消息之后,就会知道这个连接是历史连接,所以会发送报文给服务端,告诉服务端。
所以TCP设计成三次握手的目的就是为了避免重复连接。
然后可以设计成四次握手?五次握手?不可以?
答案是也是可以的,不过为了节省资源,三次握手就可以符合实际情况,所以就没必要设计成四次握手、五次握手等等情况。
1.3四次挥手关闭连接
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1
(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT
(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT
状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2
(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT
(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL
(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
1.4模拟三次握手过程中的序列号
TCP连接的建立是通过三次握手来实现的:
序号 方向 seq ack SYN ACK
----------------------------------------------------------
1 A->B 10000(ISN) 0 1 0
2 A<-B 20000(ISN) 10000+1=10001 1 1
3 A->B 10001 20000+1=20001 0 1
解释情况如下:
1: (A) –> [SYN] –> (B)
A向B发起连接请求,以一个随机数初始化A的seq,这里假设为10000,此时ACK=0
2: (A) <– [SYN/ACK] <–(B)
B收到A的连接请求后,也以一个随机数初始化B的seq,这里假设为20000,意思是:你的请求我已收到,我这方的数据流就从这个数开始。B的ACK是A的seq加1,即10000+1=10001
3: (A) –> [ACK] –> (B) A收到B的回复后,它的seq是它的上个请求的seq加1,即10000+1=10001,意思也是:你的回复我收到了,我这方的数据流就从这个数开始。A此时的ACK是B的seq加1,即20000+1=20001
1.5 数据传输过程
序号 方向 seq ack 数据长度 数据包长度
---------------------------------------------------------------------------
23 A->B 40000 70000 1460 1514
24 A<-B 70000 40000+1514-54=41460 0 54
25 A->B 41460 70000+54-54=70000 1460 1514
26 A<-B 70000 41460+1514-54=42920 0 54
解释:
23:B接收到A发来的seq=40000,ack=70000,size=1514的数据包
24:于是B向A也发一个数据包,告诉A,你的上个包我收到了。A的seq就以它收到的数据包的ack填充,ack是它收到的数据包的seq加上数据包的大小(不包括:以太网协议头=14字节,IP头=20字节,TCP头=20字节),以证实B发过来的数据全收到了。
25:A在收到B发过来的ack为41460的数据包时,一看到41460,正好是它的上个数据包的seq加上包的大小,就明白,上次发送的数据包已安全到达。于是它再发一个数据包给B。
26:B->A这个正在发送的数据包的seq也以它收到的数据包的ack填充,ack 就以它收到的数据包的seq(70000)加上包的size(54)填充,即ack=70000+54-54(全是头长,没数据项)。通过tcpdump发现确认包ack,确认传输过程中最后字节长度。
参考:TCP-IP详解卷-基础知识 IP TCP UPD 协议
减去54的原因 ,以太网封装格式(链路层使用的是Ethernet II 格式,这个格式有14字节以太网首部+4字节以太网尾部): 应用数据=size-14-20-20=size-54。(假设IP首部和TCP首部都没有可选选项)
为什么不减去以太网尾部的4字节呢? 因为在物理层上网卡要先去掉前导同步码和帧开始定界符,然后对帧进行CRC检验,如果帧校验和错,就丢弃此帧。如果校验和正确,就判断帧的目的硬件地址是否符合自己的接收条件(目的地址是自己的物理硬件地址、广播地址、可接收的多播硬件地址等),如果符合,就将帧交“设备驱动程>序”做进一步处 理。这时我们的抓包软件才能抓到数据,因此,抓包软件抓到的是去掉前导同步码、帧开始分界符、FCS之外的数据
1.6 四次挥手过程中的序列号
序号 方向 seq ack FIN ACK
--------------------------------------------------------
1 A->B 80000 90000 1 1
2 A<-B 90000 80000+1=80001 0 1
3 A<-B 90000 80001 1 1
4 A->B 80001 90000+1=90001 0 1
1:(A) –> [FIN/ACK] –> (B)
客户端A没有要发送给服务端B的数据了,想要关闭链接,则发送一个FIN=1,ACK=1的包,告诉B可以关闭连接了,我没有什么数据要给你了。
2:(A) <– [ACK] <– (B)
然后B会发送ACK=1的包给A,告诉A我知道你没有什么想给我的了,但是我还有数据要给你,你先等下,我先不想FINISH呢。
3:(A) <– [FIN/ACK] <– (B)
等B把数据都发送给A之后,B会再次发送一个包,这次FIN=1,表示我这边也想关闭了,咱俩一起关把。在2和3之间,可能还会有很多B->A的传递,ack均为80001。
4:(A) –> [ACK] –> (B)
然后A回应一个ACK,表示我知道了,一起关吧。B收到这个ACK后,就会CLOSE。但是实际上A不会直接CLOSE,还会进入一个等待时间状态TIME_WAIT,持续2倍的MSL(Maximum Segment Lifetime,报文段在网络上能存活的最大时间)。过了这个状态,才会CLOSE。
为什么要由一个TIME_WAIT
阶段呢?原因主要有两个:
1) 保证TCP的全双工连接能够可靠关闭
假如A发送的最后一次ACK丢包了,没有被B收到,那B超时之后,会再次发送一个FIN包,然后这个包被处于TIME_WAIT
状态的A收到,A会再次发送一个ACK包,并重新开始计时,一直循环这个过程,直到A在TIME_WAIT
的整个过程中都没有收到B发过来的FIN包,这说明B已经收到了A的ACK包并CLOSE了,因此A这时候才可以安心CLOSE。如果A没有TIME_WAIT状态而是直接close,那么当ACK丢包之后,B会再次发送一个FIN包,但是这个包不会被A回应,因此B最终会收到RST,误以为是连接错误,不符合可靠连接的要求。因此需要等待ACK报文到达B。
RST是TCP数据报中6个控制位之一,6个控制位的作用如下:
-
URG 紧急:当 URG=1 时,它告诉系统此报文中有紧急数据,应优先传送(比如紧急关闭),这要与紧急指针字段配合使用。
-
ACK 确认:仅当 ACK=1 时确认号字段才有效。建立 TCP 连接后,所有报文段都必须把 ACK 字段置为 1。
-
PSH 推送:若 TCP 连接的一端希望另一端立即响应,PSH 字段便可以“催促”对方,不再等到缓存区填满才发送。
-
RST复位:若 TCP 连接出现严重差错,RST 置为 1,断开 TCP 连接,再重新建立连接。
-
SYN 同步:用于建立和释放连接,稍后会详细介绍。
-
FIN 终止:用于释放连接,当 FIN=1,表明发送方已经发送完毕,要求释放 TCP 连接。
2)保证这次连接的重复数据段从网络中消失
如果A直接close了,然后向B发起了一个新的TCP连接,可能两个连接的端口号相同。一般不会有什么问题,但是如果旧的连接有一些数据堵塞了,没有达到B呢,新的握手连接就已经到B了,那么这时候,由于区分不同TCP连接是依据套接字,因此B会将这批迟到的数据认为是新的连接的数据,导致数据混乱(源IP地址和目的IP地址以及源端口号和目的端口号的组合称为套接字,新旧连接的套接字很有可能相同)如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。服务端处于被动关闭,不会出现该状态。
二.模拟字节流发送与协议包装过程
假设我们要发送一个包含“Hello, TCP!”消息的数据流,TCP如何处理这个数据流?
-
数据分割: 假设每个TCP段最多能携带5个字节的数据,消息“Hello, TCP!”将被分割为以下三个部分:
- 第一段:
Hello
- 第二段:
, TC
- 第三段:
P!
- 第一段:
-
添加TCP头: 每个数据段将添加一个TCP头,包含源端口号、目的端口号、序列号、确认号、数据偏移、控制位、窗口大小、校验和和紧急指针等字段。
TCP协议头
TCP协议头的格式如下图所示,每个字段的长度和作用分别解释如下:
0 4 8 12 16 20 24 28 32
----------------------------------------------------------
| Source Port | Destination Port |
----------------------------------------------------------
| Sequence Number |
----------------------------------------------------------
| Acknowledgment Number |
----------------------------------------------------------
| Data | Reser- | Flags | Window Size |
|Offset| ved | | |
----------------------------------------------------------
| Checksum | Urgent Pointer |
----------------------------------------------------------
| Options (if any) |
----------------------------------------------------------
| Data (variable length) |
----------------------------------------------------------
序号
(seq)用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达2^32-1
后又从0开始。
当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN
(InitialSequenceNumber)。该主机要发送数据的第一个字节序号为这个ISN
加1,因为SYN标志消耗了一个序号。既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号(ack)应当是上次已成功收到数据字节序号加1。只有ACK标志(下面介绍)为1时确认序号字段才有效。
发送ACK无需任何代价,因为32bit的确认序号字段和ACK标志一样,总是TCP首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。
TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。
字段解释
- 源端口号(Source Port,16位):发送方的端口号。
- 目的端口号(Destination Port,16位):接收方的端口号。
- 序列号(Sequence Number,32位):发送数据段的序列号,用于确保数据包按顺序到达。
- 确认号(Acknowledgment Number,32位):确认接收数据的序列号。
- 数据偏移(Data Offset,4位):表示TCP头部的长度(以32位字为单位),即TCP数据开始的偏移量。
- 保留位(Reserved,6位):保留位,当前未使用。
- 控制位(Flags,6位):包括多种控制标志位,如SYN、ACK、FIN等,分别表示连接建立、确认、结束等状态。
- 窗口大小(Window Size,16位):用于流量控制,表示接收方当前能够接收的数据量。
- 校验和(Checksum,16位):用于差错校验,确保数据完整性。
- 紧急指针(Urgent Pointer,16位):指出紧急数据的指针位置(如果设置了URG标志)。
- 选项(Options,可变长度):可选字段,用于扩展TCP协议的功能。
通过上述机制,TCP提供了一个可靠、顺序、面向连接的数据传输服务,确保数据能够在复杂的网络环境中正确传输。
序列号(Sequence Number)和确认号(Acknowledgment Number)的作用
序列号(Sequence Number)
-
定义: 序列号是一个32位的字段,用于标识TCP段中第一个字节的数据的编号。
-
作用:
- 数据分段:在数据传输过程中,发送方将数据分成多个段,每个段的第一个字节会分配一个序列号。这有助于接收方重新组装数据。
- 数据重组:接收方根据序列号将数据包按顺序重新组装,即使数据包乱序到达也能正确排序。
- 确认机制:序列号配合确认号实现数据的确认和重传。发送方在发送数据后,等待接收方的确认,如果未收到确认则认为数据可能丢失,进行重传。
确认号(Acknowledgment Number)
-
定义: 确认号也是一个32位的字段,用于告诉发送方接收方已经成功收到的数据。
-
作用:
- 确认接收:确认号表示接收方期望接收的下一个字节的序列号。例如,确认号为1001表示接收方已经成功接收到序列号为1到1000的所有数据。
- 可靠传输:通过确认号,发送方可以知道哪些数据段已经成功到达接收方,不需要重传,从而确保数据的可靠传输。
- 流量控制:通过确认号,接收方可以控制发送方的数据发送速率,防止数据包过多而导致网络拥塞。
序列号与确认号的工作流程
以下是一个示例来说明序列号和确认号在数据传输过程中的作用:
-
连接建立(握手):
- 第一步:客户端发送一个SYN段,序列号为X。
- 第二步:服务器接收到SYN段后,发送一个SYN-ACK段,序列号为Y,确认号为X+1。
- 第三步:客户端接收到SYN-ACK段后,发送一个ACK段,确认号为Y+1,序列号为X+1,连接建立。
-
数据传输:
- 发送方发送数据:假设客户端发送一个数据段,序列号为1001,数据长度为500字节。
- 接收方确认数据:服务器接收到这个数据段后,发送一个确认段,确认号为1501(表示1001到1500字节的数据已经成功接收)。
- 发送方继续发送数据:客户端根据接收方的确认号,发送序列号为1501的下一个数据段。
-
重传机制:
- 数据丢失:如果某个数据段丢失或损坏,例如,序列号为2001的数据段没有被接收方确认。
- 重传数据:客户端会在一定时间内未收到确认后,重新发送序列号为2001的数据段。
示例
假设我们有一个包含“Hello, TCP!”的消息,并假设每个TCP段最多能携带5个字节的数据,消息将被分割如下:
- 第一段:
Hello
,序列号1001 - 第二段:
, TC
,序列号1006 - 第三段:
P!
,序列号1011
传输过程:
- 发送第一段:序列号1001,数据“Hello”
- 接收确认:确认号1006(表示已经收到序列号1001到1005的数据)
- 发送第二段:序列号1006,数据“, TC”
- 接收确认:确认号1011(表示已经收到序列号1006到1010的数据)
- 发送第三段:序列号1011,数据“P!”
- 接收确认:确认号1013(表示已经收到序列号1011到1012的数据)
通过序列号和确认号的配合,TCP能够实现可靠的数据传输,确保数据按顺序且无误地到达接收方。