Linux下的高性能轻量级Web服务器(三)

3.处理用户的HTTP请求

客户端和服务器建立连接后,服务器等待客户端发送HTTP请求,并给出响应。

代码块

http_conn.h 文件

#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"
class http_conn
{
public:
    /* 文件名的最大长度 */
    static const int FILENAME_LEN = 200;
    /* 读缓冲区的大小 */
    static const int READ_BUFFER_SIZE = 2048;
    /* 写缓冲区的大小 */
    static const int WRITE_BUFFER_SIZE = 1024;
    /* HTTP请求方法 */
    enum METHOD
    {
        GET = 0,
        POST,
        HEAD,
        PUT,
        DELETE,
        TRACE,
        OPTIONS,
        CONNECT,
        PATH
    };
    /* 解析客户请求时,主状态机所处状态 */
    enum CHECK_STATE
    {
        CHECK_STATE_REQUESTLINE = 0,
        CHECK_STATE_HEADER,
        CHECK_STATE_CONTENT
    };
    /* 服务器处理HTTP请求的可能结果 */
    enum HTTP_CODE
    {
        NO_REQUEST,
        GET_REQUEST,
        BAD_REQUEST,
        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);
    /* 关闭连接 */
    void close_conn(bool real_close = true);
    /* 处理客户请求 */
    void process();
    /* 非阻塞读操作 */
    bool read_once();
    /* 非阻塞写操作 */
    bool write();
    sockaddr_in *get_address()
    {
        return &m_address;
    }
    void initmysql_result(connection_pool *connPool);

private:
    /* 初始化连接 */
    void init();
    /* 解析 HTTP 请求 */
    HTTP_CODE process_read();
    /* 填充 HTTP 应答 */
    bool process_write(HTTP_CODE ret);

    /* 下面一组函数被 process_read 调用以分析 HTTP 请求 */
    HTTP_CODE parse_request_line(char *text);
    HTTP_CODE parse_headers(char *text);
    HTTP_CODE parse_content(char *text);
    HTTP_CODE do_request();
    char *get_line() { return m_read_buf + m_start_line; };
    LINE_STATUS parse_line();

    /* 下面一组函数被 process_write 调用以填充 HTTP 应答 */
    void unmap();
    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:
    /* 所有 socket 上的事件均注册到同一个 epoll 内核事件表中,所以将 epoll 文件描述符设置为静态 */
    static int m_epollfd;
    /* 统计用户数量 */
    static int m_user_count;
    MYSQL *mysql;

private:
    /* 该 HTTP 连接的 socket 地址和对方的 socket 地址 */
    int m_sockfd;
    sockaddr_in m_address;
    
    /* 读缓冲区 */
    char m_read_buf[READ_BUFFER_SIZE];
    /* 标识读缓冲区中已经读入的客户数据的最后一个字节的下一个位置 */
    int m_read_idx;
    /* 当前正在分析的字符在读缓冲区的位置 */
    int m_checked_idx;
    /* 当前正在解析的行的起始位置 */
    int m_start_line;
    /* 写缓冲区 */
    char m_write_buf[WRITE_BUFFER_SIZE];
    /* 写缓冲区中待发送的字节数 */
    int m_write_idx;
    
    /* 主状态机当前所处状态 */
    CHECK_STATE m_check_state;
    /* 请求方法 */
    METHOD m_method;

    /* 客户请求的目标文件的完整路径,其内容为 doc_root + m_url,doc_root是网站根目录 */
    char m_real_file[FILENAME_LEN];
    /* 客户请求的文件名称 */
    char *m_url;
    /* HTTP 协议版本号 */
    char *m_version;
    /* 主机名 */
    char *m_host;
    /* HTTP 请求的消息体的长度 */
    int m_content_length;
    /* HTTP 请求是否保持连接 */
    bool m_linger;

    /* 客户请求的目标文件被 mmap 到内存中的起始位置 */
    char *m_file_address;
    /* 目标文件状态。通过它可以判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息 */
    struct stat m_file_stat;
    /* 我们将采用 writev 来执行写操作,m_iv_count 表示被写内存块的数量 */
    struct iovec m_iv[2];
    int m_iv_count;
    
    int cgi;        //是否启用的POST
    char *m_string; //存储请求头数据
    int bytes_to_send;
    int bytes_have_send;
};

#endif


http_conn.cpp 文件

#include "http_conn.h"
#include "../log/log.h"
#include <map>
#include <mysql/mysql.h>
#include <fstream>

//#define connfdET //边缘触发非阻塞
#define connfdLT //水平触发阻塞

