我是靠谱客的博主 腼腆小虾米,最近开发中收集的这篇文章主要介绍【C++实现HTTP服务器项目记录】HTTP报文处理一、HTTP报文格式二、解析HTTP请求报文三、生成HTTP响应报文四、内存映射五、获取文件属性六、高级I/O,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 一、HTTP报文格式
    • 1. 请求报文
    • 2. 响应报文
  • 二、解析HTTP请求报文
    • 1. 有限状态机
    • 2. 状态转换图
    • 3. 代码实现
  • 三、生成HTTP响应报文
    • 1. 代码实现
  • 四、内存映射
  • 五、获取文件属性
  • 六、高级I/O
    • 1. 聚集写
    • 2. 解决大文件传输问题

一、HTTP报文格式

1. 请求报文

请求报文描述
请求行用来说明请求类型、要访问的资源以及所使用的HTTP协议版本。
请求头格式为:属性名:属性值,服务端据此获取客户端的信息。
空行即使请求体为空,请求头后面的空行也必须要有。
请求体将页面表单中的组件值通过param1=value1&param2=value2键值对的形式编码成一个格式化串。
请求头字段含义
Accept浏览器可以处理的MIME类型。
Accept-Charset浏览器能识别的字符集。
Accept-Encoding浏览器可以处理的编码方式。
Accept-Language浏览器接收的语言。
Connection连接管理,可以是keep-alive或close。
Content-length请求体的长度,单位为字节。
Host服务器域名或IP地址。
Referer用户从该URL代表的页面访问当前请求的页面。
User-Agent用户的浏览器相关信息。

2. 响应报文

响应报文描述
状态行用来说明HTTP协议版本号、状态码、状态消息。
响应头用来说明客户端要使用的一些附加信息。
空行响应头后面的空行必须要有。
响应体服务器返回给客户端的文本信息。
状态码描述
200客户端请求被正常处理。
301永久重定向,该资源已被永久移动到新位置,将来对该资源的访问都要使用本响应返回的若干个URL之一。
302临时重定向,请求的资源现在临时从不同的URL中获得。
400请求报文存在语法错误。
403请求被服务器拒绝。
404请求不存在,服务器上找不到请求的资源。
500服务器在执行请求时出现错误。

二、解析HTTP请求报文

1. 有限状态机

- 为研究有限内存的计算过程和某些语言类而抽象出的一种计算模型。
- 有限状态自动机拥有有限数量的状态,每个状态可以迁移到零个或多个状态,输入字串决定执行哪个状态的迁移。
- 有限状态自动机可以表示为一个有向图。

2. 状态转换图

主状态机状态描述
CHECK_STATE_REQUESTLINE解析请求行
CHECK_STATE_HEADER解析请求头
CHECK_STATE_CONTENT解析请求体
从状态机状态描述
LINE_OK完整读取一行
LINE_BAD报文语法有误
LINE_OPEN读取的行不完整
处理结果描述
NO_REQUEST请求不完整,需要继续读取请求报文数据
GET_REQUEST获得了完整的HTTP请求
BAD_REQUESTHTTP请求报文有语法错误
INTERNAL_ERROR服务器内部错误

状态转换图

3. 代码实现

- 从状态机负责读取报文的一行,主状态机负责对该行数据进行解析。
- 主状态机内部调用从状态机,从状态机驱动主状态机。
// 从状态机负责读取报文的一行
http_conn::LINE_STATUS http_conn::parse_line()
{
    // 将要分析的字节
    char temp;
    // m_read_idx:读缓冲区中数据的最后一个字节的下一个位置
    // m_checked_idx:指向从状态机当前正在分析的字节,最终指向读缓冲区下一行的开头
    for (; m_checked_idx < m_read_idx; ++m_checked_idx)
    {
        temp = m_read_buf[m_checked_idx];
        // 当前是'r',则有可能会读取到完整行
        if (temp == 'r')
        {
            // 下一个字符的位置是读缓冲区末尾
            if ((m_checked_idx + 1) == m_read_idx)
            {
                // 读取的行不完整,需要继续接收
                return LINE_OPEN;
            }
            // 下一个字符是'n'
            else if (m_read_buf[m_checked_idx + 1] == 'n')
            {
                // 将'rn'改为''
                m_read_buf[m_checked_idx++] = '';
                m_read_buf[m_checked_idx++] = '';
                // 完整读取一行
                return LINE_OK;
            }
            // 否则报文语法有误
            return LINE_BAD;
        }
        // 当前字符是'n',则有可能读取到完整行
        // 上次读取到'r'就到读缓冲区末尾了,没有接收完整,再次接收时会出现这种情况
        else if (temp == 'n')
        {
            // 前一个字符是'r'
            if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == 'r')
            {
                // 将'rn'改为''
                m_read_buf[m_checked_idx - 1] = '';
                m_read_buf[m_checked_idx++] = '';
                // 完整读取一行
                return LINE_OK;
            }
            // 否则报文语法有误
            return LINE_BAD;
        }
    }
    // 没有找到'rn',读取的行不完整
    return LINE_OPEN;
}

