webserver-http_conn类实现HTTP报文的解析与响应

1. select/poll/epoll总结

  1. 调用函数:select和poll都是一个函数,epoll是一组函数

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

  3. 将文件描述符从用户传给内核:select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝。
    epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上。

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

  5. 应用程序索引就绪文件描述符
    select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历。
    epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可。

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

  7. 应用场景
    当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll。
    当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll。
    当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能。

2. ET、LT、EPOLLONESHOT

  1. LT水平触发模式
    epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。
    当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理。

  2. ET边缘触发模式
    epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件。
    必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain。

  3. EPOLLONESHOT
    一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket。
    我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件。

3. 主线程处理流程

  1. 主线程创建线程池并初始化,并且创建http_conn对象
  2. 建立socket连接,浏览器发送http请求
  3. 主线程使用epoll监控文件描述符事件的发生,如果epoll上存在新的事件,开始初始化用户数组user并存入user中。
  4. 如果epoll事件有读事件发生,一次性读取完客户数据,并将事件文件描述符添加到线程池的工作队列中。
    // 创建线程池,初始化线程池
    threadpool<http_conn> *pool = NULL;
    try
    {
        pool = new threadpool<http_conn>;
    }
    catch (...)
    {
        return 1;
    }

    // 创建一个数组,用于保存所有的客户端信息
    http_conn *users = new http_conn[MAX_FD];

    // 创建监听套接字,使用IPv4(PF_INET)协议族,流式协议(SOCK_STREAM),第三个参数一般写0, 流式协议默认使用TCP,报式协议默认使用UDP
    int listenfd = socket(PF_INET, SOCK_STREAM, 0); // 创建成功则返回文件描述符,失败则返回-1

    // 设置端口复用,在服务器绑定端口之前设置(SOL_SOCKET是端口复用的级别,SO_REUSEADDR表示端口复用)
    int reuse = 1; //  端口复用的值,1表示可以复用,0表示不可以复用
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    // 绑定(socket地址是一个结构体,封装了端口号和IP等信息)
    struct sockaddr_in address;   // 利用sockaddr_in创建socket地址(sockaddr_in用于IPv4,sockaddr_in6用于IPv6:)
    address.sin_family = AF_INET; // 使用IPv4协议族
    // h-host(主机字节序)、to(转换成什么)、n-network(网络字节序)、s-short unsigned short、l-long unsigned int
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(port); // 转换端口号,将主机字节序转变为网络字节序

    int ret = 0;
    ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret >= 0);

    ret = listen(listenfd, 5); // 5表示未连接的和已经连接的和的最大值
    assert(ret >= 0);

    // 创建epoll对象,和事件数组,添加
    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    // 添加到epoll对象中
    addfd(epollfd, listenfd, false);
    http_conn::m_epollfd = epollfd;
