项目03HTTP解析

本文内容

在服务器项目中,http请求的处理与响应至关重要,关系到用户界面的跳转与反馈。这里,社长将其分为上、中、下三个部分来讲解,具体的:

  • 上篇,梳理基础知识,结合代码分析http类及请求接收

  • 中篇,结合代码分析请求报文解析

  • 下篇,结合代码分析请求报文响应

基础知识方面,包括epoll、HTTP报文格式、状态码和有限状态机。

代码分析方面,首先对服务器端处理http请求的全部流程进行简要介绍,然后结合代码对http类及请求接收进行详细分析。

epoll

epoll涉及的知识较多,这里仅对API和基础知识作介绍。更多资料请查阅资料,或查阅游双的Linux高性能服务器编程 第9章 I/O复用。

epoll_create()函数

#include <sys/epoll.h>
int epoll_create(int size);

创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。

epoll_ctl()函数

#include <sys/epoll.h>
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结构体指针类型,表示内核所监听的事件,具体定义如下:

struct epoll_event {
	__uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};
  • events描述事件类型,其中epoll事件类型有以下几种

    • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

    • EPOLLOUT:表示对应的文件描述符可以写

    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

    • EPOLLERR:表示对应的文件描述符发生错误

    • EPOLLHUP:表示对应的文件描述符被挂断;

    • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的

    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

epoll_wait函数

#include <sys/epoll.h>
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
    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
    空行
    请求数据为空
  • POST
   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
  • 请求行,用来说明请求类型,要访问的资源以及所使用的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响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

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>
  • 状态行,由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和函数指针来实现,从软件工程的角度看,主要是为了封装逻辑。

带有状态转移的有限状态机示例代码。

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;
        }
    }
}

该状态机包含三种状态: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类

这一部分代码在TinyWebServer/http/http_conn.h中,主要是http类的定义。

class http_conn{
    public:
        //设置读取文件的名称m_real_file大小
        static const int FILENAME_LEN=200;
        //设置读缓冲区m_read_buf大小
        static const int READ_BUFFER_SIZE=2048;
        //设置写缓冲区m_write_buf大小
        static const int WRITE_BUFFER_SIZE=1024;
        //报文的请求方法,本项目只用到GET和POST
        enum METHOD{GET=0,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATH};
        //主状态机的状态
        enum CHECK_STATE{CHECK_STATE_REQUESTLINE=0,CHECK_STATE_HEADER,CHECK_STATE_CONTENT};
        //报文解析的结果
        enum HTTP_CODE{NO_REQUEST,GET_REQUEST,BAD_REQUEST,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);
        //关闭http连接
        void close_conn(bool real_close=true);
        void process();
        //读取浏览器端发来的全部数据
        bool read_once();
        //响应报文写入函数
        bool write();
        sockaddr_in *get_address(){
            return &m_address;  
        }
        //同步线程初始化数据库读取表
        void initmysql_result();
        //CGI使用线程池初始化数据库表
        void initresultFile(connection_pool *connPool);

    private:
        void init();
        //从m_read_buf读取,并处理请求报文
        HTTP_CODE process_read();
        //向m_write_buf写入响应报文数据
        bool process_write(HTTP_CODE ret);
        //主状态机解析报文中的请求行数据
        HTTP_CODE parse_request_line(char *text);
        //主状态机解析报文中的请求头数据
        HTTP_CODE parse_headers(char *text);
        //主状态机解析报文中的请求内容
        HTTP_CODE parse_content(char *text);
        //生成响应报文
        HTTP_CODE do_request();

