在实际开发中,一个 TCP 服务器往往要同时为多个客户端提供服务。最简单直观的方式,就是采用“一请求一线程”模型 —— 每当有客户端连接进来,服务器就创建一个新线程专门负责这个客户端的收发任务。
本文将介绍如何使用 C语言 + TCP + pthread 多线程 实现一个并发 TCP 服务器。
创建 socket
绑定 IP 和端口(bind)
开始监听连接请求(listen)
接收连接(accept)
接收与发送数据(recv / send)
关闭连接(close)
在基于 TCP 的服务端模型中,“一请求一线程”是最经典的并发模型之一。其核心原理如下:
TCP 是面向连接的协议,在通信前需先建立连接(3 次握手)。服务端需要长期监听一个端口,接收客户端的连接请求。
每当有一个客户端连接,主线程使用 accept() 接收连接。
为了不阻塞主线程,服务器为每个连接创建一个子线程(或进程)进行数据通信。
子线程读取客户端请求,处理完成后关闭连接。
这一模型具备清晰的线程分离与逻辑隔离特性,便于调试与维护。
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个 TCP socket,使用 IPv4 地址族,面向流的通信
struct sockaddr_in addr; // 定义一个 sockaddr_in 结构体,用于存储本地地址信息
memset(&addr, 0, sizeof(addr)); // 初始化该结构体为 0,清除脏数据
addr.sin_family = AF_INET; // 设置地址族为 IPv4
addr.sin_port = htons(port); // 设置端口号,使用 htons 转为网络字节序
addr.sin_addr.s_addr = INADDR_ANY; // 接收任意网卡发来的连接请求(绑定 0.0.0.0)
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); // 将 socket 与本地地址绑定
listen(sockfd, 5); // 开始监听客户端连接请求,最多允许排队 5 个连接
使用 socket() 创建 TCP 套接字
使用 bind() 将 socket 绑定到本地地址与端口
使用 listen() 开始监听客户端连接请求
while (1) {
struct sockaddr_in client_addr; // 用于保存客户端地址信息
socklen_t client_len = sizeof(client_addr); // 客户端地址结构大小
int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); // 阻塞等待客户端连接,成功则返回新的 socket 文件描述符
pthread_t thread_id; // 定义线程 id
pthread_create(&thread_id, NULL, client_routine, &clientfd); // 创建新线程,处理当前连接,传入 clientfd
}
accept():阻塞等待客户端连接
每次连接成功后,用 pthread_create() 创建线程处理
一连接一线程模型:每个连接独立分配线程,互不干扰
void *client_routine(void *arg) {
int clientfd = *(int *)arg; // 获取客户端 socket 文件描述符
while (1) {
char buffer[1024] = {0}; // 接收缓冲区,初始化为 0
int len = recv(clientfd, buffer, 1024, 0); // 从客户端读取数据
if (len <= 0) { // 如果读取失败或客户端关闭连接
close(clientfd); // 关闭客户端连接
break; // 退出循环,线程结束
}
printf("Recv: %s, %d byte(s)\n", buffer, len); // 打印接收到的消息内容和长度
}
}
使用 recv() 从客户端读取数据
若客户端断开或读取错误,则 recv() 返回 0 或负值,关闭连接
否则打印接收到的数据
启动程序
↓
调用 socket() 创建监听 socket
↓
bind() 绑定 IP 和端口
↓
listen() 开始监听客户端连接
↓
========= 循环开始 =========
↓
accept() 等待并接受客户端连接
↓
为每个连接创建线程 pthread_create()
↓
→ 在线程中调用 recv() 接收客户端数据
→ 打印接收内容
→ 如果断开或错误则 close() 并退出线程
↓
========= 循环继续 =========
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_LENGTH 1024
// 子线程函数:处理客户端通信
void *client_routine(void *arg){
int clientfd = *(int *)arg; // 获取传入的客户端 socket 描述符
while (1){
char buffer[BUFFER_LENGTH] = {0}; // 接收缓冲区,初始化为0
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0); // 接收数据
if(len <= 0){ // 出错或客户端关闭连接
close(clientfd); // 关闭连接socket
break; // 退出线程
} else {
printf("Recv: %s, %d byte(s)\n", buffer, len); // 打印接收到的数据及长度
}
}
return NULL;
}
int main(int argc, char *argv[]){
if (argc < 2){
printf("Param Error\n"); // 端口参数未提供,退出
return -1;
}
int port = atoi(argv[1]); // 获取监听端口号
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP socket
if(sockfd < 0){
perror("socket");
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 初始化地址结构
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(port); // 端口转网络字节序
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本机网卡地址
if(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0){ // 绑定地址和端口
perror("bind");
return 2;
}
if(listen(sockfd, 5) < 0){ // 开始监听,允许最大5个等待队列
perror("listen");
return 3;
}
while (1){
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));
socklen_t client_len = sizeof(client_addr);
int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); // 阻塞等待客户端连接
if(clientfd < 0){
perror("accept");
continue;
}
pthread_t thread_id;
pthread_create(&thread_id, NULL, client_routine, &clientfd); // 创建线程处理该客户端
pthread_detach(thread_id); // 线程分离,防止资源泄露
}
close(sockfd);
return 0;
}
https://github.com/0voice