tinyWebServer 学习笔记——二、HTTP 连接处理

news2024/12/1 0:35:29

文章目录

  • 一、基础知识
    • 1. epoll
    • 2. 再谈 I/O 复用
    • 3. 触发模式和 EPOLLONESHOT
    • 4. HTTP 报文
    • 5. HTTP 状态码
    • 6. 有限状态机
    • 7. 主从状态机
    • 8. HTTP_CODE
    • 9. HTTP 处理流程
  • 二、代码解析
    • 1. HTTP 类
    • 2. 读取客户数据
    • 2. epoll 事件相关
    • 3. 接收 HTTP 请求
    • 4. HTTP 报文解析
    • 5. HTTP 请求响应
  • 参考文献

一、基础知识

1. epoll

  • 创建内核事件表:int epoll_create(int size);

    • size :不起作用,只是给内核一个提示,告诉它事件表需要多大;
    • 返回值:内核事件表的文件描述符;
  • 修改内核事件表监控的文件描述符上的事件:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    • epfd :内核事件表的文件描述符;
    • op :表示三种操作:注册(EPOLL_CTL_ADD)、修改(EPOLL_CTL_MOD)、删除(EPOLL_CTL_DEL);
    • event :需要监听的事件:
    标识符事件类型
    EPOLLIN可读、对端 socket 关闭
    EPOLLOUT可写
    EPOLLPRI带外数据
    EPOLLERR错误
    EPOLLHUP文件描述符被挂断
    EPOLLET边缘触发模式
    EPOLLONESHOT只监听一次事件,每次见听完如需再次监听需重置
    • 返回值:成功时返回 0 ,失败时返回 -1 并设置 errno 。
  • 监听事件发生:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

    • events :存储内核中发生的事件;
    • maxeventsevents 的容量;
    • timeout :超时时间:-1 表示阻塞,0 表示立即返回(非阻塞),大于 0 表示毫秒;
    • 返回值:就绪的文件描述符个数,超时时返回 0 ,出错时返回 -1 。

2. 再谈 I/O 复用

  • select :使用线性表描述文件描述符集合,存在上限,每次调用需要将所有文件描述符拷贝到内核态,需要遍历判断就绪事件,适用于少量活跃的 fd;
  • poll :使用链表描述文件描述符集合,不存在上限,每次调用需要将所有文件描述符拷贝到内核态,需要遍历判断就绪事件,适用于少量活跃的 fd;
  • epoll :使用红黑树描述文件描述符集合,存在上限,通过 epoll_ctl 将要监听的文件描述符注册到红黑树上,会将就绪事件存放在新建的链表中,适用于大量不活跃的 fd;

3. 触发模式和 EPOLLONESHOT

  • LT :水平触发模式,当检测到就绪事件时,将其通知给应用程序,应用程序可以不立即处理该事件,等到下次调用 epoll_wait 时会再次报告该事件;
  • ET :边缘触发模式,当检测到就绪事件时,将其通知给应用程序,应用程序必须立即处理,并且需要一次性处理完;
  • EPOLLONESHOT :当某个 socket 的数据分两次到达时,系统可能会唤醒两个不同的线程来进行处理,若开启 EPOLLONESHOT ,则对于某个 socket 来说,只会有一个线程处理其事件,其他线程不能插手,完成一次处理后需要重置 EPOLLONESHOT 。

4. HTTP 报文

HTTP 报文分为请求报文和响应报文,前者由浏览器发送给服务器,后者由服务器应答浏览器,请求报文又分为 GET 和 POST 两种:

GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/*,*/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空
  • 第 1 行:请求行,用来说明请求类型、要访问的资源、所使用的 HTTP 版本;
  • 第 2 - 8 行:请求头部,通常包含如下信息:
    • Host :服务器所在的域名;
    • User-Agent :HTTP 客户端程序的信息,由浏览器定义并自动发送;
    • Accept :说明用户代理可处理的媒体类型;
    • Accept-Encoding :说明用户代理支持的内容编码;
    • Accept-Language :说明用户代理能够处理的自然语言集;
    • Content-Type :说明实现主体的媒体类型;
    • Content-Length:说明实现主题的大小;
    • Connection :连接管理,可以是 Keep-Alive 或 close ;
  • 第 9 行:空行;
  • 第 10 行:请求数据,也叫主体,可以添加任意其他数据;
    POST / HTTP1.1
    Host:www.wrox.com
    User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
    Content-Type:application/x-www-form-urlencoded
    Content-Length:40
    Connection: Keep-Alive
    空行
    name=Professional%20Ajax&publisher=Wiley
    
  • GET 的请求数据通常为空,POST 则包含要请求的信息。

响应报文主要有四个部分组成:状态行、消息报头、空行、响应正文:

HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行
<html>
      <head></head>
      <body>
            <!--body goes here-->
      </body>
</html>
  • 第 1 行:状态行,由 HTTP 协议版本号、状态码、状态消息组成;
  • 第 2 - 3 行:消息报头,用来说明客户端需要使用的附加信息:
    • Date :生成响应的日期和时间;
    • Content-Type :指定了 MIME 类型的 HTML ,编码类型是 UTF-8 ;
  • 第 4 行:空行;
  • 第 5 - 10 行:响应正文,为 HTML 语言。

5. HTTP 状态码

  • 1xx :指示信息,表示请求已接收,继续处理;
  • 2xx :成功,表示请求正常处理完毕:
    • 200 OK :客户端请求被正常处理;
    • 206 Partial Content :客户端进行了范围请求;
  • 3xx :重定向,要完成请求需要进一步操作:
    • 301 Moved Permanently :永久重定向,返回新的 URL ;
    • 302 Found :临时重定向,返回临时的 URL ;
  • 4xx :客户端错误:
    • 400 Bad Request :语法错误;
    • 403 Forbidden :请求被服务器拒绝;
    • 404 Not Found :请求不存在,服务器上找不到请求的资源;
  • 5xx :服务器端错误:
    • 500 Internal Server Error :服务器执行时出错。

6. 有限状态机

