TCP 连接的建立和中断

在阅读本文之前,可以先了解一些基本术语:http://name5566.com/1348.html

TCP 是一个面向连接(connection-oriented)的协议(区别于 UDP),在双方发送数据前需要在它们之间建立一个连接(connection)。

为了搞清楚 TCP 连接的建立和中断,我们从一个实际的例子说起(这里直接摘取《TCP/IP Illustrated, Volume 1》中的范例,你也可以自己做类似尝试):

 
 
  1. # 建立到 bsdi 主机 discard 服务的连接
  2. # 并在连接建立成功后中断连接
  3. svr4 % telnet bsdi discard
  4. Trying 192.82.148.3 ...
  5. Connected to bsdi.
  6. Escape character is '^]'.
  7. ^]
  8. telnet> quit
  9. Connection closed.

tcpdump 命令输出此命令产生的 segments(报文段),也就是 TCP 连接建立并中断过程中产生的 segments(一共 7 个,前 3 个为建立连接,后 4 个为断开连接):

 
 
  1. 1 0.0 svr4.1037 > bsdi.discard: S
  2. 1415531521:1415531521(0)
  3. win 4096 <mss 1024>
  4.  
  5. 2 0.002402 bsdi.discard > svr4.1037: S
  6. (0.0024) 1823083521:1823083521(0)
  7. ack 1415531522 win 4096 <mss 1024>
  8.  
  9. 3 0.007224 svr4.1037 > bsdi.discard: ack 1823083522
  10. (0.0048) win 4096
  11.  
  12. 4 4.155441 svr4.1037 > bsdi.discard: F
  13. (4.1482) 1415531522:1415531522(0)
  14. ack 1823083522 win 4096
  15.  
  16. 5 4.156747 bsdi.discard > svr4.1037: . ack 1415531523
  17. (0.0013) win 4096
  18.  
  19. 6 4.158144 bsdi.discard > svr4.1037: F
  20. (0.0014) 1823083522:1823083522(0)
  21. ack 1415531523 win 4096
  22.  
  23. 7 4.180662 svr4.1037 > bsdi.discard: . ack 1823083523
  24. (0.0225) win 4096

简单的了解一下 tcpdump 命令的输出,每个 Segments 都开始于:
源 > 目标: flags

在 TCP Header 中有 6 个 flag bits,它们可以同时有效:

  1. URG — 表示 Urgent pointer 有效
  2. ACK — 表示确认序号(Acknowledgment number)有效
  3. PSH — 接受者需要向应用程序尽可能快的传递数据
  4. RST — 重置(Reset)连接
  5. SYN — 同步(Synchronize)序号,用于发起一个连接
  6. FIN — 发送者完成了(Finished)数据的发送

在命令 tcpdump 输出的 “源 > 目标: flags” 中 flags 的含义如下:

  1. S 表示 SYN
  2. F 表示 FIN
  3. R 表示 RST
  4. P 表示 PSH

若 tcpdump 输出 “源 > 目标: .” 则表示上面的 4 个 flags 全部为 0。除了上面 4 个 flags 之外,TCP Header 另外的两个 flags:ACK 和 URG 被用另外的方式打印出。

2012-01-02-104901_625x566_scrot

上图显示了通过 Telnet 命令连接主机 bsdi 的服务 discard 并中断的过程。

连接的建立

连接的建立需要三次握手(three-way handshake):

  1. Client(连接请求方)发送 SYN segment 到 Server,SYN segment 包含了 Server 的端口号以及 Client 的 ISN(initial sequence number,上例中为 1415531521)
  2. Server 发送 SYN segment 回应 Client,SYN segment 包含了 Server 的 ISN,同时,确认 Client 的 SYN,在 Client SYN segment ISN 值上加 1(1415531522)。注意,每一个 SYN segment 都带有一个 SN(序号)
  3. Client 确认 Server 的 SYN segment,确认的 ACK segment 中 ISN 为 Server SYN segment 的 ISN 加 1

我们称发送第一个 SYN 的一方完成了一个 Active open(主动打开),另外一方完成了一个 Passive open(被动打开)。SYN Segment 带有 ISN,ISN 随着时间而改变,因此,每一个连接都有不同的 ISN。

连接的终止