//#define listenfdET //边缘触发非阻塞
#define listenfdLT //水平触发阻塞

//定义http响应的一些状态信息
const char *ok_200_title = "OK";
const char *error_400_title = "Bad Request";
const char *error_400_form = "Your request has bad syntax or is inherently impossible to staisfy.\n";
const char *error_403_title = "Forbidden";
const char *error_403_form = "You do not have permission to get file form this server.\n";
const char *error_404_title = "Not Found";
const char *error_404_form = "The requested file was not found on this server.\n";
const char *error_500_title = "Internal Error";
const char *error_500_form = "There was an unusual problem serving the request file.\n";

//当浏览器出现连接重置时,可能是网站根目录出错或http响应格式出错或者访问的文件中内容完全为空
const char *doc_root = "/home/zyz/WebServer/root";

//将表中的用户名和密码放入map
map<string, string> users;
locker m_lock;

void http_conn::initmysql_result(connection_pool *connPool)
{
    //先从连接池中取一个连接
    MYSQL *mysql = NULL;
    connectionRAII mysqlcon(&mysql, connPool);

    //在user表中检索username,passwd数据,浏览器端输入
    if (mysql_query(mysql, "SELECT username,passwd FROM user"))
    {
        LOG_ERROR("SELECT error:%s\n", mysql_error(mysql));
    }

    //从表中检索完整的结果集
    MYSQL_RES *result = mysql_store_result(mysql);

    //返回结果集中的列数
    int num_fields = mysql_num_fields(result);

    //返回所有字段结构的数组
    MYSQL_FIELD *fields = mysql_fetch_fields(result);

    //从结果集中获取下一行,将对应的用户名和密码,存入map中
    while (MYSQL_ROW row = mysql_fetch_row(result))
    {
        string temp1(row[0]);
        string temp2(row[1]);
        users[temp1] = temp2;
    }
}

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