SIGALRM在经过TIMESLOT秒后发送给目前的进程
    bool timeout = false;                              // 超时标志
    bool stop_server = false;                          // 循环条件
    client_data *user_timer = new client_data[MAX_FD]; // 创建用户的连接资源

    while (!stop_server)
    {
        // epoll_wait等待所监控文件描述符上事件的产生,大于0返回就绪的文件描述符个数,等于0表示时间到,等于-1表示失败
        int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);

        if ((number < 0) && (errno != EINTR))
        {
            LOG_ERROR("%s", "epoll failure\n");
            break;
        }

        for (int i = 0; i < number; i++)
        {

            int sockfd = events[i].data.fd;

            if (sockfd == listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof(client_address);

                int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
				//水平触发模式
                if (connfd < 0)
                {
                    LOG_ERROR("%s:error is:%d", "accept error", errno);
                    continue;
                }

                if (http_conn::m_user_count >= MAX_FD)
                {
                    // 目前连接已满,给客户端一个信息,显示服务器正忙
                    show_error(connfd, "Internal sever busy");
                    // 日志
                    LOG_ERROR("%s", "Internal sever busy");
                    continue;
                }
                // 将新的客户数据初始化放入用户数组中
                users[connfd].init(connfd, client_address);

                // 初始化新的客户的连接资源
                user_timer[connfd].address = client_address;
                user_timer[connfd].sockfd = connfd;
                // 创建定时器,并设置连接资源、回调函数、超时时间
                util_timer *timer = new util_timer;
                timer->user_data = &user_timer[connfd];
                timer->cb_func = cb_func;
                time_t cur = time(NULL);
                timer->expire = cur + 3 * TIMESLOT;
                // 连接资源绑定定时器
                user_timer[connfd].timer = timer;
                // 将定时器添加到双向链表中
                timer_lst.add_timer(timer);
            }
            else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
            {
                // 对方异常断开或者出现错误等事件
                // 移除链表上的定时器
                util_timer *timer = user_timer[sockfd].timer;
                timer->cb_func(&user_timer[sockfd]);
                if (timer)
                    timer_lst.del_timer(timer);
                users[sockfd].close_conn();
            }

            else if (events[i].events & EPOLLIN)
            {
                // 创建定时器临时变量,将该连接对应的定时器取出
                util_timer *timer = user_timer[sockfd].timer;

                if (users[sockfd].read_once())
                {
                    LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
                    Log::get_instance()->flush();

                    // 一次性读取完所有数据
                    pool->append(users + sockfd);
				}
			}
		}
	}

3.1 process函数处理报文的请求与响应

判断请求队列上是否有数据,如果有,工作线程取出任务之后,调用process函数,分别调用process_read函数和process_write函数的解析和响应。

// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process()
{
    // 解析HTTP请求
    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);
}

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

本项目只完成了GET请求报文的解析,POST请求后续改进。

  1. process_read函数通过主从状态机对请求报文进行处理,主状态机包括CHECK_STATE_REQUESTLINE(解析请求行)、CHECK_STATE_HEADER(解析请求头)、CHECK_STATE_CONTENT(解析请求体),从状态机包括LINE_OK(读取完整的行)、LINE_BAD(读取行出错)、LINE_OPEN(读取行数据不完整)。
  2. 初始化主状态机的初始状态为解析请求行,重点是进入分析数据的循环条件。
  3. switch判断属于那个主状态,若属于CHECK_STATE_REQUESTLINE,调用parse_request_line函数解析HTTP请求行,获得请求方法,目标URL,以及HTTP版本号,;若属于CHECK_STATE_HEADER,调用parse_headers函数解析HTTP请求的一个头部信息,如果返回一个GET_REQUEST(表示获得了一个完整的请求),跳转do_request函数生成响应报文;若属于
    CHECK_STATE_CONTENT,调用parse_content函数判断是否完整的读入了,如果返回GET_REQUEST,跳转do_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_check_idx;
        // printf("got 1 http line: %s\n", text);

        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响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

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部分为响应正文。

3.3 process_write函数生成相应的响应报文

  1. 通过process_read函数解析后的报文,根据返回的HTTP_CODE,进入process_write函数中相应的逻辑模块。
  2. add_status_line函数,添加状态行:http/1.1 状态码 状态消息
  3. add_headers函数添加消息报头,内部调用add_content_length和add_linger函数。
    content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据。
    connection记录连接状态,用于告诉浏览器端保持长连接。
  4. add_blank_line添加空行。
  5. 如果FILE_REQUEST,向缓冲区中写入响应报文。
  6. http_conn::write_once()函数中writev分散写,将写缓冲区和请求的目标文件两块不连续的内存分别写出去,写缓冲区中包括状态行、消息报头等。
// 根据服务器处理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(400, error_400_title);
        add_headers(strlen(error_400_form));
        if (!add_content(error_400_form))
        {
            return false;
        }
        break;
    case NO_RESOURCE:
        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);
        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;
    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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值