前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TCP 三次握手和四次挥手

TCP 三次握手和四次挥手

原创
作者头像
谛听
修改2023-10-16 15:14:27
3810
修改2023-10-16 15:14:27
举报
文章被收录于专栏:随意记录

1 背景

我们知道 UDP 协议乐观且心大,相信网络环境比较健康,数据是可以送达的,即使送达不了也没关系。而 TCP(Transmission Control Protocol,传输控制协议) 就不一样了,它是悲观且严谨,认为网络环境是恶劣的,丢包、乱序、重传和拥塞是常有的事,一言不合就可能送达不了了,因而要从算法层面来保证可靠性。

TCP 是一种面向连接的、可靠的、基于字节流的传输层协议。本文一起看一下 TCP 协议保证可靠性的机制之一:提供可靠的连接服务,通过三次握手建立连接,通过四次挥手关闭连接。

2 概念

2.1 TCP 包头部

我们先来看下 TCP 头的格式。从下图可以看成,它比 UDP 复杂得多。

首先,源端口号和目标端口号必不可少,这跟 UDP 是一样的。因为如果没有这两个端口号,数据就不知道发给哪个应用。

接下来是包的序号。给包编号的好处是可以解决乱序的问题。

还应该有确认序号。发出的包应该有确认,不然怎么知道对方收到了没?没收到就应该重新发送,直到送达,可以有效地解决丢包问题。

接下来是一些状态位。例如 SYN(Synchronize Sequence Number)是发起一个连接,ACK(Acknowledgement)是回复,FIN(Finish)是结束连接等。TCP 是面向连接的,所以双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态的变更。

还有一个重要的就是窗口大小。TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前的处理能力,避免对方发送太快或太慢。

TCP 除了做流量控制,还会做拥塞控制,对于道路的拥堵无能为力,只能控制自己的发送速度。不过拥塞窗口没有体现在 TCP 包头部。

2.2 TCP 三次握手建立连接

无论哪一方向另一方发送数据,都必须先在双方之间建立一条连接。

TCP 的连接建立,我们常常称为三次握手。

  • A:您好,我是 A。
  • B:您好 A,我是 B。
  • A:您好 B。

具体到 TCP 协议,三次握手建立连接如下图所示:

一开始,A 和 B 都处于 CLOSED 状态。B 主动监听某个端口,处于 LISTEN 状态。

  1. A 主动发起连接,发送一个 SYN 报文段,以表明自己的起始序列号,之后进入 SYN_SENT(SYN 已被发送) 状态。
  2. B 采用 SYN + ACK 报文段响应 A 的请求。其中,ACK 用来应答 A,表明收到了 A 的请求;SYN 用来表明自己的起始序列号。之后进入 SYN_RCVD (SYN 已被接收)状态。
  3. A 收到 B 的响应,发送 ACK 的 ACK,进入 ESTABLISHED 状态,因为它一发一收成功了。B 收到 ACK 的 ACK 后,也进入 ESTABLISHED 状态,因为它也一发一收成功了。

A、B 会在第一次发送 SYN 时分别生成随机初始序列号 seq=x,seq=y 接下来,每次交流时,seq=自己上一个包的序列号+1,ack=对方上一个包的序列号+1(期望收到对方下一包数据的序列号)。

可以看到,三次握手后,就可以确认双方的接收能力和发送能力是否正常、同步双方的序列号,为后面的可靠性传输做准备。

为什么序号不能都从 1 开始?

因为这样往往会出现冲突。例如,A 连上 B 后,发送了 1、2、3 三个包,但发送 3 时,丢了或有延迟,于是重新发送。后来 A 掉线了,重新连上 B,序号又从 1 开始,然后发送 2,但并没有想发送 3,但上次延迟的 3 又到了,发给了 B,B 以为这是下一个包,于是发生了错误。

为什么需要三次握手?

我们知道,三次握手主要是为了确认双方的接收能力和发送能力是否正常、同步双方的初始序列号,那么两次或四次可以吗?

  • 可用性 - 不采用两次握手的原因 1:B 不能确认 A 是否具备接收数据的能力,所以就不能建立可靠的连接。 - 不采用两次握手的原因 2:A 和 B 可以就 A 的初始化序列号达成一致,但无法就 B 的初始化序列号达成一致,所以达不到同步初始序列号的目标。 - 不采用两次握手的原因 3:防止历史连接的建立 防止 A 已经失效的连接请求报文段突然又传到 B,从而产生错误。 设想这样一种情况,A 和 B 已经建立连接,做了简单的通信后,结束了连接。但是由于 A 建立连接的时候网络环境较差,所以它会发送多个建立连接请求,有的请求过了好一会儿后终于到达了 B,B 会认为这也是一个正常请求,因此建立了连接,从而造成了资源的浪费。 如果是三次握手,A 在收到 B 的 seq+1 消息后,可以判断当前的连接是否为历史连接。如果是历史连接,就会发送终止报文 RST 给服务端,终止连接,从而避免历史连接的建立。
  • 安全性 - 不采用两次握手的原因 4:无法避免拒绝服务攻击(DDos) 服务器无法验证客户端的真伪,对每个进来的连接都分配连接资源,伪造的海量 SYN 报文很容易欺骗服务器,导致服务器开辟出大量的连接资源,很快导致服务端连接资源耗尽。等合法的连接请求到达时,服务端没有多余的资源,导致无法提供服务,这就是拒绝服务攻击(DDos)。
  • 效率
    • 不采用四次握手的原因:只要确认双方的接收能力和发送能力是否正常即可,所以相对于四次握手或更多次握手,三次握手已经足够了。

