15. TCP编程.md 21 KB

TCP编程

1. TCP协议特点

TCP(Transmission Control Protocol,传输控制协议) 是一种面向连接、可靠的字节流传输协议。

核心特性

特性 说明
面向连接 通信前必须建立连接(三次握手)
可靠传输 确认应答、超时重传、校验和
字节流 无边界保护,需要应用层处理粘包
全双工 双向同时通信
流量控制 滑动窗口机制
拥塞控制 慢启动、拥塞避免、快重传、快恢复

TCP vs UDP对比

特性 TCP UDP
连接 面向连接 无连接
可靠性 可靠 不可靠
传输方式 字节流 数据报
效率 较低 较高
适用场景 文件传输、Web 视频流、DNS

2. TCP服务器编程模板

2.1 标准流程

socket() → bind() → listen() → accept() → read()/write() → close()

2.2 代码模板

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

#define PORT 8080
#define BUF_SIZE 1024

volatile sig_atomic_t running = 1;

void sigint_handler(int sig) {
    running = 0;
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buf[BUF_SIZE];

    signal(SIGINT, sigint_handler);

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

    // 2. 设置地址复用
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 监听
    if (listen(server_fd, 128) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("TCP Server started on port %d\n", PORT);

    // 5. 接受连接(单客户端版本)
    while (running) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
        if (client_fd < 0) {
            if (running) perror("accept");
            continue;
        }

        printf("Client connected: %s:%d\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

        // 6. 通信循环
        ssize_t n;
        while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
            buf[n] = '\0';
            printf("Received %zd bytes: %s", n, buf);

            // Echo back
            if (write(client_fd, buf, n) < 0) {
                perror("write");
                break;
            }
        }

        if (n == 0) {
            printf("Client disconnected\n");
        } else {
            perror("read");
        }

        close(client_fd);
    }

    close(server_fd);
    printf("Server shutdown\n");
    return 0;
}

编译命令:

gcc -Wall -o server server.c

3. TCP客户端编程模板

3.1 标准流程

socket() → connect() → write()/read() → close()

3.2 代码模板

#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 8080
#define BUF_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buf[BUF_SIZE];

    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("inet_pton");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("Connected to server %s:%d\n", SERVER_IP, PORT);

    // 3. 通信循环
    while (1) {
        printf("Enter message (or 'quit'): ");
        if (fgets(buf, BUF_SIZE, stdin) == NULL) break;

        if (strncmp(buf, "quit", 4) == 0) break;

        if (write(sockfd, buf, strlen(buf)) < 0) {
            perror("write");
            break;
        }

        ssize_t n = read(sockfd, buf, BUF_SIZE);
        if (n <= 0) {
            if (n == 0) printf("Server closed connection\n");
            else perror("read");
            break;
        }

        buf[n] = '\0';
        printf("Echo: %s", buf);
    }

    // 4. 关闭
    close(sockfd);
    printf("Disconnected\n");
    return 0;
}

编译命令:

gcc -Wall -o client client.c

4. TCP Echo服务器/客户端完整实现

4.1 Echo服务器(支持多客户端)

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

#define PORT 8080
#define BUF_SIZE 1024

void handle_client(int client_fd, struct sockaddr_in *client_addr) {
    char buf[BUF_SIZE];
    ssize_t n;

    printf("[%d] Client connected: %s:%d\n",
           getpid(),
           inet_ntoa(client_addr->sin_addr),
           ntohs(client_addr->sin_port));

    while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
        buf[n] = '\0';
        printf("[%d] Echo: %s", getpid(), buf);

        if (write(client_fd, buf, n) < 0) {
            perror("write");
            break;
        }
    }

    printf("[%d] Client disconnected\n", getpid());
    close(client_fd);
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

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

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 128) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Echo Server listening on port %d\n", PORT);

    while (1) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
        if (client_fd < 0) {
            perror("accept");
            continue;
        }

        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            close(client_fd);
            continue;
        }

        if (pid == 0) {
            // 子进程处理客户端
            close(server_fd);
            handle_client(client_fd, &client_addr);
            exit(EXIT_SUCCESS);
        } else {
            // 父进程继续监听
            close(client_fd);
            // 避免僵尸进程
            signal(SIGCHLD, SIG_IGN);
        }
    }

    close(server_fd);
    return 0;
}

4.2 Echo客户端(完整版)

#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 8080
#define BUF_SIZE 1024

int main(int argc, char *argv[]) {
    const char *server_ip = (argc > 1) ? argv[1] : SERVER_IP;
    int port = (argc > 2) ? atoi(argv[2]) : PORT;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    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);

    printf("Connecting to %s:%d...\n", server_ip, port);

    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("Connected! Type messages (Ctrl+D to quit):\n");

    char buf[BUF_SIZE];
    while (fgets(buf, BUF_SIZE, stdin) != NULL) {
        if (write(sockfd, buf, strlen(buf)) < 0) {
            perror("write");
            break;
        }

        ssize_t n = read(sockfd, buf, BUF_SIZE);
        if (n <= 0) {
            if (n == 0) printf("Server closed\n");
            else perror("read");
            break;
        }

        buf[n] = '\0';
        printf("Echo: %s", buf);
    }

    close(sockfd);
    return 0;
}

