基于 Socket 和多线程的简单 Echo 服务器实现
在学习网络编程的时候,很多人会遇到一个经典的例子——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 socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);// 2. 配置服务器地址结构体struct sockaddr_in servaddr;servaddr.sin_family = AF_INET; // 使用 IPv4servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到本机所有网卡 IPservaddr.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. 开始监听,最大等待队列长度为 10listen(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 的多线程处理。
0voice · GitHub