http毗连处理

打印 上一主题 下一主题

主题 537|帖子 537|积分 1611

分析http类及请求接收

底子

epoll

epoll_create函数

  1. #include <sys/epoll.h>
  2. int epoll_create(int size)
复制代码
创建一个指示epoll内核变乱表的文件形貌符,该形貌符将用作其他epoll系统调用的第一个参数,size不起作用。
epoll_ctl函数

  1. #include <sys/epoll.h>
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
复制代码
该函数用于操作内核变乱表监控的文件形貌符上的变乱:注册、修改、删除


  • epfd:为epoll_creat的句柄
  • op:表示动作,用3个宏来表示:




    • EPOLL_CTL_ADD (注册新的fd到epfd),
    • EPOLL_CTL_MOD (修改已经注册的fd的监听变乱),
    • EPOLL_CTL_DEL (从epfd删除一个fd);



  • event:告诉内核须要监听的变乱
上述event是epoll_event结构体指针类型,表示内核所监听的变乱,具体定义如下:
  1. typedef union epoll_data {
  2.       void *ptr;
  3.        int fd;
  4.       uint32_t u32;
  5.       uint64_t u64;
  6. } epoll_data_t;
  7. struct epoll_event {
  8.       uint32_t events; /* Epoll 监视的事件类型 */
  9.       epoll_data_t data; /* 用户数据 */
  10. };
复制代码
events: 变乱聚集
通过位掩码的方式表示不同的变乱,可以同时设置多个,通过“|” 毗连,可选项如下。
变乱类型
形貌
EPOLLIN
文件形貌符是否可读
EPOLLOUT
文件形貌符是否可写
EPOLLRDHUP
对端关闭毗连(被动),或者套接字处于半关闭状态(主动),这个变乱会被触发。当使用边缘触发模式时,很方便写代码测试毗连的对端是否关闭了毗连
EPOLLPRI
文件形貌符是否非常
EPOLLERR
文件形貌符是否错误。如果文件形貌符已经关闭,继续写入也会收到这个变乱。这个变乱用户不设置也会被上报
EPOLLHUP
套接字被挂起,这个变乱用户不设置也会被上报
EPOLLET
设置epoll的触发模式为边缘触发模式。如果没有设置这个参数,epoll默认环境下是水平触发模式
EPOLLONESHOT
设置添加的变乱只触发一次,当epoll_wait(2)报告一次变乱后,这个文件形貌符后续所有的变乱都不会再报告。只是禁用,文件形貌符还在监督队列中,用户可以通过epoll_ctl()的EPOLL_CTL_MOD重新添加变乱
  1. #include <sys/epoll.h>
  2. int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout)
复制代码
该函数用于等待所监控文件形貌符上有变乱的产生,返回停当的文件形貌符个数


  • events:用来存内核得到变乱的聚集,
  • maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
  • timeout:是超时时间




    • -1:壅闭
    • 0:立即返回,非壅闭
    • >0:指定毫秒



  • 返回值:成功返回有多少文件形貌符停当,时间到时返回0,出错返回-1
select/poll/epoll



  • 调用函数




    • select和poll都是一个函数,epoll是一组函数



  • 文件形貌符数量




    • select通过线性表形貌文件形貌符聚集,文件形貌符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
    • poll是链表形貌,突破了文件形貌符上限,最大可以打开文件的数目
    • epoll通过红黑树形貌,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效



  • 将文件形貌符从用户传给内核




    • select和poll通过将所有文件形貌符拷贝到内核态,每次调用都须要拷贝
    • epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件形貌符注册到红黑树上



  • 内核判断停当的文件形貌符




    • select和poll通过遍历文件形貌符聚集,判断哪个文件形貌符上有变乱发生
    • epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备停当的变乱,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
    • epoll是根据每个fd上面的回调函数(停止函数)判断,只有发生了变乱的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是停当变乱,插入list



  • 应用程序索引停当文件形貌符




    • select/poll只返回发生了变乱的文件形貌符的个数,若知道是哪个发生了变乱,同样须要遍历
    • epoll返回的发生了变乱的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可



  • 工作模式




    • select和poll都只能工作在相对低效的LT模式下
    • epoll则可以工作在ET高效模式,而且epoll还支持EPOLLONESHOT变乱,该变乱能进一步减少可读、可写和非常变乱被触发的次数。



  • 应用场景




    • 当所有的fd都是活泼毗连,使用epoll,须要建立文件系统,红黑书和链表对于此来说,服从反而不高,不如selece和poll
    • 当监测的fd数目较小,且各个fd都比力活泼,建议使用select或者poll
    • 当监测的fd数目非常大,成千上万,且单位时间只有此中的一部分fd处于停当状态,这个时候使用epoll能够明显提升性能

ET、LT、EPOLLONESHOT



  • LT水平触发模式




    • epoll_wait检测到文件形貌符有变乱发生,则将其关照给应用程序,应用程序可以不立即处理该变乱。
    • 当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此变乱,直至被处理



  • ET边缘触发模式




    • epoll_wait检测到文件形貌符有变乱发生,则将其关照给应用程序,应用程序必须立即处理该变乱
    • 必须要一次性将数据读取完,使用非壅闭I/O,读取到出现eagain



  • EPOLLONESHOT




    • 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
    • 我们盼望的是一个socket毗连在任一时候都只被一个线程处理,通过epoll_ctl对该文件形貌符注册epolloneshot变乱,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,须要通过epoll_ctl重置epolloneshot变乱

HTTP报文格式

HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式天生,才能被浏览器端识别。
此中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。
请求报文

HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分构成。
此中,请求分为两种,GET和POST,具体的:


  • GET
  1. 1    GET /562f25980001b1b106000338.jpg HTTP/1.1
  2. 2    Host:img.mukewang.com
  3. 3    User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
  4. 4    AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
  5. 5    Accept:image/webp,image/*,*/*;q=0.8
  6. 6    Referer:http://www.imooc.com/
  7. 7    Accept-Encoding:gzip, deflate, sdch
  8. 8    Accept-Language:zh-CN,zh;q=0.8
  9. 9    空行
  10. 10    请求数据为空
复制代码


  • POST
  1. 1    POST / HTTP1.1
  2. 2    Host:www.wrox.com
  3. 3    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)
  4. 4    Content-Type:application/x-www-form-urlencoded
  5. 5    Content-Length:40
  6. 6    Connection: Keep-Alive
  7. 7    空行
  8. 8    name=Professional%20Ajax&publisher=Wiley
复制代码

 


  • 请求行,用来阐明请求类型,要访问的资源以及所使用的HTTP版本。
    GET阐明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的末了一部分阐明使用的是HTTP1.1版本。
  • 请求头部,紧接着请求行(即第一行)之后的部分,用来阐明服务器要使用的附加信息。




    • HOST,给出请求资源所在服务器的域名。
    • User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,而且在每个请求中主动发送等。
    • Accept,阐明用户代理可处理的媒体类型。
    • Accept-Encoding,阐明用户代理支持的内容编码。
    • Accept-Language,阐明用户代理能够处理的自然语言集。
    • Content-Type,阐明实现主体的媒体类型。
    • Content-Length,阐明实现主体的巨细。
    • Connection,毗连管理,可以是Keep-Alive或close。



  • 空行,请求头部背面的空行是必须的纵然第四部分的请求数据为空,也必须有空行。
  • 请求数据也叫主体,可以添加恣意的其他数据。
响应报文

HTTP响应也由四个部分构成,分别是:状态行、消息报头、空行和响应正文。
  1. 1HTTP/1.1 200 OK
  2. 2Date: Fri, 22 May 2009 06:07:21 GMT
  3. 3Content-Type: text/html; charset=UTF-8
  4. 4空行
  5. 5<html>
  6. 6      <head></head>
  7. 7      <body>
  8. 8            <!--body goes here-->
  9. 9      </body>
  10. 10</html>
复制代码


  • 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分构成。
    第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
  • 消息报头,用来阐明客户端要使用的一些附加信息。
    第二行和第三行为消息报头,Date:天生响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。
  • 空行,消息报头背面的空行是必须的。
  • 响应正文,服务器返回给客户端的文本信息。空行背面的html部分为响应正文。
HTTP状态码

