【C/C++】从 socket 到多线程 TCP Echo 服务器:fd、accept 与 recv/send 全流程

1. 这篇文章解决什么问题
在学习网络编程时,第一道坎不是 epoll,而是搞清楚服务端从“监听端口”到“处理客户端数据”到底经历了什么。这个项目里的 tcp_server_threads.c 是一个非常直接的 TCP Echo Server:客户端发什么,服务端就原样回什么。
它覆盖了 TCP 服务端最核心的 5 步:
socket()创建监听套接字。bind()绑定 IP 和端口。listen()让端口进入监听状态。accept()从已建立连接队列中取出一个客户端连接。recv()/send()对客户端 fd 读写数据。
2. fd 和连接不是一回事
README 中有一个很关键的点:fd 是进程级的 IO 句柄,而 TCP 连接状态在内核协议栈里维护。
accept() 之前,三次握手可能已经完成,内核里已经有连接状态;但应用层还没有拿到可读写的客户端 fd。accept() 返回之后,内核才给应用分配一个新的 fd,之后应用才能对这个 fd 调用 recv() 和 send()。
项目里的服务端监听端口是 8080:
int serverfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8080);
bind(serverfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(serverfd, 5);
这里的 serverfd 是监听 socket,只负责接收新连接,不负责承载某个客户端的数据收发。真正和客户端通信的是 accept() 返回的 clientfd。
3. accept 之后创建线程
多线程版的思路非常朴素:主线程只负责 accept(),每来一个客户端就创建一个线程处理。
while (1)
{
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int clientfd = accept(serverfd, (struct sockaddr *)&client_addr, &client_len);
if (clientfd < 0)
{
perror("accept");
continue;
}
pthread_t tid;
int *pclient = malloc(sizeof(int));
*pclient = clientfd;
pthread_create(&tid, NULL, handle_client, pclient);
pthread_detach(tid);
}
这里把 clientfd 放到堆内存里传给线程,是因为局部变量在下一轮循环可能被覆盖。线程函数处理完以后会 free(arg)。
pthread_detach(tid) 的作用是让线程结束后自动回收资源,主线程不需要再 pthread_join()。这对 echo server 这种“连接来了就独立处理”的模型比较方便。
4. recv/send 实现 Echo
线程函数的逻辑就是持续收数据、打印、再写回客户端:
void *handle_client(void *arg)
{
int clientfd = *(int *)arg;
char buffer[1024];
ssize_t n;
while ((n = recv(clientfd, buffer, sizeof(buffer) - 1, 0)) > 0)
{
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
send(clientfd, buffer, n, 0);
}
close(clientfd);
free(arg);
return NULL;
}
几个细节值得注意:
recv()返回大于 0,表示读到了数据。recv()返回 0,通常表示对端正常关闭连接。recv()返回小于 0,表示发生错误。send()这里直接把收到的n字节写回去,所以它是一个 Echo Server。
5. 编译和测试
编译:
gcc tcp_server_threads.c -o threads -pthread
启动服务端:
./threads
另开一个终端,用 nc 测试:
nc 127.0.0.1 8080
hello tcp
客户端输入 hello tcp 后,服务端会打印收到的数据,客户端也会收到同样的响应。
6. 多线程模型的优缺点
优点很明显:
- 代码直观,符合“一个连接一个处理流程”的思维。
- 阻塞式
recv()/send()也容易理解。 - 很适合作为 TCP 服务端入门代码。
缺点也很明显:
- 每个连接都创建线程,线程栈和调度成本都不低。
- 并发连接一多,系统会被线程数量拖垮。
- 适合 C10、C100 级别学习,不适合作为 C10K/C100K 的核心模型。
如果你在本地测试时遇到 bind failed,大概率是端口已经被占用,或者上一次进程还没完全退出。可以用:
ss -lntp | grep 8080
7. 小结
多线程 TCP Echo Server 是网络编程的第一块积木。理解它之后,再看 select、poll、epoll 就会更自然:后面的模型本质上都是在解决同一个问题,如何不用“每个连接一个线程”的方式管理大量 fd。
722

被折叠的 条评论
为什么被折叠?



