网络编程(三)

在之前两篇文章我们已经大概了解了互联网的工作原理,知道了一个数据包的诞生与结束中间经历的过程,那么在这篇博客我们将了解一个稳定可靠地 TCP 连接是怎么产生的,它的数据传送有什么优点和缺点?

该博客将重点介绍 传输层的 TCP 协议建立连接和断开连接的过程. UDP 因为不是可靠的连接所以就不重点介绍了.

介绍

在传输层通常遵循的协议为 TCP 和 UDP协议,并且是基于端口运行的,但是两者为应用层提供不同的服务. TCP 提供的是一种稳定,可靠地字节流服务.

面向连接意味着两个使用 TCP 的应用在数据传送之前需要建立 TCP 连接.这一过程可以理解为打电话,先拨号,等待接通,然后稳定的通信.

那么 UDP 是无连接,不可靠的数据包服务,因为无连接,所以可能会产生丢包,但是效率却较高,因为不用对方发送确认包,所以 UDP 广泛应用与游戏,直播等软件.

TCP 报文格式

在网络编程(二)中已经知道 TCP是基于端口发送数据包的,所以会记录本机端口号和目的端口号,具体格式如下:

上图有几个重要的字段需要了解:

  • 序 列号: seq 序号,占32位,用来标识从 TCP 源端向目的端发送的字节流,发送方的发送数据时对比进行标记;
  • 确认号: ACK 序号,占32位,只有 ACK 标志位位1时,确认号字段才有效, ack=seq+1;
  • 标志位:共6个,即 URG,ACK,PSH,RST,SYN,FIN 等,具体含义如下:
    1. URG: 紧急指针有效;
    2. ACK: 确认序号有效;
    3. PSH: 接收方应该尽快将报文交给应用层;
    4. RST: 重置连接;
    5. SYN: 发起一个新连接
    6. FIN: 释放一个连接.
  • 两个号码
    1. Sequene number: 顺序号码
    2. Acknowledge number: 确认号码

注意:

  • 不要将确认序号 Ack 与标志位中的 ACK 搞混了;
  • 确认方 ack=seq+1(不论哪方发送),两端才建立连接.

三次握手详解

所谓的三次握手( Three-Way Handshake)即建立 TCP 连接,就是指建立一个 TCP 连接时,需要客户端和服务端发送三个包用来确认连接的建立.在套接字编程中,这一过程由客户端执行 connect 来主动触发,整个流程如下:

三次握手

  1. 第一次握手: 客户端发送 SYN 包( seq=x)的数据包到服务器,并进入 SYN_SEND 状态,等待服务器确认;
  2. 第二次握手: 服务器收到 SYN 包,必须确认客户端的 SYN(ACK=x+1),同时自己也发送一个 SYN 包(seq=y),即 SYN+ACK 包,此时服务器进入 SYN_RCVD 状态;
  3. 第三次握手: 客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ACK=y+1),此包发送完毕,客户端和服务端都进入 ESTABLISHED 状态,完成三次握手.

注意

握手过程传送的包不包含任何数据,三次握手完毕后,客户端与服务器才正式开始传送数据.理想状态下, TCP 连接建立.在通信双方中的任何一方主动关闭连接之前, TCP 连接都将被一直保持下去.(双方都可以主动断开连接)

四次挥手详解

在建立 TCP 连接之后,客户端和服务端开始传输数据,因为这是双向连接,所以任一方都可以主动断开连接.

四次挥手

  1. 第一次挥手: 主动关闭方发送一个 FIN, 用来关闭主动方到被动方的数据传送,也就是主动关闭方告诉被动关闭方:我的数据传输已经完成了,不会再给你发送数据了(当然,在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 确认报文,主动关闭方依然会重发这些数据),但是此时主动关闭方还可以接收数据.
  2. 第二次挥手: 被动关闭方收到 FIN 包后,发送一个 ACK 给对方,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号).
  3. 第三次挥手: 被动关闭方发送一个 FIN, 用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完成,不会再给你发送数据了(但是在 FIN 发送之前发送的数据仍然需要主动关闭方发送确认包).
  4. 第四次挥手: 主动关闭方收到 FIn 后,发送一个 ACK 给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手.

TCP 状态转换图

CLOSED: 表示初始状态

LISTEN(服务器): 表示服务器的某个套接字处于坚挺状态,可以接受客户端的连接

