最近,我正好在做socket相关的实验,发现现在对计算机网络知识有一点点模糊,借此机会,熟悉一下TCP连接过程并利用WireShark工具进行测试。
Seq
序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。表示这个包的数据部分的第一位应该在整个数据流中所在的位置。ack
序号,占32位,只有ACK标志位(Acknowledgement)为1时,确认序号才有效,ack=seq+1
。指出期望收到对方下一个TCP报文段的数据载荷的第一个字节的序号,同时也是对之前收到的所有数据的确认。若ack=n
,则表明到序号n-1为止的所有数据都已正确接收,期望接收序号为n的数据。注意:
- 不要将
确认序号ack
与ACK标志位
搞混。本文将大写ACK
表示为标志位,小写ack
表示为确认号- 确认方ack=发送方seq+1
- SYN、FIN的传输虽然没有数据,但是会让下一次传输的seq增加一;但ACK的传输不会让下一次的传输seq加一(这里的下一次传输指的是同一个发送端的下一次传输)。
三次握手,是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息。
注:TCP通过MSS字段来确保分段传输正常,而MSS是在三次握手中确认的。客户端与服务端会在【选项】字段中填写MSS大小,TCP协议会选择两者的最小值作为协商后的MSS大小。除此之外,时间戳也在【选项】字段中,也会在三次握手的时候交互。
解释:
客户端向服务端发送连接请求报文段,此报文段首部中的同步位SYN
被设置为1,表明这是一个tcp连接请求报文段。序号字段seq
被设置了一个初始值x
作为TCP客户进程所选择的初始序号。
由于TCP连接建立是由TCP客户进程主动发起的,因此称为主动打开连接。
请注意TCP规定
SYN
被设置为1的报文段不能携带数据但要消耗掉一个序号。
服务端收到客户端发来的连接请求报文,对该包进行确认后结束LISTEN
阶段,并向客户端发送一段TCP报文,该报文段首部中的同步位SYN
和确认位ACK
都设置为1,表明这是一个TCP连接请求。序号字段seq
被设置了一个初始值y
,作为服务器进程所选择的初始序号。确认号字段ack
的值被设置成了x+1
,这是对TCP客户进程所选择的初始序号seq
的确认。随后服务器端进入SYN-RECV
阶段。
请注意这个报文段也不能携带数据,因为它是
SYN
被设置为1的报文段,但同样要消耗掉一个序号。
客户端接收到报文之后,此时客户端明白客户端到服务端的数据传输是正常的,因此结束SYN-SENT
阶段,并向服务端发送报文段,该报文段首部中的确认位ACK
被设置为1,表明这是一个普通的TCP确认报文段 。序号字段seq
被设置为x+1
,这是因为TCP客户进程发送的第一个TCP报文段的序号为x
,并且不携带数据,因此第二个报文段的序号为x+1
。确认号字段ack
被设置为y + 1
,这是对TCP服务器进程所选择的初始序号的确认,之后客户端进入ESTABLISHED
状态。当服务器端收到来自客户端确认收到服务器数据的报文后,得知从服务器到客户端的数据传输是正常的,从而结束 SYN-RECV
阶段,进入 ESTABLISHED
阶段,从而完成三次握手。
首先,TCP是全双工通信的,这意味着客户端在给服务器端发送信息的同时,服务器端也可以给客户端发送信息。
三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
我们将客户端与服务端理解为两个商量事情的人,如下是他们准备开始商量时的对话:
- 小王 ====> 小李 :我已经准备好开始讲了,我先从x处开始讲
- 小王 <==== 小李 :我收到了你要开始的消息,我现在已经确认好听你讲x+1处,同时,我先从y处开始讲。
- 小王 ====> 小李 :我收到了你要开始的消息,现在我已经确认好要听你讲y+1处。
注意,以上过程发生了4个事件:
那么,该次连接过程可以抽象为:
- 小王 ====> 小李 :我已经准备好开始讲了(SYN),我先从x处开始讲(SEQ=x)
- 小王 <==== 小李 :我收到了你要开始的消息(ACK),我现在已经确认好听你讲x+1处,同时,我也准备好了(SYN),我将从y处开始讲(SEQ=y)。
- 小王 ====> 小李 :我收到了你要开始的消息(ACK),现在我已经确认好要听你讲y+1处。
经过了过程1、2,小王可以确定他能发送和接收消息。
过程3之后,小李也明白了他也能发送和接收消息,此时,双方都确认发送与接收是可行的。
为什么不是两次?
因为两次握手只允许一方建立初始序列号,而另一方承认它。这意味着只有一方可以发送数据。
如果说是只选取三次握手前两次,那么服务端建立连接的请求就不会得到回应,此时服务端无法确定客户端是否接收到请求。
为什么不是四次?
四次握手是多余的,正如第一个问题的举例一样,发送ACK与SYN可以合并为一个包。
其实TCP连接需要两方都知道自己是否有接受和发送数据的能力, 第一次A发送消息,A,B都不知道自己是否有接受和发送能力, B收到A的消息后,B知道了我有接受消息的能力,B发送给A,A现在知道了我能接受消息,而且发送的消息B能接受到,也说明A有发送消息的能力,最后一次A给B发送信息,B也知道了我有发送消息的能力,至此,AB都有发送和接受消息的能力,所以是三次,刚刚好。
TCP 连接的释放需要发送四个包(执行四个步骤),因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作。
此过程假设由客户端发起断开连接请求(通常也是如此)。
解释:
首先客户端向服务器发送一段 TCP 报文表明其想要释放 TCP 连接,其中:标记位 FIN
设置为1,表示请求释放连接;序号为 Seq = u
;随后客户端进入 FIN-WAIT-1
阶段,即半关闭阶段,并且停止向服务端发送通信数据。
服务器接收到客户端请求断开连接的 FIN 报文后,结束 ESTABLISHED
阶段,进入 CLOSE-WAIT
阶段并返回一段 TCP 报文,其中:标记位 ACK
设置为1,表示接收到客户端释放连接的请求;序号为 Seq = v
;确认号 ack = u + 1
,表示是在收到客户端报文的基础上,将其序号值加 1 作为本段报文确认号ack
的值;随后服务器开始准备释放服务器端到客户端方向上的连接。客户端收到服务器发送过来的 TCP 报文后,确认服务器已经收到了客户端连接释放的请求,随后客户端结束 FIN-WAIT-1
阶段,进入 FIN-WAIT-2
阶段。
服务器端在发出 ACK
确认报文后,服务器端会将遗留的待传数据传送给客户端,待传输完成后即经过 CLOSE-WAIT
阶段,便做好了释放服务器端到客户端的连接准备,再次向客户端发出一段 TCP 报文, 其中:标记位 FIN
和 ACK
设置为1,表示已经准备好释放连接了;序号为 Seq = w
;确认号 ack = u + 1
,表示是在收到客户端报文的基础上,将其序号 Seq
的值加 1 作为本段报文确认号 ack
的值。随后服务器端结束 CLOSE-WAIT
阶段,进入 LAST-ACK
阶段。并且停止向客户端发送数据。
客户端收到从服务器发来的 TCP 报文,确认了服务器已经做好释放连接的准备,于是结束 FIN-WAIT-2
阶段,进入 TIME-WAIT
阶段,并向服务器发送一段报文,其中:标记位 ACK
设置为1,表示接收到服务器准备好释放连接的信号;序号为 Seq= u + 1
,表示是在已收到服务器报文的基础上,将其确认号 ack
值作为本段序号的值;确认号为 ack= w + 1
,表示是在收到了服务器报文的基础上,将其序号 Seq
的值作为本段报文确认号的值。随后客户端开始在 TIME-WAIT
阶段等待 2 MSL
。服务器端收到从客户端发出的 TCP 报文之后结束LAST-ACK
阶段,进入 CLOSED
阶段。由此正式确认关闭服务器端到客户端方向上的连接。客户端等待完 2 MSL
之后,结束 TIME-WAIT
阶段,进入 CLOSED
阶段,由此完成「四次挥手」。
MSL:最大段生命周期(Maximum Segment Lifetime),
简单点说,因为需要两边都断开,但又需要在断开前确定两边都没有数据需要发送。
由于TCP属于全双工通信(即数据可在两个方向上同时传递),因此,每个方向都必须要单独进行关闭(单方向的关闭称之为半关闭)。当客户端发送FIN
时,表明客户端已经没有数据需要发送,当客户端收到服务端传来的ACK
时,说明此时客户端与服务端都已经明白客户端不再有数据发送。服务端同理,因此需要四次挥手。需要注意的是,服务端在收到客户端的FIN
后,未必已经将所有数据发送给了客户端,不能立马发送FIN
给客户端,因此第二次和第四次挥手最好不要合并,所以需要四次握手。
理论上可以。其实这与三次握手本质上可以认为是一样的。
因为服务器端收到客户端的FIN
后,服务器端同时也要关闭连接,这样就可以把ACK
和FIN
合并到一起发送(即将第二次挥手与第三次合并),节省了一个包,变成了“三次挥手”。但前提是要确保服务端已经没有数据要发送。
参考RFC793(tcp传输控制协议)第16页:
Acknowledgment Number: 32 bits
If the ACK control bit is set this field contains the value of the next sequence number the sender of the segment is expecting to receive.
Once a connection is established this is always sent.
如果设置了ACK控制位,则此字段包含该段发送方期望接收的下一个序列号的值。
一旦建立了连接,就会始终发送。
这似乎是TCP协议的设计问题。因此,除了三次握手建立连接时,第一次发送的报文ACK标志不为1,其余的报文都为1。
等待2MSL的主要目的:
为了保证客户端发送的最后一个ACK
报文段(第四次挥手)能够到达服务器端。
解释:
因为客户端发送的最后一个ACK
报文段有可能在网络中被丢弃,使得处于LASK—ACK
状态的服务器端收不到对已发送的FIN+ACK
报文段(第三次挥手)的确认,那么,此时服务器端会超时重传这个FIN+ACK
报文段,而客户端就能在2MSL
时间内收到这个重传的FIN+ACK
报文段,接着客户端重传一次ACK
确认报文,重新启动2MSL计时器,一切正常之后,客户端和服务端都进入到CLOSED
状态。
如果客户端在TIME_WAIT
状态不等待一段时间,而是在发送完ACK
确认后立即释放连接,那么就无法收到服务端重传的FIN+ACK
报文段,因而也不会再发送一次确认报文段,这样,服务端就无法正常进入CLOSED
状态。
另一个目的:
防止“已失效的连接请求报文段”出现在本连接中。
解释:
客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
为什么是2MSL?:
MSL
代表最大段生命周期,它是任何报文在网络上存在的最长时间,超过这个时间,报文将被丢弃,RFC793中规定MSL
为2分钟。
简单地说,就是TIME_WAIT
等待时间得保证大于等于2次报文的最大传输时间,而2MSL是能达到此要求的最小时间。
1 个 MSL 保证四次挥手中主动关闭方最后的
ACK
报文(第四次挥手)能最终到达对端
1 个 MSL 保证另一端没有收到ACK
时,进行重传的FIN
报文能够到达
实验由Golang编写。
使用WireShark工具进行抓包。
服务端代码:
func main() {
listner, err := net.Listen("tcp", ":6666")
if err != nil {
log.Fatalln(err)
}
//最后关闭监听
defer listner.Close()
//建立连接
conn, err := listner.Accept()
if err != nil {
log.Fatalln(err)
}
//最后关闭连接
defer conn.Close()
//读取数据
buf := make([]byte, 512)
n, err := conn.Read(buf)
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(buf[:n]))
}
客户端代码:
func main() {
//发起连接
dial, err := net.Dial("tcp", ":6666")
if err != nil {
log.Fatalln(err)
}
//最后关闭连接
defer dial.Close()
//写入数据并发送
_, err = dial.Write([]byte("hello,world"))
if err != nil {
log.Fatalln(err)
}
}
实验结果:
注:6666端口为服务端,54022端口为客户端。
说明:
249~251
是三次握手过程252~253
是传输数据过程254~257
是四次挥手过程TCP三次握手与四次挥手的本质原因还是因为TCP进行的是全双工通信。
真正理解TCP建立连接与释放连接还需要深入了解TCP各种机制,建议阅读一下《TCP/IP详解》。