有限状态机是一种抽象的理论模型,使用选择语句来实现。模型要求代码存在 n 个状态,使用当前状态 cur_state 来进行标记,每次处理完任务后都对其进行更改以实现状态跳转,代码如下:

STATE_MACHINE(){
    State cur_State = type_A;
    while(cur_State != type_C){
        Package _pack = getNewPackage();
        switch() {
            case type_A:
                process_pkg_state_A(_pack);
                cur_State = type_B;
                break;
            case type_B:
                process_pkg_state_B(_pack);
                cur_State = type_C;
                break;
        }
    }
}

7. 主从状态机

主从状态机也是一种抽象的理论模型,在本项目中从状态机负责读取 HTTP 请求的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机,流程图如下:
1

流程图 [2]

从模型的角度来看,从状态机负责通用的操作处理,主状态机负责特定的操作处理,主状态机需要使用从状态机提供的数据,从状态机需要被主状态机调用,每个状态机又是一个有限状态机。本项目中主从状态机各有三种状态:

  • 主状态机:标识解析位置
    • CHECK_STATE_REQUESTLINE :解析请求行;
    • CHECK_STATE_HEADER :解析请求头;
    • CHECK_STATE_CONTENT :解析消息体,仅用于解析 POST 请求;
  • 从状态机:标识解析一行的读取状态:
    • LINE_OK :完整读取一行;
    • LINE_BAD :报文语法错误;
    • LINE_OPEN :读取的行不完整。

8. HTTP_CODE

标识了 HTTP 请求的处理结果:

  • NO_REQUEST :请求不完整,需要继续读取请求报文,跳转主程序继续检测可读事件;
  • GET_REQUEST :获得了完整的请求,调用 do_request 完成请求资源映射;
  • NO_RESOURCE :请求资源不存在,跳转 process_write 完成响应报文;
  • BAD_REQUEST :语法错误或请求资源为目录,跳转 process_write 完成响应报文;
  • FORBIDDEN_REQUEST :请求资源禁止访问,跳转 process_write 完成响应报文;
  • FILE_REQUEST :请求资源可以正常访问,跳转 process_write 完成响应报文;
  • INTERNAL_ERROR :服务器内部错误。

9. HTTP 处理流程

2

流程图 [3]
  • 浏览器发出 HTTP 连接请求;
  • 主线程创建 HTTP 对象,接收请求并将所有数据读入对应的缓存区;
  • 主线程将 HTTP 对象插入任务队列;
  • 工作线程从任务队列中取出一个任务;
  • 工作线程调用 process_read 函数,通过主从状态机解析请求报文;
  • 解析完成后,跳转 do_request 函数生成响应报文;
  • 通过 process_write 写入缓存区;
  • 发送数据给浏览器。

二、代码解析

1. HTTP 类

// http连接类
class http_conn
{
public:
    static const int FILENAME_LEN = 200;       // 文件名最大长度
    static const int READ_BUFFER_SIZE = 2048;  // 读缓存区大小
    static const int WRITE_BUFFER_SIZE = 1024; // 写缓存区大小
    // http连接的方法
    enum METHOD
    {
        GET = 0, // 申请获得资源
        POST,    // 向服务器提交数据并修改
        HEAD,    // 仅获取头部信息
        PUT,     // 上传某个资源
        DELETE,  // 删除某个资源
        TRACE,   // 要求服务器返回原始HTTP请求的内容,可用来查看服务器对HTTP请求的影响
        OPTIONS, // 查看服务器对某个特定URL都支持哪些请求方法。也可把URL设置为* ,从而获得服务器支持的所有请求方法
        CONNECT, // 用于某些代理服务器,能把请求的连接转化为一个安全隧道
        PATH     // 对某个资源做部分修改
    };
    // 主状态机状态
    enum CHECK_STATE
    {
        CHECK_STATE_REQUESTLINE = 0, // 检查请求行
        CHECK_STATE_HEADER,          // 检查头部状态
        CHECK_STATE_CONTENT          // 检查内容
    };
    // HTTP状态码
    enum HTTP_CODE
    {
        NO_REQUEST,        // 请求不完整,需要继续读取请求报文数据
        GET_REQUEST,       // 获取了完整请求
        BAD_REQUEST,       // HTTP请求报文有语法错误
        NO_RESOURCE,       // 无资源
        FORBIDDEN_REQUEST, // 禁止请求
        FILE_REQUEST,      // 文件请求
        INTERNAL_ERROR,    // 服务器内部错误
        CLOSED_CONNECTION  // 关闭连接
    };
    // 从状态机状态
    enum LINE_STATUS
    {
        LINE_OK = 0, // 读取完成
        LINE_BAD,    // 读取有错误
        LINE_OPEN    // 未读完
    };

public:
    // 构造函数
    http_conn() {}
    // 析构函数
    ~http_conn() {}

public:
    // 初始化套接字地址,函数内部会调用私有方法init
    void init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname);
    // 关闭http连接
    void close_conn(bool real_close = true);
    // 处理HTTP请求的入口函数
    void process();
    // 读取浏览器端发来的全部数据
    bool read_once();
    // 响应报文写入函数
    bool write();
    // 地址
    sockaddr_in *get_address()
    {
        return &m_address;
    }
    // 同步线程初始化数据库读取表
    void initmysql_result(connection_pool *connPool);
    // 计时器标志
    int timer_flag;
    int improv;

private:
    void init();                                            // 初始化
    HTTP_CODE process_read();                               // 从m_read_buf读取,并处理请求报文
    bool process_write(HTTP_CODE ret);                      // 向m_write_buf写入响应报文数据
    HTTP_CODE parse_request_line(char *text);               // 主状态机,解析http请求行
    HTTP_CODE parse_headers(char *text);                    // 主状态机,解析http请求头
    HTTP_CODE parse_content(char *text);                    // 主状态机,判断http请求内容
    HTTP_CODE do_request();                                 // 生成响应报文
    char *get_line() { return m_read_buf + m_start_line; }; // 用于将指针向后偏移,指向未处理的字符
    LINE_STATUS parse_line();                               // 从状态机,分析一行的内容,返回状态
    void unmap();                                           // 关闭内存映射

    // 根据响应报文格式,生成对应8个部分,以下函数均由do_request调用
    bool add_response(const char *format, ...);          // response
    bool add_content(const char *content);               // content
    bool add_status_line(int status, const char *title); // status_line
    bool add_headers(int content_length);                // headers
    bool add_content_type();                             // content_type
    bool add_content_length(int content_length);         // content_length
    bool add_linger();                                   // linger
    bool add_blank_line();                               // blank_line

