TCP 通讯详解及实战

日常开发,测试过程中,特别是在压力测试过程中,用 netstat -nat 查看,发现客户端出现大量 SYN_SENT 状态,服务端出现大量 SYN_RCVD 状态连接,下面我们一起看下

  • 为什么会有这些状态?
  • 出现这些状态原因是什么?

TCP 协议模型

IP 和端口

解决了文章最开始提到的定位的问题。
IP 在互联网中能唯一标识一台计算机,是每一台计算机的唯一标识(身份证);网络编程是和远程计算机的通信,所以必须先能定位到远程计算机;IP 帮助解决此问题;一台计算机中可能有很多进程,具体和哪一个进程进行通信,这就得靠端口来识别;  

TCP 和 UDP 协议

  • TCP 是 Tranfer Control Protocol 的简称,是一种面向连接的保证可靠传输的协议。通过 TCP 协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个 socket 之间必须建立连接,以便在 TCP 协议的基础上进行通信,当一个 socket(通常都是 server socket)等待建立连接时,另一个 socket 可以要求进行连接,一旦这两个 socket 连接起来,它们就可以进行双向数据目的地的时间以及内容的正确性都是不能被保证的传输,双方都可以进行发送或接收操作。
  • UDP 是 User Datagram Protocol 的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达。
  • 比较:
    • UDP:
      • 每个数据报中都给出了完整的地址信息,因此无需要建立发送方和接收方的连接。
      • UDP 传输数据时是有大小限制的,每个被传输的数据报必须限定在 64KB 之内。
      • UDP 是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方
    • TCP:
      • 面向连接的协议,在 socket 之间进行数据传输之前必然要建立连接,所以在 TCP 中需要连接时间。
      • TCP 传输数据大小限制,一旦连接建立起来,双方的 socket 就可以按统一的格式传输大的数据。
      • TCP 是一个可靠的协议,它确保接收方完全正确地获取发送方所发送的全部数据。
        • 应用数据分割成 TCP 认为最适合发送的数据块。这部分是通过 “MSS”(最大数据包长度)选项来控制的,通常这种机制也被称为一种协商机制,MSS 规定了 TCP 传往另一端的最大数据块的长度。值得注意的是,MSS 只能出现在 SYN 报文段中,若一方不接收来自另一方的 MSS 值,则 MSS 就定为 536 字节。一般来讲,在不出现分段的情况下,MSS 值还是越大越好,这样可以提高网络的利用率。
        • 重传机制。设置定时器,等待确认包。
        • 对首部和数据进行校验。
        • TCP 对收到的数据进行排序,然后交给应用层。
        • TCP 的接收端丢弃重复的数据。
        • TCP 还提供流量控制。(通过每一端声明的窗口大小来提供的)

TCP 状态机

TCP 状态图

  • 连接建立

    • 第一次握手:主机 A 发送位码为 syn=1, 随机产生 seq number=1234567 的数据包到服务器,主机 B 由 SYN=1 知道,A 要求建立联机;
    • 第二次握手:主机 B 收到请求后要确认联机信息,向 A 发送 ack number=(主机 A 的 seq+1),syn=1,ack=1, 随机产生 seq=7654321 的包
    • 第三次握手:主机 A 收到后检查 ack number 是否正确,即第一次发送的 seq number+1, 以及位码 ack 是否为 1,若正确,主机 A 会再发送 ack number=(主机 B 的 seq+1),ack=1,主机 B 收到后确认 seq 值与 ack=1 则连接建立成功。
    • 完成三次握手,主机 A 与主机 B 开始传送数据。
  • 连接关闭,由于 TCP 连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向的连接。收到一个 FIN 只意味着这一方向上没有数据流动,一个 TCP 连接在收到一个 FIN 后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

    • 客户端 A 发送一个 FIN,用来关闭客户 A 到服务器 B 的数据传送(报文段 4)。
    • 服务器 B 收到这个 FIN,它发回一个 ACK,确认序号为收到的序号加 1(报文段 5)。和 SYN 一样,一个 FIN 将占用一个序号。
    • 服务器 B 关闭与客户端 A 的连接,发送一个 FIN 给客户端 A(报文段 6)。
    • 客户端 A 发回 ACK 报文确认,并将确认序号设置为收到序号加 1(报文段 7)。
      附上另一张图:

