Linux 高级 IO:Epoll 模型与 TCP 服务器实现
前言
本文基于 Linux 高级 IO 中 I/O 多路转接的 epoll 接口和原理进行讲解,重点实现一个 epoll 版本的 TCP 服务器。
本文介绍 Linux 高级 IO 中的 Epoll 模型及其在 TCP 服务器中的应用。通过封装 Epoller 类管理 epoll 系统调用,结合 Socket 封装实现网络通信。文章展示了如何构建不可拷贝的服务器类,处理连接建立与数据收发,演示了事件分发逻辑及超时机制配置,实现了基于 Epoll 的高性能单线程 TCP 服务器。

本文基于 Linux 高级 IO 中 I/O 多路转接的 epoll 接口和原理进行讲解,重点实现一个 epoll 版本的 TCP 服务器。
epoll 版本的 TCP 服务器需要建立以下源文件和头文件:
nocopy 类用于实现禁止拷贝功能。通过将拷贝构造函数和赋值运算符使用 delete 关键字禁用,继承该类的公有派生类将无法进行拷贝。
#pragma once
class nocopy {
public:
nocopy() {};
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
让 Epoller 类和 EpollServer 类公有继承自 nocopy,确保 epoll 模型和 TCP 服务器只能有一份实例。
Epoller 类对 epoll 的三个系统调用函数及文件描述符 epfd 进行封装。
_epfd:epoll 模型的入口文件描述符。_timeout:超时时间,初始化为 3000 微秒(3 秒)。size:静态常量,用于 epoll_create 参数,当前标准下已废弃,设为 128。在构造函数中调用 epoll_create 创建模型,失败则打印日志;析构函数中关闭 _epfd 释放资源。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/epoll.h>
#include "Log.hpp"
#include "nocopy.hpp"
class Epoller : public nocopy {
static const int size = 128;
public:
Epoller() {
_epfd = epoll_create(size);
if (_epfd == -1) {
lg(Error, "epoll_create error: %s", strerror(errno));
} else {
lg(Info, "epoller_create success, epfd: %d", _epfd);
}
}
~Epoller() {
if (_epfd >= 0) {
close(_epfd);
}
}
private:
int _epfd;
int _timeout{3000};
};
将 epoll_wait 封装为 EpollerWait 成员函数,返回就绪事件个数。
int EpollerWait(struct epoll_event revents[], int num) {
int n = epoll_wait(_epfd, revents, num, _timeout);
return n;
}
将 epoll_ctl 封装为 EpollerUpdate 成员函数,支持添加、删除、修改节点。
int EpollerUpdate(int oper, int sock, uint32_t event) {
int n = 0;
if (oper == EPOLL_CTL_DEL) {
n = epoll_ctl(_epfd, oper, sock, nullptr);
if (n == -1) {
lg(Error, "epoll_ctl delete error");
}
} else {
struct epoll_event ev;
ev.data.fd = sock;
ev.events = event;
n = epoll_ctl(_epfd, oper, sock, &ev);
if (n == -1) {
lg(Error, "epoll_ctl error");
}
}
return n;
}
主函数中使用智能指针管理服务器对象,传入端口号 8080,调用 Init 和 Start。
#include "EpollServer.hpp"
#include <memory>
int main() {
std::unique_ptr<EpollServer> epoll_svr(new EpollServer(8080));
epoll_svr->Init();
epoll_svr->Start();
return 0;
}
EpollServer 类继承 nocopy,包含监听套接字和 Epoller 的智能指针管理。
_port:绑定端口号。_listensock_ptr:监听套接字 Sock 对象的 shared_ptr。_epoller_ptr:Epoller 对象的 shared_ptr。num:静态常量,表示获取就绪 fd 的最大个数,设为 64。Init 函数完成 Socket 创建、Bind 绑定、Listen 监听。 Start 函数进入死循环,将 listensock 添加到 epoll 中,等待事件就绪后分发给 Dispatcher 处理。
#include <iostream>
#include <memory>
#include <string>
#include <unistd.h>
#include "Log.hpp"
#include "Socket.hpp"
#include "nocopy.hpp"
#include "Epoller.hpp"
uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);
class EpollServer : public nocopy {
static const int num = 64;
public:
EpollServer(uint16_t port) : _listensock_ptr(new Sock()), _epoller_ptr(new Epoller()), _port(port) {}
void Init() {
_listensock_ptr->Socket();
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
lg(Info, "create listen socket success, fd: %d", _listensock_ptr->Fd());
}
void Accepter() {
std::string clientip;
uint16_t clientport;
int sock = _listensock_ptr->Accept(&clientip, &clientport);
if (sock > 0) {
_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
}
}
void Recver(int fd) {
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = 0;
std::cout << "get a message: " << buffer << std::endl;
std::string echo_str = "server echo$ ";
echo_str += buffer;
write(fd, echo_str.c_str(), echo_str.size());
} else if (n == 0) {
lg(Info, "client quit, me to, close fd: %d", fd);
_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
} else {
lg(Warning, "recv error, fd: %d", fd);
_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
}
void Dispatcher(struct epoll_event revs[], int n) {
for (int i = 0; i < n; i++) {
int fd = revs[i].data.fd;
uint32_t event = revs[i].events;
if (event & EVENT_IN) {
if (fd == _listensock_ptr->Fd()) {
Accepter();
} else {
Recver(fd);
}
} else if (event & EVENT_OUT) {
// 暂不处理写事件
} else {
// 其他事件忽略
}
}
}
void Start() {
_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensock_ptr->Fd(), EVENT_IN);
struct epoll_event revs[num];
for (;;) {
int n = _epoller_ptr->EpollerWait(revs, num);
if (n > 0) {
lg(Debug, "event happened, fd: %d", revs[0].data.fd);
Dispatcher(revs, n);
} else if (n == 0) {
lg(Info, "time out...");
} else {
lg(Error, "epoll wait error");
}
}
}
~EpollServer() {
_listensock_ptr->Close();
}
private:
std::shared_ptr<Sock> _listensock_ptr;
std::shared_ptr<Epoller> _epoller_ptr;
uint16_t _port;
};
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/epoll.h>
#include "Log.hpp"
#include "nocopy.hpp"
class Epoller : public nocopy {
static const int size = 128;
public:
Epoller() {
_epfd = epoll_create(size);
if (_epfd == -1) {
lg(Error, "epoll_create error: %s", strerror(errno));
} else {
lg(Info, "epoller_create success, epfd: %d", _epfd);
}
}
int EpollerWait(struct epoll_event revents[], int num) {
int n = epoll_wait(_epfd, revents, num, -1); // 阻塞等待
return n;
}
int EpollerUpdate(int oper, int sock, uint32_t event) {
int n = 0;
if (oper == EPOLL_CTL_DEL) {
n = epoll_ctl(_epfd, oper, sock, nullptr);
if (n == -1) {
lg(Error, "epoll_ctl delete error");
}
} else {
struct epoll_event ev;
ev.data.fd = sock;
ev.events = event;
n = epoll_ctl(_epfd, oper, sock, &ev);
if (n == -1) {
lg(Error, "epoll_ctl error");
}
}
return n;
}
~Epoller() {
if (_epfd >= 0) {
close(_epfd);
}
}
private:
int _epfd;
int _timeout{3000};
};
(见上文第五节完整代码)
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log {
public:
Log() { printMethod = Screen; path = "./log/"; }
void Enable(int method) { printMethod = method; }
~Log() {}
std::string levelToString(int level) {
switch (level) {
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error: return "Error";
case Fatal: return "Fatal";
default: return "";
}
}
void operator()(int level, const char* format, ...) {
time_t t = time(nullptr);
struct tm* ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(), ctime->tm_year + 1900,
ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour,
ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[2 * SIZE];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
void printLog(int level, const std::string& logtxt) {
switch (printMethod) {
case Screen: std::cout << logtxt << std::endl; break;
case Onefile: printOneFile(LogFile, logtxt); break;
case Classfile: printClassFile(level, logtxt); break;
default: break;
}
}
void printOneFile(const std::string& logname, const std::string& logtxt) {
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0) return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string& logtxt) {
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
private:
int printMethod;
std::string path;
};
Log lg;
#include "EpollServer.hpp"
#include <memory>
int main() {
std::unique_ptr<EpollServer> epoll_svr(new EpollServer(8080));
epoll_svr->Init();
epoll_svr->Start();
return 0;
}
epoll_server: Main.cc
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -f epoll_server
#pragma once
class nocopy {
public:
nocopy() {};
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
const int backlog = 10;
enum { SocketErr = 1, BindErr, ListenErr };
class Sock {
public:
Sock() {}
void Socket() {
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0) {
lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(uint16_t port) {
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(local);
if (bind(sockfd_, (struct sockaddr*)&local, len) < 0) {
lg(Fatal, "bind error, %s : %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen() {
if (listen(sockfd_, backlog) < 0) {
lg(Fatal, "listen error, %s : %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* clientip, uint16_t* clientport) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if (newfd < 0) {
lg(Warning, "accept error, %s : %d", strerror(errno), errno);
return -1;
}
char ipstr[128];
inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& serverip, uint16_t serverport) {
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));
socklen_t len = sizeof(peer);
int n = connect(sockfd_, (struct sockaddr*)&peer, len);
if (n == -1) {
std::cerr << "connect to " << serverip << ':' << serverport << "error" << std::endl;
return false;
}
return true;
}
void Close() {
if (sockfd_ > 0) {
close(sockfd_);
}
}
int Fd() { return sockfd_; }
~Sock() {}
private:
int sockfd_;
};
本文实现了基于 Linux Epoll 模型的高性能 TCP 服务器,涵盖了从基础封装到事件分发、连接管理的完整流程。通过合理设计类结构和使用智能指针,确保了资源的安全管理和高效复用。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online