HTTP有5种类型的状态码,具体的:


  • 1xx:指示信息--表示请求已接收,继续处理。
  • 2xx:成功--表示请求正常处理完毕。




    • 200 OK:客户端请求被正常处理。
    • 206 Partial content:客户端进行了范围请求。



  • 3xx:重定向--要完成请求必须进行更进一步的操作。




    • 301 Moved Permanently:永世重定向,该资源已被永世移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
    • 302 Found:临时重定向,请求的资源如今临时从不同的URI中得到。



  • 4xx:客户端错误--请求有语法错误,服务器无法处理请求。




    • 400 Bad Request:请求报文存在语法错误。
    • 403 Forbidden:请求被服务器拒绝。
    • 404 Not Found:请求不存在,服务器上找不到请求的资源。



  • 5xx:服务器端错误--服务器处理请求出错。




    • 500 Internal Server Error:服务器在实行请求时出现错误。

有限状态机

有限状态机,是一种抽象的理论模子,它能够把有限个变量形貌的状态变化过程,以可构造可验证的方式呈现出来。好比,封闭的有向图。
有限状态机可以通过if-else,switch-case和函数指针来实现,从软件工程的角度看,主要是为了封装逻辑。
带有状态转移的有限状态机示例代码。
  1. 1STATE_MACHINE(){
  2. 2    State cur_State = type_A;
  3. 3    while(cur_State != type_C){
  4. 4        Package _pack = getNewPackage();
  5. 5        switch(){
  6. 6            case type_A:
  7. 7                process_pkg_state_A(_pack);
  8. 8                cur_State = type_B;
  9. 9                break;
  10. 10            case type_B:
  11. 11                process_pkg_state_B(_pack);
  12. 12                cur_State = type_C;
  13. 13                break;
  14. 14        }
  15. 15    }
  16. 16}
复制代码
该状态机包含三种状态:type_A,type_B和type_C。此中,type_A是初始状态,type_C是竣事状态。
状态机的当前状态记录在cur_State变量中,逻辑处理时,状态机先通过getNewPackage获取数据包,然后根据当前状态对数据进行处理,处理完后,状态机通过改变cur_State完成状态转移。
有限状态机一种逻辑单位内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清楚易懂。
http处理流程

首先对http报文处理的流程进行简要先容,然后具体先容http类的定义和服务器接收http请求的具体过程。
http报文处理流程



  • 浏览器端发出http毗连请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
  • 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行剖析。
  • 剖析完之后,跳转do_request函数天生响应报文,通过process_write写入buffer,返回给浏览器端。
http类

主要是http类的定义。
stat结构体
stat 结构体是用于获取文件或文件系统信息的一个结构体,在大多数基于Unix的操作系统中,如Linux。stat 结构体包含了文件的详细信息,包括文件的属性、巨细、时间戳等。以下是 stat 结构体中包含的主要字段信息:
  1. struct stat {
  2.     dev_t     st_dev;     // 文件所属设备的ID
  3.     ino_t     st_ino;     // 文件的节点号(inode number)
  4.     mode_t    st_mode;    // 文件的类型和权限
  5.     nlink_t   st_nlink;   // 硬链接的数量
  6.     uid_t     st_uid;     // 文件所有者的用户ID
  7.     gid_t     st_gid;     // 文件所有者的组ID
  8.     dev_t     st_rdev;    // 设备ID(如果是特殊设备)
  9.     off_t     st_size;    // 文件的总大小,以字节为单位
  10.     blksize_t st_blksize; // 文件系统I/O的块大小
  11.     blkcnt_t  st_blocks;  // 分配给文件的块数
  12.     time_t    st_atime;   // 文件最后一次访问的时间(access time)
  13.     time_t    st_mtime;   // 文件最后一次修改的时间(modification time)
  14.     time_t    st_ctime;   // 文件最后一次状态更改的时间(change time)
  15. };
复制代码
在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化
  1. // 初始化HTTP连接对象
  2. void http_conn::init() {
  3.     // 将MySQL连接设置为nullptr,确保没有数据库连接
  4.     mysql = nullptr;
  5.    
  6.     // 初始化发送缓冲区字节数
  7.     bytes_to_send=  0;
  8.    
  9.     // 初始化已发送字节数
  10.     bytes_have_send = 0;
  11.    
  12.     // 设置请求解析状态为请求行检测
  13.     m_check_state = CHECK_STATE_REQUESTLINE;
  14.    
  15.     // 是否允许linger的标志位,初始化为false
  16.     m_linger = false;
  17.    
  18.     // 请求方法默认为GET
  19.     m_method = GET;
  20.    
  21.     // 请求URL和版本号初始化
  22.     m_url = 0;
  23.     m_version = 0;
  24.    
  25.     // 内容长度、主机、起始行、检查索引、读索引、写索引和CGI标志初始化
  26.     m_content_length = 0;
  27.     m_host = 0;
  28.     m_start_line = 0;
  29.     m_checked_idx = 0;
  30.     m_read_idx = 0;
  31.     m_write_idx = 0;
  32.     cgi = 0;
  33.    
  34.     // 状态和定时器标志初始化
  35.     m_state = 0;
  36.     timer_flag = 0;
  37.    
  38.     // 优化标志初始化
  39.     improv = 0;
  40.    
  41.     // 清空读缓冲区
  42.     memset(m_read_buf, '\0', READ_BUFFER_SIZE);
  43.    
  44.     // 清空写缓冲区
  45.     memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
  46.    
  47.     // 清空文件名缓存区
  48.     memset(m_real_file, '\0', FILENAME_LEN);
  49. }
复制代码
这里,对read_once进行先容。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭毗连,读取到m_read_buffer中,并更新m_read_idx。
  1. /**
  2. * @brief 从客户端套接字读取数据
  3. *
  4. * 该函数根据m_TRIGMode的值选择使用LT(水平触发)或ET(边缘触发)模式读取数据。
  5. * 在LT模式下,一旦recv函数返回就结束读取操作。
  6. * 在ET模式下,会持续调用recv直到recv返回EAGAIN或EWOULDBLOCK错误,表示没有更多数据可读。
  7. *
  8. * @return true 成功读取到数据或全部数据读取完毕
  9. * @return false 发生错误或连接关闭
  10. */
  11. bool http_conn::read_once() {
  12.     // 检查读缓冲区是否已满
  13.     if (m_read_idx >= READ_BUFFER_SIZE) {
  14.         return false;
  15.     }
  16.     int bytes_read = 0;
  17.     // LT模式读取数据
  18.     if (0 == m_TRIGMode) {
  19.         bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
  20.         m_read_idx += bytes_read;
  21.         // 如果recv返回非正数,视为错误或连接关闭
  22.         if (bytes_read <= 0) {
  23.             return false;
  24.         }
  25.         return true;
  26.     }
  27.     // ET模式读取数据
  28.     else {
  29.         while (true) {
  30.             bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
  31.             // 如果recv返回-1且错误码为EAGAIN或EWOULDBLOCK,表示没有更多数据可读
  32.             if (bytes_read == -1) {
  33.                 if (errno == EAGAIN || errno == EWOULDBLOCK) {
  34.                     break;
  35.                 }
  36.                 return false;
  37.             }
  38.             // 如果recv返回0,视为连接关闭
  39.             else if (bytes_read == 0) {
  40.                 return false;
  41.             }
  42.             m_read_idx += bytes_read;
  43.         }
  44.         return true;
  45.     }
  46. }
复制代码
epoll相干代码

项目中epoll相干代码部分包括非壅闭模式、内核变乱表注册变乱、删除变乱、重置EPOLLONESHOT变乱四种。


  • 非壅闭模式
  1. /**
  2. * 设置文件描述符为非阻塞模式
  3. *
  4. * 此函数的目的是将给定文件描述符(fd)的模式从阻塞改为非阻塞
  5. * 在非阻塞模式下,I/O 操作不会因为等待数据而阻塞当前线程
  6. *
  7. * @param fd 要设置为非阻塞模式的文件描述符
  8. * @return 返回更改前的文件描述符的阻塞选项
  9. */
  10. int setnonblocking(int fd) {
  11.     // 获取文件描述符的当前状态
  12.     int old_option = fcntl(fd, F_GETFL);
  13.     // 新选项是在原有选项上加上非阻塞标志
  14.     int new_option = old_option | O_NONBLOCK;
  15.     // 设置文件描述符为非阻塞模式
  16.     fcntl(fd, F_SETFL, new_option);
  17.     // 返回更改前的阻塞选项
  18.     return old_option;
  19. }
