TCP/IP 建立与断开连接详细过程 TCP 协议连接建立时 3 次握手的过程。
简述 TCP 协议连接建立时 3 次握手的过程。
根据 TCP 头部,说明下列 3 个包在连接建立过程中的次序 .
0020 00 50 83 aa 46 49 3e dd 33 96 37 a3 a0 12 ...P..FI>.3.7...
0030 16 a0 c4 c0 00 00 02 04 05 b4 04 02 08 0a d7 9b ................
0040 62 b7 00 56 4a 2a 01 03 03 02 b..VJ*.... ( 1 )
0020 83 aa 00 50 33 96 37 a2 00 00 00 00 a0 02 .....P3.7.......
0030 16 d0 84 1d 00 00 02 04 05 b4 04 02 08 0a 00 56 ...............V
0040 4a 2a 00 00 00 00 01 03 03 00 J*........ ( 2 )
0020 83 aa 00 50 33 96 37 a3 46 49 3e de 80 10 .....P3.7.FI>...
0030 16 d0 f3 4b 00 00 01 01 08 0a 00 56 4a 36 d7 9b ...K.......VJ6..
0040 62 b7 b. ( 3 )
解:
在 TCP/IP 协议中, TCP 协议提供可靠的连接服务,采用三次握手建立一个连接。
1 )是第二次握手, flags 位上为 12 ,二进制是 0001 0010 ,即表示有 syn 和 ack.
2 )是第一次握手, flags 位上为 02 ,二进制是 0000 0010 ,即表示有 syn 没有 ack 。
3 )是第三次握手, flags 位上为 10 ,二进制是 0001 0000 ,即表示有 ack 没有 syn 。
该连接访问的是 80 端口,是为 HTTP ( HyperText Transport Protocol ,超文本传输协议)开放的,
第一次握手:建立连接时,客户端发送 syn 包 (syn=j) 到服务器,并进入 SYN_SEND 状态,等待服务器确认; 第二次握手:服务器收到 syn 包,必须确认客户的 SYN ( ack=j+1 ),同时自己也发送一个 SYN 包( syn=k ),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态; 第三次握手:客户端收到服务器的 SYN + ACK 包,向服务器发送确认包 ACK(ack=k+1) ,此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据。
tcp- 断开连接 :
主要部分,四次握手:
断开连接其实从我的角度看不区分客户端和服务器端,任何一方都可以调用 close(or closesocket) 之类 的函数开始主动终止一个连接。这里先暂时说正常情况。当调用 close 函数断开一个连接时,主动断开的 一方发送 FIN(finish 报文给对方。有了之前的经验,我想你应该明白我说的 FIN 报文时什么东西。也就是 一个设置了 FIN 标志位的报文段。 FIN 报文也可能附加用户数据,如果这一方还有数据要发送时,将数据附 加到这个 FIN 报文时完全正常的。之后你会看到,这种附加报文还会有很多,例如 ACK 报文。我们所要把握 的原则是, TCP 肯定会力所能及地达到最大效率,所以你能够想到的优化方法,我想 TCP 都会想到。
当被动关闭的一方收到 FIN 报文时,它会发送 ACK 确认报文 ( 对于 ACK 这个东西你应该很熟悉了 ) 。这里有个 东西要注意,因为 TCP 是双工的,也就是说,你可以想象一对 TCP 连接上有两条数据通路。当发送 FIN 报文 时,意思是说,发送 FIN 的一端就不能发送数据,也就是关闭了其中一条数据通路。被动关闭的一端发送 了 ACK 后,应用层通常就会检测到这个连接即将断开,然后被动断开的应用层调用 close 关闭连接。
我可以告诉你,一旦当你调用 close(or closesocket) ,这一端就会发送 FIN 报文。也就是说,现在被动 关闭的一端也发送 FIN 给主动关闭端。有时候,被动关闭端会将 ACK 和 FIN 两个报文合在一起发送。主动 关闭端收到 FIN 后也发送 ACK ,然后整个连接关闭 ( 事实上还没完全关闭,只是关闭需要交换的报文发送 完毕 ) ,四次握手完成。如你所见,因为被动关闭端可能会将 ACK 和 FIN 合到一起发送,所以这也算不上 严格的四次握手 --- 四个报文段。
在前面的文章中,我一直没提 TCP 的状态转换。在这里我还是在犹豫是不是该将那张四处通用的图拿出来, 不过,这里我只给出断开连接时的状态转换图,摘自 <The TCP/IP Guide>【【】】
给出一个正常关闭时的 windump 信息:
14:00:38.819856 IP cd-zhangmin.1748 > 220.181.37.55.80: F 1:1(0) ack 1 win 65535 14:00:38.863989 IP 220.181.37.55.80 > cd-zhangmin.1748: F 1:1(0) ack 2 win 2920 14:00:38.864412 IP cd-zhangmin.1748 > 220.181.37.55.80: . ack 2 win 65535
补充细节:
关于以上的四次握手,我补充下细节: 1. 默认情况下 ( 不改变 socket 选项 ) ,当你调用 close( or closesocket ,以下说 close 不再重复 ) 时,如果 发送缓冲中还有数据, TCP 会继续把数据发送完。 2. 发送了 FIN 只是表示这端不能继续发送数据 ( 应用层不能再调用 send 发送 ) ,但是还可以接收数据。 3. 应用层如何知道对端关闭?通常,在最简单的阻塞模型中,当你调用 recv 时,如果返回 0 ,则表示对端 关闭。在这个时候通常的做法就是也调用 close ,那么 TCP 层就发送 FIN ,继续完成四次握手。如果你不调用 close,那么对端就会处于 FIN_WAIT_2 状态,而本端则会处于 CLOSE_WAIT 状态。这个可以写代码试试。 4. 在很多时候, TCP 连接的断开都会由 TCP 层自动进行,例如你 CTRL+C 终止你的程序, TCP 连接依然会正常关 闭,你可以写代码试试。
特别的 TIME_WAIT 状态:
从以上 TCP 连接关闭的状态转换图可以看出,主动关闭的一方在发送完对对方 FIN 报文的确认 (ACK) 报文后, 会进入 TIME_WAIT 状态。 TIME_WAIT 状态也称为 2MSL 状态。
什么是 2MSL ? MSL 即 Maximum Segment Lifetime ,也就是报文最大生存时间,引用 <TCP/IP 详解 > 中的话: “ 它 (MSL) 是任何报文段被丢弃前在网络内的最长时间。 ” 那么, 2MSL 也就是这个时间的 2 倍。其实我觉得没 必要把这个 MSL 的确切含义搞明白,你所需要明白的是,当 TCP 连接完成四个报文段的交换时,主动关闭的 一方将继续等待一定时间 (2-4 分钟 ) ,即使两端的应用程序结束。你可以写代码试试,然后用 netstat 查看下。
为什么需要 2MSL ?根据 <TCP/IP 详解 > 和 <The TCP/IP Guide> 中的说法,有两个原因: 其一,保证发送的 ACK 会成功发送到对方,如何保证?我觉得可能是通过超时计时器发送。这个就很难用 代码演示了。 其二,报文可能会被混淆,意思是说,其他时候的连接可能会被当作本次的连接。直接引用 <The TCP/IP Guide> 的说法: The second is to provide a “buffering period” between the end of this connection and any subsequent ones. If not for this period, it is possible that packets from different connections could be mixed, creating confusion.
TIME_WAIT 状态所带来的影响:
当某个连接的一端处于 TIME_WAIT 状态时,该连接将不能再被使用。事实上,对于我们比较有现实意义的 是,这个端口将不能再被使用。某个端口处于 TIME_WAIT 状态 ( 其实应该是这个连接 ) 时,这意味着这个 TCP 连接并没有断开 ( 完全断开 ) ,那么,如果你 bind 这个端口,就会失败。
对于服务器而言,如果服务器突然 crash 掉了,那么它将无法再 2MSL 内重新启动,因为 bind 会失败。解决这 个问题的一个方法就是设置 socket 的 SO_REUSEADDR 选项。这个选项意味着你可以重用一个地址。
对于 TIME_WAIT 的插曲:
当建立一个 TCP 连接时,服务器端会继续用原有端口监听,同时用这个端口与客户端通信。而客户端默认情况 下会使用一个随机端口与服务器端的监听端口通信。有时候,为了服务器端的安全性,我们需要对客户端进行 验证,即限定某个 IP 某个特定端口的客户端。客户端可以使用 bind 来使用特定的端口。
对于服务器端,当设置了 SO_REUSEADDR 选项时,它可以在 2MSL 内启动并 listen 成功。但是对于客户端,当使 用 bind 并设置 SO_REUSEADDR 时,如果在 2MSL 内启动,虽然 bind 会成功,但是在 windows 平台上 connect 会失败。 而在 linux 上则不存在这个问题。 ( 我的实验平台: winxp, ubuntu7.10)
要解决 windows 平台的这个问题,可以设置 SO_LINGER 选项。 SO_LINGER 选项决定调用 close 时, TCP 的行为。 SO_LINGER涉及到 linger 结构体,如果设置结构体中 l_onoff 为非 0 , l_linger 为 0 ,那么调用 close 时 TCP 连接 会立刻断开, TCP 不会将发送缓冲中未发送的数据发送,而是立即发送一个 RST 报文给对方,这个时候 TCP 连 接就不会进入 TIME_WAIT 状态。
如你所见,这样做虽然解决了问题,但是并不安全。通过以上方式设置 SO_LINGER 状态,等同于设置 SO_DONTLINGER 状态。
断开连接时的意外: 这个算不上断开连接时的意外,当 TCP 连接发生一些物理上的意外情况时,例如网线断开, linux 上的 TCP 实现 会依然认为该连接有效,而 windows 则会在一定时间后返回错误信息。
这似乎可以通过设置 SO_KEEPALIVE 选项来解决,不过不知道这个选项是否对于所有平台都有效。