连接的终止需要四次握手(four-way handshake)。TCP 连接是全双工的(full-duplex),数据(为了避免理解上的歧义,这里解释一下,数据的含义为应用层传递给 TCP 的数据,而非 TCP Segments)能够在两个方向上同时传递,因此每个方向都必须独立的进行关闭。当一方完成数据发送时,可以发送一个 FIN 来表明中断这个方向的数据传输,TCP 收到 FIN 后会通知应用程序另一方已经关闭了连接(收到 FIN 的一方不会再接收到数据)。发送 FIN 通常是因为应用层关闭连接的结果,一方 TCP 收到 FIN 后,仍然能发送数据(但无法接收数据),这就是半关闭(half-close),具体来说:

  1. A 发送 FIN Segment 到另一端 B
  2. B 收到 FIN Segment 后回发一个 ACK Segment(序号在 FIN Segment 的序号基础上加 1)。同于 SYN,FIN 也带有一个 SN(序号)。同时,TCP 通知应用程序另一方已经关闭了
  3. 如果 B 此时进行连接的关闭(一般来说此为应用程序而非 TCP 的行为,若应用程序不进行连接的关闭则此时处于半关闭),这将导致它的 TCP 发送一个 FIN Segment 到 A
  4. A 收到 FIN Segment 向 B 发送一个 ACK Segment(序号在 FIN 上加 1)

我们把先进行的关闭叫做主动关闭(Active close),后进行的关闭叫做被动关闭(Passive close)。在程序上实现 half-close,通过调用 API shutdown(而非 closesocket)且第二个参数传递值 1。API shutdown 不会导致 Socket 相关的资源被释放,调用 closesocket 则释放 Socket 相关资源。

下图是一个 TCP 的状态图:
TCP 的状态图
实线箭头表示 Client 的正常状态变迁
虚线箭头表示 Server 的正常状态变迁
“应用进程” 表示当应用进程执行了某个操作发生的状态变迁
“收” 表示收到 TCP Segment 时状态的变迁
“发” 表示进行状态变迁需要发送 TCP Segment

2MSL 状态

TIME_WAIT 状态又被叫做 2MSL 状态。TCP 实现必须指定 Segments 在网络中的最大生存时间(maximum segment lifetime — MSL)。MSL 是一个有限的值,因为 TCP segments 是通过 IP Datagrams 进行传输的,IP Datagrams 有限制其生存时间的 TTL 字段(是基于 hops 的,而不是定时器)。
当 TCP 执行一个主动关闭时,当最后一个 ACK Segment 发送,连接必须在 TIME_WAIT 状态下停留 2 倍的 MSL 时间,这样可以使得当 ACK Segment 丢失时,可以重传最后一个 ACK Segment(另一端超时并且重发最后的一个 FIN)。
这样带来的影响是,如果 TCP 连接在 2MSL 等待时,连接不能被重新使用(Client IP、Port 和 Server IP、Port 确定了一条连接)。我们现在通过 sock 程序来实践一下相关内容,sock 程序可以在 http://www.icir.org/christian/sock.html 下载,这里简单的说一下程序的用法:

  1. 使用 sock 作为 Client
       
       
    1. sock [options] <host> <port>

    这里 sock 作为一个 Client,连接 host 的 port 端口,这里还可以使用选项 b 来指定 Client 的 Port

  2. 使用 sock 作为 Server
       
       
    1. sock [options] -s <port>

    这里 sock 使用了选项 s 表示 sock 作为一个 Server 监听 port 端口

选项 v 用于输出冗余信息(详细信息)。我们来进行一些实践:

 
 
  1. % sock -v -s 6666

使用 telnet 连接此程序

 
 
  1. % telnet 127.0.0.1 6666

终止 sock 程序之后,再执行:

 
 
  1. % sock -v -s 6666

得到以下反馈:can’t bind local address: Address already in use
使用 netstat 命令(Windows、Linux 下均可以使用)可以获取连接的状态(这里连接的状态为 TIME_WAIT)。有一些 Socket API 可以使用 SO_REUSEADDR 选项重新使用此端口(sock 命令范例:sock -Av -s 6666),但是不能使用相同的 Socket 对创建新的连接,看一个例子:
在 sun 主机上开启一个服务,监听 6666 端口

 
 
  1. sun % sock -v -s 6666

在 bsdi 主机上连接 sun 主机的 6666 端口(bsdi 上端口为 1098),然后中断 sun 上面的服务,再尝试在 sun 上面开启一个客户端(-b 用于指定本地端口):

 
 
  1. sun % sock -b6666 bsdi 1098

这时候,得到一个错误信息:can’t bind local address: Address already in use。再尝试在 sun 主机上执行:

 
 
  1. sun % sock -A -b6666 bsdi