TCP 状态

  • CLOSED: 表示初始状态。

  • LISTEN: 表示服务器端的某个 SOCKET 处于监听状态,可以接受连接了。

  • SYN_RCVD: 这个状态表示接受到了 SYN 报文,在正常情况下,这个状态是服务器端的 SOCKET 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,基本 上用 netstat 你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次 TCP 握手过程中最后一个 ACK 报文不予发送。因此这种状态 时,当收到客户端的 ACK 报文后,它会进入到 ESTABLISHED 状态。

  • SYN_SENT: 这个状态与 SYN_RCVD 遥想呼应,当客户端 SOCKET 执行 CONNECT 连接时,它首先发送 SYN 报文,因此也随即它会进入到了 SYN_SENT 状态,并等待服务端的发送三次握手中的第 2 个报文。SYN_SENT 状态表示客户端已发送 SYN 报文。

  • ESTABLISHED:表示连接已经建立。

  • FIN_WAIT_1: 这个状态要好好解释一下,其实 FIN_WAIT_1 和 FIN_WAIT_2 状态的真正含义都是表示等待对方的 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 状态。

  • CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方 close 一个 SOCKET 后发送 FIN 报文给自己,你系统毫无疑问地会回应一个 ACK 报文给对 方,此时则进入到 CLOSE_WAIT 状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close 这个 SOCKET,发送 FIN 报文给对方,也即关闭连接。所以你在 CLOSE_WAIT 状态下,需要完成的事情是等待你去关闭连接。

  • LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文。当收到 ACK 报文后,也即可以进入到 CLOSED 可用状态了。

  • CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送 FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对方的 ACK 报文,反而却也收到了对方的 FIN 报文。什 么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时 close 一个 SOCKET 的话,那么就出现了双方同时发送 FIN 报 文的情况,也即会出现 CLOSING 状态,表示双方都正在关闭 SOCKET 连接

通讯类型

  • 短连接
    • 连接 -> 数据传输 -> 关闭连接
  • 长连接,要求长连接在没有数据通信时,定时发送数据包,以维持连接状态,短连接在没有数据传输时直接关闭就行了 长连接需要心跳包维护连接状态
    • 连接 -> 数据传输 -> 保持连接 -> 数据传输 -> 保持连接 -> …… -> 关闭连接

什么时候用长连接,短连接?
长连接主要用于在少数客户端与服务端的频繁通信,因为这时候如果用短连接频繁通信常会发生 Socket 出错,并且频繁创建 Socket 连接也是对资源的浪费。
但是对于服务端来说,长连接也会耗费一定的资源,需要专门的线程来负责维护连接状态。

  • 同步通讯–报文发送和接收是同步进行,既报文发送后等待接收返回报文。

    • 发送数据之后等待接收返回数据。同步方式一般需要考虑超时问题,即报文发上去后不能无限等待,需要设定超时时间,超过该时间发送方不再等待读返回报文,直接通知超时返回。
  • 异步通讯–报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:

    • 异步双工:接收和发送在同一个程序中,有两个不同的子进程分别负责发送和接收
    • 异步单工:接收和发送是用两个不同的程序来完成。

日常应用

我们通过了解 TCP 各个状态,可以排除和定位网络或系统故障时大有帮助。

linux 查看 tcp 的状态命令:

  1. netstat -nat  查看 TCP 各个状态的数量
  2. lsof  -i:port  可以检测到打开套接字的状况
  3. tcpdump -iany tcp port 9000 对 tcp 端口为 9000 的进行抓包

故障排查

通过端口监听判断服务启动是否正常
netstat –an| grep 8080

  1. 例如:提供 www 服务默认开的是 80 端口,提供 ftp 服务默认的端口为 21,当提供的服务没有被连接时就处于 LISTENING 状态。FTP 服务启动后首先处于侦听(LISTENING)状态。处于侦听 LISTENING 状态时,该端口是开放的,等待连接
  2. 客户端出现大量 SYN_SENT 状态
    • 当请求连接时客户端首先要发送同步信号给要访问的机器,此时状态为 SYN_SENT,如果连接成功了就变为 ESTABLISHED,正常情况下 SYN_SENT 状态非常短暂
    • 如果发现有很多 SYN_SENT 出现,那一般有这么几种情况,一是你要访问的服务器网络不好,二是服务端无法建立连接返回 ack
  3. 服务端出现大量 SYN_RCVD 状态连接
    • 同理 SYN_RCVD 状态也是非常短暂的,如果大量出现说明有可能遭到了攻击,或者是客户端网络限制
  4. 大量的 CLOSE-WAIT 状态
    • 被动关闭 (passive close) 端 TCP 接到 FIN 后,就发出 ACK 以回应 FIN 请求 (它的接收也作为文件结束符传递给上层应用程序), 并进 CLOSE_WAIT. 如果连接不关闭 CLOSE_WAIT 持续时间会非常长,如果长时间积累,可能会导致系统资源耗尽
  5. 发现系统存在大量 TIME_WAIT 状态的连接,可以修改内核参数解决,修改 TIME_WAIT 持续时间