public:
    static int m_epollfd;    // 最大文件描述符个数
    static int m_user_count; // 当前用户连接数
    MYSQL *mysql;            // 数据库指针
    int m_state;             // 读为0, 写为1

private:
    int m_sockfd;                        // 当前fd
    sockaddr_in m_address;               // 当前地址
    char m_read_buf[READ_BUFFER_SIZE];   // 存储读取的请求报文数据
    long m_read_idx;                     // 缓冲区中m_read_buf中数据的最后一个字节的下一个位置
    long m_checked_idx;                  // m_read_buf读取的位置m_checked_idx
    int m_start_line;                    // m_read_buf中已经解析的字符个数
    char m_write_buf[WRITE_BUFFER_SIZE]; // 存储发出的响应报文数据
    int m_write_idx;                     // 指示buffer中的长度
    CHECK_STATE m_check_state;           // 主状态机状态
    METHOD m_method;                     // 请求方法

    // 以下为解析请求报文中对应的6个变量
    char m_real_file[FILENAME_LEN]; // 存储读取文件的名称
    char *m_url;                    // url
    char *m_version;                // version
    char *m_host;                   // host
    long m_content_length;          // content_length
    bool m_linger;                  // linger

    char *m_file_address;           // 读取服务器上的文件地址
    struct stat m_file_stat;        // 文件状态
    struct iovec m_iv[2];           // io向量机制iovec,标识两个缓存区
    int m_iv_count;                 // 表示缓存区个数
    int cgi;                        // 是否启用的POST
    char *m_string;                 // 存储请求头数据
    int bytes_to_send;              // 待发送字节个数
    int bytes_have_send;            // 已发送字节个数
    char *doc_root;                 // 文件根目录

    map<string, string> m_users; // 用户名密码对
    int m_TRIGMode;              // 触发模式
    int m_close_log;             // 是否关闭log

    char sql_user[100];   // 用户名
    char sql_passwd[100]; // 用户密码
    char sql_name[100];   // 数据库名
};

2. 读取客户数据

// 循环读取客户数据,直到无数据可读或对方关闭连接
// 非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once()
{
    // 超出最大读缓存限制
    if (m_read_idx >= READ_BUFFER_SIZE)
    {
        return false;
    }
    // 标志有多少字节
    int bytes_read = 0;
    // LT读取数据
    if (0 == m_TRIGMode)
    {
        // 接收数据,保存到读缓存区
        bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
        // 修改m_read_idx的读取字节数
        m_read_idx += bytes_read;
        // 未读到数据
        if (bytes_read <= 0)
        {
            return false;
        }

        return true;
    }
    // ET读数据
    else
    {
        while (true)
        {
            // 接收数据,保存到读缓存区
            bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
            // 读取异常
            if (bytes_read == -1)
            {
                // 判断errno是否为重试或未发送完残留数据
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break;
                // 不是则出错
                return false;
            }
            // 读取为空
            else if (bytes_read == 0)
            {
                return false;
            }
            // 修改m_read_idx的读取字节数
            m_read_idx += bytes_read;
        }
        return true;
    }
}

2. epoll 事件相关

主要有四个:设置非阻塞模式、注册事件、删除事件、重置 EPOLLONESHOT 事件:

// 对文件描述符设置非阻塞
int setnonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

// 将内核事件表注册新事件,开启ET模式,选择开启EPOLLONESHOT
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;
    // 触发组合模式:ET模式
    if (1 == TRIGMode)
        event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    // 默认模式:LT监听、连接
    else
        event.events = EPOLLIN | EPOLLRDHUP;
    // one shot模式,保证一个socket只有一个线程操作
    if (one_shot)
        event.events |= EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

// 从内核事件表删除事件
void removefd(int epollfd, int fd)
{
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

// 将事件重置为EPOLLONESHOT事件
void modfd(int epollfd, int fd, int ev, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;

    if (1 == TRIGMode)
        event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    else
        event.events = ev | EPOLLONESHOT | EPOLLRDHUP;

    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

3. 接收 HTTP 请求

浏览器发出 HTTP 连接,主线程创建 HTTP 对象以接受请求,并将所有数据读入对应的缓存区,然后将该对象插入工作队列,工作线程从工作队列中取出一个任务进行处理:

//创建MAX_FD个http类对象
http_conn* users=new http_conn[MAX_FD];

//创建内核事件表
epoll_event events[MAX_EVENT_NUMBER];
epollfd = epoll_create(5);
assert(epollfd != -1);

//将listenfd放在epoll树上
addfd(epollfd, listenfd, false);

//将上述epollfd赋值给http类对象的m_epollfd属性
http_conn::m_epollfd = epollfd;

while (!stop_server)
{
    //等待所监控文件描述符上有事件的产生
    int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
    if (number < 0 && errno != EINTR)
    {
        break;
    }
    //对所有就绪事件进行处理
    for (int i = 0; i < number; i++)
    {
        int sockfd = events[i].data.fd;

        //处理新到的客户连接
        if (sockfd == listenfd)
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address);
//LT水平触发
#ifdef LT
            int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
            if (connfd < 0)
            {
                continue;
            }
            if (http_conn::m_user_count >= MAX_FD)
            {
                show_error(connfd, "Internal server busy");
                continue;
            }
            users[connfd].init(connfd, client_address);
#endif

//ET非阻塞边缘触发
#ifdef ET
            //需要循环接收数据
            while (1)
            {
                int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
                if (connfd < 0)
                {
                    break;
                }
                if (http_conn::m_user_count >= MAX_FD)
                {
                    show_error(connfd, "Internal server busy");
                    break;
                }
                users[connfd].init(connfd, client_address);
            }
            continue;
#endif
        }

        //处理异常事件
        else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
        {
            //服务器端关闭连接
        }

        //处理信号
        else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
        {
        }

        //处理客户连接上接收到的数据
        else if (events[i].events & EPOLLIN)
        {
            //读入对应缓冲区
            if (users[sockfd].read_once())
            {
                //若监测到读事件,将该事件放入请求队列
                pool->append(users + sockfd);
            }
            else
            {
               //服务器关闭连接
            }
        }
    }
}