复制代码
内核变乱表注册新变乱,开启EPOLLONESHOT,针对客户端毗连的形貌符,listenfd不消开启


  • 内核变乱表删除变乱
  1. /**
  2. * 从epoll句柄中移除文件描述符fd,并关闭该fd。
  3. *
  4. * @param epollfd epoll创建的文件描述符。
  5. * @param fd 需要移除的文件描述符。
  6. *
  7. * 该函数首先使用epoll_ctl函数将指定的文件描述符从epoll监控表中移除,
  8. * 然后关闭该文件描述符。这一操作通常用于不再需要监控的文件描述符,
  9. * 以减少系统资源的占用并更新epoll监控列表。
  10. */
  11. void removefd(int epollfd, int fd) {
  12.     epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
  13.     close(fd);
  14. }
复制代码
重置EPOLLONESHOT变乱
  1. //将事件重置为EPOLLONESHOT
  2. /**
  3. * 修改文件描述符在epoll中的事件类型
  4. *
  5. * @param epollfd epoll文件描述符
  6. * @param fd 需要修改的文件描述符
  7. * @param ev 新的事件类型
  8. * @param TRIGMode 触发模式,1为边缘触发模式,否则为水平触发模式
  9. *
  10. * 此函数通过epoll_ctl函数修改文件描述符fd在epoll中的事件类型根据TRIGMode的不同,
  11. * 设置不同的事件类型如果TRIGMode为1,将事件类型设置为边缘触发模式(EPOLLET),
  12. * 否则使用默认的水平触发模式在两种模式下,都会设置事件为一次性(EPOLLONESHOT)和关闭远程挂起(EPOLLRDHUP)
  13. */
  14. void modfd(int epollfd, int fd, int ev, int TRIGMode) {
  15.     epoll_event event;
  16.     event.data.fd = fd;
  17.     if (1 == TRIGMode) {
  18.         event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
  19.     }
  20.     else {
  21.         event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
  22.     }
  23.     epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
  24. }
复制代码
服务器接收http请求

浏览器端发出http毗连请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
  1. // 事件循环函数,处理所有事件,包括新客户端连接、读写事件等
  2. void WebServer::eventLoop() {
  3.     // 用于标识是否超时,用于定时器处理
  4.     bool timeout = false;
  5.     // 用于标识是否停止服务器
  6.     bool stop_server = false;
  7.     // 主循环,不断轮询和处理事件,直到stop_server为true
  8.     while (!stop_server) {
  9.         // 调用epoll_wait等待事件发生,m_epollfd为epoll句柄,events为事件数组,MAX_EVENT_NUMBER为数组大小,-1表示不超时
  10.         int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
  11.         // 如果epoll_wait返回值小于0且不是因为中断引起,则视为epoll出错
  12.         if (number < 0 && errno != EINTR ) {
  13.             LOG_ERROR("%s", "epoll failure");
  14.             break;
  15.         }
  16.         // 遍历发生的事件数组,处理每一个事件
  17.         for (int i = 0; i < number; i++) {
  18.             int sockfd = events[i].data.fd;
  19.             // 如果事件对应的socket为监听socket,则有新的客户端连接请求
  20.             if (sockfd == m_listenfd) {
  21.                 bool flag = dealclientdata();
  22.                 // 如果处理客户端数据失败,则跳过当前循环,继续等待其他事件
  23.                 if (false == flag)
  24.                     continue;
  25.             }
  26.             // 如果事件为挂起读、连接关闭或错误,则处理对应的定时器
  27.             else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
  28.                 util_timer *timer = users_timer[sockfd].timer;
  29.                 deal_timer(timer, sockfd);
  30.             }
  31.             // 如果事件对应的socket为管道的读端且有读事件,则处理信号
  32.             else if((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN)) {
  33.                 bool flag = dealwithsignal(timeout, stop_server);
  34.                 // 如果处理信号失败,则记录错误日志
  35.                 if (false == flag) {
  36.                     LOG_ERROR("%s", "deal client data failure");
  37.                 }
  38.             }
  39.             // 如果事件为读事件,则处理读操作
  40.             else if (events[i].events & EPOLLIN) {
  41.                 dealwithread(sockfd);
  42.             }
  43.             // 如果事件为写事件,则处理写操作
  44.             else if(events[i].events & EPOLLOUT) {
  45.                 dealwithwrite(sockfd);
  46.             }
  47.         }
  48.         // 如果有超时发生,则处理定时器,并记录信息
  49.         if (timeout) {
  50.             utils.timer_handler();
  51.             LOG_INFO("%s", "timer tick");
  52.             timeout = false;
  53.         }
  54.     }
  55. }
复制代码
上篇,我们对http毗连的底子知识、服务器接收请求的处理流程进行了先容,本篇将团结流程图和代码分别对状态机和服务器剖析请求报文进行详解。
状态机和服务器剖析请求报文

流程图部分,形貌主、从状态机调用关系与状态转移过程。
代码部分,团结代码对http请求报文的剖析进行详解。
流程图与状态机

从状态机负责读取报文的一行,主状态机负责对该行数据进行剖析,主状态机内部调用从状态机,从状态机驱动主状态机。


主状态机

三种状态,标识剖析位置。


  • CHECK_STATE_REQUESTLINE,剖析请求行
  • CHECK_STATE_HEADER,剖析请求头
  • CHECK_STATE_CONTENT,剖析消息体,仅用于剖析POST请求
从状态机

三种状态,标识剖析一行的读取状态。


  • LINE_OK,完备读取一行
  • LINE_BAD,报文语法有误
  • LINE_OPEN,读取的行不完备
代码分析-http报文剖析

上篇中先容了服务器接收http请求的流程与细节,简单来讲,浏览器端发出http毗连请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列后,工作线程从任务队列中取出一个任务进行处理。
各子线程通过process函数对任务进行处理,调用process_read函数和process_write函数分别完成报文剖析与报文响应两个任务。
  1. // 处理HTTP请求的主函数
  2. // 该函数负责整体控制HTTP请求的读取和写入过程
  3. void http_conn::process() {
  4.     // 尝试读取HTTP请求,并返回读取状态
  5.     HTTP_CODE read_ret = process_read();
  6.    
  7.     // 如果请求信息不完整或未准备好,不需要立即处理
  8.     if (read_ret == NO_REQUEST) {
  9.         // 调整epoll监听模式为读事件,等待更多数据到来
  10.         modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
  11.         return;
  12.     }
  13.    
  14.     // 处理写入HTTP响应,并返回写入状态
  15.     bool write_ret = process_write(read_ret);
  16.    
  17.     // 如果写入失败,关闭连接
  18.     if (!write_ret) {
  19.         close_conn();
  20.     }
  21.    
  22.     // 调整epoll监听模式为写事件,等待数据写入
  23.     modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
  24. }
复制代码
HTTP_CODE含义

表示HTTP请求的处理结果,在头文件中初始化了八种情形
  1. // 定义HTTP请求的可能状态码
  2. enum HTTP_CODE {
  3.     NO_REQUEST,      // 尚未收到请求
  4.     GET_REQUEST,     // 成功接收GET请求
  5.     BAD_REQUEST,     // 请求语法错误,无法解析
  6.     NO_REQUEST,      // 重复定义,可能是错误?
  7.     FORBIDDEN_REQUEST, // 请求资源禁止访问
  8.     FILE_REQUEST,    // 请求的资源为文件
  9.     INTERNAL_ERROR,  // 服务器内部错误
  10.     CLOSED_CONNECTION // 连接已关闭
  11. };
复制代码
在报文剖析时只涉及到四种。


  • NO_REQUEST




    • 请求不完备,须要继续读取请求报文数据



  • GET_REQUEST




    • 得到了完备的HTTP请求



  • BAD_REQUEST




    • HTTP请求报文有语法错误



  • INTERNAL_ERROR




    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

剖析报文整体流程