        //m_start_line是已经解析的字符
        //get_line用于将指针向后偏移,指向未处理的字符
        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,...);
        bool add_content(const char* content);
        bool add_status_line(int status,const char* title);
        bool add_headers(int content_length);
        bool add_content_type();
        bool add_content_length(int content_length);
        bool add_linger();
        bool add_blank_line();

    public:
        static int m_epollfd;
        static int m_user_count;
        MYSQL *mysql;

    private:
        int m_sockfd;
        sockaddr_in m_address;

        //存储读取的请求报文数据
        char m_read_buf[READ_BUFFER_SIZE];
        //缓冲区中m_read_buf中数据的最后一个字节的下一个位置
        int m_read_idx;
        //m_read_buf读取的位置m_checked_idx
        int m_checked_idx;
        //m_read_buf中已经解析的字符个数
        int m_start_line;

        //存储发出的响应报文数据
        char m_write_buf[WRITE_BUFFER_SIZE];
        //指示buffer中的长度
        int m_write_idx;

        //主状态机的状态
        CHECK_STATE m_check_state;
        //请求方法
        METHOD m_method;

        //以下为解析请求报文中对应的6个变量
        //存储读取文件的名称
        char m_real_file[FILENAME_LEN];
        char *m_url;
        char *m_version;
        char *m_host;
        int m_content_length;
        bool m_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;        //已发送字节数
};

在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化,不用过多讲解。

这里,对read_once进行介绍。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。