编译和运行:

# 编译
gcc -Wall -o echo_server echo_server.c
gcc -Wall -o echo_client echo_client.c

# 终端1:启动服务器
./echo_server

# 终端2:启动客户端
./echo_client

5. 多进程/多线程TCP服务器

5.1 多进程服务器

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

#define PORT 8080
#define BUF_SIZE 1024

void sigchld_handler(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

void handle_client(int client_fd) {
    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
        buf[n] = '\0';
        printf("[%d] Received: %s", getpid(), buf);
        write(client_fd, buf, n);
    }

    printf("[%d] Client disconnected\n", getpid());
    close(client_fd);
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

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

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 128) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    signal(SIGCHLD, sigchld_handler);

    printf("Multi-process server on port %d\n", PORT);

    while (1) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
        if (client_fd < 0) {
            perror("accept");
            continue;
        }

        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            close(client_fd);
            continue;
        }

        if (pid == 0) {
            close(server_fd);
            handle_client(client_fd);
            exit(0);
        } else {
            close(client_fd);
        }
    }

    close(server_fd);
    return 0;
}

5.2 多线程服务器

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

#define PORT 8080
#define BUF_SIZE 1024
#define MAX_CLIENTS 100

typedef struct {
    int sockfd;
    struct sockaddr_in addr;
} client_info_t;

void *client_handler(void *arg) {
    client_info_t *info = (client_info_t *)arg;
    int client_fd = info->sockfd;
    char buf[BUF_SIZE];
    ssize_t n;

    printf("[Thread %lu] Client connected: %s:%d\n",
           pthread_self(),
           inet_ntoa(info->addr.sin_addr),
           ntohs(info->addr.sin_port));

    free(info);

    while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
        buf[n] = '\0';
        printf("[Thread %lu] Echo: %s", pthread_self(), buf);
        write(client_fd, buf, n);
    }

    printf("[Thread %lu] Client disconnected\n", pthread_self());
    close(client_fd);
    return NULL;
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

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

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 128) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Multi-threaded server on port %d\n", PORT);

    while (1) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
        if (client_fd < 0) {
            perror("accept");
            continue;
        }

        client_info_t *info = malloc(sizeof(client_info_t));
        if (!info) {
            perror("malloc");
            close(client_fd);
            continue;
        }

        info->sockfd = client_fd;
        info->addr = client_addr;

        pthread_t tid;
        if (pthread_create(&tid, NULL, client_handler, info) != 0) {
            perror("pthread_create");
            free(info);
            close(client_fd);
            continue;
        }

        pthread_detach(tid);
    }

    close(server_fd);
    return 0;
}

编译命令:

# 多进程版
gcc -Wall -o server_mp server_mp.c

# 多线程版(需要链接pthread)
gcc -Wall -o server_mt server_mt.c -lpthread

6. TCP粘包问题

6.1 什么是粘包

TCP是字节流协议,没有消息边界。发送端的多次 write() 可能被合并成一次发送,接收端的 read() 可能一次读取多条消息。

示例:

发送端:write("Hello") → write("World")
接收端:read() → "HelloWorld"  (一次读取)
        或 read() → "Hello",read() → "World"  (两次读取)

6.2 解决方案

方案1:固定长度消息

#define MSG_LEN 100

// 发送端
char msg[MSG_LEN] = {0};
strncpy(msg, data, MSG_LEN - 1);
write(sockfd, msg, MSG_LEN);

// 接收端
char msg[MSG_LEN];
read(sockfd, msg, MSG_LEN);

方案2:特殊分隔符

// 发送端:以\n结尾
write(sockfd, "Hello\n", 6);

// 接收端:逐字符读取直到遇到分隔符
void recv_line(int sockfd, char *buf, int max_len) {
    int i = 0;
    char c;
    while (i < max_len - 1) {
        read(sockfd, &c, 1);
        if (c == '\n') break;
        buf[i++] = c;
    }
    buf[i] = '\0';
}

方案3:长度头(推荐)

// 消息格式:[4字节长度][实际数据]

// 发送端
void send_msg(int sockfd, const char *data, uint32_t len) {
    uint32_t net_len = htonl(len);
    write(sockfd, &net_len, 4);
    write(sockfd, data, len);
}

// 接收端
int recv_msg(int sockfd, char *buf, int max_len) {
    uint32_t net_len;
    if (read(sockfd, &net_len, 4) != 4) return -1;

    uint32_t len = ntohl(net_len);
    if (len > max_len) return -1;

    int total = 0;
    while (total < len) {
        int n = read(sockfd, buf + total, len - total);
        if (n <= 0) return -1;
        total += n;
    }
    return len;
}