process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。


  • 判断条件




    • 主状态机转移到CHECK_STATE_CONTENT,该条件涉及剖析消息体
    • 从状态机转移到LINE_OK,该条件涉及剖析请求行和请求头部
    • 两者为或关系,当条件为真则继续循环,否则退出



  • 循环体




    • 从状态机读取数据
    • 调用get_line函数,通过m_start_line将从状态机读取数据间接赋给text
    • 主状态机剖析text

  1. http_conn::HTTP_CODE http_conn::process_read(){
  2.     // 初始化行状态为正常
  3.     LINE_STATUS line_status = LINE_OK;
  4.     // 初始化返回值为未接收到请求
  5.     HTTP_CODE ret = NO_REQUEST;
  6.     // 定义一个字符指针用于存储读取的文本行
  7.     char* text=  0;
  8. //     ● 判断条件
  9. //   ○ 主状态机转移到CHECK_STATE_CONTENT,该条件涉及解析消息体
  10. //   ○ 从状态机转移到LINE_OK,该条件涉及解析请求行和请求头部
  11. //   ○ 两者为或关系,当条件为真则继续循环,否则退出
  12.     while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK)) {
  13.         // 获取解析后的文本行
  14.         text = get_line();
  15.         // 更新下一次解析的起始位置
  16.         m_start_line = m_checked_idx;
  17.         // 输出日志信息,打印解析的文本行
  18.         LOG_INFO("%s", text);
  19.         // 根据当前的解析状态,分别处理不同的HTTP请求部分
  20.         switch(m_check_state) {
  21.             // 解析请求行
  22.             case CHECK_STATE_REQUESTLINE: {
  23.                 // 解析请求行文本,返回解析结果
  24.                 ret = parse_request_line(text);
  25.                 // 如果解析出错,返回错误
  26.                 if (ret == BAD_REQUEST)
  27.                     return BAD_REQUEST;
  28.                 break;
  29.             }
  30.             // 解析请求头
  31.             case CHECK_STATE_HEADER: {
  32.                 // 解析请求头文本,返回解析结果
  33.                 ret = parse_headers(text);
  34.                 // 如果解析出错,返回错误
  35.                 if (ret == BAD_REQUEST)
  36.                     return BAD_REQUEST;
  37.                 // 如果解析完成,成功获取请求,处理请求
  38.                 else if (ret == GET_REQUEST) {
  39.                     return do_request();
  40.                 }
  41.                 break;
  42.             }
  43.             // 解析请求内容
  44.             case CHECK_STATE_CONTENT: {
  45.                 // 解析请求内容文本,返回解析结果
  46.                 ret = parse_content(text);
  47.                 // 如果解析出错,返回错误
  48.                 if (ret == BAD_REQUEST)
  49.                     return BAD_REQUEST;
  50.                 // 保持行状态为打开,继续解析内容
  51.                 line_status = LINE_OPEN;
  52.                 break;
  53.             }
  54.             // 默认情况下,返回内部错误
  55.             default:
  56.                 return INTERNAL_ERROR;
  57.         }
  58.     }
  59.     // 如果解析未完成,返回未请求状态
  60.     return NO_REQUEST;
  61. }
复制代码
从状态机逻辑

在HTTP报文中,每一行的数据由\r\n作为竣事字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行剖析,项目中便是使用了这一点。
从状态机负责读取buffer中的数据,将每行数据末端的\r\n置为\0\0,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机剖析。


  • 从状态机从m_read_buf中逐字节读取,判断当前字节是否为\r




    • 接下来的字符是\n,将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
    • 接下来达到了buffer末端,表示buffer还须要继续接收,返回LINE_OPEN
    • 否则,表示语法错误,返回LINE_BAD



  • 当前字节不是\r,判断是否是\n(一般是前次读取到\r就到了buffer末端,没有接收完备,再次接收时会出现这种环境




    • 如果前一个字符是\r,则将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK



  • 当前字节既不是\r,也不是\n




    • 表示接收不完备,须要继续接收,返回LINE_OPEN

  1. /**
  2. * 解析一行数据
  3. *
  4. * 本函数负责解析接收缓冲区中的数据,以判断是否成功接收到一行完整的数据
  5. * 主要处理以下几种情况:
  6. * 1. 行数据完整且以'\r'\n'结束
  7. * 2. 行数据只有'\n'没有'\r'
  8. * 3. 数据未接收完,需要继续接收
  9. *
  10. * @return 返回解析状态,可能为LINE_OK(解析成功)、LINE_BAD(解析失败)、LINE_OPEN(数据未接收完)
  11. */
  12. http_conn::LINE_STATUS http_conn::parse_line() {
  13.     char temp;
  14.     // 遍历已接收的数据,直到找到行结束符或处理完所有数据
  15.     for (; m_checked_idx < m_read_idx; ++m_checked_idx  ) {
  16.         temp = m_read_buf[m_checked_idx];
  17.         // 如果找到'\r',需要检查接下来是否是'\n'
  18.         if (temp == '\r') {
  19.             // 如果是文件结束符,返回LINE_OPEN
  20.             if ((m_checked_idx + 1) == m_read_idx)
  21.                 return LINE_OPEN;
  22.             // 如果接下来是'\n',则正确结束这一行
  23.             else if (m_read_buf[m_checked_idx + 1] == '\n') {
  24.                 // 将行结束符置为字符串结束符,连续两个行结束符都需处理
  25.                 m_read_buf[m_checked_idx++] = '\0';
  26.                 m_read_buf[m_checked_idx++] = '\0';
  27.                 return LINE_OK;
  28.             }
  29.             // 单独的'\r'被认为是错误的
  30.             return LINE_BAD;
  31.         }
  32.         // 如果找到单独的'\n',需要检查上一个是否应该是'\r'
  33.         else if (temp == '\n') {
  34.             // 如果前一个是'\r',则正确结束这一行
  35.             if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == 'r') {
  36.                 // 将行结束符置为字符串结束符,并跳过'\n'
  37.                 m_read_buf[m_checked_idx - 1] = '\0';
  38.                 m_read_buf[m_checked_idx ++] = '\0';
  39.                 return LINE_OK;
  40.             }
  41.             // 单独的'\n'被认为是错误的
  42.             return LINE_BAD;
  43.         }
  44.     }
  45.     // 数据未接收完,需要继续接收
  46.     return LINE_OPEN;
  47. }
复制代码
主状态机逻辑

主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行剖析前,从状态机已经将每一行的末端\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。


  • CHECK_STATE_REQUESTLINE




    • 主状态机的初始状态,调用parse_request_line函数剖析请求行
    • 剖析函数从m_read_buf中剖析HTTP请求行,得到请求方法、目标URL及HTTP版本号
    • 剖析完成后主状态机的状态变为CHECK_STATE_HEADER

  1. /**
  2. * 解析请求行
  3. *
  4. * @param text 请求行的文本
  5. * @return 请求的处理状态
  6. *
  7. * 请求行的格式为:方法 URL HTTP版本
  8. * 该函数解析传入的请求行文本,提取并验证方法、URL和HTTP版本
  9. * 如果解析过程中遇到不符合HTTP规范的请求,返回BAD_REQUEST
  10. */
  11. http_conn::HTTP_CODE http_conn::parse_request_line(char* text) {
  12.     // 提取URL
  13.     m_url = strpbrk(text, " \t");
  14.     if (!m_url) {
  15.         return BAD_REQUEST;
  16.     }
  17.     *m_url++ = '\0';
  18.     char * method = text;
  19.     // 验证并提取请求方法
  20.     if (strcasecmp(method, "GET") == 0) {
  21.         m_method = GET;
  22.     }
  23.     else if (strcasecmp(method, "POST") == 0) {
  24.         m_method = POST;
  25.         cgi = 1;
  26.     }
  27.     else
  28.         return BAD_REQUEST;
  29.     // 跳过URL中的空白字符
  30.     m_url += strspn(m_url, " \t");
  31.     // 提取HTTP版本
  32.     m_version = strpbrk(m_url, " \t");
  33.     if (!m_version)
  34.         return BAD_REQUEST;
  35.     *m_version += '\0';
  36.     m_version += strspn(m_version, " \t");
  37.     // 验证HTTP版本
  38.     if (strcasecmp(m_version, "HTTP/1.1") != 0)
  39.         return BAD_REQUEST;
  40.     // 移除URL中的协议部分
  41.     if (strncasecmp(m_url, "http://", 7) == 0) {
  42.         m_url += 7;
  43.         m_url = strchr(m_url, '/');
  44.     }
  45.     if (strncasecmp(m_url, "https://", 8) == 0)
  46.     {
  47.         m_url += 8;
  48.         m_url = strchr(m_url, '/');
  49.     }
  50.     // 验证URL格式
  51.     if (!m_url || m_url[0] != '/')
  52.         return BAD_REQUEST;
  53.     // 当URL为根路径时,设置默认资源
  54.     if (strlen(m_url) == 1)
  55.         strcat(m_url, "judge.html");
  56.     // 切换到解析头部的状态
  57.     m_check_state = CHECK_STATE_HEADER;
  58.     return NO_REQUEST;
  59. }
