从零实现 Reactor + ThreadPool TCP 服务器

从零实现 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线程和线程池的分工即可。

相关推荐
2501_912784086 小时前
Taocarts全链路反向海淘系统实战拆解:一个人+一台服务器,如何做到日处理200单?
运维·服务器·跨境电商·taocarts
程序猿追6 小时前
在轻量服务器上部署商汤SenseNova U1轻量版全记录
运维·服务器
Web打印7 小时前
web打印控件,打印模板分散部署在各客户端本地,修改后需逐台更新,能否统一部署至服务器实现集中维护
运维·服务器
NeedJava7 小时前
阿里云 ECS 美国服务器里的大文件传到国内 OSS 服务器
服务器·阿里云·云计算
TDK村田muRata8 小时前
CUS200M-12 | TDK医疗电源|直流12V 16.7A |CUS200M-12/A
服务器·人工智能·3d·机器人·无人机
百数平台8 小时前
功能更新——百数详情页“数据简报”与“关联标签页”配置指南
java·服务器·前端
浮生若城8 小时前
Linux基础I/O(2):理解“一切皆文件”与缓冲区
linux·运维·服务器
苏宸啊8 小时前
库的使用和制作
运维·服务器
wanhengidc8 小时前
云手机手游搬砖 梦境护卫队
运维·服务器·安全·web安全·智能手机