前言
完成这个项目需要掌握的前置知识:
线程池的原理与实现
io多路复用原理深度解析
io多路复用的实现
Reactor网络模型
多 Reactor 多进程 / 线程(Mutiple Reactor + ThreadPool)
也叫 one loop per thread + 线程池。既有多个Reactor来处理IO,也使用线程池来处理计算,这种模式适合既有突发IO,又有突发计算的应用。适用于网络密集型以及业务密集型。
在该网络模式中,Server 端主要被分为两部分:MainReactor 和 SubReactor。MainReactor 主要负责监听新连接的到来,将新连接分配给不同的 SubReactor,SubReactor 负责具体的数据读写操作。MainReactor 和 SubReactor 通过线程池进行通信。每一个 SubReactor 都对应一个 epoll 实例,通过监控 sockfd 上的事件来实现非阻塞 I/O,同时,通过线程池来处理具体的事件。
网络模式架构如下:
一个main Reactor负责accept连接,然后把连接挂在某个sub Reactor中,该连接的所有操作都在那个sub Reactor所处的线程中完成,计算处理交由线程池进行。有多个Reactor来处理IO,也使用线程池来处理计算,这种模式适合既有突发IO,又有突发计算的应用。
整个模型的核心包括三部分:主Reactor、子Reactor 和线程池。
本文用到的线程池已经在前面的文章实现过了,这里就不仔细介绍线程池的原理以及实现了。
线程池的原理与实现
Reactor的原理以及实现在前面的博客也已经介绍了,这里也不多展开。
Reactor网络模型
一、MainReactor类
MainReactor做的事情就是监听并接受来自客户端的连接请求,并将连接套接字添加到SubReactor器中进行处理
构造函数
造函数包含了很多参数,包括端口号、子反应器数量、线程池最大最小线程数等
private:
int Port;
int SubReactorNum;
callback OnMessage;
ThreadPool Thread_Pool;
// SubReactor* subReactors; // 声明 SubReactor 指针数组
vector<SubReactor*> subReactors;
public:
// 输入处理函数以及,SubReactorNum,以及线程池最大最小线程输
// 以及ip端口号
MainReactor(callback onMessage,
int port = 9999,
int subReactorNum = 2,
int threadMinNum = 4,
int threadMaxNum = 8)
: Thread_Pool(threadMinNum, threadMaxNum),
OnMessage(onMessage),
Port(port),
SubReactorNum(subReactorNum) {
subReactors = vector<SubReactor*>(SubReactorNum);
}
连接与监听
首先创建一个监听套接字并绑定地址和端口,然后监听
// 创建监听套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("create socket error");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
sockaddr_in addr{};
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(Port);
if (bind(listenfd, (sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind error");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(listenfd, SOMAXCONN) < 0) {
perror("listen error");
exit(EXIT_FAILURE);
}
创建 epoll 实例进入主循环
该套接字加入 epoll 实例中并监听读事件,之后进入循环。利用 epoll_wait 函数阻塞等待事件的发生。当有连接请求时,调用 accept 函数获取连接,然后将连接套接字添加到子反应器中处理。在添加套接字时,设置边缘触发模式并通过取余的方式将新增连接分配到相应的子反应器中处理。如果某个子反应器还未创建,则创建一个新的子反应器,并将连接套接字添加到其中。
在默认情况下,epoll 采用的是水平触发(Level Triggered,简称 LT)模式。 在 LT 模式下,当某个描述符上有事件发生时,epoll_wait函数会立即返回该事件,并通知应用程序进行处理。如果应用程序没有及时处理完这个事件,下次调用 epoll_wait 函数时,会再次返回该事件。如果该文件描述符的缓冲区中还有数据可读或可写,epoll_wait函数仍然会返回。
在子Reactor接受到信息时候,信息交给线程处理,如果该线程没有及时启动,则会导致缓冲区中还有数据可读,就会在此调用 epoll_wait 函数,在此把读写操作事件加入到线程池中,从而出现严重的错误。
所以子Reactor一定是使用边缘触发模式,这种模式下,只有当数据从无可读状态变为有可读状态或者从无可写状态变为有可写状态时,才会触发一次读或写事件,因此要注意一次读取或写入不完整时需要进行多次操作。
int subReactorIndex = 0;
// 首先创建一个epoll实例
int epfd = epoll_create(100);
// 将需要监听的文件描述符加入实例中
// 把这里创建的文件描述符 加入EPOLL_CTL_ADD 然后 监听读事件
// epoll_event 结构体 事件是读取
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &epev);
// 定义检测到文件描述符改变的存储数组 后面直接遍历这个就行了
struct epoll_event epevs[1024];
while (true) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if (ret == -1) {
perror("epoll_wait");
exit(-1);
}
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd =
accept(listenfd, (struct sockaddr*)&cliaddr, (socklen_t*)&len);
cout << "///new client connect "
<< " cfd" << cfd << endl;
// 把连接套接字添加到子Reactor中
// 要设置成延边触发
epoll_event event{};
// event.events = EPOLLIN | EPOLLRDHUP;
event.events = EPOLLIN | EPOLLET; // 设置边缘触发
event.data.fd = cfd;
subReactorIndex = cfd % SubReactorNum;
cout << "/add sub reactor subReactorIndex:" << subReactorIndex
<< endl;
if (!subReactors[subReactorIndex]) {
// 如果subsubReactorFds还没有创建 就创建一个
int curSubReactorFd = epoll_create(5);
cout << "create new curSubReactorFd:" << curSubReactorFd << endl;
if (curSubReactorFd < 0) {
perror("epoll_create1 error");
exit(EXIT_FAILURE);
}
subReactors[subReactorIndex] =
new SubReactor(curSubReactorFd, Thread_Pool,
OnMessage); // 创建子Reactor对象并分配内存
if (epoll_ctl(subReactors[subReactorIndex]->epollfd, EPOLL_CTL_ADD,
cfd, &event) < 0) {
perror("epoll_ctl error");
continue;
}
subReactors[subReactorIndex]->start(); // 创建并启动线程
} else {
if (epoll_ctl(subReactors[subReactorIndex]->epollfd, EPOLL_CTL_ADD,
cfd, &event) < 0) {
perror("epoll_ctl error");
continue;
}
}
}
二、SubReactor类
SubReactor负责处理已连接套接字的数据读取事件。
构造函数
造函数包含了很多参数,包括端口号、子反应器数量、线程池最大最小线程数等
private:
callback& OnMessage;
ThreadPool& threadPool;
pthread_t tid;
public:
int epollfd;
SubReactor(int epollfd, ThreadPool& threadPool, callback& OnMessage)
: epollfd(epollfd), threadPool(threadPool), OnMessage(OnMessage) {}
监听
使用 epoll_wait 函数来获取已就绪的文件描述符,。如果有数据读取事件发生,则将该任务提交到线程池中处理
void* run() {
epoll_event events[MaxEvents];
memset(&events, 0, sizeof(events));
while (true) {
int nfds = epoll_wait(epollfd, events, MaxEvents, -1);
if (nfds < 0) {
perror("epoll_wait error");
continue;
}
// 遍历所有就绪事件
for (int i = 0; i < nfds; ++i) {
int sockfd = events[i].data.fd;
// 处理已连接套接字的数据读取事件
cout << epollfd << "-->SubReactor get msg sockfd:" << sockfd
<< " nfds:" << nfds << " " << i << endl;
if (events[i].events & EPOLLIN) {
// 把读写任务提交到线程池中处理
int* num = new int(sockfd); // 先为 num 分配内存
Task task(OnMessage, num);
cout << "addTask " << endl;
threadPool.addTask(task);
}
}
}
}
启动
由于创建线程需要指定线程入口函数,而类成员函数不能直接作为线程入口函数,因此需要通过一个非成员函数或静态成员函数作为线程入口函数。在这个类中,使用了静态成员函数 threadFunc() 作为线程入口函数。当新线程启动时,会调用 threadFunc() 函数,并将 SubReactor 对象指针作为参数传入,然后在 threadFunc() 函数中调用 run()。
void start() {
pthread_create(&tid, nullptr, &SubReactor::threadFunc, this);
}
static void* threadFunc(void* arg) {
SubReactor* reactor = static_cast<SubReactor*>(arg);
return reactor->run();
}
服务器测试
响应客户端信息函数就是简单的返回客户端发送的信息。
void OnMessage(void* arg) {
int sockfd = *(int*)arg;
char buffer[1024];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n < 0) {
perror("recv error");
close(sockfd);
return;
}
if (n == 0) {
cout << "client closed" << endl;
close(sockfd);
return;
}
buffer[n] = '\0';
cout << "get sockfd :" << sockfd << " msg:" << buffer << endl;
send(sockfd, buffer, n, 0);
}
int main() {
MainReactor mainReactor(OnMessage);
mainReactor.Start();
}
客户端简单实现
由于这是一个类似于muduo库一样高性能网络库,所以也应该有一个客户端,这里简单写一个客户端,能够发送并且接受消息。
class MRTPClient {
private:
int Port;
string Ip;
int fd;
public:
// 以及ip端口号
MRTPClient(string ip, int port) : Ip(ip), Port(port) {
// 1.创建套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
printf("client socket create err!!");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, ip.c_str(), &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(port);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (ret == -1) {
printf("connect err!!");
exit(-1);
}
printf("===================client connect %s:%d success!!", ip, port);
}
// 简单的发送然后返回值
std::string Send(const std::string& request_str) {
std::string response_str;
int n = send(fd, request_str.c_str(), request_str.size(), 0);
if (n == -1) {
printf("send err!!");
exit(-1);
}
char buffer[1024];
n = recv(fd, buffer, sizeof(buffer), 0);
while (n > 0) {
response_str.append(buffer, n);
n = recv(fd, buffer, sizeof(buffer), 0);
}
if (n == -1) {
printf("recv err!!");
exit(-1);
}
return response_str;
}
};