7. 紧急数据(带外数据)

7.1 概念

TCP支持发送 紧急数据(Out-of-Band Data),也称为带外数据,用于发送需要优先处理的控制信息。

7.2 使用方法

// 发送端:使用MSG_OOB标志
const char *urgent = "URGENT!";
send(sockfd, urgent, strlen(urgent), MSG_OOB);

// 接收端
char buf[1024];
ssize_t n = recv(sockfd, buf, sizeof(buf), MSG_OOB);  // 只接收紧急数据
if (n > 0) {
    printf("Urgent data: %.*s\n", (int)n, buf);
}

7.3 注意事项

  • TCP只支持1字节的紧急数据(旧实现)
  • 紧急数据通过单字节的TCP紧急指针传输
  • 实际使用较少,现代应用多使用应用层协议

8. 嵌入式场景应用

8.1 设备间TCP通信

// 嵌入式设备作为TCP客户端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define SERVER_IP "192.168.1.100"
#define SERVER_PORT 8080

int send_sensor_data(float temperature, float humidity) {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) return -1;

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        close(sockfd);
        return -1;
    }

    // 构造JSON格式数据
    char data[128];
    snprintf(data, sizeof(data),
             "{\"temp\":%.1f,\"humi\":%.1f}",
             temperature, humidity);

    // 发送数据
    uint32_t len = htonl(strlen(data));
    write(sockfd, &len, 4);
    write(sockfd, data, strlen(data));

    close(sockfd);
    return 0;
}

8.2 远程控制服务器

// 嵌入式设备作为TCP服务器,接受远程控制命令
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 9090
#define CMD_LEN 64

void execute_command(const char *cmd) {
    if (strcmp(cmd, "LED_ON") == 0) {
        // 打开LED
        printf("LED turned ON\n");
    } else if (strcmp(cmd, "LED_OFF") == 0) {
        // 关闭LED
        printf("LED turned OFF\n");
    } else if (strcmp(cmd, "REBOOT") == 0) {
        // 重启设备
        printf("Rebooting...\n");
    }
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return -1;
    }

    int opt = 1;
    setsockopt(server_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_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

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

    if (listen(server_fd, 5) < 0) {
        perror("listen");
        close(server_fd);
        return -1;
    }

    printf("Control server listening on port %d\n", PORT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t addr_len = sizeof(client_addr);
        int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
        if (client_fd < 0) continue;

        char cmd[CMD_LEN];
        ssize_t n = read(client_fd, cmd, CMD_LEN - 1);
        if (n > 0) {
            cmd[n] = '\0';
            execute_command(cmd);
        }

        close(client_fd);
    }

    close(server_fd);
    return 0;
}

9. 注意事项

9.1 常见陷阱

  • 粘包问题:必须在应用层实现消息边界
  • 部分读写read()/write() 可能只读写部分数据
  • 阻塞问题:默认socket是阻塞的,长时间等待会影响程序
  • 连接断开:需要检测对端关闭(read() 返回0)

9.2 最佳实践

  • 使用长度头方案解决粘包
  • 循环读写确保数据完整
  • 实现超时机制避免永久阻塞
  • 使用 select()/poll()/epoll 实现并发
  • 设置 SO_REUSEADDR 避免地址占用

9.3 错误处理

// 完整的错误处理模板
ssize_t writen(int fd, const void *buf, size_t n) {
    size_t nleft = n;
    ssize_t nwritten;
    const char *ptr = buf;

    while (nleft > 0) {
        if ((nwritten = write(fd, ptr, nleft)) < 0) {
            if (errno == EINTR) continue;  // 信号中断,重试
            return -1;
        } else if (nwritten == 0) {
            break;  // 对端关闭
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n - nleft;
}

10. 面试要点

10.1 核心概念

  • TCP三次握手:SYN → SYN+ACK → ACK
  • TCP四次挥手:FIN → ACK → FIN → ACK
  • TIME_WAIT状态:等待2MSL确保对端收到最后ACK

10.2 重要问题

  • 为什么需要TIME_WAIT? 防止旧连接的数据干扰新连接
  • 如何解决TIME_WAIT过多? 设置 SO_REUSEADDR
  • TCP如何保证可靠性? 序列号、确认应答、超时重传、校验和
  • 粘包的原因和解决方案? 字节流特性 + 应用层协议

10.3 编程相关

  • accept() 返回新fd的原因:支持并发
  • listen() 的 backlog 参数:已完成连接队列长度
  • SO_REUSEADDR 的作用:允许绑定TIME_WAIT状态的地址

10.4 性能优化

  • epoll vs select/poll:O(1) vs O(n)
  • 非阻塞I/O:避免线程阻塞
  • TCP_NODELAY:禁用Nagle算法,减少延迟

相关链接

  • [[14. socket网络编程基础]]