//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void addfd(int epollfd, int fd, bool one_shot)
{
    epoll_event event;
    event.data.fd = fd;

#ifdef connfdET
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
#endif

#ifdef connfdLT
    event.events = EPOLLIN | EPOLLRDHUP;
#endif

#ifdef listenfdET
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
#endif

#ifdef listenfdLT
    event.events = EPOLLIN | EPOLLRDHUP;
#endif

    if (one_shot)
        event.events |= EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

//从内核时间表删除描述符
void removefd(int epollfd, int fd)
{
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

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

#ifdef connfdET
    event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
#endif

#ifdef connfdLT
    event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
#endif

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

int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;

//关闭连接,关闭一个连接,客户总量减一
void http_conn::close_conn(bool real_close)
{
    if (real_close && (m_sockfd != -1))
    {
        removefd(m_epollfd, m_sockfd);
        m_sockfd = -1;
        m_user_count--;
    }
}

//初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const sockaddr_in &addr)
{
    m_sockfd = sockfd;
    m_address = addr;
    //int reuse=1;
    //setsockopt(m_sockfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
    addfd(m_epollfd, sockfd, true);
    m_user_count++;
    init();
}

//初始化新接受的连接
//check_state默认为分析请求行状态
void http_conn::init()
{
    mysql = NULL;
    bytes_to_send = 0;
    bytes_have_send = 0;
    m_check_state = CHECK_STATE_REQUESTLINE;
    m_linger = false;
    m_method = GET;
    m_url = 0;
    m_version = 0;
    m_content_length = 0;
    m_host = 0;
    m_start_line = 0;
    m_checked_idx = 0;
    m_read_idx = 0;
    m_write_idx = 0;
    cgi = 0;
    memset(m_read_buf, '\0', READ_BUFFER_SIZE);
    memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
    memset(m_real_file, '\0', FILENAME_LEN);
}

//从状态机,用于分析出一行内容
//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line()
{
    char temp;
    for (; m_checked_idx < m_read_idx; ++m_checked_idx)
    {
        temp = m_read_buf[m_checked_idx];
        if (temp == '\r')
        {
            if ((m_checked_idx + 1) == m_read_idx)
                return LINE_OPEN;
            else if (m_read_buf[m_checked_idx + 1] == '\n')
            {
                m_read_buf[m_checked_idx++] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
        else if (temp == '\n')
        {
            if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r')
            {
                m_read_buf[m_checked_idx - 1] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    return LINE_OPEN;
}

//循环读取客户数据,直到无数据可读或对方关闭连接
//非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once()
{
    if (m_read_idx >= READ_BUFFER_SIZE)
    {
        return false;
    }
    int bytes_read = 0;

#ifdef connfdLT
    bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
    m_read_idx += bytes_read;

    if (bytes_read <= 0)
    {
        return false;
    }

    return true;
#endif

#ifdef connfdET
    while (true)
    {
        bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
        if (bytes_read == -1)
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;
            return false;
        }
        else if (bytes_read == 0)
        {
            return false;
        }
        m_read_idx += bytes_read;
    }
    return true;
#endif
}

//解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
    m_url = strpbrk(text, " \t");
    if (!m_url)
    {
        return BAD_REQUEST;
    }
    *m_url++ = '\0';
    char *method = text;
    if (strcasecmp(method, "GET") == 0)
        m_method = GET;
    else if (strcasecmp(method, "POST") == 0)
    {
        m_method = POST;
        cgi = 1;
    }
    else
        return BAD_REQUEST;
    m_url += strspn(m_url, " \t");
    m_version = strpbrk(m_url, " \t");
    if (!m_version)
        return BAD_REQUEST;
    *m_version++ = '\0';
    m_version += strspn(m_version, " \t");
    if (strcasecmp(m_version, "HTTP/1.1") != 0)
        return BAD_REQUEST;
    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] != '/')
        return BAD_REQUEST;
    // 当url为 / 时,显示判断界面
    if (strlen(m_url) == 1)
        strcat(m_url, "judge.html");
    m_check_state = CHECK_STATE_HEADER;
    return NO_REQUEST;
}

//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
    /* 遇到空行,表示头部字段解析完成 */
    if (text[0] == '\0')
    {
	/* 如果 HTTP 请求有消息体,则还需要读取 m_content_length 字节的消息体,状态机转移到 CHECK_STATE_CONTENT 状态 */
        if (m_content_length != 0)
        {
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        
        /* 否则说明已经得到一个完整的 HTTP 请求 */
        return GET_REQUEST;
    }
    /* 处理 Connection 头部字段 */
    else if (strncasecmp(text, "Connection:", 11) == 0)
    {
        text += 11;
        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;
        text += strspn(text, " \t");
        m_content_length = atol(text);
    }
    /* 处理 Host 头部字段 */
    else if (strncasecmp(text, "Host:", 5) == 0)
    {
        text += 5;
        text += strspn(text, " \t");
        m_host = text;
    }
    else
    {
        LOG_INFO("oop!unknow header: %s", text);
        Log::get_instance()->flush();
    }
    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] = '\0';
        //POST请求中最后为输入的用户名和密码
        m_string = text;
        return GET_REQUEST;
    }
    return NO_REQUEST;
}

//主状态机
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_start_line = m_checked_idx;
        LOG_INFO("%s", text);
        Log::get_instance()->flush();
        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;
            else if (ret == GET_REQUEST)
            {
                return do_request();
            }
            break;
        }
        case CHECK_STATE_CONTENT:
        {
            ret = parse_content(text);
            if (ret == GET_REQUEST)
                return do_request();
            line_status = LINE_OPEN;
            break;
        }
        default:
            return INTERNAL_ERROR;
        }
    }
    return NO_REQUEST;
}

/* 
	当得到一个完整、正确的 HTTP 请求时,分析目标文件的属性。
	如果目标文件存在、当前用户可读,且不是目录,则使用 mmap 将其映射到内存地址 m_file_address 处,并告诉调用者获取文件成功 
 */
http_conn::HTTP_CODE http_conn::do_request()
{
    strcpy(m_real_file, doc_root);
    int len = strlen(doc_root);
    //printf("m_url:%s\n", m_url);
    const char *p = strrchr(m_url, '/');

    //处理cgi
    if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
    {

        //根据标志判断是登录检测还是注册检测
        char flag = m_url[1];

        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/");
        strcat(m_url_real, m_url + 2);
        strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
        free(m_url_real);

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

        int j = 0;
        for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
            password[j] = m_string[i];
        password[j] = '\0';

        //同步线程登录校验
        if (*(p + 1) == '3')
        {
            //如果是注册,先检测数据库中是否有重名的
            //没有重名的,进行增加数据
            char *sql_insert = (char *)malloc(sizeof(char) * 200);
            strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
            strcat(sql_insert, "'");
            strcat(sql_insert, name);
            strcat(sql_insert, "', '");
            strcat(sql_insert, password);
            strcat(sql_insert, "')");

            if (users.find(name) == users.end())
            {

                m_lock.lock();
                int res = mysql_query(mysql, sql_insert);
                users.insert(pair<string, string>(name, password));
                m_lock.unlock();

                if (!res)
                    strcpy(m_url, "/log.html");
                else
                    strcpy(m_url, "/registerError.html");
            }
            else
                strcpy(m_url, "/registerError.html");
        }
        //如果是登录,直接判断
        //若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0
        else if (*(p + 1) == '2')
        {
            if (users.find(name) != users.end() && users[name] == password)
                strcpy(m_url, "/welcome.html");
            else
                strcpy(m_url, "/logError.html");
        }
    }

    if (*(p + 1) == '0')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/register.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    else if (*(p + 1) == '1')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/log.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

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

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

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

        free(m_url_real);
    }
    else
        strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);

    if (stat(m_real_file, &m_file_stat) < 0)
        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_real_file, O_RDONLY);
    m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    return FILE_REQUEST;
}

