小型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) + 多线程

小型WebServer项目_第1张图片

线程池

  • 采用线程池的优势

    采用线程池将任务进行一个缓存, 另外开启多个消化任务的工作者线程, 等待消化任务。一般业务比较复杂的时候, 我们可以将业务的处理和网络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);
            }
        }
    
    • 复用技术的常用底层原理

      1. select, poll : 轮询, 每次传入监视的IO事件并且依次询问是否触发. 轮询,必然带来了很多的无效轮询,万一我一万个事件仅仅只是一个活跃事件, 其他的9999次都是浪费. 时间复杂度高,低并发, 上千还可以应付. 过多了实属浪费. select可跨平台. 优势.
      2. 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进一步提升接入量## 项目技术点

你可能感兴趣的:(后端服务器开发,项目实践,c++,面试,学习,服务器)