目录
1.实现目标
2.HTTP服务器
实现高性能服务器-Reactor模型
模块划分
SERVER模块:
HTTP协议模块:
3.项目中的子功能
秒级定时任务实现
时间轮实现
正则库的简单使用
通⽤类型any类型的实现
4.SERVER服务器实现
日志宏的封装
缓冲区Buffer类实现
套接字Socket实现
事件管理Channel模块实现
描述符事件监控Poller模块实现
定时任务管理TimerWheel类实现
EventLoop线程池类实现
编辑
LoopThread模块实现
LoopThreadPool线程池模块实现
通信连接管理Connection模块实现
监听描述符管理Acceptor类实现
TcpServer模块
HTTP协议模块实现
Util⼯具类实现
HttpRequest请求类实现
HttpResponse响应类实现
HttpServer服务器模块
服务器搭建并进行测试
搭建服务器
长连接测试
测试超时连接是否销毁
错误请求处理
服务器性能达到瓶颈的处理
一次发送多个请求测试
测试大文件传输
服务器性能测试
项目源码
1.实现目标
通过实现的⾼并发服务器组件,可以简洁快速的完成⼀个⾼性能的服务器搭建。并且,通过组件内提供的不同应⽤层协议⽀持,也可以快速完成⼀个⾼性能应⽤服务器的搭建
2.HTTP服务器
HTTP概念:超⽂本传输协议是应⽤层协议,是⼀种简单的请求-响应协议,HTTP服务器本质上就是个TCP服务器,只不过在应⽤层基于HTTP协议格式进⾏数据的组织和解析来明确客⼾端的请求并完成业务处理
实现高性能服务器-Reactor模型
概念:
Reactor模式,是指通过⼀个或多个输⼊同时传递给服务器进⾏请求处理时的事件驱动处理模式。服务端程序处理传⼊多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式。使⽤ I/O多路复⽤统⼀监听事件,收到事件后分发给处理进程或线程分类:
1.单Reactor单线程:单I/O多路复⽤+业务处理
1. 通过IO多路复⽤模型进⾏客⼾端请求监控2. 触发事件后,进⾏事件处理
a. 如果是新建连接请求,则获取新建连接,并添加⾄多路复⽤模型进⾏事件监控。b. 如果是数据通信请求,则进⾏对应数据处理(接收数据,处理数据,发送响应)
优点:所有操作均在同⼀线程中完成,思想流程较为简单,不涉及进程/线程间通信及资源争抢问题。
缺点:⽆法有效利⽤CPU多核资源,很容易达到性能瓶颈。
适⽤场景:适⽤于客⼾端数量较少,且处理速度较为快速的场景2.单Reactor多线程:单I/O多路复⽤+线程池(业务处理)
1. Reactor线程通过I/O多路复⽤模型进⾏客户端请求监控
2. 触发事件后,进⾏事件处理
a. 如果是新建连接请求,则获取新建连接,并添加⾄多路复⽤模型进⾏事件监控。
b. 如果是数据通信请求,则接收数据后分发给Worker线程池进⾏业务处理。
c. ⼯作线程处理完毕后,将响应交给Reactor线程进⾏数据响应优点:充分利⽤CPU多核资源
缺点:多线程间的数据共享访问控制较为复杂,单个Reactor承担所有事件的监听和响应,在单线程中运⾏,⾼并发场景下容易成为性能瓶颈。3.多Reactor多线程:多I/O多路复⽤+线程池(业务处理)
1. 在主Reactor中处理新连接请求事件,有新连接到来则分发到⼦Reactor中监控
2. 在⼦Reactor中进⾏客户端通信监控,有事件触发,则接收数据分发给Worker线程池
3. Worker线程池分配独⽴的线程进⾏具体的业务处理
a. ⼯作线程处理完毕后,将响应交给⼦Reactor线程进⾏数据响应当前项目采用的方式:One Thread One Loop主从Reactor模型⾼并发服务器
1.主Reactor线程仅仅监控监听描述符,获取新建连接,保证获取新连接的⾼效性,提⾼服务器的并发性能。
2.主Reactor获取到新连接后分发给从属Reactor进⾏通信事件监控。而从属Reactor线程监控各⾃的描述符的读写事件进⾏数据读写以及业务处理。
3.One Thread One Loop的思想就是把所有的操作都放到⼀个线程中进⾏,⼀个线程对应⼀个事件处理的循环简单来说就是:主属Reactor用于对连接的管理,从属Reactor就是把剩下的工作全部做完
组件使用者需要自行决定是否需要线程池,自己完成实现。
模块划分
我们要实现的是⼀个带有协议⽀持的Reactor模型⾼性能服务器,因此我们可以划分成两个模块:
SERVER模块:实现Reactor模型的TCP服务器
协议模块:对当前的Reactor模型服务器提供应⽤层协议⽀持。
SERVER模块:
分成3个方面的管理:
1.监听连接管理 2.通信连接管理 3.超时连接管理
根据这3个方面的管理我们可以划分为下面多个子模块:
Buffer模块:Buffer模块是⼀个缓冲区模块,⽤于实现通信中用户态的接收缓冲区和发送缓冲区功能Socket模块:Socket模块是对套接字操作封装的⼀个模块,主要实现的socket的各项操作
Channel模块:结合Poller模块,对事件进行监控处理,分别监控可读,可写,错误,任意事件的监控,根据不同的事件调用不同的回调函数进行处理
Poller模块:Poller模块是对epoll进⾏封装的⼀个模块,主要实现epoll的IO事件添加,修改,移除,获取活跃连接功能。
Connection模块:Connection模块是对Buffer模块,Socket模块,Channel模块的⼀个整体封装,实现了对⼀个通信套接字的整体的管理,每⼀个进⾏数据通信的套接字(也就是accept获取到的新连接)都会使⽤Connection进⾏管理
Acceptor模块:Acceptor模块是对Socket模块,Channel模块的⼀个整体封装,实现了对⼀个监听套接字的整体的管理
TimerQueue模块:TimerQueue模块是实现固定时间定时任务的模块,可以理解就是要给定时任务管理器,向定时任务管理器中添加⼀个任务,任务将在固定时间后被执⾏,同时也可以通过刷新定时任务来延迟任务的执⾏。
这个模块主要是对Connection对象的⽣命周期管理,对⾮活跃连接进⾏超时后的释放功能。EventLoop模块:EventLoop模块是对Poller模块,TimerQueue模块,Socket模块的⼀个整体封装,进⾏所有描述符的事件监控,这里为了保证保证整个服务器的线程安全问题,因此要求使⽤者对于Connection的所有操作⼀定要在其对应的EventLoop线程内完成(也就是One Thread One Loop 的思想)
TcpServer模块:这个模块是⼀个整体Tcp服务器模块的封装,内部封装了Acceptor模块,EventLoopThreadPool模块
HTTP协议模块:
Util模块:这个模块是⼀个⼯具模块,主要提供HTTP协议模块所⽤到的⼀些⼯具函数,⽐如url编解码,⽂件读写等
HttpRequest模块:
这个模块是HTTP请求数据模块,⽤于保存HTTP请求数据被解析后的各项请求元素信息HttpResponse模块:
这个模块是HTTP响应数据模块,⽤于业务处理后设置并保存HTTP响应数据的的各项元素信息,最终会被按照HTTP协议响应格式组织成为响应信息发送给客户端HttpContext模块:
这个模块是⼀个HTTP请求接收的上下⽂模块,主要是为了防⽌在⼀次接收的数据中,不是⼀个完整的HTTP请求,则解析过程并未完成,⽆法进⾏完整的请求处理,需要在下次接收到新数据后继续根据上下⽂进⾏解析,最终得到⼀个HttpRequest请求信息对象HttpServer模块:
这个模块是最终给组件使⽤者提供的HTTP服务器模块了,⽤于以简单的接⼝实现HTTP服务器的搭建。
3.项目中的子功能
秒级定时任务实现
在我们的项目中需要使用到超时销毁的功能, 所以我们需要一个时间轮,能让一个连接在一定的时间进行销毁,这就需要我们写一个简单的秒级定时任务
在Linux中给我们提供了定时器:
int timerfd_create(int clockid, int flags);
clockid: CLOCK_REALTIME-系统实时时间,如果修改了系统时间就会出问题; CLOCK_MONOTONIC-从开机到现在的时间是⼀种相对时间(采用这个时间,保证准确性)
flags: 0-默认阻塞属性int timerfd_settime(int fd, int flags, struct itimerspec *new, structitimerspec *old);
fd: timerfd_create返回的⽂件描述符
flags: 0-相对时间, 1-绝对时间;默认设置为0即可.
new: ⽤于设置定时器的新超时时间
old: ⽤于接收原来的超时时间struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds */ }; struct itimerspec { struct timespec it_interval; /* 第⼀次之后的超时间隔时间 */ struct timespec it_value; /* 第⼀次超时时间 */ };
定时器会在每次超时,⾃动给fd中写⼊8字节的数据,表⽰在上⼀次读取数据到当前读取数据期间超时了多少次。这样我们每次读取定时器的时候都知道他超时了多少次,然后我们就执行多少秒的释放连接即可
定时器的使用用例:
#include <iostream> #include <unistd.h> #include <sys/timerfd.h> int main() { //int timerfd_create(int clockid, int flags); int timerfd = timerfd_create(CLOCK_MONOTONIC,0); if(timerfd < 0) { perror("timerfd_create fail"); return -1; } //int timerfd_settime(int fd, int flags,const struct itimerspec *new_value,struct itimerspec *old_value); //设置结构体 struct itimerspec itim; itim.it_value.tv_sec = 1; itim.it_value.tv_nsec = 0; //设置第一次超时时间 itim.it_interval.tv_sec = 1; itim.it_interval.tv_nsec = 0; //第一次超时之后每隔1秒超时一次 int ret = timerfd_settime(timerfd,0,&itim,nullptr); if(ret != 0) { perror("timerfd_settime fail"); return -1; } while(1){ //因为这个也是一个文件描述符,所以可以使用read进行系统调用,来读取到其中的数据 uint64_t times = 0; int ret = read(timerfd,×,8); if(ret < 0){ perror("read fail"); return -1; } std::cout<<times<<std::endl; } return 0; }
时间轮实现
如果每次我们都需要遍历一次连接来进行超时销毁,这样效率是非常低的,这里采用时间轮的思想来提高效率
我们通过Linux提供的定时器,每次将tick指针移动,到指定位置时进行销毁即可
如果我们需要使用到分级定时器,或者时级定时器,那么为了保证不消耗那么多空间,这里采用像Linux中页面的设计那么,采用多级时间轮。
这样就可以大大的节省空间了,3个时间轮就可以把一个的每一个时刻都定位到了,如果还想要精确到年,月,日也是可以通过创建更多的时间轮来实现。
这里主要实现秒级时间轮:
#include <iostream> #include <functional> #include <vector> #include <unistd.h> #include <memory> #include <unordered_map> using OnCloseTime = std::function<void()>; //定时器要执行的任务 using ReleaseTime = std::function<void()>; //删除时间管理对象中weak_ptr的信息 //定时器任务对象 class TimeTask { private: uint32_t _timeout; //超时时间 uint64_t _id; //每个任务的id bool _cancel; //取消定时任务 OnCloseTime _close_cb; //销毁定时任务的回调 ReleaseTime _release_cb; //因为时间轮中会记录一个weak_ptr对象,所以最后需要销毁 public: TimeTask(uint32_t timeout,uint64_t id,const OnCloseTime& close_cb):_timeout(timeout),_id(id),_close_cb(close_cb),_cancel(false){} ~TimeTask(){ if(_cancel== false) _close_cb(); _release_cb(); } uint64_t id() { return _id; } void SetRelease(const ReleaseTime& release_cb) { _release_cb = release_cb; } uint32_t Delay() { return _timeout; } void Cancel() { _cancel = true; } }; //时间轮管理对象 class TimeWheel { private: using WeakTask = std::weak_ptr<TimeTask>; //使用weak_ptr防止在shared_ptr直接对对象的操作 using PtrTask = std::shared_ptr<TimeTask>; //使用shared_ptr保证释放时不到0不销毁 int _capacity; //记录时间轮的大小 int _tick; //记录当前指针指向的时间,指到哪里,销毁哪里 std::vector<std::vector<PtrTask>> _wheel; //时间轮 std::unordered_map<uint64_t,WeakTask> _times; //记录id和weak_ptr之间的映射关系 private: void RemoveTask(uint64_t id) { if(_times.find(id) != _times.end()) { _times.erase(id); } } public: TimeWheel():_tick(0),_capacity(60),_wheel(_capacity){} void AddTask(uint64_t id,uint32_t delay,const OnCloseTime close_cb) { PtrTask pt(new TimeTask(delay,id,close_cb)); //设置ReleaseTask pt->SetRelease(std::bind(&TimeWheel::RemoveTask,this,id)); //把任务添加到数组中 int pos = (_tick + delay) %_capacity; _wheel[pos].push_back(pt); //将id和weakTask映射关联起来 _times[id] = WeakTask(pt); } void CancelTask(uint64_t id) { //通过id找到任务,如果没有直接返回,有的话将标志置为true if(_times.find(id) == _times.end()) return; PtrTask pt = _times[id].lock(); //获得weak_ptr中的shared_ptr pt->Cancel(); } void RefreshTask(uint64_t id) { //创建一个新的智能指针对象,然后添加到数组中 //如果在原数组中没有找到,那么直接返回 if(_times.find(id) == _times.end()) return; std::cout<<"找到了定时任务\n"; PtrTask pt = _times[id].lock(); //获得weak_ptr中的shared_ptr int delay = pt->Delay(); int pos = (_tick + delay) %_capacity; _wheel[pos].push_back(pt); } void RunTask() { _tick = (_tick+1)%_capacity; _wheel[_tick].clear(); } };
后序项目需要使用到的时候直接融合进去即可
正则库的简单使用
正则表达式(regular expression)描述了⼀种字符串匹配的模式(pattern),可以⽤来检查⼀个串是否含有某种⼦串、将匹配的⼦串替换或者从某个串中取出符合某个条件的⼦串等。
正则表达式的使⽤,可以使得HTTP请求的解析更加简单(不需要更多的操作,简化代码编写)注释中有简单的使用方法,这里不一一赘述#include <iostream> #include <string> #include <regex> int main() { // 请求: GET /hello/login?user=xiaoming&passwd=123456 HTTP/1.1\r\n std::string str = "get /hello/login?user=xiaoming&passwd=123456 HTTP/1.1"; //提取请求方法 std::smatch matches; std::regex e("(GET|POST|HEAD|DELETE|PUT) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\r|\r\n)?",std::regex::icase); //匹配请求方法的正则表达式 //(GET|POST|HEAD|DELETE|PUT) 匹配其中任意一个请求方法 //([^?]*) 匹配非?字符 *表示0次或者多次 //\\?(.*) 提取?问号之后的字符,直到遇到后面的空格 //[01] 提起0或者1其中任意一个 //(?:\r|\r\n)? ?:表示匹配某个字符串,但是不提取 后面的?表示匹配0次或者1次 bool ret = regex_match(str,matches,e); if(ret == false) return -1; std::string method = matches[1]; std::transform(method.begin(),method.end(),method.begin(),::toupper); //转换大小写 std::cout<<method<<std::endl; for(int i = 0;i<matches.size();++i) { std::cout<< i <<" : "; std::cout<<matches[i]<<std::endl; } return 0; }
通⽤类型any类型的实现
Connection中需要设置协议处理的上下⽂来控制处理节奏,由于应用层协议非常多,所以我们需要需要使用通用类型来保存不同的数据结构
实现思想:
首先Any类一定不是一个模板类,因为模板类的实例化的时候需要传类型,但是我们可以通过在Any类中设计一个父类和一个子类,其中父类不能是模板类,因为Any类访问父类指针的时候就需要父类的类型,所以我们可以把子类设计成模板类,子类继承父类,并通过重写虚函数来实现多态。当需要保存数据时,通过new一个带模板参数的子类对象来保存数据,然后让Any类中的父类指针指向这个子类对象就可以了。
实现如下:
class Any { private: //父类不是模板类,这样可以保证Any类不是模板 class holer { public: virtual ~holer() {} virtual const std::type_info& type() = 0; //设置成纯虚函数,那么子类想要实例化就必须要重写虚函数 virtual holer* clone() = 0; }; template <class T> class placeholer : public holer { public: placeholer(const T& val):_val(val){} virtual ~placeholer() {} virtual const std::type_info& type() { return typeid(T);} virtual placeholer* clone() { return new placeholer(_val);} public: T _val; }; private: Any& swap(Any& any) { std::swap(_holer,any._holer); return *this; } holer* _holer; public: Any():_holer(nullptr) {} ~Any(){ delete _holer; } Any(const Any& any):_holer(any._holer == nullptr?nullptr:any._holer->clone()){} template <class T> Any(const T& val):_holer(new placeholer<T>(val)) {} template<class T> T* get() { assert(typeid(T) == _holer->type()); return &((placeholer<T>*)_holer)->_val; } Any& operator=(Any& any) { Any(any).swap(*this); return *this; } template<class T> Any& operator=(const T& val) { Any(val).swap(*this); return *this; } };
4.SERVER服务器实现
日志宏的封装
在项目中为了方便调试,我们可以通过日志打印的方式来高效的确定和自己预期不相同的地方,关于日志宏在上一个五子棋项目中已经有比较详细的解析,主要分成以下几步:
1.通过time函数获取时间
2.通过localtime来获取具体的时间,即年月日时分秒
3.通过strftime将时间以格式化数据存放到缓冲区中
4.通过打印的方式打印出来,同时还要加上文件以及行号,线程等
#define INF 0 #define DBG 1 #define ERR 2 #define DEFAULT_LOG_LEVEL DBG #define LOG(level, format, ...) {\ if (level >= DEFAULT_LOG_LEVEL) {\ time_t t = time(NULL);\ struct tm *m = localtime(&t);\ char ts[32] = {0};\ strftime(ts, 31, "%H:%M:%S", m);\ fprintf(stdout, "[%p %s %s:%d] " format "\n", (void*)pthread_self(), ts, __FILE__, __LINE__, ##__VA_ARGS__);\ }\ } #define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__); #define DLOG(format, ...) LOG(DBG, format, ##__VA_ARGS__); #define ELOG(format, ...) LOG(ERR, format, ##__VA_ARGS__);
缓冲区Buffer类实现
Buffer类⽤于实现用户态缓冲区,提供数据缓冲,取出等功能,后序模块中的发送数据其实是像缓冲区中写入,而读取数据则是从缓冲区中取走数据。
模块模型:
成员设计:
std::vector<char> _buffer; // 缓冲区 uint64_t _read_idx; // 读偏移 uint64_t _write_idx; // 写偏移
模块主要功能:
1.获取当前写位置地址;
char *WritePosition() { return Begin() + _write_idx; }
2.确保可写空间足够;
uint64_t ReadAbleSize() { return _write_idx - _read_idx; }
(如果不够就先移动数据,因为前面和后面都是有空余空间的,如果还不够那就直接扩容)
void EnsureWriteSpace(uint64_t len)
3.获取末尾空闲空间大小; (总空间-写偏移)
uint64_t TailIdleSpace() { return _buffer.size() - _write_idx; }
4.获取前面的空闲空间大小;(读偏移)
uint64_t HeadIdleSpace() { return _read_idx; }
5.将写位置向后移动指定长度;(这里就是写入数据之后把可写下标移动)
void MoveWriteOffSet(uint64_t len)
6.获取当前读位置地址;
char *ReadPosition() { return Begin() + _read_idx; }
7.获取可读数据大小;(可读到可写的区间)
uint64_t ReadAbleSize() { return _write_idx - _read_idx; }
8.将读位置向后移动到指定长度;(处理完数据之后移动)
void MoveReadOffSet(uint64_t len)
9.清理缓冲区;(把读写偏移放到0即可)
void Clear()
由于当前模块实现并不难,这里只有一个函数需要注意一下:
如果确保可写空间足够:
扩容策略:
这样就可以高效的使用内存了,具体代码如下:
// 确保可写空间足够(头部和尾部的空间够则移动数据,不够则扩容) void EnsureWriteSpace(uint64_t len) { if (len <= TailIdleSpace()) return; else if (len <= TailIdleSpace() + HeadIdleSpace()) { // 将数据拷贝到最前面 int sz = ReadAbleSize(); std::copy(ReadPosition(), ReadPosition() + sz, Begin()); // 设置读写偏移 _read_idx = 0; _write_idx = sz; } else { // 扩容 _buffer.resize(_write_idx + len); } }
其他部分的缓冲区模块实现起来比较简单,后面会有源码
套接字Socket实现
在使用网络通信中,我们需要Socket的接口,为了方便使用,所以统一封装了一个套接字类
成员设计:
int _sockfd; // 套接字
主要接口:
1.创建套接字;
bool Create() // int socket(int domain, int type, int protocol);
2.绑定地址信息;
bool Bind(const std::string &ip, uint16_t port) // int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
3.开始监听;
bool Listen(int backlog = MAX_LISTEN) // int listen(int sockfd, int backlog);
4.向服务器发起连接;
bool Connect(const std::string &ip, uint16_t port) // int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
5.获取新连接;
int Accept() // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
6.接收数据;
ssize_t Recv(void *buf, size_t len, int flag = 0) // ssize_t recv(int sockfd, void *buf, size_t len, int flags);
7.发送数据;
ssize_t Send(const void *buf, size_t len, int flag = 0) // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
8.关闭套接字;
void Close()
9.创建一个服务端连接;(可以通过该接口快速创建一个服务端套接字)
bool CreateSerber(uint16_t port, const std::string &ip = "0.0.0.0", bool flag = false) // 1.创建套接字 2.绑定地址 3.监听 4.设置地址重用 5.设置非阻塞
10.创建一个客户端连接;(可以通过该接口快速创建一个客户端套接字)
bool CreateClient(uint16_t port, const std::string &ip) // 1.创建套接字 2.连接
11.开启地址端口重用; (如果是服务器先关闭,为了快速重启服务器可以选择)
void ReuseAddr() //通过使用该函数来实现: //int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
12.设置非阻塞; (给套接字设置,因为我们使用的epoll是需要设置套接字非阻塞的)
void SetNonBlock() // int fcntl(int fd, int cmd, ... /* arg */ );
事件管理Channel模块实现
结合EventLoop模块,通过对描述符的监控,如果某一个描述符事件就绪,那么我们只需要调用对应的回调函数即可
成员设计:
1.需要对应监控的描述符
2.EventLoop模块,进行监控
3.用一个变量来记录监控事件(本质是位图,哪一个事件需要监控哪一个bit就置为1)
4.用一个变量记录就绪事件
5.各种回调函数
int _fd; //监控的描述符 EventLoop* _loop; uint32_t _event; //监控事件 uint32_t _revent; //已经发生的事件 //各种回调函数 using EventCallback = std::function<void()>; EventCallback _read_callback; //读事件回调 EventCallback _write_callback; //写事件回调 EventCallback _error_callback; //错误事件回调 EventCallback _close_callback; //关闭事件回调 EventCallback _event_callback; //任意事件回调
主要接口:
1.设置各种回调函数,以及返回描述符,设置事件等
void SetReadCallback(const EventCallback& cb) { _read_callback = cb; } void SetWriteCallback(const EventCallback& cb) { _write_callback = cb; } void SetErrorCallback(const EventCallback& cb) { _error_callback = cb; } void SetCloseCallback(const EventCallback& cb) { _close_callback = cb; } void SetEventCallback(const EventCallback& cb) { _event_callback = cb; } int Fd() { return _fd; } uint32_t Events() { return _event; } void SetRevent(uint32_t revent) { _revent = revent; }
2.当前是否监控了可读
bool ReadAble() { return _event & EPOLLIN;}
3.当前是否监控了可写
bool WriteAble() { return _event & EPOLLOUT;}
4.启动可读事件监控
void SetRead() { _event |= EPOLLIN; Update();}
5.启动可写事件监控
void SetWrite() { _event |= EPOLLOUT; Update();}
6.关闭可读事件监控
void CloseRead() { _event &= ~EPOLLIN ; Update();}
7.关闭可写事件监控
void CloseWrite() { _event &= ~EPOLLOUT; Update();}
8.关闭全部事件监控
void CloseEvent() { _event = 0; Update();}
9.移除监控(因为这里的移除是通过EventLoop模块进行移除,所以需要在类外实现)
10.更新事件(和移除监控一样)
void Remove(); void Update();
11.处理任意事件
void HandleEvent()
其他的实现比较简单,这里处理任意事件的逻辑应该如下:
这里处理任意事件的本质就是刷新活跃度,因为后面实现每一个任务的时候是把任务压入到任务池当中,所以这里可以直接执行任务,而不需要刷新活跃度,等到事件处理完成之后再刷新活跃度,这样就可以防止一个事件可能会处理的事件很长,但是却提前刷新了活跃度导致后序结果有问题。
具体实现如下:
void HandleEvent() { //这里因为是把销毁任务压入到任务池中执行,所以,这里可以直接执行任务而不需要先刷新活跃度 //满足条件,都会触发的 if((_revent & EPOLLIN) || (_revent & EPOLLRDHUP) || (_revent & EPOLLPRI)) { if(_read_callback) _read_callback(); } //有可能释放连接的操作,一次只能处理一个 if(_revent & EPOLLOUT) { if(_write_callback) _write_callback(); } else if(_revent & EPOLLERR) { if(_error_callback) _error_callback(); } else if(_revent & EPOLLHUP) { if(_close_callback) _close_callback(); } //所有事件处理过都需要刷新活跃度 if(_event_callback) _event_callback(); }
描述符事件监控Poller模块实现
通过epoll来对描述符的IO事件监控
成员设计:
1.需要一个epoll操作句柄
2.拥有一个struct epoll_event 结构数组,监控保存所有活跃事件
3.通过hash表管理描述符以及Channel对象的管理
int _epfd; struct epoll_event _evs[MAX_EVENTSIZE]; std::unordered_map<int,Channel*> _channels; //描述符和channel的映射关系
接口设计:
1.构造函数创建一个epoll模型
Poller() { //创建一个epoll模型 _epfd = epoll_create(MAX_EVENTSIZE); if(_epfd < 0) { ELOG("EPOLL_CREATE FAILED:%s",strerror(errno)); abort(); //退出程序 } }
2.添加或者修改监控事件(在channels中找不到就添加,找到就修改)
void UpdateEvent(Channel* channel)
3.移除监控事件 (删除hash映射关系,同时移除epoll监控)
void RemoveEvent(Channel* channel)
4.开始监控,返回活跃连接
void Poll(std::vector<Channel*>* active) //int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
5.对epoll的操作 (上面函数有对epoll的操作统一使用这个接口,设计成私有函数)
void Update(Channel* channel,int op) // int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
6.判断一个事件是否被监控 (设计为私有函数)
bool HasChannel(Channel* channel)
开始监控函数需要我们注意应该有以下流程:
通过epoll_wait监控得到的事件,首先我们需要通过Channel来设置事件就绪,然后再返回活跃连接给上层进行处理
void Poll(std::vector<Channel*>* active) { //int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); int nfds = epoll_wait(_epfd,_evs,MAX_EVENTSIZE,-1); if(nfds < 0) { //这里有可能是信号打断 if(errno == EINTR) { return; } //其他原因 ELOG("EPOLL_WAIT FAILED! %s",strerror(errno)); abort(); //退出程序 } for(int i = 0;i<nfds;++i) { auto it = _channels.find(_evs[i].data.fd); assert(it != _channels.end()); //向channel设置就绪事件 it->second->SetRevent(_evs[i].events); //向外输出活跃事件的channel active->push_back(it->second); } }
定时任务管理TimerWheel类实现
在前面我们已经实现了定时任务以及时间轮了,这里我们只需要把他们融合到一起即可
这里主要说明两个回调函数的含义:
using OnCloseTime = std::function<void()>; //定时器要执行的任务 using ReleaseTime = std::function<void()>; //删除时间管理对象中weak_ptr的信息
其中OnCloseTime的回调就是每一个任务执行的时候销毁定时任务,也就是把智能指针中的引用计数--就可,如果减到0,就销毁这个任务;
ReleaseTime回调就是当引用计数减到0的时候,需要销毁这个定时任务的时候,同时我们需要把hash表中的id和WeakTask去除关联。
这里对TimeWheel模块进行解析:
成员设计:
1.需要一个数组来充当时间轮
2.我们需要使用到shared_ptr,同时我们每次刷新活跃度的时候都需要创建一个shared_ptr所以我们需要一个weak_ptr,并保存起来
3.记录id和weak_ptr的映射关闭,这样我们可以随时通过id来创建一个shared_ptr
4.因为我们添加,取消,刷新任务的时候,我们需要在线程中执行,所以我们需要EventLoop模块指针
5.我们需要对定时器事件进行管理,需要管理指针
成员如下:
using WeakTask = std::weak_ptr<TimeTask>; //使用weak_ptr防止在shared_ptr直接对对象的操作 using PtrTask = std::shared_ptr<TimeTask>; //使用shared_ptr保证释放时不到0不销毁 int _capacity; //记录时间轮的大小 int _tick; //记录当前指针指向的时间,指到哪里,销毁哪里 std::vector<std::vector<PtrTask>> _wheel; //时间轮 std::unordered_map<uint64_t,WeakTask> _times; //记录id和weak_ptr之间的映射关系 int _timerfd; //定时器描述符 EventLoop* _loop; std::unique_ptr<Channel> _timer_channel; //用于定时器事件管理
功能:
1.添加任务;2.删除任务;3.刷新任务
因为这些接口需要在各自的线程中执行,所以我们需要添加到线程中,这样就不需要考虑线程安全问题了。
//定时器任务对象 class TimeTask { private: uint32_t _timeout; //超时时间 uint64_t _id; //每个任务的id bool _cancel; //取消定时任务 OnCloseTime _close_cb; //销毁定时任务的回调 ReleaseTime _release_cb; //因为时间轮中会记录一个weak_ptr对象,所以最后需要销毁 public: TimeTask(uint32_t timeout,uint64_t id,const OnCloseTime& close_cb):_timeout(timeout),_id(id),_close_cb(close_cb),_cancel(false){} ~TimeTask(){ if(_cancel== false) _close_cb(); _release_cb(); } uint64_t id() { return _id; } void SetRelease(const ReleaseTime& release_cb) { _release_cb = release_cb; } uint32_t Delay() { return _timeout; } void Cancel() { _cancel = true; } }; //时间轮管理对象 class TimeWheel { private: using WeakTask = std::weak_ptr<TimeTask>; //使用weak_ptr防止在shared_ptr直接对对象的操作 using PtrTask = std::shared_ptr<TimeTask>; //使用shared_ptr保证释放时不到0不销毁 int _capacity; //记录时间轮的大小 int _tick; //记录当前指针指向的时间,指到哪里,销毁哪里 std::vector<std::vector<PtrTask>> _wheel; //时间轮 std::unordered_map<uint64_t,WeakTask> _times; //记录id和weak_ptr之间的映射关系 int _timerfd; //定时器描述符 EventLoop* _loop; std::unique_ptr<Channel> _timer_channel; //用于定时器事件管理 private: void RemoveTask(uint64_t id) { auto it = _times.find(id); if(it != _times.end()) { _times.erase(it); } } static int CreateTimerFd() { //int timerfd_create(int clockid, int flags); int timerfd = timerfd_create(CLOCK_MONOTONIC,0); if(timerfd < 0) { ELOG("timerfd_create fail"); abort(); } //int timerfd_settime(int fd, int flags,const struct itimerspec *new_value,struct itimerspec *old_value); //设置结构体 struct itimerspec itim; itim.it_value.tv_sec = 1; itim.it_value.tv_nsec = 0; //设置第一次超时时间 itim.it_interval.tv_sec = 1; itim.it_interval.tv_nsec = 0; //第一次超时之后每隔1秒超时一次 timerfd_settime(timerfd,0,&itim,nullptr); return timerfd; } int ReadTimerFd() { uint64_t times; int ret = read(_timerfd,×,8); if(ret < 0){ ELOG("READTIMERFD FAILED!"); abort(); } return times; } void RunTask() { _tick = (_tick+1)%_capacity; _wheel[_tick].clear(); } void Ontime() { //读取timerfd中内容,根据实时的超时次数执行任务,这里防止服务器因为处理繁忙而导致这里只进行了一次的的刷新,必须要刷新够次数 int times = ReadTimerFd(); for(int i = 0;i<times;++i) { RunTask(); } } void AddTaskInLoop(uint64_t id,uint32_t delay,const OnCloseTime close_cb) { PtrTask pt(new TimeTask(delay,id,close_cb)); //设置ReleaseTask pt->SetRelease(std::bind(&TimeWheel::RemoveTask,this,id)); //把任务添加到数组中 int pos = (_tick + delay) %_capacity; _wheel[pos].push_back(pt); //将id和weakTask映射关联起来 _times[id] = WeakTask(pt); } void CancelTaskInLoop(uint64_t id) { //通过id找到任务,如果没有直接返回,有的话将标志置为true auto it = _times.find(id); if(it == _times.end()) return; PtrTask pt = it->second.lock(); //获得weak_ptr中的shared_ptr if(pt) pt->Cancel(); } void RefreshTaskInLoop(uint64_t id) { //创建一个新的智能指针对象,然后添加到数组中 //如果在原数组中没有找到,那么直接返回 auto it = _times.find(id); if(it == _times.end()) return; PtrTask pt = it->second.lock(); //获得weak_ptr中的shared_ptr int delay = pt->Delay(); int pos = (_tick + delay) %_capacity; _wheel[pos].push_back(pt); } public: TimeWheel(EventLoop* loop):_tick(0),_capacity(60),_wheel(_capacity),_timerfd(CreateTimerFd()) ,_loop(loop),_timer_channel(new Channel(_loop,_timerfd)) { //设置读回调,并启动读监控 _timer_channel->SetReadCallback(std::bind(&TimeWheel::Ontime,this)); _timer_channel->SetRead(); } //因为当前类中有使用到数据结构,为了保证线程安全而又不用加锁的方式来提高效率,那么我们让其在一个线程中执行 void AddTask(uint64_t id,uint32_t delay,const OnCloseTime close_cb); void CancelTask(uint64_t id); void RefreshTask(uint64_t id); //这个接口存在线程安全问题,只能在EventLoop模块中使用 bool HasTimer(uint64_t id) { auto it = _times.find(id); if(it == _times.end()) { return false; } return true; } };
EventLoop线程池类实现
这个模块设计思想:
1.在线程中对描述符进行事件监控
2.有描述符就绪则对描述符进行事件处理(要保证操作都在线程中执行,保证线程安全)
3.所有事件处理完成之后再讲任务队列中的任务一一执行(前面的处理实际上是把任务压入到任务队列,然后在线程中执行)
通过下图来理解:
这样就可以保证线程安全了,因为只需要提供一把锁就可以保证,就是对task保护,线程来取出数据过程中是有可能有线程安全问题的。
类成员设计:
1.要进行事件监控 ->Poller模块
2.执行任务队列中的任务 -> 一个线程安全的任务队列
3.添加任务的时候需要定时器,需要一个事件轮
4.需要有一个事件通知的描述符,来唤醒事件监控的阻塞
类功能:
1.需要创建一个描述符就行事件通知,来唤醒事件监控的阻塞
2.该类最主要的函数就是start函数,在这个函数中,通过Poller监控的就绪事件,然后分别调用对应的回调函数对事件进行处理,压入到任务队列中,然后再将任务队列中的任务放到线程中一一执行
class EventLoop { private: using Func = std::function<void()>; std::thread::id _thread_id; //线程ID Poller _poller; int _event_fd; std::unique_ptr<Channel> _event_channel; //通过channel来管理eventfd std::vector<Func> _tasks; //任务队列 std::mutex _mutex; //保证任务队列的线程安全 TimeWheel _time_wheel; //时间轮 private: static int CreateEventfd() { int efd = eventfd(0,EFD_CLOEXEC | EFD_NONBLOCK); if(efd < 0) { ELOG("eventfd failed!"); abort(); //退出程序 } return efd; } void ReadEventFd() { uint64_t res = 0; int ret = read(_event_fd,&res,sizeof(res)); if(ret < 0) { //信号打断或者读阻塞 if(errno == EINTR || errno == EAGAIN) { return; } ELOG("READEVENTFD FAILED!"); abort(); } } void WeakUpEventFd() { uint64_t val = 1; int ret = write(_event_fd,&val,sizeof(val)); if(ret < 0) { if(errno == EINTR) { return; } ELOG("WEAKUPEVENTFD FAILED!"); abort(); } } //执行任务队列中的所有任务 void RunAllTask() { std::vector<Func> functor; { std::unique_lock<std::mutex> lock(_mutex); _tasks.swap(functor); } //执行任务 for(auto& f:functor) f(); } public: EventLoop():_thread_id(std::this_thread::get_id()),_event_fd(CreateEventfd()),_event_channel(new Channel(this,_event_fd)) ,_time_wheel(this) { //给eventChannel设置回调函数 _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd,this)); //启动读事件监控 _event_channel->SetRead(); } //如果要执行的任务在当前线程那就直接执行,不在就压入任务队列 void RunInLoop(const Func& cb) { if(IsInloop()) cb(); else QueueInLoop(cb); } //断言一个线程是否在当前线程中 void AssertInLoop() { assert(_thread_id == std::this_thread::get_id()); } //压入任务队列 void QueueInLoop(const Func& cb) { { std::unique_lock<std::mutex> lock(_mutex); _tasks.push_back(cb); } //唤醒有可能因为事件还没有就绪的阻塞线程 WeakUpEventFd(); } //判断当前线程是否是EventLoop线程 bool IsInloop() { return _thread_id == std::this_thread::get_id();} //添加/更新监控事件 void UpdateEvent(Channel* channel) { return _poller.UpdateEvent(channel);} //移除监控事件 void RemoveEvent(Channel* channel) { return _poller.RemoveEvent(channel);} //事件监控 ->就绪事件处理 -> 执行任务 void Start() { while(1) { std::vector<Channel*> actives; _poller.Poll(&actives); //就绪事件处理 for(auto& a:actives) { a->HandleEvent(); } //执行任务 RunAllTask(); } } void TimerAdd(uint64_t id,uint32_t delay,const OnCloseTime close_cb) { _time_wheel.AddTask(id,delay,close_cb); } void TimerRefresh(uint64_t id) { _time_wheel.RefreshTask(id); } void TimerCancel(uint64_t id) { _time_wheel.CancelTask(id); } bool HasTimer(uint64_t id) { return _time_wheel.HasTimer(id); } };
他们之间的模块关系图:
LoopThread模块实现
这个模块的意义:因为EventLoop模块在实例化对象的时候必须是在线程内部,如果我们创建了多个EventLoop对象,又同时创建多个线程,将各个线程id重新给EventLoop进行设置,那么在构造EventLoop对象到设置新的thread_id期间是不可控的。
我们必须先创建线程,然后在线程的入口函数中进行EventLoop对象的实例化
类成员设计:
1.创建一个线程
2.为了保证先实例了loop之后外界才能获取到,所以我们需要使用到条件变量以及互斥锁
类功能:
1.我们需要创建一个线程,绑定线程入口函数,保证了先创建线程再在线程中实例化EventLoop,这就是One Thread One Loop思想
2.提供给外部获得到实例化的EventLoop对象(需要使用条件变量控制)
class LoopThread { private: //实现获取loop和构造函数的同步关系,保证先实例化了loop之后才能获取 std::mutex _mutex; std::condition_variable _cond; EventLoop* _loop; std::thread _thread; //一个线程对应一个loop private: void ThreadEntry() { //1.创建loop 2.通过条件变量来唤醒等待线程 3.运行 EventLoop loop; //这里的临时变量生命周期跟随LoopThread { std::unique_lock<std::mutex> lock(_mutex); _cond.notify_all(); _loop = &loop; } _loop->Start(); } public: LoopThread():_loop(nullptr),_thread(std::bind(&LoopThread::ThreadEntry,this)){} EventLoop* GetLoop() { EventLoop* loop; { std::unique_lock<std::mutex> lock(_mutex); //通过条件变量保证同步 _cond.wait(lock,[&](){ return _loop != nullptr; }); loop = _loop; } return loop; } };
LoopThreadPool线程池模块实现
对所有的LoopThread进行管理分配
类成员设计:
1.用户可以根据需求来创建线程的数量2.我们这里使用RR轮转的方式进行分配
3.根据主从Reactor模型,首先我们需要一个主Reactor是一直进行工作的
4.全部的线程都需要进行管理,使用数组进行管理
类功能:
1.根据用户需要的线程数量来创建线程
2.根据RR轮转来给用户提供从属EventLoop
class LoopThreadPool { private: int _thread_count; //创建的LoopThread数量 int _next_idx; //采用RR轮转的方式进行分配 EventLoop* _base_loop; //主EventLoop跟随主线程 std::vector<LoopThread*> _threads; //管理全部的线程 std::vector<EventLoop*> _loops; //管理从属EventLoop public: LoopThreadPool(EventLoop* loop):_base_loop(loop),_thread_count(0),_next_idx(0){} void SetThreadCount(int count) { _thread_count = count; } //根据数量来创建出对应的LoopThread void Create() { if(_thread_count>0) { _threads.resize(_thread_count); _loops.resize(_thread_count); for(int i = 0;i<_thread_count;++i) { _threads[i] = new LoopThread(); _loops[i] = _threads[i]->GetLoop(); } } } EventLoop* NextLoop() { if(_thread_count == 0) return _base_loop; _next_idx = (_next_idx + 1)% _thread_count; return _loops[_next_idx]; } };
通信连接管理Connection模块实现
Connection模块是Server模块中最重要的模块,存在的意义:对连接进行管理,所有的操作都是通过这个模块完成的
类成员设计(管理方式):
1.对套接字的管理,能进行套接字的各种操作2.连接事件的管理,如可读,可写,错误,挂断,任意事件
3.缓冲区的管理,通过socket对数据进行接收发送
4.协议上下文的管理,记录数据处理的过程,即使用户没有一次性把数据全部发送过来,但是 也可以保存当前处理的阶段,方便下次处理
5.回调函数的管理,各种情况应该如何处理交给用户决定,必须有调用业务处理的回调函数
6.连接状态的管理,不同的连接状态有不同的限制
7.是否启动非活跃连接超时销毁
类功能设计:
1.发送数据:提供发送数据接口,但是这个并不是真正发送接口,而是把数据发送到发送缓冲区,然后启动写事件监控,等待写事件就绪进行发送
2.关闭连接:给用户提供的关闭连接接口,但是并不是实际的关闭,而是先看看输入输出缓冲区中有没有数据等待处理,如果有先处理,然后关闭连接
3.启动非活跃连接超时销毁
4.取消非活跃连接超时销毁
5.协议切换,一个连接接收数据之后如何进行处理,取决于上下文,以及业务处理回调函数
6.这里需要注意的是大多说的处理其实都是在线程中执行的,所以我们都需要通过bind,让其在线程中执行。
class Connection; //DISCONNECTED 断开连接状态 //CONNECTING 连接建立,但是未完成全部工作的过渡态 //CONNECTED 连接建立完成,可以通信状态 //DISCONNECTING 连接待关闭状态,等待处理后序工作之后断开连接 typedef enum {DISCONNECTED,CONNECTING,CONNECTED,DISCONNECTING }ConnectStatus; using PtrConnection = std::shared_ptr<Connection>; class Connection : public std::enable_shared_from_this<Connection> { private: int _conn_id; //连接建立的唯一id //uint64_t _timer_id 这里为了简化使用,直接使用connid来作为timerid int _socketfd; //连接对应的文件描述符 bool _enable_active_release; //是否启动非活跃连接超时销毁 EventLoop* _loop; //连接所关联的线程 Socket _socket; //套接字管理 Channel _channel; //事件管理 ConnectStatus _status; //连接状态 Buffer _in_buffer; //输入缓冲区,从套接字中读取,然后放入到缓冲区中 Buffer _out_buffer; //输出缓冲区,将待发送数据放到输出缓冲区 Any _context; //连接上下文 private: //事件处理回调函数 using ConnectCallback = std::function<void(const PtrConnection&)>; using MessageCallback = std::function<void(const PtrConnection&,Buffer*)>; using ClosedCallback = std::function<void(const PtrConnection&)>; using AnyEventCallback = std::function<void(const PtrConnection&)>; ConnectCallback _connect_callback; MessageCallback _msg_callback; ClosedCallback _closed_callback; AnyEventCallback _event_callback; //组件内关闭回调函数,组件内使用,因为使用智能指针进行Connect的管理,一旦关闭,就应该在管理的地方进行删除 ClosedCallback _server_event_callback; //为了保证线程安全,每一个接口函数都应该放入到一个线程中 void SendInLoop(Buffer& buf) { //这里并不是实际的发送接口,而是把数据放到发送缓冲区中 if(_status == DISCONNECTED) return; _out_buffer.WriteBufferAndPush(buf); //启动写事件监控 if(_channel.WriteAble() == false) _channel.SetWrite(); } void ShutDownInLoop() { //这里也不是实际的关闭操作,而是将待处理的数据处理,待发送的数据发送 _status = DISCONNECTING; //处于待关闭状态 if(_in_buffer.ReadAbleSize() > 0){ if(_msg_callback) _msg_callback(shared_from_this(),&_in_buffer); } if(_out_buffer.ReadAbleSize() > 0) { //启动写事件监控 if(_channel.WriteAble() == false) _channel.SetWrite(); } //关闭连接,不管数据有没有处理完,因为这里的数据可能不完整,不需要处理了 if(_out_buffer.ReadAbleSize() == 0) Release(); } void ReleaseInLoop() { //1.修改连接状态 _status = DISCONNECTED; //2.移除事件监控 _channel.Remove(); //3.关闭描述符 _socket.Close(); //4.取消定时销毁任务 if(_loop->HasTimer(_conn_id)) CancelActiveReleaseInLoop(); //5.调用关闭的回调函数 if(_closed_callback) _closed_callback(shared_from_this()); //组件内调用的关闭函数 if(_server_event_callback) _server_event_callback(shared_from_this()); } void EstablishedInLoop() { //1.修改状态 assert(_status == CONNECTING); _status = CONNECTED; //2.启动读事件监控 _channel.SetRead(); //3.调用连接成功回调函数 if(_connect_callback) _connect_callback(shared_from_this()); } void SetActiveReleaseInloop(int sec) { //1.修改判断标志位 _enable_active_release = true; //2.如果定时任务存在,那就延迟一下 if(_loop->HasTimer(_conn_id)){ return _loop->TimerRefresh(_conn_id); } //3.不存在就添加 _loop->TimerAdd(_conn_id,sec,std::bind(&Connection::Release,this)); } void CancelActiveReleaseInLoop() { _enable_active_release = false; if(_loop->HasTimer(_conn_id)) _loop->TimerCancel(_conn_id); } void UpgradeInLoop(const Any& context,const ConnectCallback& conn,const MessageCallback& msg, const ClosedCallback& closed,const AnyEventCallback& event) { //修改各个类成员变量即可 _context = context; _connect_callback = conn; _msg_callback = msg; _closed_callback = closed; _event_callback = event; } //五个Channel事件回调函数 //将socket数据放到接收缓冲区中,调用message_callback进行消息的读取 void HandlerRead() { //1.把数据放入到接收缓冲区 char buffer[65536]; ssize_t ret = _socket.NonBlockRecv(buffer,65535); if(ret < 0) { //出错了,不能直接关闭,而是调用ShutDownInLoop() return ShutDownInLoop(); } //如果接收到的数据是0就不需要进行消息处理 //2.调用message_callback _in_buffer.WriteAndPush(buffer,ret); if(_in_buffer.ReadAbleSize() > 0) { _msg_callback(shared_from_this(),&_in_buffer); } } //将发送缓冲区的数据发送 void HandlerWrite() { //将发送缓冲区中的数据发送出去 ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPosition(),_out_buffer.ReadAbleSize()); if(ret < 0) { //出错了就看接收缓冲区有没有实际要处理的数据,有的话处理完就实际关闭连接 if(_in_buffer.ReadAbleSize() > 0) { _msg_callback(shared_from_this(),&_in_buffer); } return Release(); } //记得把写偏移移动 _out_buffer.MoveReadOffSet(ret); if(_out_buffer.ReadAbleSize() == 0) { _channel.CloseWrite(); //关闭写事件监控 //如果是连接待关闭状态则关闭连接 if(_status == DISCONNECTING) { return Release(); } } } //描述符挂断事件处理 void HandlerClose() { if(_in_buffer.ReadAbleSize() > 0) { _msg_callback(shared_from_this(),&_in_buffer); } return Release(); } //描述符错误事件处理 void HandlerError() { HandlerClose(); } //描述符触发任意事件 void HandlerEvent() { //1.判断是否需要刷新活跃度 if(_enable_active_release == true) { _loop->TimerRefresh(_conn_id);} //2.调用组件使用者的任意事件回调 if(_event_callback) { return _event_callback(shared_from_this()); } } public: Connection(int connid,int socketfd,EventLoop* loop) :_conn_id(connid),_socketfd(socketfd),_enable_active_release(false),_loop(loop), _socket(socketfd),_channel(_loop,_socketfd),_status(CONNECTING) { //设置channel回调函数 _channel.SetReadCallback(std::bind(&Connection::HandlerRead,this)); _channel.SetWriteCallback(std::bind(&Connection::HandlerWrite,this)); _channel.SetErrorCallback(std::bind(&Connection::HandlerError,this)); _channel.SetCloseCallback(std::bind(&Connection::HandlerClose,this)); _channel.SetEventCallback(std::bind(&Connection::HandlerEvent,this)); } ~Connection() { DLOG("CONNECTION RELEASE :%p",this); } //成员变量的接口 int Fd() { return _socketfd; } //获取连接Id int Id() { return _conn_id; } //设置上下文 void SetContext(const Any& context) { _context = context; } //获取上下文信息 Any* Context() { return &_context; } //判断当前是否是连接状态 bool IsConnected() { return _status == CONNECTED; } //设置各种回调函数 void SetConnectCallback(const ConnectCallback& cb) { _connect_callback = cb; } void SetMessageCallback(const MessageCallback& cb) { _msg_callback = cb; } void SetClosedCallback(const ClosedCallback& cb) { _closed_callback = cb; } void SetEventCallback(const AnyEventCallback& cb) { _event_callback = cb; } void SetSvrCallback(const ClosedCallback& cb) { _server_event_callback = cb; } //发送数据,将数据放到发送缓冲区,启动写事件监控 void Send(const char* data,size_t len) { //这里外面传过来的是一个临时对象,有可能会销毁,所以保存一份变量保证安全性 Buffer buf; buf.WriteAndPush(data,len); _loop->RunInLoop(std::bind(&Connection::SendInLoop,this,std::move(buf))); } //这是提供给组件使用的关闭操作,但是并不是实际的关闭,需要内部进行处理 void ShutDown() { _loop->RunInLoop(std::bind(&Connection::ShutDownInLoop,this)); } //实际关闭连接的接口,销毁时应该放入任务池中,在执行完任务之后才销毁,否则可能会导致其他任务处理时,连接被释放导致内存访问错误 void Release() { _loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop,this)); } //连接建立成功之后设置channel,启动读事件监控,调用_connect_callback void Established() { _loop->RunInLoop(std::bind(&Connection::EstablishedInLoop,this)); } //启动非活跃连接销毁 void SetActiveRelease(int sec) { _loop->RunInLoop(std::bind(&Connection::SetActiveReleaseInloop,this,sec)); } //取消非活跃连接销毁 void CancelActiveRelease() { _loop->RunInLoop(std::bind(&Connection::CancelActiveReleaseInLoop,this)); } //协议切换 void Upgrade(const Any& context,const ConnectCallback& conn,const MessageCallback& msg, const ClosedCallback& closed,const AnyEventCallback& event) { //因为协议切换是需要放在线程中,并且应该立即执行,否则用户切换协议之前的数据处理就没有意义了 _loop->AssertInLoop(); _loop->RunInLoop(std::bind(&Connection::UpgradeInLoop,this,context,conn,msg,closed,event)); } };
其中模块之间的关系图帮助理解:
监听描述符管理Acceptor类实现
Acceptor模块只进行监听连接的管理,有事件新的连接到来就调用对应的回调函数进行处理即可
类成员设计:
1.创建一个监听套接字
2.需要一个新连接处理的回调函数
类功能:
1.创建一个监听套接字
2.启动读事件监控
3.事件触发,获取新连接
4.调用对应的回调函数
对于新连接如何处理应该是服务器模块来进行管理的
class Acceptor { private: EventLoop* _loop; Socket _socket; Channel _channel; using Accept_callback = std::function<void(int)>; Accept_callback _accept_cb; private: int CreateSocket(int port) { bool ret = _socket.CreateSerber(port); assert(ret); return _socket.Fd(); } void HandlerRead() { int newfd = _socket.Accept(); if(newfd < 0) return; if(_accept_cb) _accept_cb(newfd); } public: //构造函数不能立刻启用可读事件监控,否则这里有可能导致回调函数还没有设置,此时如果立刻有连接到来,会导致newfd没有得到处理,最终资源泄露 Acceptor(EventLoop* loop,int port):_loop(loop),_socket(CreateSocket(port)),_channel(_loop,_socket.Fd()) { _channel.SetReadCallback(std::bind(&Acceptor::HandlerRead,this)); } void SetAcceptCallback(const Accept_callback& cb) { _accept_cb = cb; } //开始监听,启动读事件监控 void Listen() { _channel.SetRead(); } };
模块之前的关系图:
TcpServer模块
对所有模块的整合,通过该模块实例化对象,可以非常简单的完成一个服务器搭建
类成员设计(管理):
1.Acceptor对象,创建一个监听套接字
2.EventLoop对象,baseloop对象,实现对监听套接字的事件监控
3.通过hash表来实现对新连接的管理
4.LoopThreadPool对象,创建loop线程池,对新连接进行事件监控以及处理
类功能:
1.设置从属线程池数量
2.启动服务器
3.设置各种回调函数(连接建立完成,消息,关闭,任意)
4.是否启动非活跃连接超时销毁功能
5.添加定时任务功能
class TcpServer { private: uint64_t _next_id; int _timeout; //销毁时间 bool _enable_active_release; //是否其实非活跃超时销毁 EventLoop _base_loop; //主线程 Acceptor _acceptor; //监听套接字管理的对象 LoopThreadPool _pool; //从属线程 std::unordered_map<uint64_t,PtrConnection> _conns; //管理连接 using ConnectCallback = std::function<void(const PtrConnection&)>; using MessageCallback = std::function<void(const PtrConnection&,Buffer*)>; using ClosedCallback = std::function<void(const PtrConnection&)>; using AnyEventCallback = std::function<void(const PtrConnection&)>; using Functor = std::function<void()>; ConnectCallback _connect_callback; MessageCallback _msg_callback; ClosedCallback _closed_callback; AnyEventCallback _event_callback; private: //新连接构造一个connection管理 void NewConnection(int newfd) { ++_next_id; PtrConnection conn(new Connection(_next_id,newfd,_pool.NextLoop())); conn->SetConnectCallback(_connect_callback); conn->SetMessageCallback(_msg_callback); conn->SetClosedCallback(_closed_callback); conn->SetEventCallback(_event_callback); conn->SetSvrCallback(std::bind(&TcpServer::RemoveConnection,this,std::placeholders::_1)); if(_enable_active_release) conn->SetActiveRelease(_timeout); conn->Established(); _conns.insert(std::make_pair(_next_id,conn)); } //关闭时调用,去除管理Connection void RemoveConnection(const PtrConnection& conn) { _base_loop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop,this,conn)); } void RemoveConnectionInLoop(const PtrConnection& conn) { int id = conn->Id(); _conns.erase(id); } void RunAfterInLoop(const Functor& func,int delay) { _next_id++; _base_loop.TimerAdd(_next_id,delay,func); } public: TcpServer(int port):_next_id(0),_enable_active_release(false),_acceptor(&_base_loop,port),_pool(&_base_loop) { _acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection,this,std::placeholders::_1)); _acceptor.Listen(); //将套接字挂到loop中 } void SetConnectCallback(const ConnectCallback& cb) { _connect_callback = cb; } void SetMessageCallback(const MessageCallback& cb) { _msg_callback = cb; } void SetClosedCallback(const ClosedCallback& cb) { _closed_callback = cb; } void SetEventCallback(const AnyEventCallback& cb) { _event_callback = cb; } //设置线程数量 void SetThreadCount(int count) { _pool.SetThreadCount(count); } //设置非活跃超时销毁 void SetActiveRelease(int timeout) { _timeout = timeout; _enable_active_release = true; } //添加一个任务 void RunAfter(const Functor& func,int delay) { _base_loop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop,this,func,delay)); } void Start() { _pool.Create(); _base_loop.Start();} };
HTTP协议模块实现
Util⼯具类实现
工具类中主要包含我们后面可能使用到的函数,所以统一封装成static成员
成员函数如下:
1.对字符串进行分割 ->对url进行解析的时候需要根据特殊字符进行分割
2.读取文件中的内容 ->当用户需要获取静态资源页面的时候
3.向文件中写入数据 ->当用户需要传输大文件的时候,需要把外面的文件写入到服务器中
4.url编码 -> 在网络传输过程中需要对特殊字符进行编码
5.url解码 -> 有编码就一定要在特定时候进行解码
6.通过响应状态码获取响应信息 -> 在进行响应的时候需要响应状态码,已经对应的响应描述
7.通过文件名获取mime ->在前端展示的时候需要知道这是一个什么类型的文件,响应头部中
需要进行设置
(6.7需要提前准备好hash表把所以的kv都放入到hash中,方便查找,同时应该在全局初始化,这样的话就可以防止每一次都开辟空间,造成资源的浪费)
8.判断一个文件是否是目录 ->在文件上传的时候需要保证是文件而不是目录
9.判断一个文件是否是目录 ->通过Linux中提供的stat函数进行判断
10.判断资源路径的合法性 ->访问的资源如果在web根目录的上级就会有问题,所以必须保证资源合法性 (通过访问的层数进行判断,如果访问的层数小于0,那么就是非法)
//为了提高效率,把映射关系定义成全局 std::unordered_map<int, std::string> _statu_msg = { {100, "Continue"}, {101, "Switching Protocol"}, {102, "Processing"}, {103, "Early Hints"}, {200, "OK"}, {201, "Created"}, {202, "Accepted"}, {203, "Non-Authoritative Information"}, {204, "No Content"}, {205, "Reset Content"}, {206, "Partial Content"}, {207, "Multi-Status"}, {208, "Already Reported"}, {226, "IM Used"}, {300, "Multiple Choice"}, {301, "Moved Permanently"}, {302, "Found"}, {303, "See Other"}, {304, "Not Modified"}, {305, "Use Proxy"}, {306, "unused"}, {307, "Temporary Redirect"}, {308, "Permanent Redirect"}, {400, "Bad Request"}, {401, "Unauthorized"}, {402, "Payment Required"}, {403, "Forbidden"}, {404, "Not Found"}, {405, "Method Not Allowed"}, {406, "Not Acceptable"}, {407, "Proxy Authentication Required"}, {408, "Request Timeout"}, {409, "Conflict"}, {410, "Gone"}, {411, "Length Required"}, {412, "Precondition Failed"}, {413, "Payload Too Large"}, {414, "URI Too Long"}, {415, "Unsupported Media Type"}, {416, "Range Not Satisfiable"}, {417, "Expectation Failed"}, {418, "I'm a teapot"}, {421, "Misdirected Request"}, {422, "Unprocessable Entity"}, {423, "Locked"}, {424, "Failed Dependency"}, {425, "Too Early"}, {426, "Upgrade Required"}, {428, "Precondition Required"}, {429, "Too Many Requests"}, {431, "Request Header Fields Too Large"}, {451, "Unavailable For Legal Reasons"}, {501, "Not Implemented"}, {502, "Bad Gateway"}, {503, "Service Unavailable"}, {504, "Gateway Timeout"}, {505, "HTTP Version Not Supported"}, {506, "Variant Also Negotiates"}, {507, "Insufficient Storage"}, {508, "Loop Detected"}, {510, "Not Extended"}, {511, "Network Authentication Required"} }; std::unordered_map<std::string, std::string> mim_msg = { {".aac", "audio/aac"}, {".abw", "application/x-abiword"}, {".arc", "application/x-freearc"}, {".avi", "video/x-msvideo"}, {".azw", "application/vnd.amazon.ebook"}, {".bin", "application/octet-stream"}, {".bmp", "image/bmp"}, {".bz", "application/x-bzip"}, {".bz2", "application/x-bzip2"}, {".csh", "application/x-csh"}, {".css", "text/css"}, {".csv", "text/csv"}, {".doc", "application/msword"}, {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, {".eot", "application/vnd.ms-fontobject"}, {".epub", "application/epub+zip"}, {".gif", "image/gif"}, {".htm", "text/html"}, {".html", "text/html"}, {".ico", "image/vnd.microsoft.icon"}, {".ics", "text/calendar"}, {".jar", "application/java-archive"}, {".jpeg", "image/jpeg"}, {".jpg", "image/jpeg"}, {".js", "text/javascript"}, {".json", "application/json"}, {".jsonld", "application/ld+json"}, {".mid", "audio/midi"}, {".midi", "audio/x-midi"}, {".mjs", "text/javascript"}, {".mp3", "audio/mpeg"}, {".mpeg", "video/mpeg"}, {".mpkg", "application/vnd.apple.installer+xml"}, {".odp", "application/vnd.oasis.opendocument.presentation"}, {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, {".odt", "application/vnd.oasis.opendocument.text"}, {".oga", "audio/ogg"}, {".ogv", "video/ogg"}, {".ogx", "application/ogg"}, {".otf", "font/otf"}, {".png", "image/png"}, {".pdf", "application/pdf"}, {".ppt", "application/vnd.ms-powerpoint"}, {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, {".rar", "application/x-rar-compressed"}, {".rtf", "application/rtf"}, {".sh", "application/x-sh"}, {".svg", "image/svg+xml"}, {".swf", "application/x-shockwave-flash"}, {".tar", "application/x-tar"}, {".tif", "image/tiff"}, {".tiff", "image/tiff"}, {".ttf", "font/ttf"}, {".txt", "text/plain"}, {".vsd", "application/vnd.visio"}, {".wav", "audio/wav"}, {".weba", "audio/webm"}, {".webm", "video/webm"}, {".webp", "image/webp"}, {".woff", "font/woff"}, {".woff2", "font/woff2"}, {".xhtml", "application/xhtml+xml"}, {".xls", "application/vnd.ms-excel"}, {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, {".xml", "application/xml"}, {".xul", "application/vnd.mozilla.xul+xml"}, {".zip", "application/zip"}, {".3gp", "video/3gpp"}, {".3g2", "video/3gpp2"}, {".7z", "application/x-7z-compressed"} }; class Util { public: //分割字符串 static size_t Spilt(const std::string& src,const std::string& sep,std::vector<std::string>* arr) { //"abc,sdf,ijk" "," int offset = 0; //偏移量 while(offset < src.size()) { int pos = src.find(sep,offset); if(pos == std::string::npos) { //没找到,说明后面的offset后面的字符都是结果 arr->push_back(src.substr(offset,pos-offset)); return arr->size(); } //这里可能有多个重复sep if(offset == pos){ offset = pos + sep.size(); continue; } //收集结果 arr->push_back(src.substr(offset,pos-offset)); offset = pos + sep.size(); } return arr->size(); } //读取文件内容 static bool ReadFile(const std::string& filename,std::string* buf) { std::ifstream ifs(filename,std::ios::binary); if(!ifs.is_open()) { DLOG("OPEN %s FAILED!",filename); return false; } int fsize = 0; //先把文件指针偏移到末尾,获取到文件的大小 ifs.seekg(0,ifs.end); fsize = ifs.tellg(); //回到起始,开始读取文件 ifs.seekg(0,ifs.beg); buf->resize(fsize); //读取文件 ifs.read(&(*buf)[0],fsize); if(!ifs.good()) { DLOG("READ FILE %s FAILED!",filename); ifs.close(); return false; } //记得关闭文件,防止资源泄露 ifs.close(); return true; } //写入文件 static bool WriteFile(const std::string& filename,const std::string& buf) { std::ofstream ofs(filename,std::ios::binary | std::ios::trunc); if(!ofs.is_open()) { DLOG("OPEN %s FAILED!",filename.c_str()); return false; } //向文件写入 ofs.write(buf.c_str(),buf.size()); if(!ofs.good()) { DLOG("WRITEFILE %s FAILED!",filename.c_str()); ofs.close(); return false; } ofs.close(); return true; } //url编码 static std::string UrlEnCode(const std::string& url,bool convert_space_to_plus) { //". - _ ~"以及数字字母字符采用绝对不编码,convert_space_to_plus -> 是否启用空格转+ std::string ret; for(auto ch: url) { if(ch == '.' || ch == '-' || ch == '_' || ch == '~' || isalnum(ch)) { ret += ch; continue; } //空格转+ if(convert_space_to_plus && ch == ' ') { ret += '+'; continue; } //其余都是需要转换的字符 char tmp[4] = {0}; snprintf(tmp,4,"%%%02X",ch); ret += tmp; } return ret; } static char HexToI(char ch) { if(ch >= '0' && ch <= '9') { return ch - '0'; } else if(ch >= 'a' && ch <= 'z') { return ch - 'a' + 10; } else if(ch >= 'A' && ch <= 'Z') { return ch - 'A' + 10; } return -1; //错误 } //url解码 static std::string UrlDeCode(const std::string& url,bool convert_plus_to_space) { std::string ret; for(int i = 0;i<url.size();++i) { //判断convert_plus_to_space条件是否满足 if(convert_plus_to_space && url[i] == '+') { ret += ' '; continue; } //遇到%后面的数,就把第一个数转换成16进制第二个数相加 if(url[i] == '%') { char v1 = HexToI(url[i+1]); char v2 = HexToI(url[i+2]); char res = (v1 << 4) + v2; ret += res; i += 2; continue; } //其他情况直接放入结果集即可 ret += url[i]; } return ret; } //通过响应状态码获得响应信息 static std::string StatuDesc(int statu) { auto it = _statu_msg.find(statu); if (it != _statu_msg.end()) { return it->second; } return "Unknow"; } static std::string GetMime(const std::string& filename) { //a.txt 找到文件后缀 size_t pos = filename.find_last_of('.'); if(pos == std::string::npos) { //没找到就是二进制流数据 return "application/octet-stream"; } std::string tmp = filename.substr(pos); auto it = mim_msg.find(tmp); if(it == mim_msg.end()) { return "application/octet-stream"; } return it->second; } //判断一个文件是否是目录 static bool IsDir(const std::string& filename) { struct stat st; int ret = stat(filename.c_str(),&st); if(ret < 0) { return false; } return S_ISDIR(st.st_mode); } //判断一个文件是否是普通文件 static bool IsRegular(const std::string& filename) { struct stat st; int ret = stat(filename.c_str(),&st); if(ret < 0) { return false; } return S_ISREG(st.st_mode); } //资源路径的有效性判断 static bool IsValPath(const std::string& path) { //根据层数来判断当前路径是否在web根目录下 //"/index.html" "/../index.html" std::vector<std::string> vs; Spilt(path,"/",&vs); int level = 0; for(auto& str:vs) { if(str == "..") { --level; if(level < 0) return false; //任意一层小于0都是有问题的 continue; } ++level; } return true; } };
HttpRequest请求类实现
该模块主要存储HTTP请求信息要素,提供简单的功能性接口
请求要素如下:
HTTP常见header:
Content-Type:数据类型(text/html等)。
Content-Length:正文的长度。
Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
User-Agent:声明用户的操作系统和浏览器的版本信息。
Referer:当前页面是哪个页面跳转过来的。
Location:搭配3XX状态码使用,告诉客户端接下来要去哪里访问。
Cookie:用户在客户端存储少量信息,用于实现会话(session)的功能。类成员设计:
1.请求方法
2.资源路径url
3.协议版本
4.请求正文
5.正则提取字符串 ->我们需要使用使用之前提供过的正则库已经正则表达式来对请求信息进行匹配,从而简化代码的编写
6.头部字段 ->把请求中的头部字段以kv的方式存储在hash表中,方便查询
7.查询字符串 ->在url中可能有查询字符串,同样以kv的方式存储在hash中
类功能性接口:
1.将成员变量设置为公有,这样方便直接访问
2.提供头部字段以及查询字符串的插入以及查询功能
3.获取正文长度
4.判断是否为长连接,如果是短连接就需要处理完之后直接关闭连接(Connection头部字段 Connection: close/keep-alive)
class HttpRequest { public: std::string _method; //请求方法 std::string _path; //资源路径 std::string _version;//协议版本 std::string _body; //请求正文 std::smatch _matches; //正则提取查询字符串 std::unordered_map<std::string,std::string> _headers; //头部字段 std::unordered_map<std::string,std::string> _parame; //查询字符串 public: HttpRequest():_version("HTTP/1.1"){} void ReSet() { _method.clear(); _path.clear(); _version.clear(); _body.clear(); std::smatch tmp; //用于交换清除 _matches.swap(tmp); _headers.clear(); _parame.clear(); } //插入头部字段 void SetHeader(const std::string& key,const std::string& val) { _headers.insert(std::make_pair(key,val)); } //获取指定头部的值 std::string GetHeader(const std::string& key) { auto it = _headers.find(key); if(it != _headers.end()) { return it->second; } return ""; } //查询指定头部的值是否存在 bool HasHeader(const std::string& key) { auto it = _headers.find(key); if(it != _headers.end()) { return true; } return false; } //插入查询字符串 void SetParam(const std::string& key,const std::string& val) { _parame.insert(std::make_pair(key,val)); } //获取指定查询字符串 std::string GetParam(const std::string& key) { auto it = _parame.find(key); if(it != _parame.end()) { return it->second; } return ""; } //判断是否存在某个查询字符串 bool HasParam(const std::string& key) { auto it = _parame.find(key); if(it != _parame.end()) { return true; } return false; } //获取正文长度 size_t Content_Length() { auto it = _headers.find("Content-Length"); if(it == _headers.end()) { return 0; } return std::stol(it->second); } //判断是否为短连接 bool Close() { if(HasHeader("Connection") && GetHeader("Connection") == "keep-alive") { return false; } return true; } };
HttpResponse响应类实现
类功能:存储HTTP响应信息要素,提供简单的功能性接口
响应信息要素:
类成员设计:
1.响应状态 ->我们需要知道当前处理是否有问题,所以需要给用户返回响应状态
2.是否重定向 -> 如果该url需要重定向,那么我们就需要把重定向的url保存起来
3.响应正文
4.头部字段 ->设置头部kv到hash中,方便查询
类功能性接口:
1.为了方便成员的访问,将成员设置成公有
2.头部字段的新增,查询,获取
3.正文的设置
4.重定向的设置
5.长短连接的判断
class HttpResponse { public: int _statu; //响应状态 bool _redirect_flag; //是否重定向 std::string _body; //响应正文 std::string _redirect_url; //重定向url std::unordered_map<std::string,std::string> _headers; //头部字段 public: HttpResponse():_statu(200),_redirect_flag(false){} HttpResponse(int statu):_statu(statu),_redirect_flag(false){} void ReSet() { _statu = 200; _redirect_flag = false; _body.clear(); _redirect_url.clear(); _headers.clear(); } //插入头部字段 void SetHeader(const std::string& key,const std::string& val) { _headers.insert(std::make_pair(key,val)); } //获取指定头部的值 std::string GetHeader(const std::string& key) { auto it = _headers.find(key); if(it != _headers.end()) { return it->second; } return ""; } //查询指定头部的值是否存在 bool HasHeader(const std::string& key) { auto it = _headers.find(key); if(it != _headers.end()) { return true; } return false; } //设置响应正文 void SetContent(const std::string& body,const std::string& type) { _body = body; SetHeader("Content-Length",type); } //设置重定向 void SetRedirect(const std::string& url,int statu = 302) { _statu = statu; _redirect_flag = true; _redirect_url = url; } //判断是否为短连接 bool Close() { if(HasHeader("Connection") && GetHeader("Connection") == "keep-alive") { return false; } return true; } };
HttpContent上下文类实现
该类是处理的Http请求响应的上下文模块,当一个请求没有一次性发送过来的时候,那么处理的时候就不能只处理当次的,而是要保证处理一个完整的请求,所以需要上下文模块来记录上一次处理,以便处理一个完整的请求
类成员设计:
1.我们需要一个响应状态码,因为每一次处理的结果都可能是不一样的响应状态
2.需要记录当前解析到的状态 ,方便下一次解析
3.解析得到的信息需要放到HttpRequest中
类功能:
这里之后一个函数,就是进行解析工作,但是其中分成几个子函数:
1.接收请求行 (从缓冲区中取出数据,并更新解析状态)
2.解析请求行 (通过正则表达式进行处理,并把查询字符串放入到Request)
3.接收头部 (将Buffer中数据进行分割)
4.解析头部 (以kv的形式存储在Request中)
5.接收正文 (通过Content-Length获取正文长度,并根据需求来获取正文)
需要注意的是:
任意一个状态有问题,都需要立即停止剩下的工作,应该那是没有意义的。而且上一个工作没有完成是不能进行下一步进行处理的。typedef enum { RECV_HTTP_ERROR, RECV_HTTP_LINE, RECV_HTTP_HEAD, RECV_HTTP_BODY, RECV_HTTP_OVER, }HttpRecvStatu; #define MAX_LINE 8192 class HttpContent { private: int _resp_statu; //响应状态码 HttpRecvStatu _recv_statu; //当前解析阶段 HttpRequest _request; //已经解析得到的信息 private: bool ParseLine(const std::string& line) { std::smatch matches; std::regex e("(GET|POST|HEAD|DELETE|PUT) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\r|\r\n)?",std::regex::icase); //匹配请求方法的正则表达式 bool ret = regex_match(line,matches,e); if(ret == false) return false; //结果如下: //0 : GET /hello/login?user=xiaoming&passwd=123456 HTTP/1.1 //1 : GET //2 : /hello/login //3 : user=xiaoming&passwd=123456 //4 : HTTP/1.1 _request._method = matches[1]; //这里统一转换为大写,兼容性更强 std::transform(_request._method.begin(),_request._method.end(),_request._method.begin(),::toupper); //这里资源路径需要解码且不需要+转空格 _request._path = Util::UrlDeCode(matches[2],false); _request._version = matches[4]; //这个查询字符串要特殊处理一下 std::string param = matches[3]; std::vector<std::string> vs; Util::Spilt(param,"&",&vs); //通过等号进行继续分割,得到key val for(auto& str:vs) { size_t pos = str.find('='); if(pos == std::string::npos) { //没找到,认为是有问题的,返回错误 _resp_statu = 400; //BAD REQUEST _recv_statu = RECV_HTTP_ERROR; return false; } //这里得到的key和val也是需要进行解码的且需要空格转+ std::string key = Util::UrlDeCode(str.substr(0,pos),true); std::string val = Util::UrlDeCode(str.substr(pos+1),true); _request.SetParam(key,val); } return true; } bool RecvLine(Buffer* buf) { if(_recv_statu != RECV_HTTP_LINE) return false; //接收一行 std::string line = buf->GetLineAndPop(); if(line.size() == 0) { //没有一行,有可能是一行的数据太大了,也可能是不够一行的数据 if(buf->ReadAbleSize() > MAX_LINE) { //{414, "URI Too Long"} _resp_statu = 414; _recv_statu = RECV_HTTP_ERROR; return false; } return true; } if(line.size() > MAX_LINE) { _resp_statu = 414; _recv_statu = RECV_HTTP_ERROR; return false; } bool ret = ParseLine(line); if(ret == false) { return false; } //首行处理完毕,进入头部阶段 _recv_statu = RECV_HTTP_HEAD; return true; } bool RecvHead(Buffer* buf) { if(_recv_statu != RECV_HTTP_HEAD) return false; //这里是有多行数据,而且数据都是以key: val的形式 while(true) { std::string line = buf->GetLineAndPop(); if(line.size() == 0) { //没有一行,有可能是一行的数据太大了,也可能是不够一行的数据 if(buf->ReadAbleSize() > MAX_LINE) { //{414, "URI Too Long"} _resp_statu = 414; _recv_statu = RECV_HTTP_ERROR; return false; } return true; } if(line.size() > MAX_LINE) { _resp_statu = 414; _recv_statu = RECV_HTTP_ERROR; return false; } if(line == "\n" || line == "\r\n") break; //这里说明头部已经解析完成 bool ret = ParseHead(line); if(ret == false) return false; } //头部解析完成,进行正文解析 _recv_statu = RECV_HTTP_BODY; return true; } bool ParseHead(std::string& line) { //去掉换行符 if(line.back() == '\n') line.pop_back(); if(line.back() == '\r') line.pop_back(); //这里都是key: val的形式 size_t pos = line.find(": "); if(pos == std::string::npos) { //解析失败 _resp_statu = 400; _recv_statu = RECV_HTTP_ERROR; return false; } std::string key = line.substr(0,pos); std::string val = line.substr(pos+2); _request.SetHeader(key,val); return true; } bool RecvBody(Buffer* buf) { if(_recv_statu != RECV_HTTP_BODY) return false; //正文数据大小为0,则直接返回即可 size_t content_len = _request.Content_Length(); if(content_len == 0){ //解析完毕,直接返回 _recv_statu = RECV_HTTP_OVER; return true; } //获得剩下应该获取的正文长度 size_t real_len = content_len - _request._body.size(); //缓冲区的数据大于剩余正文长度,取出当前所需 if(buf->ReadAbleSize() >= real_len) { _request._body.append(buf->ReadPosition(),real_len); //这里不要忘了向后移动 buf->MoveReadOffSet(real_len); _recv_statu = RECV_HTTP_OVER; return true; } //这里的缓冲区数据不够,则把数据放到正文中,然后返回即可 _request._body.append(buf->ReadPosition(),buf->ReadAbleSize()); //这里不要忘了向后移动 buf->MoveReadOffSet(buf->ReadAbleSize()); return true; } public: HttpContent():_resp_statu(200),_recv_statu(RECV_HTTP_LINE){}; void Reset() { _resp_statu = 200; _recv_statu = RECV_HTTP_LINE; _request.ReSet(); } int Statu() { return _resp_statu; } HttpRecvStatu RecvStatu() { return _recv_statu; } HttpRequest& RecvReq() { return _request; } //接收并解析请求 void HttpRecvReqest(Buffer* buffer) { //根据不同的状态进入不同的函数,这里一个函数处理完成之后会立刻进入下一个函数,完成全过程 switch(_recv_statu){ case RECV_HTTP_LINE: RecvLine(buffer); case RECV_HTTP_HEAD: RecvHead(buffer); case RECV_HTTP_BODY: RecvBody(buffer); } } };
HttpServer服务器模块
该类主要是实现HTTP服务器的搭建,可以让用户快速搭建使用服务器
实现思想:
我们可以通过一个请求路由表来做统一的处理,这个表记录了那个请求对应哪一个方法,通过hash的方式,什么方法,怎么处理这个有用户自己决定,服务器只需要执行对应的函数即可。优势:用户只需要实现业务处理函数,处理好请求以及处理函数的映射关系,添加到服务器中;而服务器只需要接收数据,解析数据,查找路由表中的映射关系,同时执行对应的函数即可
类成员设计:
1.各种方法的请求路由表 (GET/POST/PUT/DELETE)
2.静态资源根目录 ->实现静态资源获取的处理
3.高性能的TCP服务器 ->进行连接的IO处理
服务器处理流程:
1.从socket接受数据,放入到接收缓冲区2.调用OnMessage回调函数进行处理
3.对请求进行解析,得到一个HttpRequest,里面填写好所有的请求要素
4.通过请求路由查找对应的方法进行处理
4.1如果是静态资源的请求 -> 实体文件资源的请求
将静态资源文件中的数据读取出来填到HttpResponse中
4.2如果是功能性请求 -> 查找对应的路由函数,执行对应的方法,并填写HttpResponse
5.业务处理完成之后,已经得到一个HttpResponse结构,组织http响应并进行发送
类功能函数设计:
1.添加请求 ->建立函数映射表(GET/POST/PUT/DELETE)2.设置静态资源根目录
3.设置是否启动超时连接关闭
4.设置线程池中线程的数量
5.启动服务器 (调用TcpServer中的start接口)
class HttpServer { private: using Functor = std::function<void(const HttpRequest&,HttpResponse*)>; using Handler = std::vector<std::pair<std::regex,Functor>>; //各种方法的路由表,第一个是正则表达式,第二个是方法 Handler _get_route; Handler _post_route; Handler _put_route; Handler _delete_route; std::string _base_dir; //静态资源根目录 TcpServer _server; private: void ErrorHandler(const HttpRequest& req,HttpResponse* resp) { //1.组织一个错误的页面 std::string body; body += "<html>"; body += "<head>"; body += "<meta charset='utf-8'>"; body += "</head>"; body += "<body>"; body += "<h1>"; body += std::to_string(resp->_statu); body += " "; body += Util::StatuDesc(resp->_statu); body += "</h1>"; body += "</body>"; body += "</html>"; //2.讲页面数据放入到resp中 resp->SetContent(body,"text/html"); } //根据http来组织httpResponse各要素 void WriteResponse(const PtrConnection& conn,HttpRequest& req,HttpResponse& resp) { //1.完善头部字段 if(req.Close() == true) { resp.SetHeader("Connection","Close"); } else{ resp.SetHeader("Connection","keep-alive"); } if(!resp._body.empty() && !resp.HasHeader("Content-Length")) { resp.SetHeader("Content-Length",std::to_string(resp._body.size())); } if(!resp._body.empty() && !resp.HasHeader("Content-Type")) { resp.SetHeader("Content-Type","application/octet-stream"); } //是否设置重定向 if(resp._redirect_flag) { resp.SetHeader("Location",resp._redirect_url); } //2.填充resp信息 协议版本,响应状态码,响应状态码描述 std::stringstream str; str << req._version <<" "<< std::to_string(resp._statu) << " " << Util::StatuDesc(resp._statu) << "\r\n"; //响应头部 for(auto& head : resp._headers) { str << head.first <<": " << head.second << "\r\n"; } str << "\r\n"; str << resp._body; //3.发送数据 conn->Send(str.str().c_str(),str.str().size()); } //静态资源处理 bool IsFilerHandler(const HttpRequest& req) { //1.必须设置静态资源根目录 if(req._path.empty()) { return false; } //2.必须是GET或者是HEAD方法 if(req._method != "GET" && req._method != "HEAD"){ return false; } //3.必须是合法的资源路径 if(!Util::IsValPath(req._path)) { return false; } //4.资源必须存在 //先添加上静态资源根目录,防止后面出错用另外一个变量来接收 std::string req_path = _base_dir + req._path; // "/" 最后一个字符是/,那么添加上"index.html" if(req_path.back() == '/'){ req_path += "index.html"; } //判断是否是一个文件 if(!Util::IsRegular(req_path)){ return false; } return true; } bool FilerHandler(HttpRequest& req,HttpResponse* resp) { //先确定路径 std::string req_path = _base_dir + req._path; if(req_path.back() == '/'){ req_path += "index.html"; } //把要获取的资源读取出来放到resp中的body中,并设置mime bool ret = Util::ReadFile(req_path,&(resp->_body)); if(ret == false){ return false; } //设置mime resp->SetHeader("Content-Type",Util::GetMime(req_path)); return true; } //功能性资源处理 void Dispatcher(HttpRequest& req,HttpResponse* resp,Handler& handlers) { //通过查看不同的方法的路由表,如果找到方法就执行对应的方法,如果没有找到就返回404 for(auto& handler:handlers) { const std::regex& re = handler.first; const Functor& func = handler.second; bool ret = std::regex_match(req._path,req._matches,re); if(ret == false) { continue; //没有找到,继续查找 } //找到就执行对应的函数 return func(req,resp); } //返回404 resp->_statu = 404; } void Route(HttpRequest& req,HttpResponse* resp) { //分成两种 -> 如果是静态资源请求则调用静态资源请求处理 //如果是功能性资源请求则调用对应的函数处理 if(IsFilerHandler(req)) { FilerHandler(req,resp); } //功能性请求 if(req._method == "GET" || req._method == "HEAD"){ return Dispatcher(req,resp,_get_route); } else if(req._method == "POST"){ return Dispatcher(req,resp,_post_route); }else if(req._method == "PUT"){ return Dispatcher(req,resp,_put_route); } else if(req._method == "DELETE"){ return Dispatcher(req,resp,_delete_route); } //不是上述方法,则返回405 resp->_statu = 405; //Method Not Allowed } //设置上下文 void Onconnect(const PtrConnection& conn) { conn->SetContext(HttpContent()); DLOG("New Connection %p",conn.get()); } //解析+处理 void OnMessage(const PtrConnection& conn,Buffer* buf) { while(buf->ReadAbleSize() > 0) { //1.获取上下文 HttpContent* content = conn->Context()->get<HttpContent>(); //2.通过解析上下文得到httprequest content->HttpRecvReqest(buf); HttpRequest& req = content->RecvReq(); HttpResponse resp(content->Statu()); //这里数据解析可能出错 if(content->Statu() >= 400) { //进行错误响应,关闭连接 ErrorHandler(req,&resp); //填充一个错误的页面返回 WriteResponse(conn,req,resp); //组织+响应 //关闭连接之前应该重置一下上下文,防止状态一直处理错误的状态 content->Reset(); //这里出错了,为了提高服务器的效率,直接把缓冲区中的数据清空 buf->MoveReadOffSet(buf->ReadAbleSize()); conn->ShutDown(); //关闭连接 return; } //这里有可能解析还没有完成,重新开始 if(content->RecvStatu() != RECV_HTTP_OVER){ return; } //3.请求路由 Route(req,&resp); //4.组织httpresponse并发送 WriteResponse(conn,req,resp); //5.重置上下文 content->Reset(); //6.是否是短连接,关闭 if(resp.Close()) conn->ShutDown(); } } public: HttpServer(int port,int timeout = DEFAULT_TIMEOUT):_server(port) { _server.SetActiveRelease(timeout); _server.SetConnectCallback(std::bind(&HttpServer::Onconnect,this,std::placeholders::_1)); _server.SetMessageCallback(std::bind(&HttpServer::OnMessage,this,std::placeholders::_1,std::placeholders::_2)); } void SetBaseDir(const std::string& path){ assert(Util::IsDir(path) == true); _base_dir = path; } //建立正则表达式和对应处理函数的映射关系 void Get(const std::string& pattern,const Functor& func){ _get_route.push_back(std::make_pair(std::regex(pattern),func)); } void Post(const std::string& pattern,const Functor& func){ _post_route.push_back(std::make_pair(std::regex(pattern),func)); } void Put(const std::string& pattern,const Functor& func){ _put_route.push_back(std::make_pair(std::regex(pattern),func)); } void Delete(const std::string& pattern,const Functor& func){ _delete_route.push_back(std::make_pair(std::regex(pattern),func)); } void SetThreadCount(int count) { _server.SetThreadCount(count); } void Listen(){ _server.Start(); } };
服务器搭建并进行测试
搭建服务器
我们需要编写简单的业务处理函数来填写路由表对应的处理方法,然后再调用接口来启动服务器即可
#include "http.hpp" #define WWWROOT "./wwwroot/" std::string RequestStr(const HttpRequest& req) { std::stringstream ss; //请求方法 资源路径 协议版本 ss << req._method <<" " << req._path <<" " << req._version <<"\r\n"; //查询字符串 for(auto& it : req._parame) { ss << it.first <<": " <<it.second << "\r\n"; } //请求头部 for(auto& it : req._headers) { ss << it.first <<": " <<it.second << "\r\n"; } ss << "\r\n"; //请求正文 ss << req._body; return ss.str(); } void GetFile(const HttpRequest& req,HttpResponse* resp) { resp->SetContent(RequestStr(req),"text.plain"); //sleep(15); //这里模拟服务器处理时间很长,可能会导致的其他连接超时销毁 } void Login(const HttpRequest& req,HttpResponse* resp) { resp->SetContent(RequestStr(req),"text.plain"); } void PutFile(const HttpRequest& req,HttpResponse* resp) { std::string req_path = WWWROOT + req._path; Util::WriteFile(req_path,req._body); } void DeleteFile(const HttpRequest& req,HttpResponse* resp) { resp->SetContent(RequestStr(req),"text.plain"); } int main() { HttpServer server(8080); server.SetBaseDir(WWWROOT); server.SetThreadCount(3); server.Get("/hello",GetFile); server.Post("/login",Login); server.Put("/submit.txt",PutFile); server.Delete("/submit.txt",DeleteFile); server.Listen(); return 0; }
长连接测试
#include "../source/server.hpp" int main() { Socket cli_sock; cli_sock.CreateClient(8080, "127.0.0.1"); std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n"; while(1) { assert(cli_sock.Send(req.c_str(),req.size()) != -1); char buffer[1024]; assert(cli_sock.Recv(buffer,1023) != -1); DLOG("[%s]",buffer); sleep(3); } cli_sock.Close(); return 0; }
测试结果:
这里服务器设置了30秒的超时连接销毁,通过一定时间的观察我们可以得出长连接测试是没问题的。
测试超时连接是否销毁
#include "../source/server.hpp" int main() { Socket cli_sock; cli_sock.CreateClient(8080, "127.0.0.1"); std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n"; while(1) { sleep(40); } cli_sock.Close(); return 0; }
测试结果:
我们可以看到服务器这里经过了30秒之后就自动关闭了,而客户端那里由于设置了死循环,所以没有任何反应,但是这也足够测试出来结果是没有问题的
错误请求处理
//只发送一次小的请求,服务器得不到完整的数据,就不会进行业务处理 //给服务器发送多条小的请求,服务器会把后面的请求当正文处理,但是后面的请求会失败 #include "../source/server.hpp" int main() { Socket cli_sock; cli_sock.CreateClient(8080, "127.0.0.1"); std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhow are you?"; while(1) { assert(cli_sock.Send(req.c_str(),req.size()) != -1); assert(cli_sock.Send(req.c_str(),req.size()) != -1); assert(cli_sock.Send(req.c_str(),req.size()) != -1); char buffer[1024]; assert(cli_sock.Recv(buffer,1023) != -1); DLOG("[%s]",buffer); sleep(3); } cli_sock.Close(); return 0; }
测试结果:
我们可以看到确实这里只处理了一次,把后面的请求也放入到了第一次请求的请求正文中了,同时服务器也没有关闭,所以测试还是没有问题的
服务器性能达到瓶颈的处理
理论上,如果服务器性能达到瓶颈,那么怎么处理应该都不为过,这里我们采用这种方式来模拟服务器性能达到瓶颈:通过请求GET方法,但是让其睡眠来模拟
测试代码:
//测试在服务器达到瓶颈的时候对连接的处理,其他的连接可能因为这个连接处理而导致超时销毁 #include "../source/server.hpp" int main() { signal(SIGCHLD,SIG_IGN); for(int i = 0;i<10;++i) { pid_t pid = fork(); if(pid < 0) { DLOG("FORK ERROR"); return -1; } else if(pid == 0) { //子进程 Socket cli_sock; cli_sock.CreateClient(8080, "127.0.0.1"); std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n"; while(1) { assert(cli_sock.Send(req.c_str(),req.size()) != -1); char buffer[1024]; assert(cli_sock.Recv(buffer,1023) != -1); DLOG("[%s]",buffer); sleep(3); } cli_sock.Close(); exit(0); } } while(1) sleep(1); return 0; }
测试结果:
client:
server:
我们可以看到性能瓶颈并没有导致服务器进行关闭,这些连接都得到了合理得处理结果
一次发送多个请求测试
//一次性向服务器发送多个请求,查看服务器的响应情况 //每一条请求都应该得到正确的处理 #include "../source/server.hpp" int main() { Socket cli_sock; cli_sock.CreateClient(8080, "127.0.0.1"); std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n"; req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n"; req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n"; while(1) { assert(cli_sock.Send(req.c_str(),req.size()) != -1); char buffer[1024]; assert(cli_sock.Recv(buffer,1023) != -1); DLOG("[%s]",buffer); sleep(3); } cli_sock.Close(); return 0; }
测试结果:
它们都得到了预期的处理结果,服务器也一直在处理中,知道用户关闭客户端
测试大文件传输
#include "../source/http/http.hpp" int main() { Socket cli_sock; cli_sock.CreateClient(8080, "127.0.0.1"); std::string req = "PUT /submit.txt HTTP/1.1\r\nConnection: keep-alive\r\n"; std::string body; bool ret = Util::ReadFile("./hello.txt",&body); req += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n"; assert(cli_sock.Send(req.c_str(),req.size()) != -1); //传输正文 assert(cli_sock.Send(body.c_str(),body.size()) != -1); char buffer[1024]; assert(cli_sock.Recv(buffer,1023) != -1); DLOG("[%s]",buffer); sleep(3); cli_sock.Close(); return 0; }
这里传送300M的文件给服务器,因为这里使用的是云服务器,资源没有那么多,就使用300M来测试
通过上面的指令我们就可以申请到一个300M的文件大写,这里为了让文件内容不全为0,这里追加了字符到文件中
测试结果:
我们在服务器这里也得到了一个文件:
这是我们只需要通过md5sum来比较以下这两个文件是否相同即可:
这两个文件内容也是一样的,说明我们的测试是没有问题的
服务器性能测试
通过webbench来对服务器进行测试:
测试环境:服务器是两核2G带宽4M的云服务器
5000个客户端的情况下:
使用webbench以5000并发量对服务器发送请求,进行1分钟测试,得出的QPS为:124859
即每秒处理的包的数量
项目源码
项目源码