复制代码
让我们逐行分析这段代码,并为每个部分提供详细的解释和示例。
  1. http_conn::HTTP_CODE http_conn::parse_request_line(char* text) {
复制代码
这行代码定义了一个名为 parse_request_line 的函数,属于 http_conn 类。它接收一个 char* 类型的参数 text,该参数表示请求行的文本。函数的返回类型是 http_conn::HTTP_CODE,表示剖析请求行后的处理状态。
  1.     m_url = strpbrk(text, " \t");
  2.     if (!m_url) {
  3.         return BAD_REQUEST;
  4.     }
  5.     *m_url++ = '\0';
复制代码


  • strpbrk(text, " \t") 用于查找 text 中第一个空格或制表符的位置,并返回指向它的指针。这个指针即为 m_url。
  • 如果没有找到空格或制表符(意味着请求行格式不精确),函数会返回 BAD_REQUEST。
  • *m_url++ = '\0'; 将空格或制表符替换为字符串竣事符 \0,然后将 m_url 移动到下一个字符,指向 URL 的起始位置。
示例:


  • 输入 "GET /index.html HTTP/1.1",m_url 将指向 " /index.html HTTP/1.1" 中的第一个空格,并通过 *m_url++ = '\0' 将 "GET" 与背面的部分分割开,m_url 如今指向 "/index.html HTTP/1.1"。
  1.     char * method = text;
  2.     if (strcasecmp(method, "GET") == 0) {
  3.         m_method = GET;
  4.     }
  5.     else if (strcasecmp(method, "POST") == 0) {
  6.         m_method = POST;
  7.         cgi = 1;
  8.     }
  9.     else
  10.         return BAD_REQUEST;
复制代码


  • char* method = text; 保存方法字符串的起始位置。
  • strcasecmp 忽略巨细写地比力 method 与 "GET" 或 "OST",并根据结果设置 m_method(表示请求方法)为 GET 或 POST。
  • 如果方法既不是 GET 也不是 POST,则返回 BAD_REQUEST。
示例:


  • 输入 "GET /index.html HTTP/1.1",method 是 "GET",所以 m_method 被设置为 GET。
  • 如果输入 "DELETE /index.html HTTP/1.1",则会返回 BAD_REQUEST。
  1.     m_url += strspn(m_url, " \t");
复制代码


  • strspn(m_url, " \t") 计算 m_url 中一连出现的空缺字符的数量,m_url += 会跳过这些空缺字符,使得 m_url 指向实际的 URL 开始位置。
示例:


  • 输入 "GET /index.html HTTP/1.1",此时 m_url 会跳过额外的空格,指向 "/index.html"。
  1.     m_version = strpbrk(m_url, " \t");
  2.     if (!m_version)
  3.         return BAD_REQUEST;
  4.     *m_version += '\0';
  5.     m_version += strspn(m_version, " \t");
复制代码


  • strpbrk(m_url, " \t") 查找 URL 后的第一个空格或制表符的位置,并将其指针保存为 m_version。
  • 如果没有找到空格或制表符,返回 BAD_REQUEST。
  • 将找到的空格或制表符替换为字符串竣事符 \0,然后跳过任何多余的空格,使 m_version 指向 HTTP 版本号的开始位置。
示例:


  • 输入 "GET /index.html HTTP/1.1",m_version 指向 "HTTP/1.1"。
  1.     if (strcasecmp(m_version, "HTTP/1.1") != 0)
  2.         return BAD_REQUEST;
复制代码


  • strcasecmp 忽略巨细写地比力 m_version 与 "HTTP/1.1"。如果版本不是 "HTTP/1.1",返回 BAD_REQUEST。
示例:


  • 输入 "GET /index.html HTTP/1.1",这行将成功通过。
  • 输入 "GET /index.html HTTP/2.0",会返回 BAD_REQUEST。
  1.     if (strncasecmp(m_url, "http://", 7) == 0) {
  2.         m_url += 7;
  3.         m_url = strchr(m_url, '/');
  4.     }
  5.     if (strncasecmp(m_url, "https://", 8) == 0)
  6.     {
  7.         m_url += 8;
  8.         m_url = strchr(m_url, '/');
  9.     }
复制代码


  • 这些代码用于移除 http:// 或 https:// 前缀,使 m_url 只包含路径部分。
  • strchr(m_url, '/') 查找路径的开始位置(第一个斜杠)。
示例:


  • 输入 "GET http://example.com/index.html HTTP/1.1",m_url 将变为 "/index.html"。
  1.     if (!m_url || m_url[0] != '/')
  2.         return BAD_REQUEST;
复制代码


  • 检查 m_url 是否为空或者第一个字符是否为 /。如果不是,则返回 BAD_REQUEST。
示例:


  • 输入 "GET http://example.com/index.html HTTP/1.1",将通过此检查。
  • 输入 "GET example.com/index.html HTTP/1.1",将返回 BAD_REQUEST。
  1.     if (strlen(m_url) == 1)
  2.         strcat(m_url, "judge.html");
复制代码


  • 如果 m_url 只包含一个字符 /,则将默认路径设置为 judge.html。
示例:


  • 输入 "GET / HTTP/1.1",将把 m_url 设置为 "/judge.html"。
  1.     m_check_state = CHECK_STATE_HEADER;
  2.     return NO_REQUEST;
复制代码


  • 剖析完请求行后,设置 m_check_state 为 CHECK_STATE_HEADER,表示接下来将剖析请求头部。
  • 返回 NO_REQUEST,表示剖析成功,等待进一步的处理。
总结:
这段代码用于剖析 HTTP 请求行,通过渐渐分割并验证请求行的不同部分(方法、URL、HTTP版本)来确定其正当性。

剖析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。



  • CHECK_STATE_HEADER




    • 调用parse_headers函数剖析请求头部信息
    • 判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则阐明是GET请求,则报文剖析竣事。
    • 若剖析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。
    • connection字段判断是keep-alive还是close,决定是长毗连还是短毗连
    • content-length字段,这里用于读取post请求的消息体长度

  1. /**
  2. * 解析HTTP请求的头部信息
  3. *
  4. * @param text 指向接收缓冲区中当前解析的位置
  5. * @return 返回HTTP请求的状态代码,表示请求的状态
  6. *
  7. * 功能描述:
  8. * 本函数旨在解析HTTP请求的头部信息。根据传入的文本参数,识别不同的HTTP头部字段,
  9. * 并在解析完成后更新类的内部状态。特别地,函数会识别内容长度、连接类型和主机信息,
  10. * 并对持久连接作出处理。
  11. *
  12. * 注意事项:
  13. * 函数返回NO_REQUEST表示请求消息尚未解析完成,需要继续解析更多的数据;返回GET_REQUEST
  14. * 表示请求消息已经完全解析成功。
  15. */
  16. http_conn::HTTP_CODE http_conn::parse_headers(char* text) {
  17.     // 如果text为空字符串,表示头部信息解析完毕
  18.     if (text[0] == '\0') {
  19.         // 如果内容长度不为0,下一步进入内容解析状态
  20.         if (m_content_length != 0) {
  21.             m_check_state = CHECK_STATE_CONTENT;
  22.             return NO_REQUEST;
  23.         }
  24.         // 如果内容长度为0,表示请求头部已经完全解析,可以处理请求
  25.         return GET_REQUEST;
  26.     }
  27.     else if (strncasecmp(text, "Connection:", 11) == 0) {
  28.         // 解析连接类型头部,跳过头部名称
  29.         text += 11;
  30.         // 跳过头部值前的空白字符
  31.         text += strspn(text, " \t");
  32.         // 如果连接类型为keep-alive,设置m_linger为true,表示需要保持连接
  33.         if (strcasecmp(text, "keep-alive") == 0) {
  34.             m_linger = true;
  35.         }
  36.     }
  37.     else if (strncasecmp(text, "Content-length:", 15) == 0) {
  38.         // 解析内容长度头部,跳过头部名称
  39.         text += 15;
  40.         // 跳过头部值前的空白字符
  41.         text += strspn(text, " \t");
  42.         // 设置内容长度
  43.         m_content_length = atol(text);
  44.     }
  45.     else if (strncasecmp(text, "Host:", 5) == 0) {
  46.         // 解析主机头部,跳过头部名称
  47.         text += 5;
  48.         // 跳过头部值前的空白字符
  49.         text += strspn(text, " \t");
  50.         // 设置主机信息
  51.         m_host = text;
  52.     }
  53.     else {
  54.         // 遇到未知的头部信息,记录日志
  55.         LOG_INFO("oop!unkonw header: %s", text);
  56.     }
  57.     // 表示请求消息尚未解析完成,需要继续解析更多的数据
  58.     return NO_REQUEST;
  59. }
复制代码
如果仅仅是GET请求,如项目中的欢迎界面,那么主状态机只设置之前的两个状态足矣。
因为在上篇推文中我们曾说道,GET和POST请求报文的区别之一是有无消息体部分,GET请求没有消息体,当剖析完空行之后,便完成了报文的剖析。
但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装。
为此,我们须要在剖析报文的部分添加剖析消息体的模块。
  1. while((m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)||((line_status=parse_line())==LINE_OK))
复制代码
那么,这里的判断条件为什么要写成如许呢?
在GET请求报文中,每一行都是\r\n作为竣事,所以对报文进行拆解时,仅用从状态机的状态line_status=parse_line())==LINE_OK语句即可。
但,在POST请求报文中,消息体的末端没有任何字符,所以不能使用从状态机的状态,这里转而使用主状态机的状态作为循环入口条件。
那背面的&& line_status==LINE_OK又是为什么?
剖析完消息体后,报文的完备剖析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT,也就是说,符合循环入口条件,还会再次进入循环,这并不是我们所希望的。
为此,增长了该语句,并在完成消息体剖析后,将line_status变量更改为LINE_OPEN,此时可以跳出循环,完成报文剖析任务。


  • CHECK_STATE_CONTENT




    • 仅用于剖析POST请求,调用parse_content函数剖析消息体
    • 用于保存post请求消息体,为背面的登录和注册做准备

  1. /**
  2. * 解析HTTP请求的主体内容
  3. *
  4. * @param text 指向读取到的请求主体内容的指针
  5. * @return 返回解析后的请求状态
  6. */
  7. http_conn::HTTP_CODE http_conn::parse_content(char* text) {
  8.     // 检查是否读取到了足够的主体内容
  9.     if (m_read_idx >= (m_content_length + m_checked_idx)) {
  10.         // 终止字符串,并将其赋值给m_string成员变量
  11.         text[m_content_length] = '\0';
  12.         m_string = text;
  13.         // 请求解析完成,返回GET_REQUEST状态
  14.         return GET_REQUEST;
  15.     }
  16.     // 请求解析未完成,返回NO_REQUEST状态
  17.     return NO_REQUEST;
  18. }