/* 对内存映射区执行 munmap 操作 */
void http_conn::unmap()
{
    if (m_file_address)
    {
        munmap(m_file_address, m_file_stat.st_size);
        m_file_address = 0;
    }
}

/* 发送 HTTP 响应 */
bool http_conn::write()
{
    int temp = 0;

    //表示响应报文为空,一般不会出现这种情况
    if (bytes_to_send == 0)
    {
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        init();
        return true;
    }

    while (1)
    {
        //将响应报文的状态行、消息头、空行和响应正文发送给浏览器端,temp为发送的字节数
        temp = writev(m_sockfd, m_iv, m_iv_count);

        //发送成功
        if (temp > 0)
        {
            //更新已发送字节
            bytes_have_send += temp;
        }

        //发送失败
        if (temp <= -1)
        {
            //判断缓冲区是否满了
            if (errno == EAGAIN)
            {
                //判断第一个iovec头部信息的数据是否发送完
                if (bytes_have_send >= m_iv[0].iov_len)
                {
                    //头部信息已发送完,改变iov指针,发送第二个iovec数据
                    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;
                }
                else
                {
                    //继续发送第一个iovec头部信息的数据
                    m_iv[0].iov_base = m_write_buf + bytes_have_send;
                    m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
                }
                //重新注册写事件
                modfd(m_epollfd, m_sockfd, EPOLLOUT);
                return true;
            }
            //发送失败,但不是缓冲区问题,取消映射
            unmap();
            return false;
        }

        //更新还需发送的字节数
        bytes_to_send -= temp;

        //判断数据是否全部发送完
        if (bytes_to_send <= 0)
        {
            //数据已经全部发送完,取消映射
            unmap();

            //在epoll树上重置EPOLLIN事件
            modfd(m_epollfd, m_sockfd, EPOLLIN);

            //浏览器的请求为长连接
            if(m_linger)
            {
                //重新初始化HTTP对象
                init();
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

/* 往写缓冲区中写入待发送的数据 */
bool http_conn::add_response(const char *format, ...)
{
    if (m_write_idx >= WRITE_BUFFER_SIZE)
        return false;
    va_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("request:%s", m_write_buf);
    Log::get_instance()->flush();
    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)
{
    add_content_length(content_len);
    add_linger();
    add_blank_line();
}

bool http_conn::add_content_length(int content_len)
{
    return add_response("Content-Length:%d\r\n", content_len);
}

bool http_conn::add_content_type()
{
    return add_response("Content-Type:%s\r\n", "text/html");
}

bool http_conn::add_linger()
{
    return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}

bool http_conn::add_blank_line()
{
    return add_response("%s", "\r\n");
}

bool http_conn::add_content(const char *content)
{
    return add_response("%s", content);
}

/* 根据服务器处理 HTTP 请求的结果,决定返回给客户的内容 */
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;
    }
    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);
            m_iv[0].iov_base = m_write_buf;
            m_iv[0].iov_len = m_write_idx;
            m_iv[1].iov_base = m_file_address;
            m_iv[1].iov_len = m_file_stat.st_size;
            m_iv_count = 2;
            bytes_to_send = m_write_idx + m_file_stat.st_size;
            return true;
        }
        else
        {
            const char *ok_string = "<html><body></body></html>";
            add_headers(strlen(ok_string));
            if (!add_content(ok_string))
                return false;
        }
    }
    default:
        return false;
    }
    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;
}

/* 由线程池中的工作线程调用,这是处理 HTTP 请求的入口函数 */
void http_conn::process()
{
    HTTP_CODE read_ret = process_read();
    if (read_ret == NO_REQUEST)
    {
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        return;
    }
    bool write_ret = process_write(read_ret);
    if (!write_ret)
    {
        close_conn();
    }
    modfd(m_epollfd, m_sockfd, EPOLLOUT);
}