这时候,得到一个错误信息:active open error: Address already in use
第二次调用 sock -b6666 bsdi 1098 失败,是因为端口 6666 处在 2MSL 等待状态,第三次调用 sock -A -b6666 bsdi 失败(-A 表示开启了 Socket 的 SO_REUSEADDR 选项),因为使用了 -A 选项,我们可以将本地端口设置为 6666,但是进行 Active open 的时候还是会得到一个错误,因为连接本身还是处于 2MSL 状态则无法建立连接。我们再看下面的情况:
在 sun 上面启动服务,监听端口 6666(假定现在仍然处于 2MSL 状态,这里使用 -A 选项)

 
 
  1. sun % sock -A -s 6666

在 bsdi 上启动客户端程序

 
 
  1. bsdi % sock -b1098 sun 6666

我们得到了一个奇怪的结果:连接成功。这违反了 TCP 规范,因为连接还处于 2MSL 状态。这种行为被大多数基于 Berkeley 的实现版本支持,这些实现允许新连接请求到达一个仍然处于 TIME_WAIT(2MSL)状态的连接(服务器需要 Active close),只要满足新的序号大于这个连接先前的替身连接的最后的序号(一个连接的新的实例被叫做这个连接的替身)。

FIN_WAIT_2 状态

从上面的 TCP 状态图可以看出,Active close 时,首先 A 发送 FIN 到 B,进入 FIN_WAIT_1 状态,收到 B 发送过来的 ACK 则进入 FIN_WAIT_2 状态。这个时候,如果 B 发送 FIN 给 A,A 则能进入 TIME_WAIT 状态,如果 B 不发送 FIN 给 A,A 将进行等待。基于 Berkeley 的实现版本通过以下手段来避免在 FIN_WAIT_2 状态下的无限等待:如果连接空闲 10 分钟 75 秒,TCP 则进入 CLOSE 状态(这也就是 Half-close 连接的空闲等待时间)。

另一种终止连接的方式:异常终止连接

前面说到的通过发送 FIN 终止连接的方式被叫做有序释放(orderly release),因为所有的数据都已经发送完成后才发送 FIN,正常情况下不会有任何数据的丢失。但是我们也可以发送一个 RST Segment 来释放一个连接,这种释放被叫做异常释放(abortive release)。异常终止一个连接有以下几个特点:

  1. 丢弃任何准备发送的数据,立即发送 RST Segment
  2. RST 接收方会区分另外一端执行的是异常关闭还是正常关闭,其将通知上层应用复位(Reset)
半打开(Half-open)连接

如果一方已经关闭连接而另一方却不知道,我们将这样的 TCP 连接称之为 Half-open(半打开的,区别于概念 Half-close)。实际中,出现 Half-open 一般是 Client 直接切断了电源,而不是结束应用程序后关机。如果 Client 直接切断了电源且这时候没有任何数据需要传送,那么 Server 将永远不知道 Client 已经消失。具体的来说一个例子:我们在一台主机上执行 Telnet client,连接一台 Server,然后断开 Server 的以太网电缆,然后重启 Server,这样就在 Client 上存在了一个 Half-open 的连接。然后重新连接 Server 的电缆,再尝试从 Client 发送一行数据到 Server。由于 Server 的 TCP 已经重启,对于此 Half-open 连接一无所知,这时候 TCP 规定 Server 回应 Client 一个 RST Segment,Client 这时候显示连接已经被远程服务器中断。

使用 keepalive 选项可以帮助我们检测另一方是否已经消失,虽然很多实现版本带有 keepalive 选项,但实际上 keepalive 并不是 TCP 规范的一部分(不少人认为这个不是 TCP 应该提供的功能,而应该应用程序自己完成)。一般来说,Server 才会去使用 keepalive(也有很多时候,开发者在应用程序中根据自己的需求设计而不是使用 keepalive),这样可以知道 Client 是否已经崩溃。具体的细节是:

  1. 如果一个连接 2 个小时都未活跃,那么 Server 就会发送一个侦探 Segment 到 Client
  2. 如果 Client 收到侦探 Segment 给于回应,Server 得知 Client 正常则 TCP 会重置 keepalive 定时器设定其在 2 小时后触发,如果在定时器触发前有数据进行交换,定时器会在数据交换后再进行重置(2 小时后触发)
  3. 如果 Client 已经崩溃或者断电(或重启),Server 将无法获取侦探 Segment 的响应,75 秒后超时,Server 共侦探 10 次,每个间隔 75 秒,如果 Server 没有收到一个响应,那么认为 Client 已经关闭连接
  4. 如果 Client 已经断电并且重启好了,Client 在收到 Server 的侦探 Segment 后会发送一个 RST Segment 给 Server(正如我们前面谈到的例子),Server 中断连接

注意,2 个小时这个值是可以被改变的(有一些系统此值是一个系统级变量,改变它会影响到使用此选项的所有用户)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值