复制代码
状态机和HTTP报文剖析是项目中最繁琐的部分,这次我们一举办理掉它

上一篇详解中,我们对状态机和服务器剖析请求报文进行了先容。
本篇,我们将先容服务器怎样响应请求报文,并将该报文发送给浏览器端。首先先容一些底子API,然后团结流程图和代码对服务器响应请求报文进行详解。
服务器响应请求报文

上一篇详解中,我们对状态机和服务器剖析请求报文进行了先容。
本篇,我们将先容服务器怎样响应请求报文,并将该报文发送给浏览器端。首先先容一些底子API,然后团结流程图和代码对服务器响应请求报文进行详解。
底子API部分,先容stat、mmap、iovec、writev。
流程图部分,形貌服务器端响应请求报文的逻辑,各模块间的关系。
代码部分,团结代码对服务器响应请求报文进行详解。
底子API

为了更好的源码阅读体验,这里提前对代码中使用的一些API进行简要先容,更丰富的用法可以自行查阅资料。
stat

stat函数用于取得指定文件的文件属性,并将文件属性存储在结构体stat里,这里仅对此中用到的成员进行先容。
  1. 1#include <sys/types.h>
  2. 2#include <sys/stat.h>
  3. 3#include <unistd.h>
  4. 4
  5. 5//获取文件属性,存储在statbuf中
  6. 6int stat(const char *pathname, struct stat *statbuf);
  7. 7
  8. 8struct stat
  9. 9{
  10. 10   mode_t    st_mode;        /* 文件类型和权限 */
  11. 11   off_t     st_size;        /* 文件大小,字节数*/
  12. 12};
复制代码
mmap

用于将一个文件或其他对象映射到内存,提高文件的访问速率。
  1. 1void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
  2. 2int munmap(void* start,size_t length);
复制代码


  • start:映射区的开始所在,设置为0时表示由系统决定映射区的起始所在
  • length:映射区的长度
  • prot:盼望的内存掩护标记,不能与文件的打开模式辩论




    • PROT_READ 表示页内容可以被读取



  • flags:指定映射对象的类型,映射选项和映射页是否可以共享




    • MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件



  • fd:有效的文件形貌符,一般是由open()函数返回
  • off_t offset:被映射对象内容的起点
iovec

定义了一个向量元素,通常,这个结构用作一个多元素的数组。
  1. struct iovec {
  2.         void      *iov_base;      /* starting address of buffer */
  3.         size_t    iov_len;        /* size of buffer */
  4.     };
复制代码


  • iov_base指向数据的所在
  • iov_len表示数据的长度
writev

writev函数用于在一次函数调用中写多个非一连缓冲区,偶尔也将这该函数称为聚集写。
  1. #include <sys/uio.h>
  2. ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
复制代码


  • filedes表示文件形貌符
  • iov为前述io向量机制结构体iovec
  • iovcnt为结构体的个数
若成功则返回已写的字节数,若出错则返回-1。writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应即是所有缓冲区长度之和。
特别注意: 循环调用writev时,须要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然须要通过遍历iovec来计算新的基址,别的写入数据的“竣事点”可能位于一个iovec的中间某个位置,因此须要调整临界iovec的io_base和io_len。
流程图

浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行剖析,根据剖析结果HTTP_CODE,进入相应的逻辑和模块。
此中,服务器子线程完成报文的剖析与响应;主线程监测读写变乱,调用read_once和http_conn::write完成数据的读取与发送。


HTTP_CODE含义

表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文剖析与响应中只用到了七种。


  • NO_REQUEST




    • 请求不完备,须要继续读取请求报文数据
    • 跳转主线程继续监测读变乱



  • GET_REQUEST




    • 得到了完备的HTTP请求
    • 调用do_request完成请求资源映射



  • NO_RESOURCE




    • 请求资源不存在
    • 跳转process_write完成响应报文



  • BAD_REQUEST




    • HTTP请求报文有语法错误或请求资源为目录
    • 跳转process_write完成响应报文



  • FORBIDDEN_REQUEST




    • 请求资源克制访问,没有读取权限
    • 跳转process_write完成响应报文



  • FILE_REQUEST




    • 请求资源可以正常访问
    • 跳转process_write完成响应报文



  • INTERNAL_ERROR




    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

代码分析

do_request