HTTP报文格式

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

请求报文

HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。其中,请求分为两种,GET和POST。

  1. 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
空行
请求数据为空

  1. 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版本。
  • 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
  1. HOST,给出请求资源所在服务器的域名。
  2. User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
  3. Accept,说明用户代理可处理的媒体类型。
  4. Accept-Encoding,说明用户代理支持的内容编码。
  5. Accept-Language,说明用户代理能够处理的自然语言集。
  6. Content-Type,说明实现主体的媒体类型。
  7. Content-Length,说明实现主体的大小。
  8. Connection,连接管理,可以是Keep-Alive或close。
  • 空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
  • 请求数据也叫主体,可以添加任意的其他数据。

注意:在 HTTP 报文中,每一行的数据由 \r\n 作为结束字符,空行则是仅仅是字符\r\n。

响应报文

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有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:服务器在执行请求时出现错误。

处理 HTTP 请求

通过 read_once() 方法将客户端的 HTTP 请求读入读缓冲区后,各子线程通过 process() 函数对任务进行处理,调用 process_read() 函数和 process_write() 函数分别完成报文解析与报文响应两个任务。

void http_conn::process()
{
    HTTP_CODE read_ret = process_read();

    //NO_REQUEST,表示请求不完整,需要继续接收请求数据
    if(read_ret == NO_REQUEST)
    {
        //注册并监听读事件
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        return;
    }

    //调用process_write完成报文响应
    bool write_ret = process_write(read_ret);
    if(!write_ret)
    {
        close_conn();
    }
    
    //注册并监听写事件
    modfd(m_epollfd, m_sockfd, EPOLLOUT);
}

}

        通过调用 process_read() 函数将客户端的求报文进行解析,只有通过解析,才能知道用户请求的内容是什么,是请求图片,还是视频,或是其他请求,我们根据这些请求返回相应的HTML页面等。
        项目中使用主从状态机的模式进行解析,从状态机(parse_line)负责读取报文的一行,主状态机(process_read)责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。每解析一部分都会将整个请求的 m_check_state 状态改变,状态机也就是根据这个状态来进行不同部分的解析跳转的:

  • parse_request_line(text)

解析请求行,也就是GET中的GET /562f25980001b1b106000338.jpg HTTP/1.1这一行,或者POST中的POST / HTTP1.1这一行。通过请求行的解析我们可以判断该 HTTP 请求的类型(GET/POST),而请求行中最重要的部分就是 URL 部分,我们会将这部分保存下来用于后面的生成 HTTP 响应。

  • parse_headers(text)

解析请求头部,GET和POST中空行以上,请求行以下的部分。

  • parse_content(text)

解析请求数据,对于GET来说这部分是空的,因为这部分内容已经以明文的方式包含在了请求行中的URL部分了;只有POST的这部分是有数据的,项目中的这部分数据为用户名和密码,我们会根据这部分内容做登录和校验,并涉及到与数据库的连接。

其流程图如下:


从状态机详解

从状态机负责读取 buffer 中的数据,将每行数据末尾的 \r\n 置为 \0\0,并更新从状态机在buffer中读取的位置 m_checked_idx,以此来驱动主状态机解析。