4. HTTP 报文解析

HTTP 报文解析流程如下:

  • process_read 函数:通过 while 循环,将主从状态机进行封装,对报文的每一行进行处理:
    • 将从状态设为 LINE_OK ,作为循环的入口条件;
    • 首先在循环中会解析请求行;
    • 然后解析请求头,若是 GET 请求则到此为止,若是 POST 请求则继续;
    • 解析消息体,同时处理从状态防止再次解析;
    • 循环的判断条件有特殊含义,注释中已给出说明;
  • parse_line 函数:从状态机,负责解析一行数据:
    • 在 HTTP 报文中,每一行数据由 \r\n 作为结束,据此可以判断行;
    • 每次找到并处理 \r\n 后,将其置为 \0\0 ;
    • 读取中,若当前字符为 \r ,可能出现三种情况:
      • 下一个字符为 \n ,将 m_checked_idx 指向下一行的开头,返回 LINE_OK ;
      • 读到了缓存区末尾,标识还需要继续接收数据,返回 LINE_OPEN ;
      • 其他,标识语法错误,返回 LINE_BAD ;
    • 若当前字节为 \n ,则意味着中途没接收完整,对应上一种情况的第二条,此时判断前一个字符是否为 \r 即可;
    • 若当前字符不为上述两种情况,则接收不完整,返回 LINE_OPEN ;
    • 由于已将 \r\n 改为了 \0\0 ,因此主状态机可以直接进行字符串处理;
  • get_line 函数:用于将指针向后偏移,指向未处理的字符,即处理一行数据;
  • parse_request_line 函数:主状态机,负责解析 HTTP 请求的请求行:
    • 初始状态为 CHECK_STATE_REQUESTLINE ;
    • m_read_buf 中解析 HTTP 请求行,获取请求方法、目标 URL 和 HTTP 版本号;
    • 将状态改为 CHECK_STATE_HEADER ;
  • parse_headers 函数:主状态机,负责解析 HTTP 请求的请求头:
    • 由于请求头和空行的处理使用的同一个函数,因此需要通过根据当前的 text 首位是不是 \0 来判断是空行还是请求头;
    • 请求头有多行,因此需要根据具体含义进行解析;
    • 如果是 GET 请求,则到此为止;如果是 POST 请求,则还需解析消息体(将状态改为 CHECK_STATE_CONTENT);
  • parse_content 函数:主状态机,负责解析 HTTP 请求的消息体;
// 解析+响应的入口函数
void http_conn::process()
{
    // 报文解析,获取状态码
    HTTP_CODE read_ret = process_read();
    if (read_ret == NO_REQUEST)
    {
        // 注册并监听读事件
        modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
        return;
    }
    // 报文响应,是否成功
    bool write_ret = process_write(read_ret);
    if (!write_ret)
    {
        close_conn();
    }
    // 注册并监听写事件
    modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}

// 用于将指针向后偏移,指向未处理的字符,即处理一行数据
char* http_conn::get_line() { return m_read_buf + m_start_line; }; 

// 解析http请求,调用了主状态机、从状态机
http_conn::HTTP_CODE http_conn::process_read()
{
    // 初始化从状态机状态、HTTP请求解析结果
    LINE_STATUS line_status = LINE_OK;
    HTTP_CODE ret = NO_REQUEST;
    char *text = 0;

    // 循环处理,由从状态机驱动
    // 前一部分判断条件用于处理请求数据(消息体),因为这部分最后面没有\r\n,无法用从状态机判断
    // 前一部分判断条件主要使用主状态机来判断消息体,而因为这部分处理完后状态并没有发生改变,因此还需要从状态机来标记只处理一次
    while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
    {
        // 指向读缓存区目标行的位置
        text = get_line();

        //m_start_line是每一个数据行在m_read_buf中的起始位置
        //m_checked_idx表示从状态机在m_read_buf中读取的位置
        m_start_line = m_checked_idx;
        LOG_INFO("%s", text);

        // 主状态机三种状态转换
        switch (m_check_state)
        {
        // 解析请求行
        case CHECK_STATE_REQUESTLINE:
        {
            ret = parse_request_line(text);
            if (ret == BAD_REQUEST)
                return BAD_REQUEST;
            break;
        }
        // 解析请求头
        case CHECK_STATE_HEADER:
        {
            ret = parse_headers(text);
            if (ret == BAD_REQUEST)
                return BAD_REQUEST;
            //完整解析GET请求后,跳转到报文响应函数
            else if (ret == GET_REQUEST)
            {
                return do_request();
            }
            break;
        }
        // 解析消息体
        case CHECK_STATE_CONTENT:
        {
            ret = parse_content(text);
            //完整解析POST请求后,跳转到报文响应函数
            if (ret == GET_REQUEST)
                return do_request();
            line_status = LINE_OPEN;
            break;
        }
        // 都不是则表示服务器错误
        default:
            return INTERNAL_ERROR;
        }
    }
    return NO_REQUEST;
}

