# 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 代码模板 ```c #include #include #include #include #include #include #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; } ``` **编译命令:** ```bash gcc -Wall -o server server.c ``` --- ## 3. TCP客户端编程模板 ### 3.1 标准流程 ``` socket() → connect() → write()/read() → close() ``` ### 3.2 代码模板 ```c #include #include #include #include #include #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; } ``` **编译命令:** ```bash gcc -Wall -o client client.c ``` --- ## 4. TCP Echo服务器/客户端完整实现 ### 4.1 Echo服务器(支持多客户端) ```c #include #include #include #include #include #include #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客户端(完整版) ```c #include #include #include #include #include #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; } ``` **编译和运行:** ```bash # 编译 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 多进程服务器 ```c #include #include #include #include #include #include #include #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 多线程服务器 ```c #include #include #include #include #include #include #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; } ``` **编译命令:** ```bash # 多进程版 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:固定长度消息 ```c #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:特殊分隔符 ```c // 发送端:以\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:长度头(推荐) ```c // 消息格式:[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 使用方法 ```c // 发送端:使用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通信 ```c // 嵌入式设备作为TCP客户端 #include #include #include #include #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 远程控制服务器 ```c // 嵌入式设备作为TCP服务器,接受远程控制命令 #include #include #include #include #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 错误处理 ```c // 完整的错误处理模板 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网络编程基础]]