同样,我们在设计中往往也是需要考虑各种异常情况的,这样才能提高程序的健壮性。

2.3 TCP 四次挥手关闭连接

看完了建立连接,我们看下关闭连接,关闭连接通常被称为四次挥手。

  • A:我说完了。
  • B:好的,我知道了。

这时候,只是 A 没有要说的了,即 A 不会再发送数据,但 B 能不能在 ACK 的时候直接关闭呢?不行的,很可能 B 还有话要说,还是可以发送数据,所以称为半关闭状态。

这个时候,A 可以选择不再接收数据,也可以选择最后在接收一段数据,等待 B 也主动关闭。

  • B: 嗨 A,我也说完了,拜拜。
  • A:好的,拜拜。

这样整个连接就关闭了。

具体到 TCP 协议,四次挥手关闭连接如下图所示:

客户端和服务端都处于 ESTABLISHED 状态。

  1. A 请求关闭连接,发送 FIN 报文段,进入 FIN_WAIT_1(终止等待-1)状态。
  2. B 响应 A 请求,发送 ACK 应答报文段,进入 CLOSE_WAIT(关闭等待)状态。A 收到响应后,进入 FIN_WAIT_2(终止等待-2)状态。
  3. 过了一会儿,B 的数据也传送完了,想要关闭连接,发送 FIN 报文段,进入 LAST_ACK(最后确认)状态。
  4. A 收到请求后,发送 ACK 应答报文段,进入 TIME_WAIT(时间等待)状态。B 收到应答后,进入 CLOSED 状态。 A 等待 2MSL 后,进入 CLOSED 状态。

为什么客户端在四次挥手后还要等待 2MSL 后才会真正关闭连接?

MSL(Maximum Segment Lifetime,报文最大生存时间),是任何报文在网络上存在的最长时间,超过这个时间的报文将被丢弃。

  • 为了保证 A 最后一次挥手的报文能够到达 B 如果 A(主动关闭方)发送的最后一条 ACK 丢失,B 就会觉得 A 没有收到我发送的 FIN,从而重新发送 FIN。如果 A 等待 2MSL,它就可以再次发出 ACK 并重新启动 2MSL 计时器。
  • 为了确保旧连接的数据包从网络中消失 A 直接关闭连接还有另外一个问题,A 的端口直接空出来了,但是 B 原来发过的很多包还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的旧包。虽然序列号是重新生成的,但为了保险,A 还是要在 TIME_WAIT 状态等 2MSL,以确保 B 发送的所有旧包都从网络中消失。

为什么需要四次挥手?

TCP 是全双工的(任何时刻数据都可以双向收发), A 到 B 是一个通道,B 到 A 又是另一个通道。

当 A 执行完第一次挥手后,只能证明 A 不会再向 B 请求数据,B 返回确认后,便不再接收 A 的数据。

但是 B 可能还在给 A 发送数据,A 还是可以接收数据的。只有当 B 需要把数据传输完毕后才能发送关闭请求,且确认 A 接收后,两边才会真正断开连接。

3 Socket 编程

Socket 封装了底层 TCP / IP 协议栈的功能,供应用层使用。Socket 在不同的操作系统上有不同的版本,我们下面看下 Linux 系统上的 C 语言版本。

为了简单起见,以下程序省略了一些错误处理。

服务端 server.c:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc , char *argv[]) {
    // Create a socket with IPv4 domain and TCP protocol
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        printf("Fail to create a socket.");
    }

    struct sockaddr_in server_address, client_address;
    unsigned int addr_len = sizeof(client_address);
    bzero(&server_address, sizeof(server_address));

    // Bind the socket with the values address and port from the sockaddr_in structure
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(8700);
    bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address));

    // listen on specified port with a maximum of 5 requests
    listen(sockfd, 5);

    char input_buffer[256] = {};
    char message[] = {"Hi, client!"};

    while (1) {
        // Accept connection signals from the client
        int client_sockfd = accept(sockfd, (struct sockaddr*)&client_address, &addr_len);

        // Receive data sent by the client
        recv(client_sockfd, input_buffer, sizeof(input_buffer), 0);
        printf("[2] Recv: %s\n",input_buffer);

        sleep(1);

        // Send data to the client
        send(client_sockfd, message, sizeof(message), 0);
        printf("[3] Send: %s\n", message);

        sleep(1);

        // Terminate the socket connection
        close(client_sockfd);
        printf("[6] Close Socket\n");
    }
    return 0;
}

