小型WebServer项目

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 小型WebServer项目

项目技术点

  1. http协议的报文结构封装
  2. Linux网络编程 (POSIX API)
  3. IO多路复用技术 epoll (ET/LT)
  4. Linux多线程编程, 线程间同步与互斥
  5. C语言宏替换做预处理 (简化日志函数接口参数)
  6. C语言可变参数包的访问操作
  7. makefile自动化编译

项目主体模块

  • Linux操作系统
  • vscode远程ssh连接服务器进行开发
  • 线程池模块
  • reactor活跃事件反应堆模块
  • Log日志系统模块
  • http协议封装模块

整体架构

Reactor(eventloop) + 多线程

线程池

  • 采用线程池的优势

采用线程池将任务进行一个缓存, 另外开启多个消化任务的工作者线程, 等待消化任务。一般业务比较复杂的时候, 我们可以将业务的处理和网络IO做一个解耦合. 分离. 使得reactor活跃事件反应堆不至于被复杂耗时的业务拖慢, 进而造成服务端卡顿. 用户体验感不佳,服务端并发性能下降的问题.

线程池实现

  1. 阻塞的任务队列
  2. 提前开启的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收集起来. 并返回到用户态. 然后我们根据事件的不同类型将事件分发出去完成处理.

发技术

  1. 多进程
  2. 多线程
  3. 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

项目扩展方向

  1. 引入cgi技术, 实现交互
  2. 引入数据库
  3. 长连接
  4. 将reactor升级为主从reactor进一步提升接入量## 项目技术点
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
27天前
|
监控 应用服务中间件 网络安全
部署Django应用:使用Gunicorn和Nginx构建高效的生产环境
部署Django应用:使用Gunicorn和Nginx构建高效的生产环境
101 0
|
5月前
|
开发框架 中间件 PHP
Laravel框架:优雅构建PHP Web应用的秘诀
**Laravel 框架简介:** Laravel是PHP的优雅Web开发框架,以其简洁语法、强大功能和良好开发者体验闻名。它强调代码的可读性和可维护性,加速复杂应用的构建。基础步骤包括安装PHP和Composer,然后运行`composer create-project`创建新项目。Laravel的路由、控制器和Blade模板引擎简化了HTTP请求处理和视图创建。模型和数据库迁移通过Eloquent ORM使数据库操作直观。Artisan命令行工具、队列、事件和认证系统进一步增强了其功能。【6月更文挑战第26天】
45 1
|
4月前
|
Java 应用服务中间件 Linux
Tomcat安装部署[单机软件],可以让用户开发的WEB应用程序,变成可以被访问的网页,Tomcat的使用需要jdk环境
Tomcat安装部署[单机软件],可以让用户开发的WEB应用程序,变成可以被访问的网页,Tomcat的使用需要jdk环境
|
11月前
|
XML 应用服务中间件 数据库
django2.2.4项目 部署 centos7.3 环境, tomcat与nginx相互切换
django2.2.4项目 部署 centos7.3 环境, tomcat与nginx相互切换
75 0
|
SQL JavaScript 应用服务中间件
在windows服务器上部署一个单机项目以及前后端分离项目
在windows服务器上部署一个单机项目以及前后端分离项目
|
前端开发 JavaScript Ubuntu
「Web应用架构」5分钟把前端应用程序部署到NGINX
「Web应用架构」5分钟把前端应用程序部署到NGINX
|
开发框架 .NET API
分布式服务器框架之服务器+Web站点+类库工程创建
类库Servers.Core、Servers.Common、Servers.Model、Servers.Hotfix 四个库项目都选择.Net Core平台,如果找不到这个模板的话需要安装.Net通用开发工具包,因为这个框架可以实现跨平台,所以选择了.Net Core。是微软最新一代的平台虚拟机框架。一直点击下一步
分布式服务器框架之服务器+Web站点+类库工程创建
|
域名解析 缓存 运维
Nginx反向代理web程序解决谷歌跨越问题配置详解
路由转发:源ip和目标ip都不会改变只改变mac地址,只能在私网使用 客户端10.0.0.1要访问web服务器172.16.1.7,也就是客户端直接通过路由去访问web服务器,首先请求的源ip是10.0.0.1目标ip是172.16.1.7,源mac是pc目标mac是web,当源ip到达路由器时查询路由表,在转发到web服务器。这时源ip和目标ip不会改变,但是源mac地址会换成路由器的mac地址,最终访问到web服务器,web服务器记录的访问ip依旧是客户端的源ip
1014 0
Nginx反向代理web程序解决谷歌跨越问题配置详解
wildfly 21中应用程序的部署
wildfly 21中应用程序的部署
下一篇
无影云桌面