从零实现 Reactor + ThreadPool TCP 服务器
1.引言
本文将从什么是reactor,以及为什么需要reactor开始讲起,在reactor模型的基础上引入线程池,最终实现一个TCP服务器。
2.Reactor
什么是reactor?
你可以理解成 一种高并发IO的管理模型。
为什么需要reactor?我们一步一步看。
最原始的模型:阻塞IO。
最直观的写法:
c
while (1)
{
int connfd = accept(listenfd);
char buf[1024];
recv(connfd, buf, sizeof(buf), 0);
process(buf);
send(connfd, reply, len, 0);
close(connfd);
}
整个服务器是单线程执行的。单线程模型最怕是什么?耗时操作。上面的处理逻辑有耗时操作吗?当然有!执行到recv的时候,如果客户端的数据还没有准备好,那么将会阻塞整个线程。因为服务器是单线程的,所以就意味着在等待客户端数据就绪到处理完毕调用close关闭客户端连接的这段时间内,服务器是不能够处理其他客户端连接的。高并发场景下,响应会很慢。这就是阻塞IO存在的问题。
一连接一线程
既然单线程的服务器会因为处理某个客户端连接而进入阻塞状态,导致无法处理其他客户端数据的情况,那么大家就很自然的想到:一个客户端连接交给一个线程来处理。

写出来的代码就类似下面这样子:
c
while(1)
{
connfd = accept();
std::thread([connfd]{
while(1)
{
recv(connfd,...);
process();
send(connfd,...);
}
}).detach();
}
好像是可以并发了,但是新问题也来了。
线程不是免费的,假设有10000个连接,那么就需要10000个线程,Linux默认的线程栈为8MB,那么10000个线程就是80000MB,大约80GB。内存直接爆炸。另外,10000个线程,需要频繁的切换CPU上下文,可能会导致CPU大部分时间在切换线程。还有,现实中,服务器线程99%在等网络,1%在真正计算。比如,大部分线程调用recv()阻塞住了,CPU有可能是空闲的。
这就是经典的C10K问题------如何让服务器高效的支持1万个并发网络连接。
非阻塞IO出现
考虑到大部分线程可能在等待网络数据,于是大家想到:不让线程睡觉。所以就把fd设置为非阻塞的,调用recv()时,如果没有数据,直接返回。
于是,我们的代码通常会写成这样:
c
while(1)
{
recv(fd);
...
}
这叫 轮询 。
有10000个连接,我们就需要不断检查这10000个连接是否有数据,导致CPU疯狂空转。
所以,综上,我们的需求就变成:
- 不想阻塞
- 不想轮询
- 谁有数据,内核通知我,然后我在处理
这就是事件驱动模型。
IO多路复用出现
Linux提供了select/poll/epoll三种机制,核心思想都是一个线程管理多个连接。
比如epoll,我们只需要调用epoll_wait(),拿到准备就绪的fd即可。这时候,1个线程可以轻松管理10000个连接。
但是,随之而来的就是代码的复杂度变高了。于是,就有了reactor。
Reactor本质上就是对多路复用的封装,把代码的复杂度给组织起来。
reactor的三个组成部分:
- 多路复用器: select/poll/epoll。
- 事件分发器: 将从多路复用那里获取的就绪事件,分法给对应的处理函数。
- 事件处理器: 事件的处理函数。

如果没引入线程池,那么Reactor线程既要从内核拿到注册的就绪事件,事件的整个处理过程还要亲力亲为。这就注定了事件处理的效率不会太高。
于是,引入了线程池。

