计算机网络:运输层 - TCP首部格式 & 连接的创建与释放
- TCP首部格式
- 源端口 目的端口
- 序号
- 确认号
- 数据偏移
- 保留
- 控制位
- 窗口
- 检验和
- 紧急指针
- TCP连接创建 - 三次握手
- TCP传输过程
- TCP连接释放 - 四次挥手
TCP首部格式
TCP
的首部如下:
首部的前20 byte
是固定的,后面的选项
字段可变。
源端口 目的端口
源端口
和目的端口
各占2 byte
,即填入通信的两个进程使用的端口号。
序号
占4 byte
,范围是 [ 0 , 2 32 − 1 ] {\red{[0, 2^{32} - 1]}} [0,232−1] ,TCP
是面向字节流的,在TCP
中传输的每一个字节都要按顺序进行编号。整个传输过程中,第一个字节的值是任意的,由发送方随机设定,后续所有字节都由第一个字节的编号以及偏移量得出。比如整个TCP
连接中,第一个字节编号为x
,那么第201
个字节的编号就是x + 201
。
如果某个字节在编号时,超出了 2 32 − 1 2^{32} - 1 232−1 ,此时从0
开始重新计数。
确认号
占4 byte
,含义是:期望收到对方下一个报文段的第一个字节的序号。
例如,主机B
收到了来自A
的编号为501
的数据报,数据报长度为200
。这说明主机B
收到了编号为501 - 700
字节的数据。那么主机B
接下来就期望收到701
开始的数据,此时确认号就设为701
。
若确认号为 n,说明 n - 1 及之前的所有数据都已经收到了
数据偏移
占4 bit
,其含义为:报文起始处
到数据起始处
的距离,简单理解就是整个首部的长度。
由于TCP
带有填充字段,所以长度是不确定的,需要该字段来指明长度。另外的,由于只占该字段只占4 bit
,能表示的范围是 [ 0 , 2 4 − 1 ] {\red{[0, 2^{4} - 1]}} [0,24−1],也就是[0, 15]
,而数据偏移
字段以4 byte
为单位。所以整个首部的长度最长为60 byte
,进而说明选项字段
的长度不超过40 byte
。
保留
占6 bit
,目前没有用,使用时设为全0
。
控制位
接着就是连续的留个控制位
。
紧急 URG:当URG = 1
,表明紧急指针
字段有效,告诉系统此报文有紧急数据,需要尽快传送,此时该报文就无需排队,直接插入到队列的首部立马发送出去。
确认 ACK:当ACK = 1
时,确认号
字段才有效。TCP
规定:在连接建立后所有的传送报文段,ACK
必须置为1
。
推送 PSH:发送该报文后,如果希望尽快收到对方的回应,就可以PSH = 1
。接受方收到该报文后,会立刻把该报文提交给应用进程,而不是等到接收缓存满了才提交。
复位 RST:当RST = 1
,表示TCP
连接出现重大差错,必须释放连接。也可以用来拒绝一个连接或报文段。
同步 SYN:用于建立连接
- 当
SYN = 1
并且ACK = 0
,表明这是一个连接请求报文
,(ACK = 0
的情况,一般来说整个TCP
连接只有此处ACK = 0
) - 当
SYN = 1
并且ACK = 1
,表明这是一个连接同意报文
终止 FIN:用于释放一个连接,当FIN = 1
,表明这是一个释放连接的报文
窗口
占2 byte
,用于指明自己的接收窗口,对方收到该报文后,读取窗口
字段,就知道自己接下来可以发送多少数据了。
检验和
占2 byte
,检验范围包括首部
和数据
两部分,与UDP
一样,在计算检验和时,要加上伪首部
。
如图:
只有计算检验和
时才会存在伪首部
,实际上其不存在,TCP 数据报
中只有首部
和数据
。
紧急指针
占2 byte
,仅在URG = 1
时才有效。当URG = 1
,说明这是一个紧急的报文,此时要立刻发出去,整个数据报会插队到其他数据报的前面。那么计算机怎么知道这个紧急报文的长度是多少?
此时就需要紧急指针
字段,其指明了本报文中紧急数据的字节数,当把所有紧急数据处理完后,就要恢复正常状态,把之前的数据发送出去。而什么时候紧急数据发送完,就是依靠紧急指针
字段指明的紧急数据的字节数。
讲解完报文的格式后,我们来讲解TCP
连接的创建与释放。后续会用到一些标识符,接下来我解释一下每个标识符对应数据报首部的哪一个字段:
seq
:对应首部中的序号
字段,指明希望收到的下一个数据的序号是什么ack
:对应首部中的确认号
字段,表明xxx之前的所有数据都已经收到了ACK
:对应首部中的确认位
字段,表明确认号
有效SYN
:对应首部中的同步位
字段,用于创建TCP
连接FIN
:对应首部中的终止位
字段,用于终止TCP
连接
TCP连接创建 - 三次握手
假设现有一台客户
主机A
,一台服务器
主机B
,现在A
申请向B
发起TCP连接
。
处于创建连接的过程中,SYN
就起作用了,如图:
首先令SYN = 1
,表明当前正在创建连接,创建连接的报文分两种情况:
- 当
SYN = 1
并且ACK = 0
,表明这是一个连接请求报文
,(ACK = 0
的第一种情况) - 当
SYN = 1
并且ACK = 1
,表明这是一个连接同意报文
当前A
正在发起连接的请求,所以此时ACK = 0
,注意:后续只要不标明的位,都是0。
而发送数据是要对每个字节进行编号的,第一个字节的编号由主机随机生成,此时seq = x
表明:第一个字节的编号为x
。
当A
发起请求后,此时B
就要同意这个连接:
同意连接是SYN = 1
,ACK = 1
,表明当前报文用于同意一个连接。此时主机B
也要生成第一个字节的编码,也就是seq = y
。
B
在回应时,还有一个字段ack = x + 1
,ack
表示:我希望收到的下一个数据的编号。
比如说某一次报文发送时,第一个字节的编号为666
,总数据长度为200 byte
,那么接收方就收到了[666, 865]
的所有数据,此时回应报文为ack = 866
表明下一个数据的编号为866
。
那么目前来说刚刚TCP请求报文
的编号为seq = x
,现在我回应ack = x + 1
,是不是可以理解为:整个TCP请求报文
只携带一个字节的数据呢?
TPC
规定:SYN = 1
的报文不允许携带数据,但是消耗掉一个seq
其实SYN = 1
的报文不携带数据部分,但是TCP
强制规定了其要消耗掉一个seq
,因此刚刚序号x
视为被消耗了,下一个字节的序号为x + 1
。
当A
收到B
的确认后,此时A
也要再给B
做一次确认:
这个确认,是对第二个报文的确认,此时SYN = 0
,因为其既不是连接请求
,也不是连接同意
。ACK = 1
表明ack
字段有效。seq = x + 1
,因为第一个报文seq = x
,并且SYN = 1
要消耗掉一个需要,此时就用下一个序号x + 1
。
ack = y + 1
是因为,刚刚B
发送的报文seq = y
,而SYN = 1
要消耗掉一个序号,此时希望收到的下一个序号为y + 1
。
接下来考虑一个问题:为什么需要三次报文交换才能建立连接?明明A
发送一个请求,B
发送一个同意,就表明双方都准备建立连接了,为什么不直接开始传输数据,而是还要第三次确认?
这是因为在B
发送第二个同意报文后,A
可能还没准备好接收数据。比如说B
发送的同意报文丢失了,此时A
还在等待B
的同意,而B
以为A
已经可以发送数据了。结果B
发送了一段数据后,A
根本不接收,因为A
在等B
的同意。此时就是A
没有准备好。
因此A
要发送第三个报文,来表明自己已经准备好了,对面可以开始发送数据了。
另外的,第三个报文是可以携带数据的,此时A
发送第三个报文时,表明连接建立完毕了,于是A
就顺带可以把一部分数据先通过该报文传输过去!
对于
FIN = 0
,SYN = 0
且ACK = 1
的报文,可以携带数据,携带多少数据就消耗多少序号,如果不携带数据,就不消耗序号
现在连接已经创建完毕,就可以正常数据传输了!
TCP传输过程
很多地方都只讲了连接创建与释放的过程,反而没有说明传输的过程。其实这个过程也很重要,本博客再简单讲解一下传输的过程。
如图所示:
现在TCP
连接建立时,第三个报文携带了100
个数据(data用于说明这个报文携带了多少数据)。
随后A
又紧接着发送了300
个数据:
此时seq = x + 101
,这是因为刚刚的第三个报文携带了100 byte
的数据,其中第一个字节的编号为x + 1
,说明我已经把[x + 1, x + 100]
的数据发出去了,接下来的300
字节,第一个编号就是x + 101
了。
而ack = y + 1
,这是因为上一次收到B
的报文是SYN = 1
,ACK = 1
的连接同意报文,序号为y
,消耗掉一个序号后变为y + 1
,即下一个希望收到的编号为y + 1
。
随后B
发送了一个长度为200 byte
的报文:
此时seq = y + 1
,这是因为上一次B
发送的报文是seq = y
,而SYN = 1
消耗掉一个序号,这次第一个字节使用的序号为y + 1
。ack = x + 401
表明[x, x + 400]
的所有数据都收到了,下一个希望收到的序号是x + 401
,
随后A
再发送一个100 byte
的报文:
这时seq = x + 401
,因为之前发送了[x, x + 400]
的数据,下一个字节编号为x + 401
。ack = y + 201
表明[y, y + 200]
的数据都受到了。
以此类推,直到连接释放。
TCP连接释放 - 四次挥手
当TCP
连接传输数据完毕,此时就可以释放连接了。
TCP
连接释放可以由任意一方发起,假设现在A
发起释放连接:
首先要把FIN = 1
,表明A
发起了一个释放连接的请求。而seq = u
,表明当前的报文编号为u
,也说明之前A
传输的最后一个字节编号为u - 1
。ack = v
,表明A
收到的来自B
的最后一个字节是u
。
A
发起释放连接的请求,只说明A
要传送的数据已经完毕了,可以释放连接了。但是B
可能还有没有传送完的数据:
首先B
发送一个ACK
报文,表明自己已经收到了刚刚FIN = 1
的报文。
TCP
规定:FIN = 1
的报文,就算不携带数据也要消耗一个序号
A
发送的连接释放报文中,FIN = 1
并且不携带数据,那么也要消耗掉一个序号。因此B
希望收到的下一个序号是u + 1
。
如果B
收到该报文,那么ack = u + 1
,否则ack = u
,这样发送方就可以根据下一个报文得知B
有没有收到连接释放的请求包围了。
随后B
可以继续发送自己之前没发完的数据,这期间B
发送的报文FIN = 0
,表明B
还有数据要发,没这么快终止连接。
剩下B
发送的所有报文中ack = u + 1
,因为刚刚A
发送了一个FIN = 1
的报文。
而seq = v
,表明自己现在发送的数据中,第一个字节序号为v
。
当B
传输完自己的所有数据后,在发送释放连接的同意报文:
FIN = 1
表明这是一个连接释放的报文,在两个FIN = 1
的报文中间,B
还发了一些报文,导致序号一直增加,假设现在增加到了w
,那么seq = w
。
当B
发送完最后一个FIN = 1
的连接释放报文后,A
最后发送一个确认报文:
这是因为B
无法保证自己发出去的报文A
一定可以接收到,如果B
发送的FIN = 1
的报文丢失了,此时B
以为自己以为结束TCP
连接,而A
还在一直等待B
发出FIN = 1
的报文。所以要对这个FIN = 1
的报文最后做一次确认。
当这四个报文传输完毕,A
不能直接结束,而要等待2 MSL
:
MSL(Maximum Segment Lifetime,最大报文段生存时间)
:指的是一个TCP
报文在网络中存活的最长时间。
这是因为A
传送的最后一个确认报文也有可能丢失,B
如过发现A
没有回应,超时计时器结束就重传FIN = 1
的报文。而这个报文一定可以在2 * MSL
期间到达,所以如果A
在2MSL
期间没有收到B
的报文,说明最后一个报文B
收到了,可以释放连接了。