简单的回显服务器
同时为多个客户端提供服务。
简单的回显服务器 是 CoddyKit 上的免费 C Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C Academy 课程共包含 4 节课。
我们要构建什么
回显服务器接受 TCP 连接,并将客户端发送的所有字节原样发回。它是网络编程中的“hello world”。
我们将基于 epoll 构建它,使单个线程能够同时服务多个客户端。组成部分包括:监听套接字、epoll 循环,以及针对每个客户端的先读后写处理。
创建监听套接字
首先使用 socket() 创建一个 IPv4 TCP 端点。三个参数分别是 AF_INET、SOCK_STREAM 和协议 0。
该调用会返回一个文件描述符;我们将对它执行绑定、设置非阻塞,并最终向 epoll 注册。请检查每个系统调用是否返回 -1。
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) { perror("socket"); exit(1); }重用地址
绑定之前,请使用 setsockopt() 设置 SO_REUSEADDR。这样服务器就能重启并重新绑定到同一端口,而无需等待 TIME_WAIT 状态结束。
如果不设置它,快速重启通常会因 EADDRINUSE 失败,这在开发过程中会令人十分困扰。
int yes = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);绑定到端口
使用地址族、端口和地址填充 struct sockaddr_in。端口请使用 htons(),并使用 INADDR_ANY 监听所有接口。
然后调用 bind(),将套接字绑定到该地址。htons 会将主机字节序转换为网络字节序,这是必需的。
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
bind(listen_fd, (struct sockaddr*)&addr, sizeof addr);监听并设置为非阻塞
调用 listen() 将套接字标记为被动套接字,并为待处理连接设置等待队列长度。SOMAXCONN 请求系统允许的最大值。
然后将套接字切换为非阻塞模式,使 accept() 永远不会阻塞事件循环。现在它已经可以向 epoll 注册了。
listen(listen_fd, SOMAXCONN);
set_nonblocking(listen_fd);注册监听套接字
创建 epoll 实例,并使用 EPOLLIN 添加监听套接字。这里的可读事件表示有新的客户端正在等待接受。
我们将 fd 存储在 data.fd 中,以便在循环内部识别监听套接字。
int epfd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);接受客户端
监听套接字可读时,循环调用 accept(),直到返回 EAGAIN。每个新客户端套接字都会被设置为非阻塞,并注册 EPOLLIN。
从此以后,只要该客户端发送了我们可以回显的数据,epoll 就会通知我们。
int c = accept(listen_fd, NULL, NULL);
if (c != -1) {
set_nonblocking(c);
struct epoll_event cev = { .events = EPOLLIN, .data.fd = c };
epoll_ctl(epfd, EPOLL_CTL_ADD, c, &cev);
}回显逻辑
对于已就绪的客户端,使用 recv() 将数据读入缓冲区。如果读到了字节,就使用 send() 将相同的字节直接发回。
这种简单的先读后写就是回显。在生产环境中,您应当缓冲未发送的字节;但在演示中,我们假设较小的回复可以放入发送缓冲区。
char buf[4096];
ssize_t r = recv(fd, buf, sizeof buf, 0);
if (r > 0) {
send(fd, buf, r, 0);
}检测断开连接
recv() 返回 0 表示客户端执行了有序关闭。返回负值且错误不是 EAGAIN 时,表示发生了真正的错误。
无论是哪种情况,都请从 epoll 中移除该描述符,并调用 close() 释放资源、停止后续事件。
if (r == 0 || (r < 0 && errno != EAGAIN)) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}完整循环
将各个部分组合起来:等待事件,根据描述符是监听套接字还是客户端进行分支处理,然后采取相应操作。
这个紧凑的循环就是一个功能完整的并发回显服务器。一个线程即可服务多个客户端,无需为每个连接创建线程。
for (;;) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) accept_clients();
else echo_or_close(events[i].data.fd);
}
}测试与后续步骤
在 Linux 上编译,然后使用 nc localhost 8080 连接;您输入的内容都会原样返回。打开多个终端即可验证并发处理能力。
要进一步增强它:缓冲部分发送的数据,处理 EPOLLOUT,切换到 EPOLLET 并将数据完全读尽,同时处理半关闭情况下的 EPOLLRDHUP。
快速检查
测试您对回显服务器流程的理解。
回顾
您构建了一个 epoll 回显服务器:创建套接字、设置 SO_REUSEADDR、绑定、监听,并将其设置为非阻塞模式。注册监听套接字,在循环中接受客户端连接,并使用 recv()/send() 进行回显。
将返回值为 0 视为断开连接,并妥善关闭连接。在此基础上,您可以逐步构建出高性能的单线程网络服务器。
常见问题解答
「简单的回显服务器」课时是免费的吗?
是的 — 「简单的回显服务器」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。
「简单的回显服务器」这节课中我会学到什么?
同时为多个客户端提供服务。 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「简单的回显服务器」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C Academy 课中编写并运行代码吗?
能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 阻塞与非阻塞 I/O
- 设置 epoll
- 事件循环
- 简单的回显服务器