引入了线程池之后,Reactor只需要负责读、写、接受连接,数据的解析、处理等业务全部交给线程池来处理。
代码上:
c
reactor.register(fd, EPOLLIN, readHandler);
注册回调。
事件来时:
c
readHandler(fd);
你可能会有疑惑,为什么不把读和写也交给线程池来处理?
这里主要是进行一个职责的划分,Reactor负责socket IO,线程池负责业务处理,避免并发问题。
如果你不太了解线程池,可以看看我篇博客,这里引入的线程池几乎和该博客中的一致。
完整代码:
c
#include <iostream>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string>
#include <fcntl.h>
#include <thread>
#include <chrono>
#define DEFAULT_THREAD_NUM 4
#define PORT 8080
#define MAX_LISTEN_QUEUE 128
#define MAX_EVENTS 1024
#define BUFFER_SIZE 1024
class ThreadPoll {
public:
ThreadPoll(size_t thread_num = DEFAULT_THREAD_NUM):
_thread_num(thread_num),
_start(false)
{}
ThreadPoll(const ThreadPoll&) = delete;
ThreadPoll& operator=(const ThreadPoll&) = delete;
ThreadPoll(ThreadPoll&&) = delete;
ThreadPoll& operator=(ThreadPoll&&) = delete;
~ThreadPoll() { destroy(); }
void init() {
std::unique_lock<std::mutex> lock(_mutex);
_workers.reserve(_thread_num);
for (int i = 0; i < _thread_num; i++) {
_workers.emplace_back(std::thread([this](){
work_loop();
}));
}
_start = true;
printf("[%s:%d]Thread poll start...\n", __func__, __LINE__);
}
template<class F, class... Args>
auto submit(F&& func, Args&&... args)
->std::future<std::invoke_result_t<F, Args...>> {
using return_type = std::invoke_result_t<F, Args...>;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(func), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(_mutex);
_tasks.emplace([task](){ (*task)(); });
}
_cond.notify_one();
return res;
}
void destroy() {
{
std::unique_lock<std::mutex> lock(_mutex);
_start = false;
}
_cond.notify_all();
for (auto& worker : _workers) {
if (worker.joinable()) worker.join();
}
printf("[%s:%d]destroy threadPoll...\n", __func__, __LINE__);
}
private:
void work_loop() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(_mutex);
_cond.wait(lock, [this](){
return !_start || !_tasks.empty();
});
if (!_start && _tasks.empty()) return;
task = std::move(_tasks.front());
_tasks.pop();
}
task();
}
}
private:
std::queue<std::function<void()>> _tasks;
size_t _thread_num;
std::vector<std::thread> _workers;
std::condition_variable _cond;
std::mutex _mutex;
bool _start;
};
/**
* @brief Channel类封装了文件描述符和对应的事件回调函数
* accept_callback:处理新连接事件的回调函数
* read_callback:处理可读事件的回调函数
* write_callback:处理可写事件的回调函数
* 通过Channel类,我们可以将文件描述符和对应的事件处理逻辑进行绑定,方便在事件循环中进行事件分发和处理
*/
class Channel {
public:
int fd;
std::function<void()> accept_callback;
std::function<void()> read_callback;
std::function<void()> write_callback;
};
/**
* @brief Connection类封装了连接相关的信息,包括文件描述符、输入缓冲区和输出缓冲区
* fd:连接的文件描述符
* inbuf:用于存储从客户端接收的数据的输入缓冲区
* outbuf:用于存储要发送给客户端的数据的输出缓冲区
* 通过Connection类,我们可以将连接相关的信息进行封装,方便在事件处理过程中进行数据的读写和管理
*/
struct Connection {
int fd;
std::string inbuf;
std::string outbuf;
};
class Reactor {
public:
/**
* @brief 创建epoll对象,同时初始化线程池
*
*/
Reactor() {
epfd = epoll_create(1);
poll.init();
}
~Reactor() { close(epfd); }
/**
* @brief 初始化服务器,创建监听套接字,并将其注册到epoll对象中
*/
void init_server() {
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
printf("[%s:%d]listen_fd create faild!\n", __func__, __LINE__);
return;
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
printf("[%s:%d]listen_fd bind faild!\n", __func__, __LINE__);
return;
}
if (listen(listen_fd, MAX_LISTEN_QUEUE) == -1) {
printf("[%s:%d]listen_fd listen faild!\n", __func__, __LINE__);
return;
}
epoll_event ev;
ev.data.fd = listen_fd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
Channel ch;
ch.fd = listen_fd;
ch.accept_callback = [this](){
handle_accept();
};
channels[listen_fd] = std::move(ch);
}
/**
* @brief 事件循环监听
* epoll_wait:多路复用器
* events:事件分发器
* callback:事件处理器
*/
void loop() {
epoll_event events[MAX_EVENTS];
while (true) {
int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nready; i++) {
int fd = events[i].data.fd;
auto& ch = channels[fd];
if (fd == listen_fd) {
ch.accept_callback();
}else {
if (events[i].events & EPOLLIN) {
ch.read_callback();
}
if (events[i].events & EPOLLOUT) {
ch.write_callback();
}
}
}
}
}
private:
static void set_nonblock(int fd) {
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
void handle_accept() {
sockaddr client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1) {
printf("[%s:%d]accept faild!\n", __func__, __LINE__);
return;
}
set_nonblock(client_fd);
epoll_event ev;
ev.data.fd = client_fd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
Connection conn;
conn.fd = client_fd;
connections[client_fd] = std::move(conn);
Channel ch;
ch.fd = client_fd;
ch.read_callback = [this, client_fd](){
handle_read(client_fd);
};
ch.write_callback = [this, client_fd](){
handle_write(client_fd);
};
channels[client_fd] = std::move(ch);
printf("[%s:%d]new client[fd=%d]\n", __func__, __LINE__, client_fd);
}
void handle_read(int fd) {
char buf[BUFFER_SIZE];
// recv在reactor线程,业务处理后面交给线程池
int n = recv(fd, buf, sizeof(buf), 0);
if (n <= 0) {
close_conn(fd);
return;
}
std::string req(buf, n);
// 向线程池提交任务
printf("[%s:%d]submit task to threadpoll\n", __func__, __LINE__);
poll.submit([this, fd, req](){
process_business(fd, req);
});
}
void process_business(int fd, std::string req) {
// 模拟处理耗时
printf("[%s:%d]threadpoll processing...\n", __func__, __LINE__);
std::this_thread::sleep_for(std::chrono::seconds(1));
auto& conn = connections[fd];
conn.outbuf = "server reply: " + req;
epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLOUT;//写期间允许继续读
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
void handle_write(int fd) {
auto& conn = connections[fd];
if (conn.outbuf.empty()) return;
send(fd, conn.outbuf.data(), conn.outbuf.size(), 0);
conn.outbuf.clear();
epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
printf("[%s:%d]send to client...\n", __func__, __LINE__);
}
void close_conn(int fd) {
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
channels.erase(fd);
connections.erase(fd);
printf("[%s:%d]close connection...\n", __func__, __LINE__);
}
private:
int epfd;
int listen_fd;
ThreadPoll poll;
std::unordered_map<int, Channel> channels;
std::unordered_map<int, Connection> connections;
};
int main() {
Reactor reactor;
reactor.init_server();
reactor.loop();
return 0;
}
上述代码的关键模块,都有详细注释,希望对你有所帮助。
3.结语
本文的关键在于,Reactor模型中如何引入线程池,在哪引入线程池,以及引入的线程池起到了哪些作用。除此之外,明白Reactor线程和线程池的分工即可。