process_read函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的BAD_REQUEST,一部分是do_request的返回结果.该函数将网站根目录和url文件拼接,然后通过stat判断该文件属性。别的,为了提高访问速率,通过mmap进行映射,将平凡文件映射到内存逻辑所在。
为了更好的明白请求资源的访问流程,这里对各种各页面跳转机制进行简要先容。此中,浏览器网址栏中的字符,即url,可以将其抽象成ip:port/xxx,xxx通过html文件的action属性进行设置。
m_url为请求报文中剖析出的请求资源,以/开头,也就是/xxx,项目中剖析后的m_url有8种环境。


  • /




    • GET请求,跳转到judge.html,即欢迎访问页面



  • /0




    • POST请求,跳转到register.html,即注册页面



  • /1




    • POST请求,跳转到log.html,即登录页面



  • /2CGISQL.cgi




    • POST请求,进行登录校验
    • 验证成功跳转到welcome.html,即资源请求成功页面
    • 验证失败跳转到logError.html,即登录失败页面



  • /3CGISQL.cgi




    • POST请求,进行注册校验
    • 注册成功跳转到log.html,即登录页面
    • 注册失败跳转到registerError.html,即注册失败页面



  • /5




    • POST请求,跳转到picture.html,即图片请求页面



  • /6




    • POST请求,跳转到video.html,即视频请求页面



  • /7




    • POST请求,跳转到fans.html,即关注页面

如果各人对上述设置方式不明白,不消担心。具体的登录和注册校验功能会在第12节进行详解,到时候还会针对html进行先容。

  1. // 处理HTTP请求的主要函数
  2. // 根据不同的URL请求来定位资源文件并进行相应的处理
  3. http_conn::HTTP_CODE http_conn::do_request() {
  4.     // 将doc_root路径复制到m_real_file中,作为基础路径
  5.     strcpy(m_real_file, doc_root);
  6.     // 获取基础路径的长度
  7.     int len = strlen(doc_root);
  8.     // 查找URL中的最后一个'/'字符
  9.     const char* p = strrchr(m_url, '/');
  10.     // 处理cgi请求
  11.     if (cgi == 1 && (*(p + 1) == '2' || *(p+ 1) == '3')) {
  12.         // 通过URL中的标志判断是登录验证还是注册验证
  13.         char flag = m_url[1];
  14.         // 动态构造真实的URL路径
  15.         char *m_url_real = (char*) malloc(sizeof(char) * 200);
  16.         strcpy(m_url_real, "/");
  17.         strcat(m_url_real, m_url+ 2);
  18.         strncpy(m_real_file + len, m_url_real,FILENAME_LEN - len - 1);
  19.         free(m_url_real);
  20.         // 提取用户名和密码
  21.         char name[100], password[100];
  22.         int i;
  23.         for (int i = 5; m_string[i] != '&'; ++i)
  24.             name[i - 5] = m_string[i];
  25.         name[i - 5 ] = '\0';
  26.         int j = 0;
  27.         for (j = i + 10; m_string[i] = '\0'; ++i, ++j)
  28.             password[j] = m_string[i];
  29.         password[j] = '\0';
  30.         // 处理注册请求
  31.         if (*(p + 1) == '3') {
  32.             // 检查数据库中是否有同名用户
  33.             // 若没有,则插入新用户数据
  34.             char* sql_insert = (char*)malloc(sizeof(char) * 200);
  35.             strcpy(sql_insert, " INSERT INTO user (username, passwd) VALUES(");
  36.             strcat(sql_insert, "'");
  37.             strcat(sql_insert, name);
  38.             strcat(sql_insert, "', '");
  39.             strcat(sql_insert, password);
  40.             strcat(sql_insert,"')");
  41.             if (users.find(name) == users.end()){
  42.                 // 执行数据库插入操作
  43.                 m_lock.lock();
  44.                 int res = mysql_query(mysql, sql_insert);
  45.                 users.insert(pair<string, string> (name, password));
  46.                 m_lock.unlock();
  47.                 // 根据操作结果重定向用户
  48.                 if (!res) {
  49.                     strcpy(m_url, "/log.html");
  50.                 }
  51.                 else {
  52.                     strcpy(m_url, "/registerError.html");
  53.                 }
  54.             }
  55.             else {
  56.                 // 若已存在同名用户,重定向到注册错误页面
  57.                 strcpy(m_url, "/registerError.html");
  58.             }
  59.         }
  60.         // 处理登录请求
  61.         else if (*(p + 1) ==  '2') {
  62.             // 验证用户名和密码
  63.             if (users.find(name) != users.end() && users[name] == password) {
  64.                 strcpy(m_url, "/welcome.html");
  65.             }
  66.             else {
  67.                 strcpy(m_url, "/logError.html");
  68.             }
  69.         }
  70.         // 其他特殊请求的处理
  71.         if (*(p + 1) == '0') {
  72.             char* m_url_real = (char*)malloc(sizeof(char) * 200);
  73.             strcpy(m_url_real, "/register.html");
  74.             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
  75.             free(m_url_real);
  76.         }
  77.         else if (*(p + 1) == '1')
  78.         {
  79.             char *m_url_real = (char *)malloc(sizeof(char) * 200);
  80.             strcpy(m_url_real, "/log.html");
  81.             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
  82.             free(m_url_real);
  83.         }
  84.         else if (*(p + 1) == '5')
  85.         {
  86.             char *m_url_real = (char *)malloc(sizeof(char) * 200);
  87.             strcpy(m_url_real, "/picture.html");
  88.             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
  89.             free(m_url_real);
  90.         }
  91.         else if (*(p + 1) == '6')
  92.         {
  93.             char *m_url_real = (char *)malloc(sizeof(char) * 200);
  94.             strcpy(m_url_real, "/video.html");
  95.             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
  96.             free(m_url_real);
  97.         }
  98.         else if (*(p + 1) == '7')
  99.         {
  100.             char *m_url_real = (char *)malloc(sizeof(char) * 200);
  101.             strcpy(m_url_real, "/fans.html");
  102.             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
  103.             free(m_url_real);
  104.         }
  105.         // 默认情况,直接使用URL构造文件路径
  106.         else
  107.             strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
  108.             
  109.         }
  110.     // 检查文件状态
  111.     if (stat(m_real_file, &m_file_stat) < 0)
  112.         return NO_RESOURCE;
  113.     // 检查文件权限
  114.     if (!(m_file_stat.st_mode & S_IROTH)) {
  115.         return FORBIDDEN_REQUEST;
  116.     }
  117.     // 检查是否为目录
  118.     if (S_ISDIR(m_file_stat.st_mode)) {
  119.         return BAD_REQUEST;
  120.     }
  121.     // 打开文件并映射到内存
  122.     int fd = open(m_real_file, O_RDONLY);
  123.     m_file_address = (char* )mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
  124.     close(fd);
  125.     return FILE_REQUEST;
  126. }
复制代码

 
process_write

根据do_request的返回状态,服务器子线程调用process_write向m_write_buf中写入响应报文。


  • add_status_line函数,添加状态行:http/1.1 状态码 状态消息
  • add_headers函数添加消息报头,内部调用add_content_length和add_linger函数




    • content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据
    • connection记录毗连状态,用于告诉浏览器端保持长毗连



  • add_blank_line添加空行