// 解析HTTP请求行
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
    // 返回请求行中最先含有 空格 或 't'的位置
    m_url = strpbrk(text, " t");

    // 如果没有 't' 或 空格
    if (!m_url)
    {
        // HTTP请求报文有语法错误
        return BAD_REQUEST;
    }
    // 将该位置改为'',用于将请求类型取出
    *m_url++ = '';
    char *method = text;

    // 忽略大小写比较
    if (strcasecmp(method, "GET") == 0)
    {
        // GET请求
        m_method = GET;
    }
    else if (strcasecmp(method, "POST") == 0)
    {
        // POST请求
        m_method = POST;
        cgi = 1;
    }
    else
    {
        // HTTP请求报文有语法错误
        return BAD_REQUEST;
    }

    // 此时m_url跳过了第一个 空格 或 't',但不知道之后是否还有
    // 将m_url向后偏移,通过查找继续跳过 空格 和 't',指向请求资源的第一个字符
    m_url += strspn(m_url, " t");

    m_version = strpbrk(m_url, " t");
    // 如果没有 't' 或 空格
    if (!m_version)
    {
        // HTTP请求报文有语法错误
        return BAD_REQUEST;
    }
    // 将该位置改为'',用于将请求资源取出
    *m_version++ = '';

    m_version += strspn(m_version, " t");
    // 仅支持HTTP/1.1
    if (strcasecmp(m_version, "HTTP/1.1") != 0)
    {
        // HTTP请求报文有语法错误
        return BAD_REQUEST;
    }

    // 对请求资源前7/8个字符进行判断
    // 有些报文的请求资源中会带有http://,这里需要对这种情况进行单独处理
    if (strncasecmp(m_url, "http://", 7) == 0)
    {
        m_url += 7;
        // 返回首次出现'/'的位置的指针
        m_url = strchr(m_url, '/');
    }

    if (strncasecmp(m_url, "https://", 8) == 0)
    {
        m_url += 8;
        // 返回首次出现'/'的位置的指针
        m_url = strchr(m_url, '/');
    }

    if (!m_url || m_url[0] != '/')
    {
        // HTTP请求报文有语法错误
        return BAD_REQUEST;
    }

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

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

// 解析HTTP请求头与空行
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
    // 解析空行
    if (text[0] == '')
    {
        if (m_content_length != 0)
        {
            // POST请求,主状态机跳转到消息体处理状态
            m_check_state = CHECK_STATE_CONTENT;
            // 请求不完整
            return NO_REQUEST;
        }
        // GET请求,获得了完整的HTTP请求
        return GET_REQUEST;
    }
    // 解析HTTP请求头各字段
    // Connection
    else if (strncasecmp(text, "Connection:", 11) == 0)
    {
        text += 11;
        // 跳过't'与空格
        text += strspn(text, " t");
        if (strcasecmp(text, "keep-alive") == 0)
        {
            m_linger = true;
        }
    }
    // Content-length
    else if (strncasecmp(text, "Content-length:", 15) == 0)
    {
        text += 15;
        // 跳过't'与空格
        text += strspn(text, " t");
        m_content_length = atol(text);
    }
    // Host
    else if (strncasecmp(text, "Host:", 5) == 0)
    {
        text += 5;
        // 跳过't'与空格
        text += strspn(text, " t");
        m_host = text;
    }
    else
    {
        LOG_INFO("Unknown Request Header Field: %s", text);
    }
    // 请求不完整
    return NO_REQUEST;
}

// 解析HTTP请求体 
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{
    if (m_read_idx >= (m_content_length + m_checked_idx))
    {
        text[m_content_length] = '';
        // POST请求中输入的用户名和密码
        m_string = text;
        // 获得了完整的HTTP请求
        return GET_REQUEST;
    }
    // 请求不完整
    return NO_REQUEST;
}

