在学习网络编程的时候,很多人会遇到一个经典的例子——Echo 服务器。所谓 Echo,就是“你说什么,我就原封不动地回什么”。这篇文章我们将通过一个 C 语言 + Socket + 多线程 的小例子,来实现一个简单的 Echo 服务器,并逐步分析其中的原理。
1. 场景类比
想象一下你去了一家快递公司,那里有一个接待大厅(服务器)。
大厅的门口有个接待员(
accept
),不断等待新的顾客(客户端)进入。每来一个顾客,就会安排一个客服人员(线程
pthread_create
),专门负责和这个顾客对话。客服的工作很简单:顾客说一句话(
recv
),客服就原样重复一句(send
)。
这就是 Echo 服务的核心逻辑。
2. 代码功能概述
这份代码主要实现了以下功能:
创建 TCP 服务器 Socket(绑定本地 2000 端口)。
监听客户端连接请求。
每当有客户端连接,就创建一个线程专门负责和该客户端通信。
通信逻辑:客户端发来什么消息,服务器就原样返回什么。
也就是说,这是一个最简单的 多线程 TCP Echo 服务器。
3. 核心原理
在深入代码之前,我们先理解几个关键点:
(1)Socket
socket(AF_INET, SOCK_STREAM, 0)
AF_INET
表示 IPv4。SOCK_STREAM
表示使用 TCP 协议。得到的
sockfd
就像一个 监听用的电话机。
(2)bind 和 listen
bind()
:把这个“电话机”绑定到一个固定号码(IP:Port)。listen()
:让电话机进入“监听模式”,等待别人拨号。
(3)accept
accept()
:接电话,一旦有客户端打进来,就生成一个新的“分机电话”clientfd
,用来和这个客户端通信。
注意:服务器端有两个 socket,一个是 监听 socket(接电话用),一个是 通信 socket(聊天用)。
(4)recv 和 send
recv()
:接收客户端发来的消息。send()
:把消息再发回去。
(5)多线程
pthread_create()
:每接入一个客户端,就新开一个线程,保证多个客户端互不干扰。
4. 代码分块解析
下面我们逐步拆解代码。
(1)创建 socket 并绑定端口
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(2000);
if(-1 == bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr))){
printf("bind failed: %s\n",strerror(errno));
}
这段代码创建了一个 TCP socket,并绑定到 0.0.0.0:2000
,也就是本机的 2000 端口。
(2)开始监听
listen(sockfd,10);
printf("listen finished\n");
listen()
表示可以同时挂起最多 10 个客户端的连接请求。
(3)等待客户端连接
while(1){
printf("accept\n");
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
printf("accept finished\n");
pthread_t thid;
pthread_create(&thid, NULL, client_thread, &clientfd);
}
每当有一个客户端连接,都会生成一个新的 clientfd
,然后用 pthread_create
创建一个新线程处理该连接。
(4)线程处理函数
void *client_thread(void *arg){
int clientfd = *(int *)arg;
while(1){
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
printf("RECV: %s\n", buffer);
count = send(clientfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
每个线程的逻辑非常简单:
读取客户端消息(
recv
)。原样返回(
send
)。
这就是 Echo 的精髓。
5. 整体运行流程图
文字流程如下:
客户端A连接 ──┐
客户端B连接 ──┼──> accept() ---> 新线程A <-> 客户端A
客户端C连接 ──┘ 新线程B <-> 客户端B
新线程C <-> 客户端C
服务器就像一个前台,每接入一个客户,就派一个新客服(线程)来一对一服务。
6.完整代码
#include <errno.h> // 提供错误码 errno
#include <stdio.h> // 标准输入输出库
#include <sys/socket.h> // socket 相关函数和数据结构
#include <netinet/in.h> // sockaddr_in 结构体、htons/htonl 等
#include <string.h> // 字符串处理
#include <pthread.h> // POSIX 线程库
// ==============================
// 客户端线程函数
// ==============================
void *client_thread(void *arg){
// 取出传入的参数(clientfd 是客户端连接的 socket 描述符)
int clientfd = *(int *)arg;
while(1){
char buffer[1024] = {0}; // 缓冲区,用于接收数据
// 接收客户端发送的数据
int count = recv(clientfd, buffer, 1024, 0);
printf("RECV: %s\n", buffer);
// 将接收到的数据原样发送回客户端(Echo)
count = send(clientfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
// ==============================
// 主函数
// ==============================
int main(){
// 1. 创建一个 TCP socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 配置服务器地址结构体
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET; // 使用 IPv4
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到本机所有网卡 IP
servaddr.sin_port = htons(2000); // 监听端口 2000
// 3. 绑定 socket 和端口
if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){
// 如果绑定失败,打印错误信息
printf("bind failed: %s\n", strerror(errno));
}
// 4. 开始监听,最大等待队列长度为 10
listen(sockfd, 10);
printf("listen finished\n");
// 用于存储客户端信息
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
#if 0 // 方式一:只接受一次客户端请求,然后收发一次数据后退出
printf("accept\n");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finished\n");
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
printf("RECV:%s\n", buffer);
count = send(clientfd, buffer, count, 0);
printf("SEND:%d\n", count);
#elif 0 // 方式二:循环处理多个客户端,但串行执行(一个客户端处理完才处理下一个)
while(1){
printf("accept\n");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finished\n");
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
printf("RECV:%s\n", buffer);
count = send(clientfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
#else // 方式三(最终版本):每个客户端使用一个线程来处理
while(1){
printf("accept\n");
// 阻塞等待客户端连接
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finished\n");
// 创建线程处理该客户端连接
pthread_t thid;
pthread_create(&thid, NULL, client_thread, &clientfd);
// 注意:此处传入 &clientfd 存在风险,最好用 malloc 分配空间再传入
}
#endif
// 等待用户输入,防止程序提前退出
getchar();
printf("exit\n");
return 0;
}
7. 总结与扩展
这段代码实现了一个最小可用的 多线程 TCP Echo 服务器,主要用来理解:
Socket 的创建与绑定。
accept 的作用(连接建立)。
recv/send 的通信逻辑。
pthread 的多线程处理。