//循环读取客户数据,直到无数据可读或对方关闭连接
bool http_conn::read_once()
{
    if(m_read_idx>=READ_BUFFER_SIZE)
    {
        return false;
    }
    int bytes_read=0;
    while(true)
    {
        //从套接字接收数据,存储在m_read_buf缓冲区
        bytes_read=recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE-m_read_idx,0);
        if(bytes_read==-1)
        {    
            //非阻塞ET模式下,需要一次性将数据读完
            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;
}

epoll相关代码

项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种。

  • 非阻塞模式
1//对文件描述符设置非阻塞
2int setnonblocking(int fd)
3{
4    int old_option = fcntl(fd, F_GETFL);
5    int new_option = old_option | O_NONBLOCK;
6    fcntl(fd, F_SETFL, new_option);
7    return old_option;
8}
  • 内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,listenfd不用开启
 1void addfd(int epollfd, int fd, bool one_shot)
 2{
 3    epoll_event event;
 4    event.data.fd = fd;
 5
 6#ifdef ET
 7    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
 8#endif
 9
10#ifdef LT
11    event.events = EPOLLIN | EPOLLRDHUP;
12#endif
13
14    if (one_shot)
15        event.events |= EPOLLONESHOT;
16    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
17    setnonblocking(fd);
18}

内核时间表删除事件

1void removefd(int epollfd, int fd)
2{
3    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
4    close(fd);
5}
  • 重置EPOLLONESHOT事件
 1void modfd(int epollfd, int fd, int ev)
 2{
 3    epoll_event event;
 4    event.data.fd = fd;
 5
 6#ifdef ET
 7    event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
 8#endif
 9
10#ifdef LT
11    event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
12#endif
13
14    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
15}

服务器接收http请求

浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。

 1//创建MAX_FD个http类对象
 2http_conn* users=new http_conn[MAX_FD];
 3
 4//创建内核事件表
 5epoll_event events[MAX_EVENT_NUMBER];
 6epollfd = epoll_create(5);
 7assert(epollfd != -1);
 8
 9//将listenfd放在epoll树上
10addfd(epollfd, listenfd, false);
11
12//将上述epollfd赋值给http类对象的m_epollfd属性
13http_conn::m_epollfd = epollfd;
14
15while (!stop_server)
16{
17    //等待所监控文件描述符上有事件的产生
18    int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
19    if (number < 0 && errno != EINTR)
20    {
21        break;
22    }
23    //对所有就绪事件进行处理
24    for (int i = 0; i < number; i++)
25    {
26        int sockfd = events[i].data.fd;
27
28        //处理新到的客户连接
29        if (sockfd == listenfd)
30        {
31            struct sockaddr_in client_address;
32            socklen_t client_addrlength = sizeof(client_address);
33//LT水平触发
34#ifdef LT
35            int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
36            if (connfd < 0)
37            {
38                continue;
39            }
40            if (http_conn::m_user_count >= MAX_FD)
41            {
42                show_error(connfd, "Internal server busy");
43                continue;
44            }
45            users[connfd].init(connfd, client_address);
46#endif
47
48//ET非阻塞边缘触发
49#ifdef ET
50            //需要循环接收数据
51            while (1)
52            {
53                int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
54                if (connfd < 0)
55                {
56                    break;
57                }
58                if (http_conn::m_user_count >= MAX_FD)
59                {
60                    show_error(connfd, "Internal server busy");
61                    break;
62                }
63                users[connfd].init(connfd, client_address);
64            }
65            continue;
66#endif
67        }
68
69        //处理异常事件
70        else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
71        {
72            //服务器端关闭连接
73        }
74
75        //处理信号
76        else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
77        {
78        }
79
80        //处理客户连接上接收到的数据
81        else if (events[i].events & EPOLLIN)
82        {
83            //读入对应缓冲区
84            if (users[sockfd].read_once())
85            {
86                //若监测到读事件,将该事件放入请求队列
87                pool->append(users + sockfd);
88            }
89            else
90            {
91               //服务器关闭连接
92            }
93        }
94
95    }
96}

流程图与状态机

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

主状态机

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

  • 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函数分别完成报文解析与报文响应两个任务。

 1void http_conn::process()
 2{
 3    HTTP_CODE read_ret=process_read();
 4
 5    //NO_REQUEST,表示请求不完整,需要继续接收请求数据
 6    if(read_ret==NO_REQUEST)
 7    {
 8        //注册并监听读事件
 9        modfd(m_epollfd,m_sockfd,EPOLLIN);
10        return;
11    }
12
13    //调用process_write完成报文响应
14    bool write_ret=process_write(read_ret);
15    if(!write_ret)
16    {
17        close_conn();
18    }
19    //注册并监听写事件
20    modfd(m_epollfd,m_sockfd,EPOLLOUT);
21}

本篇将对报文解析的流程和process_read函数细节进行详细介绍。

HTTP_CODE含义

表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析时只涉及到四种。

  • 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//m_start_line是行在buffer中的起始位置,将该位置后面的数据赋给text
 2//此时从状态机已提前将一行的末尾字符\r\n变为\0\0,所以text可以直接取出完整的行进行解析
 3char* get_line(){
 4    return m_read_buf+m_start_line;
 5}
 6
 7
 8http_conn::HTTP_CODE http_conn::process_read()
 9{
10    //初始化从状态机状态、HTTP请求解析结果
11    LINE_STATUS line_status=LINE_OK;
12    HTTP_CODE ret=NO_REQUEST;
13    char* text=0;
14
15    //这里为什么要写两个判断条件?第一个判断条件为什么这样写?
16    //具体的在主状态机逻辑中会讲解。
17
18    //parse_line为从状态机的具体实现
19    while((m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)||((line_status=parse_line())==LINE_OK))
20    {
21        text=get_line();
22
23        //m_start_line是每一个数据行在m_read_buf中的起始位置
24        //m_checked_idx表示从状态机在m_read_buf中读取的位置
25        m_start_line=m_checked_idx;
26
27        //主状态机的三种状态转移逻辑
28        switch(m_check_state)
29        {
30            case CHECK_STATE_REQUESTLINE:
31            {
32                //解析请求行
33                ret=parse_request_line(text);
34                if(ret==BAD_REQUEST)
35                    return BAD_REQUEST;
36                break;
37            }
38            case CHECK_STATE_HEADER:
39            {
40                //解析请求头
41                ret=parse_headers(text);
42                if(ret==BAD_REQUEST)
43                    return BAD_REQUEST;
44
45                //完整解析GET请求后,跳转到报文响应函数
46                else if(ret==GET_REQUEST)
47                {
48                    return do_request();
49                }
50                break;
51            }
52            case CHECK_STATE_CONTENT:
53            {
54                //解析消息体
55                ret=parse_content(text);
56
57                //完整解析POST请求后,跳转到报文响应函数
58                if(ret==GET_REQUEST)
59                    return do_request();
60
61                //解析完消息体即完成报文解析,避免再次进入循环,更新line_status
62                line_status=LINE_OPEN;
63                break;
64            }
65            default:
66            return INTERNAL_ERROR;
67        }
68    }
69    return NO_REQUEST;
70}

从状态机逻辑

上一篇的基础知识讲解中,对于HTTP报文的讲解遗漏了一点细节,在这里作为补充。

在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//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
 3
 4//m_read_idx指向缓冲区m_read_buf的数据末尾的下一个字节
 5//m_checked_idx指向从状态机当前正在分析的字节
 6http_conn::LINE_STATUS http_conn::parse_line()
 7{
 8    char temp;
 9    for(;m_checked_idx<m_read_idx;++m_checked_idx)
10    {
11        //temp为将要分析的字节
12        temp=m_read_buf[m_checked_idx];
13
14        //如果当前是\r字符,则有可能会读取到完整行
15        if(temp=='\r'){
16
17            //下一个字符达到了buffer结尾,则接收不完整,需要继续接收
18            if((m_checked_idx+1)==m_read_idx)
19                return LINE_OPEN;
20            //下一个字符是\n,将\r\n改为\0\0
21            else if(m_read_buf[m_checked_idx+1]=='\n'){
22                m_read_buf[m_checked_idx++]='\0';
23                m_read_buf[m_checked_idx++]='\0';
24                return LINE_OK;
25            }
26            //如果都不符合,则返回语法错误
27            return LINE_BAD;
28        }
29
30        //如果当前字符是\n,也有可能读取到完整行
31        //一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况
32        else if(temp=='\n')
33        {
34            //前一个字符是\r,则接收完整
35            if(m_checked_idx>1&&m_read_buf[m_checked_idx-1]=='\r')
36            {
37                m_read_buf[m_checked_idx-1]='\0';
38                m_read_buf[m_checked_idx++]='\0';
39                return LINE_OK;
40            }
41            return LINE_BAD;
42        }
43    }
44
45    //并没有找到\r\n,需要继续接收
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//解析http请求行,获得请求方法,目标url及http版本号
 2http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
 3{
 4    //在HTTP报文中,请求行用来说明请求类型,要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。
 5    //请求行中最先含有空格和\t任一字符的位置并返回
 6    m_url=strpbrk(text," \t");
 7
 8    //如果没有空格或\t,则报文格式有误
 9    if(!m_url)
10    {
11        return BAD_REQUEST;
12    }
13
14    //将该位置改为\0,用于将前面数据取出
15    *m_url++='\0';
16
17    //取出数据,并通过与GET和POST比较,以确定请求方式
18    char *method=text;
19    if(strcasecmp(method,"GET")==0)
20        m_method=GET;
21    else if(strcasecmp(method,"POST")==0)
22    {
23        m_method=POST;
24        cgi=1;
25    }
26    else
27        return BAD_REQUEST;
28
29    //m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有
30    //将m_url向后偏移,通过查找,继续跳过空格和\t字符,指向请求资源的第一个字符
31    m_url+=strspn(m_url," \t");
32
33    //使用与判断请求方式的相同逻辑,判断HTTP版本号
34    m_version=strpbrk(m_url," \t");
35    if(!m_version)
36        return BAD_REQUEST;
37    *m_version++='\0';
38    m_version+=strspn(m_version," \t");
39
40    //仅支持HTTP/1.1
41    if(strcasecmp(m_version,"HTTP/1.1")!=0)
42        return BAD_REQUEST;
43
44    //对请求资源前7个字符进行判断
45    //这里主要是有些报文的请求资源中会带有http://,这里需要对这种情况进行单独处理
46    if(strncasecmp(m_url,"http://",7)==0)
47    {
48        m_url+=7;
49        m_url=strchr(m_url,'/');
50    }
51
52    //同样增加https情况
53    if(strncasecmp(m_url,"https://",8)==0)
54    {
55        m_url+=8;
56        m_url=strchr(m_url,'/');
57    }
58
59    //一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源
60    if(!m_url||m_url[0]!='/')
61        return BAD_REQUEST;
62
63    //当url为/时,显示欢迎界面
64    if(strlen(m_url)==1)
65        strcat(m_url,"judge.html");
66
67    //请求行处理完毕,将主状态机转移处理请求头
68    m_check_state=CHECK_STATE_HEADER;
69    return NO_REQUEST;
70}

解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的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//解析http请求的一个头部信息
 2http_conn::HTTP_CODE http_conn::parse_headers(char *text)
 3{
 4    //判断是空行还是请求头
 5    if(text[0]=='\0')
 6    {
 7        //判断是GET还是POST请求
 8        if(m_content_length!=0)
 9        {
10            //POST需要跳转到消息体处理状态
11            m_check_state=CHECK_STATE_CONTENT;
12            return NO_REQUEST;
13        }
14        return GET_REQUEST;
15    }
16    //解析请求头部连接字段
17    else if(strncasecmp(text,"Connection:",11)==0)
18    {
19        text+=11;
20
21        //跳过空格和\t字符
22        text+=strspn(text," \t");
23        if(strcasecmp(text,"keep-alive")==0)
24        {
25            //如果是长连接,则将linger标志设置为true
26            m_linger=true;
27        }
28    }
29    //解析请求头部内容长度字段
30    else if(strncasecmp(text,"Content-length:",15)==0)
31    {
32        text+=15;
33        text+=strspn(text," \t");
34        m_content_length=atol(text);
35    }
36    //解析请求头部HOST字段
37    else if(strncasecmp(text,"Host:",5)==0)
38    {
39        text+=5;
40        text+=strspn(text," \t");
41        m_host=text;
42    }
43    else{
44        printf("oop!unknow header: %s\n",text);
45    }
46    return NO_REQUEST;
47}

如果仅仅是GET请求,如项目中的欢迎界面,那么主状态机只设置之前的两个状态足矣。

因为在上篇推文中我们曾说道,GET和POST请求报文的区别之一是有无消息体部分,GET请求没有消息体,当解析完空行之后,便完成了报文的解析。

但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装。

为此,我们需要在解析报文的部分添加解析消息体的模块。

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//判断http请求是否被完整读入
 2http_conn::HTTP_CODE http_conn::parse_content(char *text)
 3{
 4    //判断buffer中是否读取了消息体
 5    if(m_read_idx>=(m_content_length+m_checked_idx)){
 6
 7        text[m_content_length]='\0';
 8
 9        //POST请求中最后为输入的用户名和密码
10        m_string = text;
11
12        return GET_REQUEST;
13    }
14    return NO_REQUEST;
15}

基础API

stat

stat函数用于取得指定文件的文件属性,并将文件属性存储在结构体stat里,这里仅对其中用到的成员进行介绍。

 1#include <sys/types.h>
 2#include <sys/stat.h>
 3#include <unistd.h>
 4
 5//获取文件属性,存储在statbuf中
 6int stat(const char *pathname, struct stat *statbuf);
 7
 8struct stat 
 9{
10   mode_t    st_mode;        /* 文件类型和权限 */
11   off_t     st_size;        /* 文件大小,字节数*/
12};

mmap

用于将一个文件或其他对象映射到内存,提高文件的访问速度。

1void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
2int munmap(void* start,size_t length);
  • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址

  • length:映射区的长度

  • prot:期望的内存保护标志,不能与文件的打开模式冲突

    • PROT_READ 表示页内容可以被读取
  • flags:指定映射对象的类型,映射选项和映射页是否可以共享

    • MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件
  • fd:有效的文件描述符,一般是由open()函数返回

  • off_toffset:被映射对象内容的起点

iovec

定义了一个向量元素,通常,这个结构用作一个多元素的数组。

1struct 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>
2ssize_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完成数据的读取与发送。 alt

HTTP_CODE含义

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

  • NO_REQUEST
  1. 请求不完整,需要继续读取请求报文数据
  2. 跳转主线程继续监测读事件
  • GET_REQUEST
  1. 获得了完整的HTTP请求
  2. 调用do_request完成请求资源映射
  • NO_RESOURCE
  1. 请求资源不存在
  2. 跳转process_write完成响应报文
  • BAD_REQUEST
  1. HTTP请求报文有语法错误或请求资源为目录
  2. 跳转process_write完成响应报文
  • FORBIDDEN_REQUEST
  1. 请求资源禁止访问,没有读取权限
  2. 跳转process_write完成响应报文
  • FILE_REQUEST
  1. 请求资源可以正常访问
  2. 跳转process_write完成响应报文
  • INTERNAL_ERROR
  1. 服务器内部错误,该结果在主状态机逻辑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,即欢迎访问页面

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

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

/2CGISQL.cgi

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

/3CGISQL.cgi

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

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

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

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

如果大家对上述设置方式不理解,不用担心。具体的登录和注册校验功能会在第12节进行详解,到时候还会针对html进行介绍。

 1//网站根目录,文件夹内存放请求的资源和跳转的html文件
 2const char* doc_root="/home/qgy/github/ini_tinywebserver/root";
 3
 4http_conn::HTTP_CODE http_conn::do_request()
 5{
 6    //将初始化的m_real_file赋值为网站根目录
 7    strcpy(m_real_file,doc_root);
 8    int len=strlen(doc_root);
 9
10    //找到m_url中/的位置
11    const char *p = strrchr(m_url, '/'); 
12
13    //实现登录和注册校验
14    if(cgi==1 && (*(p+1) == '2' || *(p+1) == '3'))
15    {
16        //根据标志判断是登录检测还是注册检测
17
18        //同步线程登录校验
19
20        //CGI多进程登录校验
21    }
22
23    //如果请求资源为/0,表示跳转注册界面
24    if(*(p+1) == '0'){
25        char *m_url_real = (char *)malloc(sizeof(char) * 200);
26        strcpy(m_url_real,"/register.html");
27
28        //将网站目录和/register.html进行拼接,更新到m_real_file中
29        strncpy(m_real_file+len,m_url_real,strlen(m_url_real));
30
31        free(m_url_real);
32    }
33    //如果请求资源为/1,表示跳转登录界面
34    else if( *(p+1) == '1'){
35        char *m_url_real = (char *)malloc(sizeof(char) * 200);
36        strcpy(m_url_real,"/log.html");
37
38        //将网站目录和/log.html进行拼接,更新到m_real_file中
39        strncpy(m_real_file+len,m_url_real,strlen(m_url_real));
40
41        free(m_url_real);
42    }
43    else
44        //如果以上均不符合,即不是登录和注册,直接将url与网站目录拼接
45        //这里的情况是welcome界面,请求服务器上的一个图片
46        strncpy(m_real_file+len,m_url,FILENAME_LEN-len-1);
47
48    //通过stat获取请求资源文件信息,成功则将信息更新到m_file_stat结构体
49    //失败返回NO_RESOURCE状态,表示资源不存在
50    if(stat(m_real_file,&m_file_stat)<0)
51        return NO_RESOURCE;
52
53    //判断文件的权限,是否可读,不可读则返回FORBIDDEN_REQUEST状态
54    if(!(m_file_stat.st_mode&S_IROTH))
55        return FORBIDDEN_REQUEST;
56    //判断文件类型,如果是目录,则返回BAD_REQUEST,表示请求报文有误
57    if(S_ISDIR(m_file_stat.st_mode))
58        return BAD_REQUEST;
59
60    //以只读方式获取文件描述符,通过mmap将该文件映射到内存中
61    int fd=open(m_real_file,O_RDONLY);
62    m_file_address=(char*)mmap(0,m_file_stat.st_size,PROT_READ,MAP_PRIVATE,fd,0);
63
64    //避免文件描述符的浪费和占用
65    close(fd);
66
67    //表示请求文件存在,且可以访问
68    return FILE_REQUEST;
69}

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中的内容。

 1bool http_conn::add_response(const char* format,...)
 2{
 3    //如果写入内容超出m_write_buf大小则报错
 4    if(m_write_idx>=WRITE_BUFFER_SIZE)
 5        return false;
 6
 7    //定义可变参数列表
 8    va_list arg_list;
 9
10    //将变量arg_list初始化为传入参数
11    va_start(arg_list,format);
12
13    //将数据format从可变参数列表写入缓冲区写,返回写入数据的长度
14    int len=vsnprintf(m_write_buf+m_write_idx,WRITE_BUFFER_SIZE-1-m_write_idx,format,arg_list);
15
16    //如果写入的数据长度超过缓冲区剩余空间,则报错
17    if(len>=(WRITE_BUFFER_SIZE-1-m_write_idx)){
18        va_end(arg_list);
19        return false;
20    }
21
22    //更新m_write_idx位置
23    m_write_idx+=len;
24    //清空可变参列表
25    va_end(arg_list);
26
27    return true;
28}
29
30//添加状态行
31bool http_conn::add_status_line(int status,const char* title)
32{
33    return add_response("%s %d %s\r\n","HTTP/1.1",status,title);
34}
35
36//添加消息报头,具体的添加文本长度、连接状态和空行
37bool http_conn::add_headers(int content_len)
38{
39    add_content_length(content_len);
40    add_linger();
41    add_blank_line();
42}
43
44//添加Content-Length,表示响应报文的长度
45bool http_conn::add_content_length(int content_len)
46{
47    return add_response("Content-Length:%d\r\n",content_len);
48}
49
50//添加文本类型,这里是html
51bool http_conn::add_content_type()
52{
53    return add_response("Content-Type:%s\r\n","text/html");
54}
55
56//添加连接状态,通知浏览器端是保持连接还是关闭
57bool http_conn::add_linger()
58{
59    return add_response("Connection:%s\r\n",(m_linger==true)?"keep-alive":"close");
60}
61//添加空行
62bool http_conn::add_blank_line()
63{
64    return add_response("%s","\r\n");
65}
66
67//添加文本content
68bool http_conn::add_content(const char* content)
69{
70    return add_response("%s",content);
71}

响应报文分为两种,一种是请求文件的存在,通过io向量机制iovec,声明两个iovec,第一个指向m_write_buf,第二个指向mmap的地址m_file_address;一种是请求出错,这时候只申请一个iovec,指向m_write_buf。

  • iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据。
  • 成员iov_len表示实际写入的长度
 1bool http_conn::process_write(HTTP_CODE ret)
 2{
 3    switch(ret)
 4    {
 5        //内部错误,500
 6        case INTERNAL_ERROR:
 7        {
 8            //状态行
 9            add_status_line(500,error_500_title);
10            //消息报头
11            add_headers(strlen(error_500_form));
12            if(!add_content(error_500_form))
13                return false;
14            break;
15        }
16        //报文语法有误,404
17        case BAD_REQUEST:
18        {
19            add_status_line(404,error_404_title);
20            add_headers(strlen(error_404_form));
21            if(!add_content(error_404_form))
22                return false;
23            break;
24        }
25        //资源没有访问权限,403
26        case FORBIDDEN_REQUEST:
27        {
28            add_status_line(403,error_403_title);
29            add_headers(strlen(error_403_form));
30            if(!add_content(error_403_form))
31                return false;
32            break;
33        }
34        //文件存在,200
35        case FILE_REQUEST:
36        {
37            add_status_line(200,ok_200_title);
38            //如果请求的资源存在
39            if(m_file_stat.st_size!=0)
40            {
41                add_headers(m_file_stat.st_size);
42                //第一个iovec指针指向响应报文缓冲区,长度指向m_write_idx
43                m_iv[0].iov_base=m_write_buf;
44                m_iv[0].iov_len=m_write_idx;
45                //第二个iovec指针指向mmap返回的文件指针,长度指向文件大小
46                m_iv[1].iov_base=m_file_address;
47                m_iv[1].iov_len=m_file_stat.st_size;
48                m_iv_count=2;
49                //发送的全部数据为响应报文头部信息和文件大小
50                bytes_to_send = m_write_idx + m_file_stat.st_size;
51                return true;
52            }
53            else
54            {
55                //如果请求的资源大小为0,则返回空白html文件
56                const char* ok_string="<html><body></body></html>";
57                add_headers(strlen(ok_string));
58                if(!add_content(ok_string))
59                    return false;
60            }
61        }
62        default:
63            return false;
64    }
65    //除FILE_REQUEST状态外,其余状态只申请一个iovec,指向响应报文缓冲区
66    m_iv[0].iov_base=m_write_buf;
67    m_iv[0].iov_len=m_write_idx;
68    m_iv_count=1;
69    return true;
70}

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),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性。

 1bool http_conn::write()
 2{
 3    int temp = 0;
 4
 5    int newadd = 0;
 6
 7    //若要发送的数据长度为0
 8    //表示响应报文为空,一般不会出现这种情况
 9    if(bytes_to_send==0)
10    {
11        modfd(m_epollfd,m_sockfd,EPOLLIN);
12        init();
13        return true;
14    }
15
16    while (1)
17    {   
18        //将响应报文的状态行、消息头、空行和响应正文发送给浏览器端
19        temp=writev(m_sockfd,m_iv,m_iv_count);
20
21        //正常发送,temp为发送的字节数
22        if (temp > 0)
23        {
24            //更新已发送字节
25            bytes_have_send += temp;
26            //偏移文件iovec的指针
27            newadd = bytes_have_send - m_write_idx;
28        }
29        if (temp <= -1)
30        {
31            //判断缓冲区是否满了
32            if (errno == EAGAIN)
33            {
34                //第一个iovec头部信息的数据已发送完,发送第二个iovec数据
35                if (bytes_have_send >= m_iv[0].iov_len)
36                {
37                    //不再继续发送头部信息
38                    m_iv[0].iov_len = 0;
39                    m_iv[1].iov_base = m_file_address + newadd;
40                    m_iv[1].iov_len = bytes_to_send;
41                }
42                //继续发送第一个iovec头部信息的数据
43                else
44                {
45                    m_iv[0].iov_base = m_write_buf + bytes_to_send;
46                    m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
47                }
48                //重新注册写事件
49                modfd(m_epollfd, m_sockfd, EPOLLOUT);
50                return true;
51            }
52            //如果发送失败,但不是缓冲区问题,取消映射
53            unmap();
54            return false;
55        }
56
57        //更新已发送字节数
58        bytes_to_send -= temp;
59
60        //判断条件,数据已全部发送完
61        if (bytes_to_send <= 0)
62        {
63            unmap();
64
65            //在epoll树上重置EPOLLONESHOT事件
66            modfd(m_epollfd,m_sockfd,EPOLLIN);
67
68            //浏览器的请求为长连接
69            if(m_linger)
70            {
71                //重新初始化HTTP对象
72                init();
73                return true;
74            }
75            else
76            {
77                return false;
78            }
79        }
80    }
81}

书中原代码的write函数不严谨,这里对其中的Bug进行了修复,可以正常传输大文件。

全部评论

相关推荐

喜欢走神的孤勇者练习时长两年半:池是池,发是发,我曾池,我现黑
点赞 评论 收藏
分享
有趣的牛油果开挂了:最近这个阶段收到些杂七杂八的短信是真的烦
点赞 评论 收藏
分享
点赞 4 评论
分享
牛客网
牛客企业服务