云计算百科
云计算领域专业知识百科平台

Linux 学习笔记(四十):TCP 编程——从理论到 EchoServer 实战

📌 核心结论(TL;DR):

  • TCP 核心特性:面向连接(三次握手)、可靠传输、面向字节流。

  • 服务端五步曲:socket() -> bind() -> listen() -> accept() -> read()/write()。

  • 极其重要:accept() 成功后返回的是一个全新的文件描述符 connfd,专门用于和该客户端通信,原来的 listenfd 继续监听。

  • 断线感知:TCP 中 read() 返回 0,代表对端(客户端)主动关闭了连接,而不是出错。

  • 踩坑防范:服务端重启常遇 Address already in use,需用 setsockopt() 设置 SO_REUSEADDR 端口复用。

  • 一、 TCP 核心流程解析

    TCP 就像打电话,必须先建立连接。

    • 服务器端:socket创建电话机,bind插卡绑定固定号码,listen开机待机,accept接听。

    • 客户端:socket创建电话机,connect直接拨号。
      (注:客户端不需要手动 bind,connect 时操作系统会自动分配随机端口)

    二、 为什么 accept 要返回新的套接字?

    listen_fd 是负责监听是否有新连接进来的(只负责拉客),而 accept 返回的 conn_fd 才是真正用来和当前客户端通信的。如果不这样做,服务器就无法同时为多个客户端服务。

    三、 实战:单进程 TCP Echo 服务器

    (这里只展示核心逻辑,完整代码见文末)

    // 端口复用,解决重启报错问题
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 接收连接
    int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);

    // 通信与断线判断
    int n = read(conn_fd, buffer, BUF_SIZE);
    if (n > 0) {
    // 正常收到数据
    write(conn_fd, buffer, n);
    } else if (n == 0) {
    // 重点:n == 0 代表客户端关闭了连接 (收到了 FIN 包)
    printf("客户端已断开连接。\\n");
    break;
    }

    四、 踩坑记录:Address already in use

    • 现象:强制 Ctrl+C 结束服务器后,立刻重新运行,报错 bind error: Address already in use。

    • 原因:TCP 连接断开时,主动断开方会进入 TIME_WAIT 状态,占用端口一段时间(通常是 2MSL,约 1 分钟),防止最后一个 ACK 丢失导致连接混乱。

    • 解决:在 bind 前调用 setsockopt 设置 SO_REUSEADDR,允许端口复用。

    五、 局限性

    当前的服务器是单进程、单线程的。一旦 accept 成功,服务器就陷入了 while(read) 的阻塞。此时,第二个客户端连都连不进来。

    自我实战操作

    cd ~/linux_study/network_test

    服务端 tcp_server.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>

    #define PORT 9999
    #define BUF_SIZE 1024

    int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 端口复用,防止重启时 Address already in use
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(listen_fd, 5);
    printf("TCP 服务器启动,监听端口 %d…\\n", PORT);

    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);

    // 核心:阻塞等待客户端连接
    int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
    printf("接到客户端 %s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    char buffer[BUF_SIZE];
    while (1) {
    int n = read(conn_fd, buffer, BUF_SIZE – 1);
    if (n > 0) {
    buffer[n] = '\\0';
    printf("收到消息: %s\\n", buffer);
    write(conn_fd, buffer, n); // Echo回显
    } else if (n == 0) {
    // 核心:客户端断开
    printf("客户端断开了连接。\\n");
    break;
    }
    }
    close(conn_fd);
    close(listen_fd);
    return 0;
    }

    客户端 tcp_client.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>

    #define SERVER_IP "127.0.0.1"
    #define PORT 9999
    #define BUF_SIZE 1024

    int main() {
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("connect"); return 1;
    }

    char buffer[BUF_SIZE];
    while (1) {
    printf("请输入: ");
    fgets(buffer, BUF_SIZE, stdin);
    buffer[strcspn(buffer, "\\n")] = 0;
    if (strcmp(buffer, "quit") == 0) break;

    write(sock_fd, buffer, strlen(buffer));
    int n = read(sock_fd, buffer, BUF_SIZE – 1);
    if (n > 0) {
    buffer[n] = '\\0';
    printf("回显: %s\\n", buffer);
    }
    }
    close(sock_fd);
    return 0;
    }

    编译运行:开两个终端,跑起服务端和客户端,发消息测试。测试完在客户端按 Ctrl+C 断开,看看服务端是不是打印了“客户端断开”。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux 学习笔记(四十):TCP 编程——从理论到 EchoServer 实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!