上述涉及的5个函数,均是内部调用add_response函数更新m_write_idx指针和缓冲区m_write_buf中的内容。
  1. /**
  2. * @brief 将给定的格式化字符串添加到响应中
  3. *
  4. * 本函数用于向HTTP响应中添加一段格式化的字符串。它使用vsnprintf函数来进行格式化,
  5. * 并确保添加的内容不会超出写缓冲区的大小。如果内容超出了剩余的缓冲区空间,函数将返回false,
  6. * 表示失败。否则,它将更新写入索引m_write_idx以反映新添加的内容长度,并记录日志信息。
  7. *
  8. * @param format 格式化字符串的格式,与printf中的格式字符串类似
  9. * @param ... 可变参数列表,与format对应的实际值
  10. * @return true 成功添加了格式化字符串到响应中
  11. * @return false 因为缓冲区空间不足,未能成功添加格式化字符串
  12. */
  13. bool http_conn::add_response(const char* format, ...) {
  14.     // 检查剩余缓冲区空间是否足够,如果不够则返回false
  15.     if (m_write_idx >= WRITE_BUFFER_SIZE)
  16.         return false;
  17.    
  18.     // 初始化可变参数列表
  19.     va_list arg_list;
  20.     va_start(arg_list, format);
  21.    
  22.     // 进行格式化输出,注意避免缓冲区溢出
  23.     int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
  24.    
  25.     // 如果格式化内容长度超过剩余缓冲区空间,表示失败
  26.     if (len >= (WRITE_BUFFER_SIZE - 1- m_write_idx)) {
  27.         va_end(arg_list);
  28.         return false;
  29.     }
  30.    
  31.     // 更新写入索引
  32.     m_write_idx += len;
  33.     // 释放可变参数列表资源
  34.     va_end(arg_list);
  35.    
  36.     // 记录日志,输出当前请求的响应内容
  37.     LOG_INFO("request:%s", m_write_buf);
  38.     return true;
  39. }
  40. // 向HTTP响应中添加状态行
  41. // @param status: HTTP状态码
  42. // @param title: 状态标题
  43. // @return: 添加是否成功
  44. bool http_conn::add_status_line(int status, const char* title) {
  45.     return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
  46. }
  47. // 添加HTTP响应头,包括Content-Length、Connection和空白行
  48. // @param content_len: 内容长度
  49. // @return: 添加是否成功
  50. bool http_conn::add_headers(int content_len) {
  51.     return add_content_length(content_len) && add_linger() && add_blank_line();
  52. }
  53. // 添加Content-Length响应头
  54. // @param content_len: 内容长度
  55. // @return: 添加是否成功
  56. bool http_conn::add_content_length(int content_len) {
  57.     return add_response("Content-Length:%d\r\n", content_len);
  58. }
  59. // 添加Content-Type响应头
  60. // @return: 添加是否成功
  61. bool http_conn::add_content_type() {
  62.     return add_response("Content-Type:%s\r\n", "text/html");
  63. }
  64. // 添加Connection响应头,决定连接是否保持
  65. // @return: 添加是否成功
  66. bool http_conn::add_linger() {
  67.     return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive": "close");
  68. }
  69. // 添加空白行,标志响应头的结束
  70. // @return: 添加是否成功
  71. bool http_conn::add_blank_line() {
  72.     return add_response("%s", "\r\n");
  73. }
  74. // 添加响应内容
  75. // @param content: 响应内容
  76. // @return: 添加是否成功
  77. bool http_conn::add_content(const char* content) {
  78.     return add_response("%s", content);
  79. }
复制代码
响应报文分为两种,一种是请求文件的存在,通过io向量机制iovec,声明两个iovec,第一个指向m_write_buf,第二个指向mmap的所在m_file_address;一种是请求出错,这时候只申请一个iovec,指向m_write_buf。


  • iovec是一个结构体,内里有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据。
  • 成员iov_len表示实际写入的长度
  1. // 根据不同的HTTP响应码处理写入内容到HTTP响应中
  2. // @param ret: HTTP响应码
  3. // @return: 处理是否成功
  4. bool http_conn::process_write(HTTP_CODE ret) {
  5.     switch(ret) {
  6.         case INTERNAL_ERROR: {
  7.             add_status_line(500, error_500_title);
  8.             add_headers(strlen(error_500_form));
  9.             if (!add_content(error_500_form)) {
  10.                 return false;
  11.             }
  12.             break;
  13.         }
  14.         case BAD_REQUEST: {
  15.             add_status_line(404, error_404_title);
  16.             add_headers(strlen(error_400_form));
  17.             if (!add_content(error_404_form)) {
  18.                 return false;
  19.             }
  20.             break;
  21.         }
  22.         case FORBIDDEN_REQUEST: {
  23.             add_status_line(403, error_403_title);
  24.             add_headers(strlen(error_403_form));
  25.             if (!add_content(error_403_form)) {
  26.                 return false;
  27.             }
  28.             break;
  29.         }
  30.         case FILE_REQUEST: {
  31.             add_status_line(200, ok_200_title);
  32.             if (m_file_stat.st_size != 0) {
  33.                 m_iv[0].iov_base = m_write_buf;
  34.                 m_iv[0].iov_len = m_write_idx;
  35.                 m_iv[1].iov_base = m_file_address;
  36.                 m_iv[1].iov_len = m_file_stat.st_size;
  37.                 m_iv_count = 2;
  38.                 bytes_to_send = m_write_idx + m_file_stat.st_size;
  39.                 return true;
  40.             }
  41.             else {
  42.                 const char* ok_string = "<html><body></body></html>";
  43.                 add_headers(strlen(ok_string));
  44.                 if (!add_content(ok_string)) {
  45.                     return false;
  46.                 }
  47.             }
  48.         }
  49.         default:
  50.             return false;
  51.     }
  52.     m_iv[0].iov_base = m_write_buf;
  53.     m_iv[0].iov_len = m_write_idx;
  54.     m_iv_count = 1;
  55.     bytes_to_send = m_write_idx;
  56.     return true;
  57. }
复制代码
http_conn::write

服务器子线程调用process_write完成响应报文,随后注册epollout变乱。服务器主线程检测写变乱,并调用http_conn::write函数将响应报文发送给浏览器端。
该函数具体逻辑如下:
在天生响应报文时初始化byte_to_send,包括头部信息和文件数据巨细。通过writev函数循环发送响应报文数据,根据返回值更新byte_have_send和iovec结构体的指针和长度,并判断响应报文整体是否发送成功。


  • 若writev单次发送成功,更新byte_to_send和byte_have_send的巨细,若响应报文整体发送成功,则取消mmap映射,并判断是否是长毗连.




    • 长毗连重置http类实例,注册读变乱,不关闭毗连,
    • 短毗连直接关闭毗连



  • 若writev单次发送不成功,判断是否是写缓冲区满了。




    • 若不是因为缓冲区满了而失败,取消mmap映射,关闭毗连
    • 若eagain则满了,更新iovec结构体的指针和长度,并注册写变乱,等待下一次写变乱触发(当写缓冲区从不可写变为可写,触发epollout),因此在此期间无法立即接收到同一用户的下一请求,但可以包管毗连的完备性。

  1. // 从内存中取消映射文件
  2. void http_conn::unmap() {
  3.     if (m_file_address) {
  4.         munmap(m_file_address, m_file_stat.st_size); // 使用munmap函数取消对文件的内存映射
  5.         m_file_address = 0; // 清空文件地址,表示不再有文件映射
  6.     }
  7. }
  8. // 将数据写入到客户端socket中
  9. bool http_conn::write() {
  10.     int temp = 0;
  11.     if (bytes_to_send == 0) { // 如果没有数据需要发送
  12.         modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); // 修改epoll事件为读事件,准备下一次读取
  13.         init(); // 重置连接对象,为下一次请求做准备
  14.         return true; // 成功处理空发送请求
  15.     }
  16.     while(1) {
  17.         temp = writev(m_sockfd, m_iv, m_iv_count); // 使用writev进行scatter write(分散写)
  18.         if (temp < 0) { // 如果写入失败
  19.             if (errno == EAGAIN) { // 如果是因为缓冲区满,EPOLLOUT事件会再次触发
  20.                 modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode); // 重新注册EPOLLOUT事件
  21.                 return true; // 表示处理将被再次尝试
  22.             }
  23.             unmap(); // 取消文件内存映射
  24.             return false; // 写入失败,返回false
  25.         }
  26.         bytes_have_send += temp; // 更新已经发送的字节数
  27.         bytes_to_send -= temp; // 更新剩余待发送的字节数
  28.         if (bytes_have_send >= m_iv[0].iov_len){ // 如果第一部分数据已经发送完毕
  29.             m_iv[0].iov_len = 0; // 置空第一部分数据的长度
  30.             m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx); // 更新第二部分数据的基地址
  31.             m_iv[1].iov_len= bytes_to_send; // 更新第二部分数据的长度
  32.         }
  33.         else { // 如果第一部分数据未发送完毕
  34.             m_iv[0].iov_base = m_write_buf + bytes_have_send; // 更新第一部分数据的基地址
  35.             m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send; // 更新第一部分数据的长度
  36.         }
  37.         if (bytes_to_send <= 0) { // 如果所有数据发送完毕
  38.             unmap(); // 取消文件内存映射
  39.             modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); // 修改epoll事件为读事件,准备下一次读取
  40.             if (m_linger) { // 如果设置为保持连接
  41.                 init(); // 重置连接对象,为下一次请求做准备
  42.                 return true; // 表示成功处理发送请求
  43.             }
  44.             else { // 如果设置为非保持连接
  45.                 return false; // 表示处理完成,连接将被关闭
  46.             }
  47.         }
  48.     }
  49. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊落一身雪

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表