YN_RCVD(服务器): 这个状态表示服务器接收到了客户端的 SYN 报文,在正常情况下,这个状态是服务端的 SOCKET 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用 netstat 是很难看到这种状态的,因此这种状态时,当收到客户端的 ACK 报文后,他会进入 ESTABLISHED 状态

SYN_SENT: 这个状态与 SYN_RCVD 相对应,当客户端SOCKET 执行 CONNECT 连接时,它首先发送 SYN 报文,因此也随机会进入 SYN_SENT, 并等待服务端发送三次连接中的第二个报文. SYN_SENT 表示客户端已发送 SYN 请求连接报文.

ESTABLISHED: 表示连接已经建立

FIN_WAIT_1: 其实 FIN_WAIT_1和 FIN_WAIT_1状态的真正含义都是表示等待对方的 FIN 报文.而这两种状态的区别是: FIN_WAIT_1状态实际上是当SOCKET 在 ESTABLISHED 状态时,它想主动关闭连接,向对方发送了 FIN 报文,此时该 SOCKET 即进入到 FIN_WAIT_1状态.而当对方回应ACK 报文后,则进入到 FIN_WAIT_2状态,当然在实际的正常情况下,无论对方在何种情况,都应该回应 ACK 报文,所以 FIN_WAIT_1状态一般是不容易见到的,而 FIN_WAIT_2可以用 netstat 看到

FIN_WAIT_2: 实际上 FIN_WAIT_2状态下的 SOCKET, 表示半连接,也即有一方要求 CLOSE 连接,但另外还告诉对方,我暂时还有点数据需要传送,等会再关闭连接

TIME_WAIT: 表示收到了对方的 FIN 报文,并发送了 ACK 报文,就等2MSL 后即可回到 CLOSED 可用状态了(初始状态).如果 FIN_WAIT_1状态下,收到了对方同时带 FIN标志和 ACK 标志的报文时,可以直接进入到 TIME_WAIT 状态,而无需经过 FIN_WAIT_2状态

注意:

MSL( 最大分段生存期)指明 TCP 报文在 internet 上最长生存时间,每个具体的 TCP 实现都必须选择一个确定的 MSL 值. RFC1122建议为2分钟,但 BSD 传统实现了采用30秒. TIME_WAIT 状态最大保持时间是2*MSL, 也就是1-4分钟.

结论:

在 TIME_WAIT 下等待2MSL, 只是为了尽最大努力保证四次握手正常关闭.确保老的报文段在网络中消失,不会影响新建立的连接.

CLOSING: 这种状态比较特殊,实际情况中很少见.正常情况下,当你发送 FIN 报文后,按理来说应该先收到(或同时受到)对方的 ACK 报文,再收到对方的 FIN 报文.但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对方的 ACK报文,反而收到了对方的 FIN 报文.那么什么情况下会出现这种情况.那就是双方几乎在同时 close 一个 SOCKET 的时候,那么久出现了双方同时发送 FIN 报文的情况,也即会出现 CLOSING 状态,表示双方都正在关闭 SOCKET 连接.

CLOSE_WAIT: 这种状态的含义其实是在表示等待关闭.当对方 close 一个 SOCKET 后发送 FIN 报文给自己,系统毫无疑问会回应一个 ACK 报文给对方,此时则进入 CLOSE_WAIT 状态.接下来就需要考虑是否还有数据需要发送给对方,如果没有的话,那么就可以 close 这个 SOCKET, 发送 FIN 报文给对方,也即关闭连接.所以在 CLOSE_WAIT 状态下,需要等待你去关闭连接

LAST_ACK: 它是被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文.当收到 ACK 报文后,也即可以进入到 CLOSED可用状态.( 初始状态)

补充:

  1. 默认情况下,当调用 close 时,如果发送缓冲中还有数据, TCP 会继续把数据发送完;
  2. 发送了 FIN 只是表示这端不能继续发送数据(应用层不能调用 send 发送),但是仍然可以接收数据;
  3. 应用层如何知道对端关闭?通常,在最简单的阻塞模型中,当你调用 recv 时,如果返回0,则表示对端关闭.在这个时候通常的做法就是也调用close, 那么会发送 FIN, 完成四次握手.如果不调用 close, 那么对端就会处于 FIN_WAIT_2状态,而本端则会处于 CLOSE_WAIT状态.
  4. 很多时候, TCP 连接的断开都是有 TCP 层自动进行,例如使用 CTRL_C 终止程序, TCP 连接依然会正常关闭.