// 解析读缓冲区中HTTP请求报文
http_conn::HTTP_CODE http_conn::process_read()
{
    // 初始化从状态机状态:完整读取一行
    LINE_STATUS line_status = LINE_OK;
    // 初始化报文解析的结果:请求不完整
    HTTP_CODE ret = NO_REQUEST;
    // 当前解析的一段报文
    char *text = 0;

    // (主状态机状态 == 解析消息体 && 从状态机状态 == 完整读取一行) || (返回的从状态机状态 == 完整读取一行)
    while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) ||
           ((line_status = parse_line()) == LINE_OK))
    {
        // 将指针向后偏移,指向未处理的字符
        text = get_line();
        // m_checked_idx:指向从状态机当前正在分析的字节 -> 最终指向下一行的开头
        m_start_line = m_checked_idx;
        LOG_INFO("Currently parsing: %s", text);

        // 主状态机状态
        switch (m_check_state)
        {
        // 解析请求行
        case CHECK_STATE_REQUESTLINE:
        {
            ret = parse_request_line(text);
            if (ret == BAD_REQUEST)
            {
                return BAD_REQUEST;
            }
            break;
        }
        // 解析请求头
        case CHECK_STATE_HEADER:
        {
            ret = parse_headers(text);
            if (ret == BAD_REQUEST)
            {
                return BAD_REQUEST;
            }
            // 完整解析GET请求
            else if (ret == GET_REQUEST)
            {
                // 跳转到报文响应函数
                return do_request();
            }
            break;
        }
        // 解析消息体
        case CHECK_STATE_CONTENT:
        {
            // 判断读缓冲区中是否读取了消息体
            ret = parse_content(text);
            // 完整解析POST请求
            if (ret == GET_REQUEST)
            {
                // 跳转到报文响应函数
                return do_request();
            }
            // 解析完消息体即完成报文解析,避免再次进入循环,更新从状态机状态
            line_status = LINE_OPEN;
            break;
        }
        // 否则服务器内部错误
        default:
            return INTERNAL_ERROR;
        }
    }
    // 请求不完整
    return NO_REQUEST;
}

三、生成HTTP响应报文

1. 代码实现

// 组成响应报文
bool http_conn::add_response(const char *format, ...)
{
    // 写缓冲区中数据的最后一个字节的下一个位置 >= 写缓冲区大小
    if (m_write_idx >= WRITE_BUFFER_SIZE)
    {
        return false;
    }
    // 定义可变参数列表
    va_list arg_list;
    // 将变量arg_list初始化为传入参数
    va_start(arg_list, format);
    // 将数据从可变参数列表写入写缓冲区,返回写入数据的长度
    int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
    // 写入的数据长度 >= 缓冲区剩余空间
    if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
    {
        // 清空可变参列表
        va_end(arg_list);
        return false;
    }
    // 更新写缓冲区中数据的最后一个字节的下一个位置
    m_write_idx += len;
    // 清空可变参列表
    va_end(arg_list);

    LOG_INFO("Response: %s", m_write_buf);

    return true;
}

// 添加响应行
bool http_conn::add_status_line(int status, const char *title)
{
    // HTTP协议版本号 状态码 状态消息
    return add_response("%s %d %srn", "HTTP/1.1", status, title);
}

// 添加响应头
bool http_conn::add_headers(int content_len)
{
    // 响应报文长度、连接状态、空行
    return add_content_length(content_len) &&
           add_linger() &&
           add_blank_line();
}

// 响应头添加响应报文的长度
bool http_conn::add_content_length(int content_len)
{
    return add_response("Content-Length:%drn", content_len);
}

// 响应头添加连接状态
bool http_conn::add_linger()
{
    return add_response("Connection:%srn", (m_linger == true) ? "keep-alive" : "close");
}

// 添加空行
bool http_conn::add_blank_line()
{
    return add_response("%s", "rn");
}

// 响应头添加文本类型
bool http_conn::add_content_type()
{
    return add_response("Content-Type:%srn", "text/html");
}

// 响应头添加响应正文
bool http_conn::add_content(const char *content)
{
    return add_response("%s", content);
}

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

四、内存映射

