【webserver】 第8节 响应报文的生成

代码开源

GitHub - PetterZhukov/webserver_HTTP: 使用了线程池,通过epoll实现的Proctor版本的web服务器。参考了游双老师的《Linux高性能服务器编程》以及牛客网的《Linux高并发服务器开发》课程。在自己复现的基础上进行模块的整合并添加一些小更改。所有代码拥有完备的注释。

 

 

目录

8.1 生成响应报文的基本思路

8.2 前置知识

1. 内存映射

2. 可变参数

3. unordered_map,tuple

8.3 生成响应报文的具体流程

8.4具体实现


8.1 生成响应报文的基本思路

        经过前面的介绍,对于主线程的操作、子线程关于分析请求报文的操作都介绍完毕,现在就是最后一步——响应报文的生成了。

        经过请求报文的分析后,对于请求头的分析我们获得了主机的各项信息,是否保持连接等;通过对请求行的分析我们了解了对面要获取的资源位置、HTTP协议版本、HTTP请求方法。

        因此生成响应报文的思路如下:

  • 通过分析请求报文的返回结果,是否有语法错误
  • 若没有语法错误,对URL中的文件进行判断,查看是否可以访问
  • 若文件可以访问,则对其进行内存映射,将映射后的结果作为响应正文
  • 通过刚刚判定的信息生成响应的状态行、响应头
  • 将这二者传给主线程,修改epoll事件表激活写事件

8.2 前置知识

1. 内存映射

    #include <sys/mman.h>
    void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
        - 功能:将一个文件或者设备的数据映射到内存中
        - 参数:
            - void *addr: NULL, 由内核指定
            - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
                    获取文件的长度:stat lseek
            - prot : 对申请的内存映射区的操作权限
                -PROT_EXEC :可执行的权限
                -PROT_READ :读权限
                -PROT_WRITE :写权限
                -PROT_NONE :没有权限
                要操作映射内存,必须要有读的权限。
                PROT_READ、PROT_READ|PROT_WRITE
            - flags :
                - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
                - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
            - fd: 需要映射的那个文件的文件描述符
                - 通过open得到,open的是一个磁盘文件
                - 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
                    prot: PROT_READ                open:只读/读写 
                    prot: PROT_READ | PROT_WRITE   open:读写
                    - prot <= open
            - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。
        - 返回值:返回创建的内存的首地址
            失败返回MAP_FAILED,(void *) -1

    int munmap(void *addr, size_t length);
        - 功能:释放内存映射
        - 参数:
            - addr : 要释放的内存的首地址
            - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。

2. 可变参数

int vsnprintf (char * sbuf, size_t n, const char * format, va_list arg );
    - 参数sbuf:用于缓存格式化字符串结果的字符数组

    -参数
        n:限定最多打印到缓冲区sbuf的字符的个数为n-1个
        format:格式化限定字符串
        arg:可变长度参数列表
    -返回:
        成功:若空间足够打印的长度(不包括'\0')
        失败:负数

文档中的原文是

If the output was truncated due to this limit, then the return value is the number of characters (excluding the terminating null byte) which would  have  been  written  to  the  final  string  if  enough space had been available. 

所以还是要判断一下有没有溢出

用法:


#include <stdarg.h>
void MyPrintF( const char * format, ... )    // 可变参数列表
{
	va_list args;    // args负责接受可变参数列表
	va_start (args, format);    // 通过format来对args进行初始化
	vsnprintf (sbuf,SBUF_SIZE,format, args);   // 使用vsnprintf
	va_end (args);    // 结束	
}

3. unordered_map,tuple

        这两个是C++11的内容,使用这个主要是因为报文头的返回信息有多个,要是用switch进行分类有大量重复代码,且扩充很麻烦,我希望可以对其进行复用,因此使用了一个unordered_map<int, tuple<int, const char *, const char *>> 来存储报文状态码以及对应的状态码描述以及回送的正文。

        因为int, const char *, const char *有三个成员不方便使用pair,因此我使用了tuple,它不限制里面的成员数量,操作起来也比单独建立一个类容易。

8.3 生成响应报文的具体流程

        0.进行请求报文分析的时候,进行URL分析。使用内存映射映射文件,不能映射则报错

        1.根据状态码,若为错误状态码,则提取对应的报错介绍和报错信息,作为响应头和响应正文;若为正确的状态码,则提取成功信息作为响应头,提取的内存映射作为响应正文

        2.根据当前的响应头和响应正文的情况,对写缓冲区进行相应的写入(状态行、相应头)

        3.将写事件写入epoll事件表,之后主线程会自动将写缓冲区中的前半部分报文和响应正文通过分散写传回给客户端(见前文Write)

8.4具体实现

请求报文分析后进行的对应处理操作

