☰
Linux 学习笔记(四十):TCP 编程——从理论到 EchoServer 实战
2026/10/30 4:51:45 网站建设 项目流程

📌 核心结论(TL;DR):

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

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

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

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

  5. 踩坑防范:服务端重启常遇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断开,看看服务端是不是打印了“客户端断开”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询