Reactor
点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
基于上一篇epoll的学习,现在我们也知道epoll的工作模式有两种,一种默认LT工作模式,另一种是ET模式。关于epoll的LT工作模式我们已经写过了。接下来我们写一份基于ET模式下的Reator,处理所有的IO。
Reactor = 如何正确的处理IO+协议定制+业务逻辑处理
下面我们写一个简洁版的Reactor,它是一个半同步半异步IO,具体它什么原理,怎么做的,有什么特征。我们在代码层面上解开它的面纱。代码写完总结就理解了。其实Reactor是在Liunx网络中,最常用,最频繁的一种网络IO设计模式!
我们是这样打算的,对错误码,日志函数,套接字,epoll做封装然后在写服务器的时候用到的时候调用即可。错误码,日志函数,套接字以前我们封装过今天直接用就行了。
错误码封装
#pragma once
enum
{
USAGG_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
EPOLL_CREATE_ERR
};
日志函数封装
#pragma once
#include<iostream>
#include<string>
#include<stdio.h>
#include <cstdarg>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include<fstream>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"
const char* level_to_string(int level)
{
switch(level)
{
case DEBUG: return "DEBUG";
case NORMAL: return "NORMAL";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
case FATAL: return "FATAL";
}
}
//时间戳变成时间
char* timeChange()
{
time_t now=time(nullptr);
struct tm* local_time;
local_time=localtime(&now);
static char time_str[1024];
snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\
local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \
local_time->tm_min, local_time->tm_sec);
return time_str;
}
void logMessage(int level,const char* format,...)
{
//[日志等级] [时间戳/时间] [pid] [message]
//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024
//获取时间
char* nowtime=timeChange();
char logprefix[NUM];
snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());
//
char logconten[NUM];
va_list arg;
va_start(arg,format);
vsnprintf(logconten,sizeof logconten,format,arg);
std::cout<<logprefix<<logconten<<std::endl;
};
今天这里我们把套接字封装改一下,以前把它相关接口都写成静态的了,不需要对象直接调用了。今天呢,都改成非静态的,未来这个类提供的方法都要以对象调用访问。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Err.hpp"
#include "Log.hpp"
using namespace std;
class Sock
{
const static int backlog = 32;
const static int defaultsock = -1;
public:
Sock(int sock = defaultsock) : _listensock(sock)
{
}
~Sock()
{
if (_listensock != defaultsock)
close(_listensock);
}
public:
int sock()
{
// 1. 创建socket文件套接字对象
_listensock= socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
//方便外面获取_listensock
int Fd()
{
return _listensock;
}
void Bind(int port)
{
// 2. bind绑定自己的网络信息
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;
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
}
void Listen()
{
// 3. 设置socket 为监听状态
if (listen(_listensock, backlog) < 0)
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
int Accept(string *clientip, uint16_t *clientport,int* err)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
*err=errno;
if (sock < 0){
}
//logMessage(ERROR, "accept error, next");
else
{
//logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?
*clientip = inet_ntoa(peer.sin_addr);
*clientport = ntohs(peer.sin_port);
}
return sock;
}
private:
int _listensock;
};
对于一个epoll来说,首先要先给epoll创建出来,然后epoll的接口都要用到epoll创建成功的返回值。所以直接在这个epoll类中定义个成员变量直接就把创建epoll成功的返回值拿到。然后服务器不需要直接拿着这个返回值,而直接调用这个类对象使用里面的提供的接口就行了。epoll这里先写一个大体框架,后面需要什么了再加
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>
#include "Err.hpp"
#include "Log.hpp"
using namespace std;
const int defaultepfd = -1;
const int size = 128;
class Epoller
{
public:
Epoller(int epfd = defaultepfd) : _epfd(epfd)
{
}
~Epoller()
{
if (_epfd != defaultepfd)
close(_epfd);
}
public:
private:
int _epfd;//创建epoll返回值
};
目前调用逻辑,后面在加内容
#include "TcpServer.hpp"
#include "Err.hpp"
#include <memory>
static void usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(USAGG_ERR);
}
uint16_t port=atoi(argv[1]);
std::unique_ptr<TcpServer> uls(new TcpServer(port));
uls->initServer();
uls->Dispatch();
return 0;
}
服务器大体框架
#pragma once
#include <iostream>
#include "Sock.hpp"
#include "Err.hpp"
#include "Log.hpp"
#include "Epoller.hpp"
const int defaultport = 8080;
class TcpServer
{
public:
TcpServer(int port = defaultport) : _port(port)
{
}
~TcpServer()
{
}
void initServer()
{
}
void Dispatch()//事件派发
{
}
public:
private:
uint16_t _port;
Sock _sock;
Epoller _epoll;
};
接下来就是编写服务器了
初始化服务器
void initServer()
{
// 1.创建套接字
_sock.sock();
_sock.Bind(_port);
_sock.Listen();
// 2.创建epoll模型
}
创建套接字之后,我们要在创建一个epoll模型,因此我们在Epoller写一个创建epoll模型的接口,然后服务器直接调用就行了
```cpp
void Create()
{
_epfd = epoll_create(size);
if (_epfd < 0)
{
logMessage(FATAL, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));
exit(EPOLL_CREATE_ERR);
}
logMessage(NORMAL, "epoll create success, epfd: %d", _epfd);
}
void initServer()
{
// 1.创建套接字
_sock.sock();
_sock.Bind(_port);
_sock.Listen();
// 2.创建epoll模型
_epoll.Create();
// 3.将目前唯一的一个sock,添加到epoll中
}
创建好套接字,epoll模型接下来我们先把_listensock套接字添加到epoll里,看看这样写有没有什么问题
不过先在Epoller类中补充一个用户告诉内核你要帮我关心对应fd的什么事件。
// user -> kernel
bool AddEvents(int sock, uint32_t event)
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
if (n < 0)
{
logMessage(ERROR, "sock join epoll fail");
return false;
}
return true;
}
写好后我们直接调用就好了
void initServer()
{
// 1.创建套接字
_sock.sock();
_sock.Bind(_port);
_sock.Listen();
// 2.创建epoll模型
_epoll.Create();
// 3.将目前唯一的一个sock,添加到epoll中
_epoll.AddEvents(_sock.Fd(),EPOLLIN | EPOLLET);
}
但是现在有小一个问题,前面说过的,如果未来你的套接字工作模式是ET模式,那么该套接字必须处于非阻塞,_listensock套接字创建处理默认就是阻塞的,因此我们需要将文件描述符设为非阻塞!
我们在高级IO的非阻塞IO哪里就已经写过一个将一个fd变成非阻塞了,现在拿过来用就行了,不过我们也还是把它封装起来。然后调用。
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
class Util
{
public:
static bool SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
return false;
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
return true;
}
};
到目前为止好像貌似没问题了,但是真的吗?
void initServer()
{
// 1.创建套接字
_sock.sock();
_sock.Bind(_port);
_sock.Listen();
// 2.创建epoll模型
_epoll.Create();
// 3. 将目前唯一的一个sock,添加到epoller中, 之前需要先将对应的fd设置成为非阻塞
Util::SetNonBlock(_sock.Fd());
_epoll.AddEvents(_sock.Fd(),EPOLLIN | EPOLLET);
}
我们回过头看看之前写过的epoll服务器的代码,这里处理普通sock就绪事件的问题,你怎么保证你把本轮收到的数据都读完?即使把本轮数据都读完了,就一定能够读到一个完整的请求吗?即使未来在这里循环读取,可以反正我们非阻塞也都写过。那就能保证读到一个完整请求吗?
不一定!
那没读到完整的请求我们是不是只能把本轮读到的数据暂时保存到buffer里,可是暂时保存到buffer里,你保存了,那别人怎么办? 你这个sock和其他sock说你们别着急先别覆盖我的数据,buffer里面保存的是我的数据,你们先不要写。你想多了!
你读完之后整个这个代码区间就全部释放掉了,因为这是栈上面的空间。所以即便你没读完,或者你把这轮读完了。那么下轮在读,buffer早就释放了。你可能本轮读到后半部分但前半部分已经没有了,可能不到下次,下一个循环进来就给你清空了。
所以怎么样进行正确读写,光有一个套接字和定义一个栈上的缓存区远远不够!
所以我们需要给每一个文件描述符,都要把输入,输出用户层缓存区都要给它带上!
现在我们清楚知道历史代码的问题,我们要正确写整个服务,现在已经不够了。我们需要将每一个套接字进行封装,每一个套接字都要包含自己对应的输入,输出缓存区空间,只有每一个套接字都有自己对应的缓存空间,读取的时候把数据读取赞存在自己对应的缓存区中,没读完下次在读,这时候在添加到我自己的缓存区里,不就行了吗,这个时候谁和谁都不揉在一起。所以我们在封装一个类! 也就是说我认为未来每一个套接字都看类对象。
class Connection
{
public:
Connection(int sock = defaultport) : sock_(sock)
{
}
~Connection()
{
if (sock_ == defaultsock)
close(sock_);
}
public:
int sock_;//这个类对应的套接字是谁
string inbuffer_;//输入缓存区,这里我们暂时没有考虑处理图片,视频等二进制信息,不然stirng就不太合适了