项目技术点
- http协议的报文结构封装
- Linux网络编程 (POSIX API)
- IO多路复用技术 epoll (ET/LT)
- Linux多线程编程, 线程间同步与互斥
- C语言宏替换做预处理 (简化日志函数接口参数)
- C语言可变参数包的访问操作
- makefile自动化编译
项目主体模块
- Linux操作系统
- vscode远程ssh连接服务器进行开发
- 线程池模块
- reactor活跃事件反应堆模块
- Log日志系统模块
- http协议封装模块
整体架构
Reactor(eventloop) + 多线程
线程池
- 采用线程池的优势
采用线程池将任务进行一个缓存, 另外开启多个消化任务的工作者线程, 等待消化任务。一般业务比较复杂的时候, 我们可以将业务的处理和网络IO做一个解耦合. 分离. 使得reactor活跃事件反应堆不至于被复杂耗时的业务拖慢, 进而造成服务端卡顿. 用户体验感不佳,服务端并发性能下降的问题.
线程池实现
- 阻塞的任务队列
- 提前开启的workthreads等待消化任务
结构定义+数据成员
typedef void (*CB)(void*); //线程回调函数, 线程任务 struct task { void* data; //存储数据 int fd; //对应sockfd struct reactor* reactor; //任务所属反应堆 CB cb; //回调任务. task_cb struct task* next; }; struct taskqueue { struct task* front; struct task* tail; pthread_mutex_t lock; //锁, 阻塞队列 pthread_cond_t cond; //条件变量, 通知消费. 任务到来 int is_running; //控制线程池的开启或关闭 };
重要成员函数
extern struct taskqueue* init_taskqueue(); //创建任务队列 extern void push_task(struct taskqueue* tq, struct task* ptask);//push任务 extern struct task* pop_task(struct taskqueue* tq); //pop任务 extern void* thread_routine(void* arg); //线程函数 extern void start_threadspool(struct taskqueue* taskq, int n); //开启线程池 extern void clear_threadspool(struct taskqueue* taskq); //销毁线程池
Reactor反应堆
- reactor反应堆的优势
reactor反应堆就是一个活跃事件的收集器, 事件循环机制. 存在事件收集器, 事件处理器两个重要模块. 工作原理就是提前注册好对于感兴趣的IO事件的监视. 当IO到来的时候, 操作系统内核底层会触发底层设置好的回调函数将所有到来的活跃IO收集起来. 并返回到用户态. 然后我们根据事件的不同类型将事件分发出去完成处理.
发技术
- 多进程
- 多线程
- IO复用技术 (多IO复用一个阻塞的系统调用)
IO多路复用之epoll
IO多路复用技术就是阻塞一个线程去同时监视多个IO事件. 多路IO复用一个监控IO事件的系统调用. 这样做的优势在哪里. 充分利用CPU, 阻塞单路同时监视多个IO事件. 然后进行分发活跃IO事件进行处理. 多路IO,或者说多路事件意味着并发量的提升。
事件注册
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, event); //向操作系统内核中添加一个结点. 向内核注册监视的event事件 epfd: epoll句柄 EPOLL_CTL_ADD: 添加事件 EPOLL_CTL_MOD: 修改事件 EPOLL_CTL_DEL: 删除事件 sockfd: 描述符 event: 事件结构体指针, struct epoll_event*
事件收集器
nready = epoll_wait(epfd, events, MAX_EVENTS, -1); //阻塞等待收集到来活跃的IO事件 nready: 就绪事件数目 events: event数组, 存储事件 MAX_EVENTS: 数组容量 -1: 一直阻塞等待. timeout: 阻塞返回事件, 定时触发. 超时处理
- 事件分发器
//循环分发到来的活跃IO事件 for (; i < nready; ++i) { struct epoll_event ev = evs[i];//拿出事件 int mask = 0; if (ev.events & EPOLLIN) mask |= EPOLLIN; if (ev.events & EPOLLOUT) mask |= EPOLLOUT; if (ev.events & EPOLLHUP) mask |= EPOLLIN|EPOLLOUT; //将事件触发调用 if (ev.events & EPOLLIN) { struct event_item* item = ev.data.ptr; item->callback(item->fd, mask, item->arg); } if (ev.events & EPOLLOUT) { struct event_item* item = ev.data.ptr; item->callback(item->fd, mask, item->arg); } }
复用技术的常用底层原理
select, poll : 轮询, 每次传入监视的IO事件并且依次询问是否触发. 轮询,必然带来了很多的无效轮询,万一我一万个事件仅仅只是一个活跃事件, 其他的9999次都是浪费. 时间复杂度高,低并发, 上千还可以应付. 过多了实属浪费. select可跨平台. 优势.
epoll: 对功能进行分离。 仅仅只对感兴趣的事件进行一个处置. 而不再是傻傻的一直轮询,询问你有没有事件到来. 而且将事件的关注,监视下称到内核, 不再需要每次都拷贝监视IO事件到内核,而是将事件注册监视和收集活跃事件给分离开来. 采取回调的方式,类似于中断处理,事件活跃后会自动的在内核中将其收集到一个就绪队列中. 然后返回给用户态完成处理.
epoll触发方式(ET/LT)
1. LT: level tiggered,水平触发。不停的触发, 只要socket缓冲区中存在数据就会不停的触发epoll_wait的返回. 问题所在:对于监控sockfd缓冲区中的数据不进行处理. 就会一直不停的触发epoll_wait的返回. 效率低 2. ET: edage tiggered, 边沿触发, 边沿,也就是说只触发一次。触发且仅触发一次. 不论一次之后缓冲区中还有没有剩余数据,都将不再进行触发. 数据从无到有的时候触发那一次. 用户态缓存必须开的足够大, 如果一次无法完成所有数据的读取. 将不会再触发epoll_wait. 等到下一次IO活跃才会再触发epoll_wait收集活跃IO事件 3. 但是如果我们能保证一次性绝对将所有数据处理完, LT其实和ET效率也是一样的,并不会造成说LT更多的epoll_wait调用效率低下的问题
Logger模块
引入日志系统的优势
日志系统可以记录大量的debug信息,便于我们在程序运行过程中出现问题的定位调试. 特别是在比较大型的项目中的bug定位, 阅读日志信息也是很重要的一环.
日志系统的组成
日志系统往往存在日志文件句柄 + 日志级别 + 日志接口 三个重要的组成部分. 日志文件我采取的是FILE* C语言文件操作实现. 日志级别往往分为: debug信息, info 信息, warn信息, error信息, fatal信息这几个组成部分. 日志接口函数往往需要打印: 时间 + 线程id + 日志级别 + 文件:行号 + 日志内容. 等重要参数
日志系统的实现
//日志等级, 级别 enum Level { DEBUG=0,//debug信息 INFO,//普通信息 WARN,//警告 ERROR,//错误信息 FATAL,//致命错误 LEVEL_COUNT//日志级别数目 }; struct Logger { FILE* _fp;//日志文件 }; extern const char* _level[LEVEL_COUNT];//存储日志级别 static struct Logger* g_logger; //初始化日志系统, 仅仅初始化一次 int init_logger(); //初始化日志系统 void destroy_logger();//销毁日志系统 //登记各种级别的日志函数, 其实都是调用了log void log_debug(const char *file, int line, const char* fmt, ...); void log_info(const char *file, int line, const char* fmt, ...); void log_warn(const char *file, int line, const char* fmt, ...); void log_error(const char *file, int line, const char* fmt, ...); void log_fatal(const char *file, int line, const char* fmt, ...); //登记日志 void log(int level, const char *file, int line, const char* fmt, va_list ap);
核心接口
//登记日志. 核心 void log(int level, const char *file, int line, const char* fmt, va_list ap) { //先获取本地时间 time_t t = time(NULL); struct tm* ptm = localtime(&t); char buff[32] = "0"; //strftime strftime(buff, sizeof(buff), "%Y-%m-%d %H:%M:%S ", ptm); flockfile(g_logger->_fp); fprintf(g_logger->_fp, buff);//log time fprintf(g_logger->_fp, "%s ", _level[level]);//log level fprintf(g_logger->_fp, "%s:%d ", file, line); vfprintf(g_logger->_fp, fmt, ap); fprintf(g_logger->_fp, "\r\n"); fflush(g_logger->_fp); funlockfile(g_logger->_fp); }
宏替换大法, 隐藏参数. 减少参数传入
#define _DEBUG #define _INFO #define _ERROR #define _WARN #define _FATAL #ifdef _DEBUG #define debug(fmt, args...) \ log_debug(__FILE__, __LINE__, fmt, ##args) #else #define debug(fmt, args...) #endif #ifdef _ERROR #define error(fmt, args...) \ log_error(__FILE__, __LINE__, fmt, ##args) #else #define error(fmt, args...) #endif #ifdef _WARN #define warn(fmt, args...) \ log_warn(__FILE__, __LINE__, fmt, ##args) #else #define warn(fmt, args...) #endif #ifdef _INFO #define info(fmt, args...) \ log_info(__FILE__, __LINE__, fmt, ##args) #else #define info(fmt, args...) #endif #ifdef _FATAL #define fatal(fmt, args...) \ log_fatal(__FILE__, __LINE__, fmt, ##args) #else #define fatal(fmt, args...) #endif
协议模块
应用层协议: HTTP
请求报文: 请求行, 请求头部, 请求正文. 解析并获取
响应报文: 状态行: status_line, 响应头部, 响应正文 封装.
我采取的是返回静态网页, 静态资源. 可以引入cgi技术,向cgi服务器请求动态的处理数据进行返回. 请求第三方服务.
传输层协议: TCP
字节流传输协议. 面向连接, 可靠的传输层通信协议.
可靠机制:核心在于应答. 没有及时应答需要超时重传, 保证可靠. 根据网络拥塞和双方收发能力来调节数据收发速率. 以适应网络和双方来达到尽量可靠. 实在丢了,或者数据不完整还可以重传
HTTP更多细节, 可查看博客如下:
https://mp.csdn.net/mp_blog/creation/editor/124895219
项目扩展方向
- 引入cgi技术, 实现交互
- 引入数据库
- 长连接
- 将reactor升级为主从reactor进一步提升接入量## 项目技术点