0Pricing
C Academy · 课时

简单的回显服务器

同时为多个客户端提供服务。

简单的回显服务器 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 阻塞与非阻塞 I/O
  2. 设置 epoll
  3. 事件循环
  4. 简单的回显服务器
← 返回 C Academy