问题:

  1. 为什么建立连接协议是三次握手,而关闭连接是四次挥手呢?

    这是因为服务端的 LISTEN 状态下的 SOCKET 收到 SYN 的请求连接时,可以把 ACK和 SYN(ACK起应答作用,而 SYN 起同步作用)放在一个报文里一起发送.但是关闭连接时,当收到对方的 FIN 报文通知时,它仅仅表示对方没有数据发送了,但是另一方未必所有的数据 都全部发送完全了,所以可能不会立马关闭 SOCKET, 也即你可能还需要发送一些数据给对方之后,再发送 FIN 报文给对方表示你同意现在关闭连接了,所以这里的 ACK 报文和 FIN 报文是分开发送的.

  2. 为什么不能用两次握手进行连接?

    在三次握手中,总共需要完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已经准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认.

    现在把三次握手改成仅需要两次握手,是可能会发生死锁的.考虑计算机客户端和服务端之间的通信,假定客户端给服务端发送一个连接请求分组,服务端收到了这个分组,并发送了确认应答分组.按照两次握手的协定,服务端认为链接已经成功的建立了,可以开始发送数据分组.可是,客户端在服务端的应答分组在传输中被丢失的情况下,将不会知道服务端是否已准备好,不知道服务端建立什么样的序列号,客户端甚至会怀疑服务端是否收到自己的连接请求分组.在这种情况下,客户端认为连接还未建立成功,将忽略服务端发来的任何数据分组,只等待连接确认应答分组.而服务端在发出的数据分组超时后,重复发送同样的数据分组,就形成了死锁.

  3. 为什么 TIME_WAIT 状态需要等2MSL 后才能返回到 CLOSED 状态?

    什么是 MSL? MSL 即Maximum Segment Lifetime, 也就是报文最大生存时间.’MSL 是任何报文段被丢弃前在网络内的最长时间.’那么,2MSL 也就是这个时间的两倍,当 TCP 连接完成四个报文段的交换时,主动关闭的一方将继续等待一定时间(2-4)分钟,即使两端的应用程序结束.

    为什么需要2MSL 呢.

    第一,虽然双方都同意关闭连接了,而且握手的四个报文也都协调和发送完毕,按理可以直接回到 CLOSED 状态(就好比从 SYN_SEND 状态到 ESTABLISH 状态那样);但是因为对方处于 LAST_ACK 状态下的 SOCKET 可能会因为超时未收到 ACK 报文,而重发 FIN 报文,所以这个 TIME_WAIT 状态的作用就是用来重发可能丢失的 ACK 报文.

    第二,报文可能会被混淆,意思是说其他时候的连接可能会被当做本次的连接.

    当某个连接的一端处于 TIME_WAIT 状态时,该连接将不能再被使用.事实上,对于我们比较有现实意义的是,这个端口将不能再被使用.某个端口处于 TIME_WAIT(其实应该是这个连接) 状态时,这意味着这个 TCP 连接并没有断开(完全断开),那么.如果你 bind 这个端口,就会失败.对于服务器而言,如果服务器突然 crash 掉了,那么他将无法在2MSL 内重新启动,因为 bind 会失败.解决这个问题的一个方法就是设置 SOCKET 的 SO_REUSEADDR 选项.这个选项意味着可以重用一个地址.

    当建立一个 TCP 连接时,服务端会继续用原有端口监听,同时用这个端口与客户端通信.而客户端默认情况下会使用一个随机端口与服务端的监听端口通信.有时候,为了服务端的安全性,我们需要对客户端进行验证,即限定某个 IP 的某个特定端口的客户端.客户端可以使用 bind 来使用特定的端口.对于服务端,当设置了 SO_REUSEADDR 选项时,它可以在2MSL 内启动并 listen成功.但是对于客户端,当使用 bind 并设置 SO_REUSEADDR 时,如果在2MSL 内启动,虽然 bind 会成功,但是在 windows 平台上 connect 会失败.而在 linux 是哪个不存在这个问题.

    要解决 windows 平台的问题,可以设置 SO_LISTEN 选项. 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 则会在一定时间后返回错误信息.

Categories AI