客户端 client.c:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc , char *argv[]) {
    // Create a socket with IPv4 domain and TCP protocol
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        printf("Fail to create a socket.");
    }

    struct sockaddr_in info;
    bzero(&info, sizeof(info));

    // Connect to the server
    info.sin_family = AF_INET;
    info.sin_addr.s_addr = inet_addr("127.0.0.1");
    info.sin_port = htons(8700);
    int err = connect(sockfd, (struct sockaddr *)&info, sizeof(info));
    if (err == -1) {
        printf("Connection error");
    }

    // Send message to the server
    char message[] = {"Hi, server! I'm client."};
    send(sockfd, message, sizeof(message), 0);
    printf("[1] Send: %s\n", message);

    // Receive a message from the server
    char receiveMessage[100] = {};
    recv(sockfd, receiveMessage, sizeof(receiveMessage), 0);
    printf("[4] Recv: %s\n", receiveMessage);

    // Terminate the socket connection
    close(sockfd);
    printf("[5] Close Socket\n");
    return 0;
}

程序运行结果如下。

服务端:

代码语言:javascript
复制
$ gcc server.c -o server # Linux 上编译 C 语言程序
$ ./server               # Linux 上运行服务端
[2] Recv: Hi, server! I'm client.
[3] Send: Hi, client!
[6] Close Socket

新打开一个终端,运行客户端:

代码语言:javascript
复制
$ gcc client.c -o client # Linux 上编译 C 语言程序
$ ./client               # Linux 上运行客户端
[1] Send: Hi, server! I'm client.
[4] Recv: Hi, client!
[5] Close Socket

如果在运行客户端之前先打开一个新的终端并运行 tcpdump 命令进行抓包:

代码语言:javascript
复制
sudo tcpdump -S -i any tcp port 8700

可以得到如下输出:

代码语言:javascript
复制
# 三次握手
# 标志位:SYN
14:04:19.846717 IP localhost.57400 > localhost.8700: Flags [S], seq 3120777010, win 65495, options [mss 65495,sackOK,TS val 1331636511 ecr 0,nop,wscale 7], length 0
# 标志位:SYN + ACK
14:04:19.846741 IP localhost.8700 > localhost.57400: Flags [S.], seq 1145627628, ack 3120777011, win 65483, options [mss 65495,sackOK,TS val 1331636511 ecr 1331636511,nop,wscale 7], length 0
# 标志位:ACK
14:04:19.846761 IP localhost.57400 > localhost.8700: Flags [.], ack 1145627629, win 512, options [nop,nop,TS val 1331636511 ecr 1331636511], length 0

# 收发数据
14:04:19.846794 IP localhost.57400 > localhost.8700: Flags [P.], seq 3120777011:3120777035, ack 1145627629, win 512, options [nop,nop,TS val 1331636511 ecr 1331636511], length 24
14:04:20.847301 IP localhost.8700 > localhost.57400: Flags [P.], seq 1145627629:1145627641, ack 3120777035, win 512, options [nop,nop,TS val 1331637512 ecr 1331636511], length 12
14:04:20.847328 IP localhost.57400 > localhost.8700: Flags [.], ack 1145627641, win 512, options [nop,nop,TS val 1331637512 ecr 1331637512], length 0

# 四次挥手
# 标志位:FIN + ACK
14:04:20.847499 IP localhost.57400 > localhost.8700: Flags [F.], seq 3120777035, ack 1145627641, win 512, options [nop,nop,TS val 1331637512 ecr 1331637512], length 0
# 标志位:ACK
14:04:20.888037 IP localhost.8700 > localhost.57400: Flags [.], ack 3120777036, win 512, options [nop,nop,TS val 1331637553 ecr 1331637512], length 0
# 标志位:FIN + ACK
14:04:21.847617 IP localhost.8700 > localhost.57400: Flags [F.], seq 1145627641, ack 3120777036, win 512, options [nop,nop,TS val 1331638512 ecr 1331637512], length 0
# 标志位:ACK
14:04:21.847644 IP localhost.57400 > localhost.8700: Flags [.], ack 1145627642, win 512, options [nop,nop,TS val 1331638512 ecr 1331638512], length 0

也可以尝试将以上服务端和客户端程序分别运行在两台机器上,并将 IP 地址 "127.0.0.1" 替换成所在机器的真实 IP,体验下两台机器间的通信。

4 小结

TCP 通过三次握手建立连接,通过四次挥手断开连接。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 背景
  • 2 概念
    • 2.1 TCP 包头部
      • 2.2 TCP 三次握手建立连接
        • 2.3 TCP 四次挥手关闭连接
        • 3 Socket 编程
        • 4 小结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档