- 为了提高资源文件访问速度,通过内存映射,将普通文件映射到内存,内存映射区对应的内存空间在进程的用户区。
- 拓展:进程间通信使用的内存映射区在每个进程内部都有一块。由于每个进程的地址空间是独立的,各个进程不能直接访问对方的内存映射区。
- 解决方法:需要通信的进程需要将各自的内存映射区和同一个磁盘文件进行映射。
void *mmap(
    void *addr,    // 创建内存映射区的位置,一般委托内核分配,指定为NULL。
    size_t length, // 创建的内存映射区的大小,单位:字节。
    int prot,      // 内存映射区的操作权限。PROT_READ:读内存映射区;PROT_WRITE: 写内存映射区。
    int flags,     // MAP_SHARED: 多个进程共享数据;MAP_PRIVATE: 映射区是私有的,不能同步给其他进程。
    int fd,        // 文件描述符,内存映射区通过该文件描述符和磁盘文件建立关联。
    off_t offset   // 磁盘文件的偏移量。
);
成功:返回一个内存映射区的起始地址。
失败: MAP_FAILED。

五、获取文件属性

int stat(
    const char *pathname, // 要获取属性信息的文件名
    struct stat *buf      // 传出文件信息
);

struct stat
{
    dev_t st_dev;         // 文件的设备编号
    ino_t st_ino;         // Inode节点
    mode_t st_mode;       // 文件的类型和存取的权限
    nlink_t st_nlink;     // 硬连接数目
    uid_t st_uid;         // 用户ID
    gid_t st_gid;         // 组ID
    dev_t st_rdev;        // 若此文件为设备文件,则为其设备编号
    off_t st_size;        // 文件字节数
    blksize_t st_blksize; // 块大小(文件系统的I/O缓冲区大小)
    blkcnt_t st_blocks;   // block的块数
    time_t st_atime;      // 最后一次访问时间
    time_t st_mtime;      // 最后一次修改文件内容的时间
    time_t st_ctime;      // 最后一次改变文件属性的时间
};

六、高级I/O

1. 聚集写

struct iovec
{
    void *iov_base; // 指向数据的起始地址
    size_t iov_len; // 数据的长度
};

// 以iov[0],iov[1]至iov[iovcnt-1]的顺序从缓冲区中聚集输出数据。
// 只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用的开销。
ssize_t writev(
    int filedes,             // 文件描述符
    const struct iovec *iov, // IO向量机制结构体
    int iovcnt               // IO向量机制结构体的个数
);
成功:返回已写的字节数
失败:返回-1

2. 解决大文件传输问题

// 将响应报文从写缓冲区写出
bool http_conn::write()
{
    int temp = 0;
    // 要发送的数据长度为0
    // 表示响应报文为空,一般不会出现这种情况
    if (bytes_to_send == 0)
    {
        // 注册并监听读事件
        modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
        // 初始化新接受的连接
        init();
        return true;
    }

    // 解决大文件传输的问题:每次传输后都要更新下次传输的文件起始位置和长度。
    while (1)
    {
        // 将响应报文的响应行、响应头、空行和响应体发送给浏览器端
        temp = writev(m_sockfd, m_iv, m_iv_count);

        if (temp < 0)
        {
            // 写缓冲区满了
            if (errno == EAGAIN)
            {
                // 注册并监听写事件
                // 等待下一次写事件触发,在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性。
                modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
                return true;
            }
            // 释放内存映射区
            unmap();
            return false;
        }
        // 更新已发送字节数
        bytes_have_send += temp;
        // 更新剩余发送字节数
        bytes_to_send -= temp;

        // 第一个iovec的数据已发送完,发送第二个iovec的数据
        if (bytes_have_send >= m_iv[0].iov_len)
        {
            // 不再继续发送响应头信息
            m_iv[0].iov_len = 0;
            m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
            m_iv[1].iov_len = bytes_to_send;
        }
        // 继续发送第一个iovec的数据
        else
        {
            m_iv[0].iov_base = m_write_buf + bytes_have_send;
            m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
        }
        // 判断条件,数据已全部发送完
        if (bytes_to_send <= 0)
        {
            // 释放内存映射区
            unmap();
            // 注册并监听读事件
            modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
            // 浏览器的请求是否为长连接
            if (m_linger)
            {
                // 短连接,重新初始化HTTP对象
                init();
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

最后

以上就是腼腆小虾米为你收集整理的【C++实现HTTP服务器项目记录】HTTP报文处理一、HTTP报文格式二、解析HTTP请求报文三、生成HTTP响应报文四、内存映射五、获取文件属性六、高级I/O的全部内容,希望文章能够帮你解决【C++实现HTTP服务器项目记录】HTTP报文处理一、HTTP报文格式二、解析HTTP请求报文三、生成HTTP响应报文四、内存映射五、获取文件属性六、高级I/O所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(70)

评论列表共有 0 条评论

立即
投稿
返回
顶部