// 从状态机,用于分析出一行内容
// 返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
//m_read_idx指向缓冲区m_read_buf的数据末尾的下一个字节
//m_checked_idx指向从状态机当前正在分析的字节
http_conn::LINE_STATUS http_conn::parse_line()
{
    char temp;
    // 逐字节读
    for (; m_checked_idx < m_read_idx; ++m_checked_idx)
    {
        //temp为将要分析的字节
        temp = m_read_buf[m_checked_idx];
        //如果当前是\r字符,则有可能会读取到完整行
        if (temp == '\r')
        {
            //下一个字符达到了buffer结尾,则接收不完整,需要继续接收
            if ((m_checked_idx + 1) == m_read_idx)
                return LINE_OPEN;
            //下一个字符是\n,将\r\n改为\0\0
            else if (m_read_buf[m_checked_idx + 1] == '\n')
            {
                // 标记缓存结束,返回LINE_OK
                m_read_buf[m_checked_idx++] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            //如果都不符合,则返回语法错误
            return LINE_BAD;
        }
        //如果当前字符是\n,也有可能读取到完整行
        //一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况
        else if (temp == '\n')
        {
            //前一个字符是\r,则接收完整
            if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r')
            {
                m_read_buf[m_checked_idx - 1] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    // 没读到换行符和回车符,说明行未读完
    return LINE_OPEN;
}

// 主状态机,解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
    //在HTTP报文中,请求行用来说明请求类型,要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。
    //请求行中最先含有空格和\t任一字符的位置并返回
    m_url = strpbrk(text, " \t");

    //如果没有空格或\t,则报文格式有误
    if (!m_url)
    {
        return BAD_REQUEST;
    }
    
    //将该位置改为\0,用于将前面数据取出
    *m_url++ = '\0';
    
    //取出数据,并通过与GET和POST比较,以确定请求方式
    char *method = text;
    if (strcasecmp(method, "GET") == 0)
        m_method = GET;
    else if (strcasecmp(method, "POST") == 0)
    {
        m_method = POST;
        cgi = 1;
    }
    // 返回坏请求标志
    else
        return BAD_REQUEST;
    
    //m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有
    //将m_url向后偏移,通过查找,继续跳过空格和\t字符,指向请求资源的第一个字符
    m_url += strspn(m_url, " \t");
    
    //使用与判断请求方式的相同逻辑,判断HTTP版本号
    m_version = strpbrk(m_url, " \t");
    if (!m_version)
        return BAD_REQUEST;
    *m_version++ = '\0';
    // 移动到版本的位置进行标记
    m_version += strspn(m_version, " \t");

    //仅支持HTTP/1.1
    if (strcasecmp(m_version, "HTTP/1.1") != 0)
        return BAD_REQUEST;
    
    //对请求资源前7个字符进行判断
    //这里主要是有些报文的请求资源中会带有http://,这里需要对这种情况进行单独处理
    if (strncasecmp(m_url, "http://", 7) == 0)
    {
        m_url += 7;
        // 记录网站根目录后面的地址,包括/符号
        m_url = strchr(m_url, '/');
    }

    //同样增加https情况
    if (strncasecmp(m_url, "https://", 8) == 0)
    {
        m_url += 8;
        m_url = strchr(m_url, '/');
    }

    //一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源
    if (!m_url || m_url[0] != '/')
        return BAD_REQUEST;

    // //当url为/时,显示欢迎界面
    if (strlen(m_url) == 1)
        strcat(m_url, "judge.html");

    //请求行处理完毕,将主状态机转移处理请求头
    m_check_state = CHECK_STATE_HEADER;
    return NO_REQUEST;
}

// 主状态机,解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
    // 头部信息为空
    if (text[0] == '\0')
    {
        //判断是GET还是POST请求
        if (m_content_length != 0)
        {
            //POST需要跳转到消息体处理状态
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        // 状态为获取请求
        return GET_REQUEST;
    }
    //解析请求头部连接字段
    else if (strncasecmp(text, "Connection:", 11) == 0)
    {
        text += 11;
        //跳过空格和\t字符
        text += strspn(text, " \t");
        // 设置保持活跃标记
        if (strcasecmp(text, "keep-alive") == 0)
        {
            //如果是长连接,则将linger标志设置为true
            m_linger = true;
        }
    }
    //解析请求头部内容长度字段
    else if (strncasecmp(text, "Content-length:", 15) == 0)
    {
        text += 15;
        text += strspn(text, " \t");
        // 设置内容长度
        m_content_length = atol(text);
    }
    //解析请求头部HOST字段
    else if (strncasecmp(text, "Host:", 5) == 0)
    {
        text += 5;
        text += strspn(text, " \t");
        // 保存host
        m_host = text;
    }
    // 意外情况
    else
    {
        LOG_INFO("oop!unknow header: %s", text);
    }
    return NO_REQUEST;
}

// 主状态机,处理content字段,判断http请求是否被完整读入
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{
    //判断buffer中是否读取了消息体
    if (m_read_idx >= (m_content_length + m_checked_idx))
    {
        // 标记已读完的部分
        text[m_content_length] = '\0';
        // POST请求中最后为输入的用户名和密码
        m_string = text;
        // 还需读取请求
        return GET_REQUEST;
    }
    return NO_REQUEST;
}

5. HTTP 请求响应

HTTP 报文响应流程如下:

  • do_request 函数:对解析后的请求进行分析,并对 URL 进行处理,返回请求的状态:
    • / :GET 请求,跳转 judge.html ,即欢迎页面;
    • /0 :POST 请求,跳转 register.html ,即注册页面;
    • /1 :POST 请求,跳转 log.html ,即登录页面;
    • /2 :POST 请求,进行登录校验,成功跳转 welcome.html ,即资源请求成功页面;失败跳转 logError.html ,即登陆失败页面;
    • /3 :POST 请求,进行注册校验,跳转同上;
    • /5 :POST 请求,跳转 picture.html ,即图片请求页面;
    • /6 :POST 请求,跳转 video.html ,即视频请求页面;
    • /7 :POST 请求,跳转 fans.html ,即关注页面;
    • 若资源存在且访问正常,就将其映射到内存中准备发送;
  • add_response 函数:构造响应报文的公共接口,被各类消息报头构造函数调用;
  • process_write 函数:向 m_write_buf 中写入响应报文,响应报文分两种:
    • 一种是文件存在,通过 io 向量机制 iovec 声明 2 个 iovec ,第一个指向 m_write_buf ,第二个指向 mmap 的地址 m_file_address
    • 第二种是请求出错,只申请一个 iovec ;
    • 注册 EPOLLOUT 事件,服务器主线程监测到事件后调用 write 函数;
  • write 函数:将响应报文发送给浏览器端:
    • 根据已发送数据大小判断发送是否完成;
    • 根据 EAGAIN 判断缓冲区是否已满;
    • 每次发送数据后需要更新已发送字数;
    • 发送完成后需要重置 HTTP 对象并重置 EPOLLONESHOT 事件。
// 响应http请求,检验、分配、响应请求所需的资源
http_conn::HTTP_CODE http_conn::do_request()
{
    //将初始化的m_real_file赋值为网站根目录
    strcpy(m_real_file, doc_root);

    // 记录文件路径长度
    int len = strlen(doc_root);
    // printf("m_url:%s\n", m_url);

    //找到m_url中/的位置
    const char *p = strrchr(m_url, '/');

    // //实现登录和注册校验,cgi=1启用post
    // *(p+1):0注册POST、1登录POST、2登录校验POST、3注册校验POST、5picture页面POST、6video页面POST、7fans页面POST
    if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
    {
        // 根据标志判断是登录检测还是注册检测,即/符号后的第一位
        char flag = m_url[1];
        // 申请url空间
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        // 存入/
        strcpy(m_url_real, "/");
        // 存入/后的第二位之后的url
        strcat(m_url_real, m_url + 2);
        // 文件存储区在文件路径之后存入真实url
        strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
        // 释放真实url存储空间
        free(m_url_real);

        // 将用户名和密码提取出来
        // user=123&passwd=123
        char name[100], password[100];
        int i;
        for (i = 5; m_string[i] != '&'; ++i)
            name[i - 5] = m_string[i];
        name[i - 5] = '\0';

        int j = 0;
        for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
            password[j] = m_string[i];
        password[j] = '\0';
        // 表示注册
        if (*(p + 1) == '3')
        {
            // 如果是注册,先检测数据库中是否有重名的
            // 没有重名的,进行增加数据
            char *sql_insert = (char *)malloc(sizeof(char) * 200);
            strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
            strcat(sql_insert, "'");
            strcat(sql_insert, name);
            strcat(sql_insert, "', '");
            strcat(sql_insert, password);
            strcat(sql_insert, "')");
            // 没找到则insert数据
            if (users.find(name) == users.end())
            {
                m_lock.lock();
                int res = mysql_query(mysql, sql_insert);
                users.insert(pair<string, string>(name, password));
                m_lock.unlock();

                if (!res)
                    strcpy(m_url, "/log.html");
                else
                    strcpy(m_url, "/registerError.html");
            }
            else
                strcpy(m_url, "/registerError.html");
        }
        // 如果是登录,直接判断
        // 若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0
        else if (*(p + 1) == '2')
        {
            if (users.find(name) != users.end() && users[name] == password)
                strcpy(m_url, "/welcome.html");
            else
                strcpy(m_url, "/logError.html");
        }
    }
    //如果请求资源为/0,表示跳转注册界面
    if (*(p + 1) == '0')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/register.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    //如果请求资源为/1,表示跳转登录界面
    else if (*(p + 1) == '1')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/log.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    // 指向图片页面
    else if (*(p + 1) == '5')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/picture.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    // 指向视频页面
    else if (*(p + 1) == '6')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/video.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    // 指向粉丝页面
    else if (*(p + 1) == '7')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/fans.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    // 指向原始路径
    else
        strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
    
    //通过stat获取请求资源文件信息,成功则将信息更新到m_file_stat结构体
    //失败返回NO_RESOURCE状态,表示资源不存在
    if (stat(m_real_file, &m_file_stat) < 0)
        return NO_RESOURCE;

    //判断文件的权限,是否可读,不可读则返回FORBIDDEN_REQUEST状态
    if (!(m_file_stat.st_mode & S_IROTH))
        return FORBIDDEN_REQUEST;

    //判断文件类型,如果是目录,则返回BAD_REQUEST,表示请求报文有误
    if (S_ISDIR(m_file_stat.st_mode))
        return BAD_REQUEST;
    
    //以只读方式获取文件描述符,通过mmap将该文件映射到内存中
    int fd = open(m_real_file, O_RDONLY);
    m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    
    //避免文件描述符的浪费和占用
    close(fd);

    //表示请求文件存在,且可以访问
    return FILE_REQUEST;
}

// 构造响应报文的接口
bool http_conn::add_response(const char *format, ...)
{
    // 如果写入内容超出m_write_buf大小则报错
    if (m_write_idx >= WRITE_BUFFER_SIZE)
        return false;

    // 定义可变参数列表
    va_list arg_list;

    // 将变量arg_list初始化为传入参数
    va_start(arg_list, format);

    // 将数据format从可变参数列表写入缓冲区写,返回写入数据的长度
    int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);

    // 如果写入的数据长度超过缓冲区剩余空间,则报错
    if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
    {
        va_end(arg_list);
        return false;
    }

    // 更新m_write_idx位置
    m_write_idx += len;

    // 清空可变参列表
    va_end(arg_list);

    LOG_INFO("request:%s", m_write_buf);
    return true;
}

// 添加状态行的接口
bool http_conn::add_status_line(int status, const char *title)
{
    return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}

// 添加消息报头的接口,具体的添加文本长度、连接状态和空行
bool http_conn::add_headers(int content_len)
{
    return add_content_length(content_len) && add_linger() &&
           add_blank_line();
}

// 添加Content-Length的接口,表示响应报文的长度
bool http_conn::add_content_length(int content_len)
{
    return add_response("Content-Length:%d\r\n", content_len);
}

// 添加文本类型的接口,这里是html
bool http_conn::add_content_type()
{
    return add_response("Content-Type:%s\r\n", "text/html");
}

// 添加连接状态的接口,通知浏览器端是保持连接还是关闭
bool http_conn::add_linger()
{
    return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}

// 添加空行的接口
bool http_conn::add_blank_line()
{
    return add_response("%s", "\r\n");
}

// 添加文本content的接口
bool http_conn::add_content(const char *content)
{
    return add_response("%s", content);
}

// 构造响应报文,处理好各缓存区的指针,为发送数据做准备
bool http_conn::process_write(HTTP_CODE ret)
{
    // 根据HTTP状态码构造响应头
    switch (ret)
    {
    // 内部错误,500
    case INTERNAL_ERROR:
    {
        // 状态行
        add_status_line(500, error_500_title);
        // 消息报头
        add_headers(strlen(error_500_form));
        if (!add_content(error_500_form))
            return false;
        break;
    }
    // 报文语法有误,404
    case BAD_REQUEST:
    {
        add_status_line(404, error_404_title);
        add_headers(strlen(error_404_form));
        if (!add_content(error_404_form))
            return false;
        break;
    }
    // 资源没有访问权限,403
    case FORBIDDEN_REQUEST:
    {
        add_status_line(403, error_403_title);
        add_headers(strlen(error_403_form));
        if (!add_content(error_403_form))
            return false;
        break;
    }
    // 文件存在,200
    case FILE_REQUEST:
    {
        add_status_line(200, ok_200_title);
        // 如果请求的资源存在
        if (m_file_stat.st_size != 0)
        {
            // 初始化各种指针
            add_headers(m_file_stat.st_size);
            // 第一个iovec指针指向响应报文缓冲区,长度指向m_write_idx
            m_iv[0].iov_base = m_write_buf;
            m_iv[0].iov_len = m_write_idx;
            // 第二个iovec指针指向mmap返回的文件指针,长度指向文件大小
            m_iv[1].iov_base = m_file_address;
            m_iv[1].iov_len = m_file_stat.st_size;
            m_iv_count = 2;
            // 发送的全部数据为响应报文头部信息和文件大小
            bytes_to_send = m_write_idx + m_file_stat.st_size;
            return true;
        }
        else
        {
            // 如果请求的资源大小为0,则返回空白html文件
            const char *ok_string = "<html><body></body></html>";
            add_headers(strlen(ok_string));
            if (!add_content(ok_string))
                return false;
        }
    }
    default:
        return false;
    }
    // 除FILE_REQUEST状态外,其余状态只申请一个iovec,指向响应报文缓冲区
    m_iv[0].iov_base = m_write_buf;
    m_iv[0].iov_len = m_write_idx;
    m_iv_count = 1;
    bytes_to_send = m_write_idx;
    return true;
}

// 发送数据,即写入文件描述符
bool http_conn::write()
{
    int temp = 0;

    // 若要发送的数据长度为0
    // 表示响应报文为空,一般不会出现这种情况
    if (bytes_to_send == 0)
    {
        // 将事件重置为EPOLLONESHOT
        modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
        // 初始化
        init();
        return true;
    }
    // 循环处理
    while (1)
    {
        // 将响应报文的状态行、消息头、空行和响应正文发送给浏览器端
        temp = writev(m_sockfd, m_iv, m_iv_count);

        if (temp < 0)
        {
            if (errno == EAGAIN)
            {
                // 重新注册写事件
                modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
                return true;
            }
            unmap();
            return false;
        }
        // 正常发送,temp为发送的字节数
        bytes_have_send += temp;
        // 更新已发送字节数
        bytes_to_send -= temp;
        // 第一个iovec头部信息的数据已发送完,发送第二个iovec数据
        if (bytes_have_send >= m_iv[0].iov_len)
        {
            // 不再继续发送头部信息
            m_iv[0].iov_len = 0;
            m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
            m_iv[1].iov_len = bytes_to_send;
        }
        // 继续发送第一个iovec头部信息的数据
        else
        {
            m_iv[0].iov_base = m_write_buf + bytes_have_send;
            m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
        }
        // 判断条件,数据已全部发送完
        if (bytes_to_send <= 0)
        {
            // 释放内存
            unmap();
            // 在epoll树上重置EPOLLONESHOT事件
            modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
            // 浏览器的请求为长连接
            if (m_linger)
            {
                // 重新初始化HTTP对象
                init();
                return true;
            }
            // 不保持
            else
            {
                return false;
            }
        }
    }
}

参考文献

[1] 最新版Web服务器项目详解 - 04 http连接处理(上)
[2] 最新版Web服务器项目详解 - 05 http连接处理(中)
[3] 最新版Web服务器项目详解 - 06 http连接处理(下)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/527669.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenText 数据迁移解决方案的工作原理及其优势

OpenText Migrate 让迁移变得简单 选择正确的迁移技术您所需要了解的事情 无痛迁移 当谈到停机的常见原因时&#xff0c;灾难往往会得到最多的关注。 但灾难只是导致停机的一小部分原因。 计划的停机时间造成了资源的真正消耗&#xff0c;许多纯粹为灾难恢复应运而生的工具和…

【Simulink】 0基础入门教程 P2 常用模块的使用介绍

目录 常用模块介绍 (1) relational operator&#xff0c;用于数值的大小比较 (2) compare to constant&#xff0c;用于和数值做大小比较 (3)logical operator&#xff0c;用于逻辑运算 与运算 或运算 非运算 (4) switch&#xff0c;类似于C语言中的if 语句&#xff0c…

stm32 MCU液晶TM1622 HT1622驱动调试

本文使用的例程软件工程代码如下 (1条消息) stm32MCU液晶TM1622HT1622驱动调试&#xff0c;源代码&#xff0c;实际项目使用资源-CSDN文库 HT1622/HT1622G/TM1622是一款常用的LCD驱动芯片 TM1622/HT1622厂家不一样&#xff0c;但是芯片功能基本上一直&#xff0c;硬件上基本…

『C++』C++的类型转换

「前言」文章是关于C特殊类型转换 「归属专栏」C嘎嘎 「笔者」枫叶先生(fy) 「座右铭」前行路上修真我 「枫叶先生有点文青病」 「每篇一句」 有些事不是看到了希望才去坚持&#xff0c; 而是因为坚持才会看到希望。 ——《十宗罪》 目录 一、C语言中的类型转换 二、为什么C需…

tinyWebServer 学习笔记——三、定时器处理非活跃链接

文章目录 一、基础知识1. 概念2. API3. 信号处理机制 二、代码解析1. 信号处理函数2. 信号通知逻辑3. 定时器4. 定时器容器5. 定时任务处理函数6. 使用定时器 参考文献 一、基础知识 1. 概念 非活跃&#xff1a;指客户端与服务器建立连接后&#xff0c;长时间不交换数据&…

第二章 数据的表示和运算

1.进位计数制 其他进制转十进制 二进制<——> 八进制&#xff0c;十六进制 (注意&#xff1a;小数部分也是从右往左算十进制——>任意进制&#xff08;整数部分&#xff09; 十进制——>任意进制&#xff08;小数部分&#xff09; 十进制转二进制&#xff08;拼凑…

【gitee流水线实现自动化部署】

首先进入自己的gitee仓库 创建流水线 配置基本信息 名称标识 事件监听 -----触发条件 主要是任务排编内 vue前端则选择node构建 这些就是字面意思 若无特殊需求 按照默认的即可 构建完之后添加新任务 主机部署 选择部署 主机部署 添加主机组 新建主机组 自主导入 之后配…

配置Git

1.安装Git git官网 2.配置Git 在点击桌面上的Git Bash快捷图标中输入&#xff1a; 配置用户名&#xff1a; git config --global user.name "username" //&#xff08; "username"是自己的账户名&#xff0c;&#xff09; 配置邮箱&#xff1a; git…

Mac终端主题配置

如果你不想安装item2这类第三方终端&#xff0c;可以试试我下面的步骤&#xff0c;先上效果图&#xff0c;如果感觉还符合你的胃口&#xff0c;可以继续读下去啦!!! 1.下载item2的主题安装包 https://github.com/mbadolato/iTerm2-Color-Schemes 2.解压缩&#xff0c;打开…

电脑断电文件丢失如何找回?给你支几招!

电脑断电文件丢失如何找回&#xff1f;我好不容易熬夜加班做的活动方案&#xff0c;正当将U盘文件转移到笔记本电脑的时候&#xff0c;没有注意笔记本的电量&#xff0c;在转移数据的过程中突然断电了。我的电脑一下子就“熄”了&#xff0c;方案都没来得及保存。这真是一个悲剧…

06. git关联远程仓库

大家好&#xff0c;前面几节&#xff0c;我们用很长的篇幅介绍了git本地使用过程中的一些基本命令&#xff0c;本节开始&#xff0c;我们介绍通过远程仓库多人协作的时候&#xff0c;基本操作以及遇见的问题。 本节内容预告&#xff1a; 1、github 与gitlab简介 2、git本地连接…

【软考高项】项目范围管理中的需求跟踪矩阵说明

文章目录 需求跟踪矩阵的创建角色步骤 需求跟踪矩阵变更角色步骤 需求跟踪矩阵是把产品需求从其来源连接到能满足需求的可交付成果的一种表格。使用需求跟踪矩阵&#xff0c;把每个需求与业务目标或项目目标联系起来&#xff0c;有助于确保每个需求都具有业务价值。 需求跟踪矩…

从零开始Vue3+Element Plus的后台管理系统(三)——按需自动引入组件和unplugin-vue-components

按需导入Element Plus遇到页面卡顿问题 本项目使用Element Plus的方式是按需自动导入 首先安装unplugin-vue-components 和 unplugin-auto-import这两款插件 npm install -D unplugin-vue-components unplugin-auto-import然后把下列代码插入到你的 Vite 配置文件中 Vite# …

Salesforce Experience Cloud_体验云顾问认证考试-备考攻略 (内含模拟练习题)

Salesforce Experience Cloud顾问认证专为具有Experiences应用程序实施和咨询经验的顾问设计的&#xff0c;适用于使用Experience平台的声明性自定义功能展示其在设计、配置、构建和实施Salesforce Experience应用程序方面的技能和知识的备考者。 备考者需要有6个月的Experien…

周赛345(模拟、分类讨论、DFS求完全联通分量)

文章目录 周赛345[2682. 找出转圈游戏输家](https://leetcode.cn/problems/find-the-losers-of-the-circular-game/)模拟 [2683. 相邻值的按位异或](https://leetcode.cn/problems/neighboring-bitwise-xor/)方法一&#xff1a;分类讨论&#xff08;反向思考&#xff09;方法二…

Android 调用TTS语音引擎过程及问题记录

Android 调用TTS引擎过程及问题记录 前言 背景是需要在华为平板上部署一个能够进行相关中文语音提示的APP&#xff0c;华为系统为鸿蒙3.0&#xff0c;对应Android API 12. Android 调用TTS引擎 调用TTS引擎之前&#xff0c;首先要确认自己的设备中是否安装了相关的文本转语音…

从《水浒传》看项目管理

水浒传和项目管理&#xff0c;这两个看似毫不相关的话题&#xff0c;其实有着惊人的相似之处。你没听错&#xff0c;就是水浒传&#xff01;这个充满了江湖义气和刀光剑影的故事&#xff0c;竟然能给我们现代人提供一些关于项目管理的启示。别怀疑&#xff0c;跟我一起来看看吧…

nginx liunx最新版本安装flask部署

一、nginx安装 1.进入Nginx官网的资源下载页&#xff1a;http://nginx.org/en/download.html 2.下载nginx-1.22.1.tar.gz&#xff0c; 3解压&#xff1a; tar -zxvf nginx-1.22.1.tar.gz解压完成后会在当前目录下得到一个新的nginx文件夹 4.终端进入nginx文件夹目录&#x…

C++ -- AVL树插入实现和测试

文章目录 1. AVL树概念2. AVL树满足性质3. AVL节点结构4. 插入操作5. 检测6. 完整代码 1. AVL树概念 AVL树就是自平衡二叉查找树&#xff0c;为了解决二叉树退化为单链表&#xff0c;使得增删查改时间度为O(N)&#xff0c;这里采用控制平衡策略达到效率是O(logN)。 2. AVL树满足…

Golang结构体入门

目录 结构体基础 结构体示例 为结构体定义方法 组合结构体 嵌套结构体 指针结构体 匿名字段 面向对象 封装 继承 多态 结构体基础 1.结构体是值类型&#xff1a;在Go语言中&#xff0c;结构体是一种值类型&#xff0c;与数组和基本数据类型一样。当结构体被赋值给一…