// 在分析完成以后进行具体的处理
http_conn::HTTP_CODE http_conn::do_request()
{
    // 更新
    int sumlen = strlen(m_url) + strlen(root_directory) + 1;
    snprintf(m_filename, std::min((int)(FILENAME_MAXLEN), sumlen), "%s%s", root_directory, m_url);
    printf("m_filename :%s\n", m_filename);
    // m_filename = "resources" + "/xxxxxxx"

    // 获取文件相关信息
    int ret = stat(m_filename, &m_file_stat);
    if (ret == -1)
    {
        perror("stat");
        return NO_RESOURCE;
    }

    // 判断访问权限
    if (!(m_file_stat.st_mode & S_IROTH))
    {
        return FORBIDDEN_REQUEST;
    }

    // 判断是否是目录
    if (S_ISDIR(m_file_stat.st_mode))
    {
        return BAD_REQUEST;
    }

    // 对文件操作 只读方式打开
    int fd = open(m_filename, O_RDONLY);
    // 创建内存映射
    m_address_mmap = (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, ...)
{
    if (m_write_index >= WRITE_BUFFER_SIZE)
    {
        return false; // 已满
    }
    va_list args;
    va_start(args, format);
    int len = vsnprintf(m_write_buf + m_write_index, WRITE_BUFFER_SIZE - m_write_index - 1, format, args);
    // vsnprintf 用法类似snprintf,输入的最大长度为__maxlen-1
    // 调用args和format进行可变参数输入
    // 返回值为若空间足够则输入的长度
    if (len > WRITE_BUFFER_SIZE - m_write_index - 1)
    {
        // 说明输入的字符溢出
        return false;
    }
    m_write_index += len; // 更新写缓冲区的长度
    va_end(args);
    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, time_t time)
{
    if (!add_content_length(content_len)) return false;
    if (!add_content_type()) return false;
    if (!add_connection()) return false;
    if (!add_date(time)) return false;
    if (!add_blank_line()) return false;
    return true;
}
// 响应头部组件
//      content-length
bool http_conn::add_content_length(int content_len)
{
    return add_response("Content-Length: %d\r\n", content_len);
}
//      Content-Type
bool http_conn::add_content_type()
{
    // 虑区分是图片 / html/css
    char *format_file = strrchr(m_filename, '.');
    return add_response("Content-Type: %s\r\n", format_file == NULL ? "text/html" : (format_file + 1));
}
//      keep_alive / close
bool http_conn::add_connection()
{
    return add_response("Connection: %s\r\n", (m_keepalive == true) ? "keep-alive" : "close");
}
//      发送时间
bool http_conn::add_date(time_t t)
{
    char timebuf[50];
    strftime(timebuf, 80, "%Y-%m-%d %H:%M:%S", localtime(&t));
    return add_response("Date: %s\r\n", timebuf);
}
//      空白结束行
bool http_conn::add_blank_line()
{
    return add_response("\r\n");
}

 

 生成响应报文部分,调度上述的模块

//================== 生成返回的报文 ====================
bool http_conn::process_write(HTTP_CODE ret)
{
    /*
        NO_REQUEST : 请求不完整,需要继续读取客户数据
        GET_REQUEST : 表示获得了一个完成的客户请求
        BAD_REQUEST : 表示客户请求语法错误
        NO_RESOURCE : 表示服务器没有资源
        FORBIDDEN_REQUEST : 表示客户对资源没有足够的访问权限
        FILE_REQUEST : 文件请求,获取文件成功
        INTERNAL_ERROR : 表示服务器内部错误
        CLOSED_CONNECTION : 表示客户端已经关闭连接了
    */

    int status = std::get<0>(response_info[ret]);
    const char *title = std::get<1>(response_info[ret]);
    const char *form = std::get<2>(response_info[ret]);
    if (ret == FILE_REQUEST)
    { // OK,发送报文头和文件
        if (!add_status_line(status, title))
            return false;
        if (!add_headers(m_file_stat.st_size, time(NULL)))
            return false; // 发送本地时间
        m_iv[0].iov_base = m_write_buf;
        m_iv[0].iov_len = m_write_index;
        m_iv[1].iov_base = m_address_mmap;
        m_iv[1].iov_len = m_file_stat.st_size;
        m_iv_count = 2;

        // 维护发送长度
        bytes_to_send = m_write_index + m_file_stat.st_size;

        return true;
    }
    else if (response_info.find(ret) != response_info.end())
    { // 发送错误信息
        if (!add_status_line(status, title))
            return false;
        if (!add_headers(strlen(form), time(NULL)))
            return false; // 发送本地时间

        m_iv[0].iov_base = m_write_buf;
        m_iv[0].iov_len = m_write_index;
        m_iv[1].iov_base = (char *)form;
        m_iv[1].iov_len = strlen(form) + 1;
        m_iv_count = 2;
        // 维护发送长度
        bytes_to_send = m_iv[0].iov_len + m_iv[1].iov_len;

        return true;
    }
    else
        return false;
}

如下是之前介绍的线程池中调用的process部分,也是子线程的业务逻辑

void http_conn::process()
{
    // 解析HTTP请求
    HTTP_CODE read_ret = process_read();
    #ifdef process_read_result
        printf("process_read result : %d\n", read_ret);
    #endif
    if (read_ret == NO_REQUEST)
    { // 读的没有问题,则修改fd,让其再次使用
        modfd(st_m_epollfd, m_sockfd, EPOLLIN);
        return;
    }

    // 生成响应报文
    bool write_ret = process_write(read_ret);
    if (!write_ret)
    {
        close_conn();
    }

    // close之后时候要执行modfd
    modfd(st_m_epollfd, m_sockfd, EPOLLOUT);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值