3、TCP客户端与服务端:TCP三次握手、四次挥手、TCP客户端实现、TCP服务端实现

各位同学,咱们今天聊点实在的。TCP 这个东西,说白了就是网络通讯的「定海神针」。你上网、刷视频、发消息,底层全是它在撑着。我做了十几年嵌入式通讯,可以负责任地告诉你:搞懂 TCP,你就掌握了网络编程的半壁江山。

这一节,咱们不扯虚的。直接上硬菜:三次握手、四次挥手,然后手撸一个客户端和服务端。嗯,跟着我的节奏走,保证你听完就能用。

3.1 TCP 三次握手:连接是怎么「谈」出来的?

先问大家一个问题:为什么 TCP 要握三次手?两次不行吗?四次呢?

我当年刚入行时也觉得这问题很玄乎。后来在项目里调试一个工业控制器的远程升级功能,发现设备老是连不上服务器。抓包一看,好家伙,握手阶段就卡住了。从那以后,我对三次握手有了刻骨铭心的理解。

三次握手的本质,说白了就是「确认双方都能收发数据」。你想想看,如果只握两次,服务端怎么知道客户端已经准备好接收数据了?

流程是这样的:

  1. 第一次握手(SYN):客户端发送一个 SYN 包,告诉服务端「我想跟你连」。此时客户端进入 SYN_SENT 状态。
  2. 第二次握手(SYN+ACK):服务端收到后,回复 SYN+ACK,意思是「收到你的请求,我也准备好了」。服务端进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):客户端再发一个 ACK,表示「好的,我知道了,咱们开始传数据吧」。双方进入 ESTABLISHED 状态。

核心要点:三次握手不仅仅是建立连接,它还在协商一些关键参数,比如最大报文段长度(MSS)、窗口缩放因子等。这些细节,很多教材不讲,但实际项目中经常踩坑。

我曾经遇到过一个案例:某嵌入式设备因为 TCP 窗口太小,导致传输速度只有理论值的十分之一。查了两天才发现,是握手阶段协商的窗口参数有问题。所以,别小看这三次握手。

3.2 TCP 四次挥手:分手也要体面

连接建立要握手,断开连接要挥手。四次挥手,比握手多一次,为什么?

原因很简单:TCP 是全双工的。也就是说,数据可以双向同时传输。断开时,每一方向都要单独说再见。

流程是这样的:

  1. 第一次挥手(FIN):主动关闭方(比如客户端)发送 FIN,表示「我这边没数据要发了」。进入 FIN_WAIT_1 状态。
  2. 第二次挥手(ACK):被动关闭方(服务端)回复 ACK,表示「收到,我知道你发完了」。但此时服务端可能还有数据要发,所以连接还没完全断。主动方进入 FIN_WAIT_2 状态。
  3. 第三次挥手(FIN):服务端数据发完了,发送 FIN,表示「我也没数据了」。
  4. 第四次挥手(ACK):客户端回复 ACK,然后进入 TIME_WAIT 状态,等待 2MSL(最大报文生存时间)后彻底关闭。

避坑指南:我曾经在做一个远程数据采集系统时,发现服务端频繁出现端口被占用的错误。查了半天,原来是客户端没有正确处理 TIME_WAIT 状态,导致端口无法立即复用。解决方案是设置 SO_REUSEADDR 选项。嗯,这个坑我替你们踩过了。

3.3 TCP 客户端实现:从零开始写一个

理论讲完了,咱们来点实际的。我习惯用 C 语言写嵌入式网络程序,因为可控性高。下面是一个最简的 TCP 客户端实现:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[1024] = {0};

    // 1. 创建 socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return -1;
    }

    // 2. 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");

    // 3. 连接服务器(触发三次握手)
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        return -1;
    }

    printf("Connected to server\n");

    // 4. 发送数据
    char *msg = "Hello from client";
    send(sockfd, msg, strlen(msg), 0);

    // 5. 接收响应
    recv(sockfd, buffer, sizeof(buffer), 0);
    printf("Server response: %s\n", buffer);

    // 6. 关闭连接(触发四次挥手)
    close(sockfd);
    return 0;
}

个人经验:实际项目中,我一般会在 connect 之后加一个超时处理。因为嵌入式设备网络环境复杂,如果服务器没响应,程序会一直卡在 connect 上。用非阻塞 socket + select 可以优雅解决这个问题。

3.4 TCP 服务端实现:做个靠谱的「接客」程序

服务端比客户端复杂一些。它要监听、接受连接、处理多个客户端。咱们先写一个单线程的版本:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // 1. 创建 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket failed");
        return -1;
    }

    // 2. 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("bind failed");
        return -1;
    }

    // 3. 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        return -1;
    }

    printf("Server listening on port 8080\n");

    // 4. 接受客户端连接(完成三次握手)
    client_fd = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
    if (client_fd < 0) {
        perror("accept failed");
        return -1;
    }

    // 5. 接收数据
    recv(client_fd, buffer, sizeof(buffer), 0);
    printf("Received: %s\n", buffer);

    // 6. 发送响应
    char *response = "Hello from server";
    send(client_fd, response, strlen(response), 0);

    // 7. 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}

注意:上面的代码只能处理一个客户端。实际项目中,我会用多线程或者 select/epoll 来处理并发。特别是嵌入式场景,资源有限,我建议用 select 模型,既简单又高效。

3.5 核心流程图:TCP 通讯全貌

下面这张图,是我自己画的。它把三次握手、数据传输、四次挥手串在了一起。你盯着看三分钟,整个流程就刻在脑子里了。

客户端 服务端 CLOSED CLOSED SYN (seq=x) SYN_SENT SYN+ACK (seq=y, ack=x+1) SYN_RCVD ACK (ack=y+1) ESTABLISHED ESTABLISHED 数据传输阶段 FIN (seq=u) FIN_WAIT_1 ACK (ack=u+1) FIN_WAIT_2 CLOSE_WAIT FIN (seq=v) LAST_ACK ACK (ack=v+1) TIME_WAIT CLOSED

3.6 常见问题与避坑指南

问题 现象 解决方案
连接超时 connect 卡住不动 设置非阻塞 socket + 超时处理
端口被占用 bind 返回 EADDRINUSE 使用 SO_REUSEADDR 选项
粘包问题 接收端一次收到多条消息 自定义应用层协议,添加消息头和长度字段
半关闭状态 一方关闭,另一方还在发数据 使用 shutdown() 而不是 close() 实现优雅关闭

重要提醒:嵌入式系统中,TCP 的缓冲区大小往往有限。我建议你在实际项目中,根据网络带宽和 CPU 性能,合理调整 SO_RCVBUF 和 SO_SNDBUF。太小了丢包,太大了浪费内存。

好了,这一节的内容就到这里。TCP 的三次握手和四次挥手,是网络通讯的基石。客户端和服务端的实现,是动手能力的体现。我建议你照着代码敲一遍,然后抓个包看看握手过程。嗯,实践出真知。


公众号:蓝海资料掘金营,微信deep3321