从状态机从 m_read_buf 中逐字节读取,读取的当前字节有以下三种情况:

  • 当前字节为 \r 时
  1. 如果下一个字符是 \n,则将 \r\n 修改成 \0\0,将 m_checked_idx 指向下一行的开头,返回 LINE_OK
  2. 如果已经达到了 buffer 末尾,表示 buffer 还需要继续接收,返回 LINE_OPEN
  3. 如果不是以上两种情况,表示语法错误,返回 LINE_BAD
  • 当前字节是\n时(一般是上次读取到\r就到了buffer末尾,没有接收完整,再次接收时会出现这种情况
  • 如果前一个字符是 \r,则将 \r\n 修改成 \0\0,将 m_checked_idx 指向下一行的开头,返回LINE_OK
  • 当前字节既不是 \r,也不是 \n
  • 表示接收不完整,需要继续接收,返回 LINE_OPEN

从状态机的三种状态,标识解析一行的读取状态。

  • LINE_OK,完整读取一行
  • LINE_BAD,报文语法有误
  • LINE_OPEN,读取的行不完整


从状态机函数:parse_line()

// 从状态机函数,用于分析一行内容
http_conn::LINE_STATUS http_conn::parse_line()
{
    // temp 为将要分析的字节
    char temp;
    
    // m_read_idx 指向缓冲区 m_read_buf 的数据末尾的下一个字节
    // m_checked_idx 指向从状态机当前正在分析的字节
    for (; m_checked_idx < m_read_idx; ++m_checked_idx)
    {
        temp = m_read_buf[m_checked_idx];
        if (temp == '\r')
        {
            if ((m_checked_idx + 1) == m_read_idx)
                return LINE_OPEN;
            else if (m_read_buf[m_checked_idx + 1] == '\n')
            {
                m_read_buf[m_checked_idx++] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
        else if (temp == '\n')
        {
            if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r')
            {
                m_read_buf[m_checked_idx - 1] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    return LINE_OPEN;
}


主状态机详解

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

HTTP_CODE含义

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

  • NO_REQUEST
    请求不完整,需要继续读取请求报文数据
  • GET_REQUEST
    获得了完整的HTTP请求
  • BAD_REQUEST
    HTTP请求报文有语法错误
  • INTERNAL_ERROR
    服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发


主状态机函数:process_read()

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

    // 判断条件将在后面讲解,parse_line() 函数为从状态机
    while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
    {
	// get_line()获取行在 buffer 中的起始位置,parse_line 已将\r\n改为\0\0
        text = get_line();
        
        // m_start_line 是每一个数据行在 m_read_buf 中的起始位置
        // m_checked_idx 表示从状态机在 m_read_buf 中读取的位置
        m_start_line = m_checked_idx;
        LOG_INFO("%s", text);
        Log::get_instance()->flush();

        //主状态机的三种状态转移逻辑
        switch (m_check_state)
        {
            case CHECK_STATE_REQUESTLINE:
            {
	        // 调用 parse_request_line(text) 解析请求行
                ret = parse_request_line(text);
                if (ret == BAD_REQUEST)
                    return BAD_REQUEST;
                break;
            }
            case CHECK_STATE_HEADER:
            {
	        // 调用 parse_headers(text) 解析请求头
                ret = parse_headers(text);
                if (ret == BAD_REQUEST)
                    return BAD_REQUEST;
                else if (ret == GET_REQUEST)
                {
	            //完整解析GET请求后,跳转到报文响应函数
                    return do_request();
                }
                break;
            }
            case CHECK_STATE_CONTENT:
            {
	        // 调用 parse_content(text) 解析请求数据,如果是 GET 则没有请求数据
                ret = parse_content(text);
                //完整解析POST请求后,跳转到报文响应函数
                if (ret == GET_REQUEST)
                    return do_request();
                
                //解析完消息体即完成报文解析,避免再次进入循环,更新line_status
                line_status = LINE_OPEN;
                break;
            }
            default:
                return INTERNAL_ERROR;
        }
    }
    return NO_REQUEST;
}


解析请求行函数

// 解析http请求行,获得请求方法,目标url及http版本号 
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
    //在 HTTP 报文中,请求行中的各个部分之间通过 \t 或 空格 分隔。
    // 返回请求行中最先含有 空格 和 \t 任一字符的位置
    m_url = strpbrk(text, " \t");
    
    // 如果返回值为 NULL 则 表示报文格式错误
    if (!m_url)
    {
        return BAD_REQUEST;
    }
	
    // 将该位置改为\0,用于将前面数据取出
    *m_url++ = '\0';
    
    char *method = text;
    if (strcasecmp(method, "GET") == 0)
        m_method = GET;
    else if (strcasecmp(method, "POST") == 0)
    {
        m_method = POST;
        cgi = 1;
    }
    else
        return BAD_REQUEST;
    
    /* 
	    m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有
	    通过 strspn 函数继续跳过空格和\t字符,让 m_url 指向请求资源的第一个字符
     */    
    m_url += strspn(m_url, " \t");
    m_version = strpbrk(m_url, " \t");
    if (!m_version)
        return BAD_REQUEST;
    *m_version++ = '\0';
    m_version += strspn(m_version, " \t");
    
    // 仅支持HTTP/1.1
    if (strcasecmp(m_version, "HTTP/1.1") != 0)
        return BAD_REQUEST;
    
    // 对url的前7个字符进行判断,因为有些报文的请求资源中会带有http://,需要对这种情况进行单独处理
    if (strncasecmp(m_url, "http://", 7) == 0)
    {
        m_url += 7;
        m_url = strchr(m_url, '/');
    }
	
    //同样增加 HTTPS 的情况
    if (strncasecmp(m_url, "https://", 8) == 0)
    {
        m_url += 8;
        m_url = strchr(m_url, '/');
    }
	
    //一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源
    if (!m_url || m_url[0] != '/')
        return BAD_REQUEST;
        
    //当url为/时,显示判断界面
    if (strlen(m_url) == 1)
        strcat(m_url, "judge.html");

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


解析请求头函数

//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{	
	/*
		在报文中,请求头和空行的处理使用的同一个函数
		这里通过判断当前的text首位是不是\0字符
		若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。
	 */
    if (text[0] == '\0')
    {
	//判断是GET还是POST请求
        if (m_content_length != 0)
        {
	    //POST需要跳转到消息体处理状态
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        return GET_REQUEST;
    }
    //解析请求头部连接字段
    else if (strncasecmp(text, "Connection:", 11) == 0)
    {
        text += 11;
        
        //跳过空格和\t字符
        text += strspn(text, " \t");
        if (strcasecmp(text, "keep-alive") == 0)
        {
	    //如果是长连接,则将linger标志设置为true
            m_linger = true;
        }
    }
    //解析请求头部内容长度字段
    else if (strncasecmp(text, "Content-length:", 15) == 0)
    {
        text += 15;
        text += strspn(text, " \t");
        m_content_length = atol(text);
    }
    //解析请求头部HOST字段
    else if (strncasecmp(text, "Host:", 5) == 0)
    {
        text += 5;
        text += strspn(text, " \t");
        m_host = text;
    }
    else
    {
        //printf("oop!unknow header: %s\n",text);
        LOG_INFO("oop!unknow header: %s", text);
        Log::get_instance()->flush();
    }
    return NO_REQUEST;
}

        如果仅仅是GET请求,那么主状态机只设置之前的两个状态足矣。因为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,此时可以跳出循环,完成报文解析任务。


解析请求数据函数

//仅用于 POST 请求时,解析请求数据
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{
	//判断buffer中是否读取了消息体
    if (m_read_idx >= (m_content_length + m_checked_idx))
    {
        text[m_content_length] = '\0';
        
        //POST请求中最后为输入的用户名和密码
        m_string = text;
        
        return GET_REQUEST;
    }
    return NO_REQUEST;
}



响应请求报文

基础API介绍

stat

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

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

//获取文件属性,存储在statbuf中
int stat(const char *pathname, struct stat *statbuf);

struct stat 
{
   mode_t    st_mode;        /* 文件类型和权限 */
   off_t     st_size;        /* 文件大小,字节数*/
};


mmap

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

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length); //解除内存映射
  • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址
  • length:映射区的长度
  • prot:期望的内存保护标志,不能与文件的打开模式冲突
  • PROT_READ 表示映射区域可以被读取
  • PROT_WRITE 映射区域可被写入
  • PROT_NONE 映射区域不能存取
  • PROT_EXEC 映射区域可被执行
  • flags:影响映射区域的各种特性
  • MAP_PRIVATE,对映射区域的写入操作会产生一个映射文件的复制,即私人的"写入时复制" 对此区域作的任何修改都不会写回原来的文件内容。
  • MAP_FIXED,如果参数 start 所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
  • MAP_SHARED,对应射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
  • MAP_ANONYMOUS,建立匿名映射,此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
  • MAP_DENYWRITE,只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
  • MAP_LOCKED,将映射区域锁定住,这表示该区域不会被置换(swap)。
  • fd:有效的文件描述符,一般是由open()函数返回
  • off_toffset:被映射对象内容的起点


iovec结构体

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

struct iovec {
    void      *iov_base;      /* starting address of buffer */
    size_t    iov_len;        /* size of buffer */
};


writev

writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。

#include <sys/uio.h>
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 时,需要重新处理 iovec 中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是 iovec 数组,计量单位是 iovcnt,而不是字节数,我们仍然需要通过遍历 iovec 来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界 iovec 的 io_base 和 io_len。

流程图

服务器处理完请求报文后调用 process_write 函数完成响应报文的编写,再通过 http_conn::write 函数完成数据的读取与发送。


HTTP_CODE 在头文件中初始化了八种情形,在报文解析与响应中只用到了七种。

  • 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 函数的返回值是对请求的文件分析后的结果,其返回值有两种情况:

  1. 是语法错误导致的 BAD_REQUEST
  2. 是 do_request 的返回结果


do_request 函数将网站的根目录和url文件拼接,然后通过stat判断该文件的属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。


m_url 为请求报文中解析出的请求资源,以 / 开头,项目中解析后的 m_url 有8种情况。

  1. /
  • GET请求,跳转到judge.html,即欢迎访问页面
  1. /0
  • POST请求,跳转到register.html,即注册页面
  1. /1
  • POST请求,跳转到log.html,即登录页面
  1. /2CGISQL.cgi
  • POST请求,进行登录校验
  • 验证成功跳转到 welcome.html,即资源请求成功页面
  • 验证失败跳转到 logError.html,即登录失败页面
  1. /3CGISQL.cgi
  • POST请求,进行注册校验
  • 注册成功跳转到 log.html,即登录页面
  • 注册失败跳转到 registerError.html,即注册失败页面
  1. /5
  • POST请求,跳转到 picture.html,即图片请求页面
  1. /6
  • POST请求,跳转到 video.html,即视频请求页面
  1. /7
  • POST请求,跳转到 fans.html,即关注页面

http_conn::HTTP_CODE http_conn::do_request()
{
    //将初始化的m_real_file赋值为网站根目录
    strcpy(m_real_file, doc_root);
    int len = strlen(doc_root);
    
    //找到m_url中 / 的位置
    const char *p = strrchr(m_url, '/');

    //处理cgi,在本系列最后的文章中会进行详细讲解
    if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
    {
	//根据标志判断是登录检测还是注册检测
        
        //同步线程登录校验

	//CGI多进程登录校验
    }
	
    // 如果请求资源为/0,表示跳转注册界面
    if (*(p + 1) == '0')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/register.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

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

	//将网站目录和/log.html进行拼接,更新到m_real_file中
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    // 如果请求资源为 /5,跳转到 picture.html
    else if (*(p + 1) == '5')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/picture.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    // 如果请求资源为 /6,跳转到 video.html
    else if (*(p + 1) == '6')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/video.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    // 如果请求资源为 /7,跳转到 fans.html
    else if (*(p + 1) == '7')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/fans.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    else
	//如果以上均不符合,即不是登录和注册,直接将url与网站目录拼接
        //这里的情况是welcome界面,请求服务器上的一个图片
        strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
	
    //通过 stat 获取请求资源文件信息,成功则将信息更新到 m_file_stat 结构体
    //失败返回 NO_RESOURCE 状态,表示资源不存在
    if (stat(m_real_file, &m_file_stat) < 0)
        return NO_RESOURCE;

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

    //判断文件类型,如果是目录,则返回BAD_REQUEST,表示请求报文有误
    if (S_ISDIR(m_file_stat.st_mode))
        return BAD_REQUEST;

    //以只读方式获取文件描述符,通过mmap将该文件映射到内存中
    int fd = open(m_real_file, O_RDONLY);
    m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

    //避免文件描述符的浪费和占用
    close(fd);
	
    //表示请求文件存在,且可以访问
    return FILE_REQUEST;
}

process_write()

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

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


process_write()调用以下 6 个函数完成响应报文的编写:

  • add_status_line函数,添加状态行:http/1.1 状态码 状态消息
  • add_headers函数添加消息报头,内部调用add_content_lengthadd_linger函数
  • content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据
  • connection记录连接状态,用于告诉浏览器端保持长连接
  • add_blank_line添加空行
  • add_content添加响应正文


上述函数均是内部调用 add_response 函数更新 m_write_idx 指针和缓冲区 m_write_buf 中的内容。

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

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

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

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

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

    //更新m_write_idx位置
    m_write_idx += len;
    va_end(arg_list);
    LOG_INFO("request:%s", m_write_buf);
    Log::get_instance()->flush();
    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)
{
    add_content_length(content_len);
    add_linger();
    add_blank_line();
}

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

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

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

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

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

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


书中原代码的write函数不严谨,这里对其中的Bug进行了修复,可以正常传输大文件。
书中源代码未正确更新iovec结构体的指针,忽略了成功发送数据,但是数据未全部发送完的情况。

bool http_conn::write()
{
    int temp = 0;
    
    // 要发送的数据长度为 0,表示响应报文为空,一般不会出现这种情况 
    if (bytes_to_send == 0)
    {
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        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);
                return true;
            }
            unmap();
            return false;
        }
        
        // 更新 已经发送 和 还需发送 的字节数
        bytes_have_send += temp;
        bytes_to_send -= temp;
        
        // 成功发送一部分数据
        // 判断成功发送的数据长度是否大于 m_iv[0].iov_len
        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;
        }
        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)
            {
                init();
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}
posted @ 2023-02-28 15:23  夜听风雨声`  阅读(443)  评论(0)    收藏  举报