小型WebServer项目

简介: 小型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进一步提升接入量## 项目技术点
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
前端开发 网络协议 Dubbo
超详细Netty入门,看这篇就够了!
本文主要讲述Netty框架的一些特性以及重要组件,希望看完之后能对Netty框架有一个比较直观的感受,希望能帮助读者快速入门Netty,减少一些弯路。
96069 33
超详细Netty入门,看这篇就够了!
|
前端开发 网络协议
netty整合websocket(完美教程)
本文是一篇完整的Netty整合WebSocket的教程,介绍了WebSocket的基本概念、使用Netty构建WebSocket服务器的步骤和代码示例,以及如何创建前端WebSocket客户端进行通信的示例。
2230 2
netty整合websocket(完美教程)
|
数据采集 机器学习/深度学习 数据可视化
2023年美赛C题Wordle预测问题一建模及Python代码详细讲解
本文通过Python代码详细讲解了2023年美赛C题Wordle预测问题一的建模过程,包括数据预处理、特征工程、相关性分析以及线性回归模型的应用。
293 1
2023年美赛C题Wordle预测问题一建模及Python代码详细讲解
|
NoSQL 搜索推荐 网络协议
Java NIO、BIO、 AIO 与 同步、阻塞、非阻塞、异步IO 简析
我相信大部分人看到这些名词,都是一头雾水的,如果你去搜索引擎搜索,那么恭喜你,你又会被各种文章中的高大上的名词搞得云里雾里。那么,我们应该怎么理清这么名词之间的关系呢? 所谓 同步/异步/阻塞/非阻塞 IO ,是指操作系统中的对 IO 处理的不同方法,而 Java 对这些不同操作方法做了一些包装,由此有了 BIO / NIO / AIO 几种操作接口。 我不想复制一些高大上的概念,只是想尽量好好说话,说清楚他们之间的关系。 需求 有 A、B、C、D 四个线程可以生产文件,假设他们的返回的文件是一样的,对应我们的服务端 有 E、F、G、H 四个线程在随机时间向服务端上传一个文本,并且要求
|
Kubernetes 安全 关系型数据库
Helm入门(一篇就够了)
Helm快速入门
27863 0
|
12天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23473 11
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
|
16天前
|
人工智能 缓存 BI
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro,跑完 Skills —— OA 审批、大屏、报表、部署 5 大实战场景后的真实体验 ![](https://oscimg.oschina.net/oscnet/up608d34aeb6bafc47f
5215 19
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
|
17天前
|
人工智能 JSON BI
DeepSeek V4 来了!超越 Claude Sonnet 4.5,赶紧对接 Claude Code 体验一把
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro 的真实体验与避坑记录 本文记录我将 Claude Code 对接 DeepSeek 最新模型(V4Pro)后的真实体验,测试了 Skills 自动化查询和积木报表 AI 建表两个场景——有惊喜,也踩
6232 15