五莲网站建设公司引擎搜索大全
文章目录
- 一、基础知识
- 1. epoll
- 2. 再谈 I/O 复用
- 3. 触发模式和 EPOLLONESHOT
- 4. HTTP 报文
- 5. HTTP 状态码
- 6. 有限状态机
- 7. 主从状态机
- 8. HTTP_CODE
- 9. HTTP 处理流程
- 二、代码解析
- 1. HTTP 类
- 2. 读取客户数据
- 2. epoll 事件相关
- 3. 接收 HTTP 请求
- 4. HTTP 报文解析
- 5. HTTP 请求响应
- 参考文献
一、基础知识
1. epoll
-
创建内核事件表:
int epoll_create(int size);
size
:不起作用,只是给内核一个提示,告诉它事件表需要多大;- 返回值:内核事件表的文件描述符;
-
修改内核事件表监控的文件描述符上的事件:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
:内核事件表的文件描述符;op
:表示三种操作:注册(EPOLL_CTL_ADD)、修改(EPOLL_CTL_MOD)、删除(EPOLL_CTL_DEL);event
:需要监听的事件:
标识符 事件类型 EPOLLIN 可读、对端 socket 关闭 EPOLLOUT 可写 EPOLLPRI 带外数据 EPOLLERR 错误 EPOLLHUP 文件描述符被挂断 EPOLLET 边缘触发模式 EPOLLONESHOT 只监听一次事件,每次见听完如需再次监听需重置 - 返回值:成功时返回 0 ,失败时返回 -1 并设置 errno 。
-
监听事件发生:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events
:存储内核中发生的事件;maxevents
:events
的容量;timeout
:超时时间:-1 表示阻塞,0 表示立即返回(非阻塞),大于 0 表示毫秒;- 返回值:就绪的文件描述符个数,超时时返回 0 ,出错时返回 -1 。
2. 再谈 I/O 复用
select
:使用线性表描述文件描述符集合,存在上限,每次调用需要将所有文件描述符拷贝到内核态,需要遍历判断就绪事件,适用于少量活跃的 fd;poll
:使用链表描述文件描述符集合,不存在上限,每次调用需要将所有文件描述符拷贝到内核态,需要遍历判断就绪事件,适用于少量活跃的 fd;epoll
:使用红黑树描述文件描述符集合,存在上限,通过epoll_ctl
将要监听的文件描述符注册到红黑树上,会将就绪事件存放在新建的链表中,适用于大量不活跃的 fd;
3. 触发模式和 EPOLLONESHOT
- LT :水平触发模式,当检测到就绪事件时,将其通知给应用程序,应用程序可以不立即处理该事件,等到下次调用
epoll_wait
时会再次报告该事件; - ET :边缘触发模式,当检测到就绪事件时,将其通知给应用程序,应用程序必须立即处理,并且需要一次性处理完;
- EPOLLONESHOT :当某个 socket 的数据分两次到达时,系统可能会唤醒两个不同的线程来进行处理,若开启 EPOLLONESHOT ,则对于某个 socket 来说,只会有一个线程处理其事件,其他线程不能插手,完成一次处理后需要重置 EPOLLONESHOT 。
4. HTTP 报文
HTTP 报文分为请求报文和响应报文,前者由浏览器发送给服务器,后者由服务器应答浏览器,请求报文又分为 GET 和 POST 两种:
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 行:请求行,用来说明请求类型、要访问的资源、所使用的 HTTP 版本;
- 第 2 - 8 行:请求头部,通常包含如下信息:
- Host :服务器所在的域名;
- User-Agent :HTTP 客户端程序的信息,由浏览器定义并自动发送;
- Accept :说明用户代理可处理的媒体类型;
- Accept-Encoding :说明用户代理支持的内容编码;
- Accept-Language :说明用户代理能够处理的自然语言集;
- Content-Type :说明实现主体的媒体类型;
- Content-Length:说明实现主题的大小;
- Connection :连接管理,可以是 Keep-Alive 或 close ;
- 第 9 行:空行;
- 第 10 行:请求数据,也叫主体,可以添加任意其他数据;
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
- GET 的请求数据通常为空,POST 则包含要请求的信息。
响应报文主要有四个部分组成:状态行、消息报头、空行、响应正文:
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>
- 第 1 行:状态行,由 HTTP 协议版本号、状态码、状态消息组成;
- 第 2 - 3 行:消息报头,用来说明客户端需要使用的附加信息:
- Date :生成响应的日期和时间;
- Content-Type :指定了 MIME 类型的 HTML ,编码类型是 UTF-8 ;
- 第 4 行:空行;
- 第 5 - 10 行:响应正文,为 HTML 语言。
5. HTTP 状态码
- 1xx :指示信息,表示请求已接收,继续处理;
- 2xx :成功,表示请求正常处理完毕:
- 200 OK :客户端请求被正常处理;
- 206 Partial Content :客户端进行了范围请求;
- 3xx :重定向,要完成请求需要进一步操作:
- 301 Moved Permanently :永久重定向,返回新的 URL ;
- 302 Found :临时重定向,返回临时的 URL ;
- 4xx :客户端错误:
- 400 Bad Request :语法错误;
- 403 Forbidden :请求被服务器拒绝;
- 404 Not Found :请求不存在,服务器上找不到请求的资源;
- 5xx :服务器端错误:
- 500 Internal Server Error :服务器执行时出错。
6. 有限状态机
有限状态机是一种抽象的理论模型,使用选择语句来实现。模型要求代码存在 n 个状态,使用当前状态 cur_state
来进行标记,每次处理完任务后都对其进行更改以实现状态跳转,代码如下:
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;}}
}
7. 主从状态机
主从状态机也是一种抽象的理论模型,在本项目中从状态机负责读取 HTTP 请求的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机,流程图如下:
从模型的角度来看,从状态机负责通用的操作处理,主状态机负责特定的操作处理,主状态机需要使用从状态机提供的数据,从状态机需要被主状态机调用,每个状态机又是一个有限状态机。本项目中主从状态机各有三种状态:
- 主状态机:标识解析位置
- CHECK_STATE_REQUESTLINE :解析请求行;
- CHECK_STATE_HEADER :解析请求头;
- CHECK_STATE_CONTENT :解析消息体,仅用于解析 POST 请求;
- 从状态机:标识解析一行的读取状态:
- LINE_OK :完整读取一行;
- LINE_BAD :报文语法错误;
- LINE_OPEN :读取的行不完整。
8. HTTP_CODE
标识了 HTTP 请求的处理结果:
- NO_REQUEST :请求不完整,需要继续读取请求报文,跳转主程序继续检测可读事件;
- GET_REQUEST :获得了完整的请求,调用 do_request 完成请求资源映射;
- NO_RESOURCE :请求资源不存在,跳转 process_write 完成响应报文;
- BAD_REQUEST :语法错误或请求资源为目录,跳转 process_write 完成响应报文;
- FORBIDDEN_REQUEST :请求资源禁止访问,跳转 process_write 完成响应报文;
- FILE_REQUEST :请求资源可以正常访问,跳转 process_write 完成响应报文;
- INTERNAL_ERROR :服务器内部错误。
9. HTTP 处理流程
- 浏览器发出 HTTP 连接请求;
- 主线程创建 HTTP 对象,接收请求并将所有数据读入对应的缓存区;
- 主线程将 HTTP 对象插入任务队列;
- 工作线程从任务队列中取出一个任务;
- 工作线程调用 process_read 函数,通过主从状态机解析请求报文;
- 解析完成后,跳转 do_request 函数生成响应报文;
- 通过 process_write 写入缓存区;
- 发送数据给浏览器。
二、代码解析
1. HTTP 类
// http连接类
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, // 要求服务器返回原始HTTP请求的内容,可用来查看服务器对HTTP请求的影响OPTIONS, // 查看服务器对某个特定URL都支持哪些请求方法。也可把URL设置为* ,从而获得服务器支持的所有请求方法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, // HTTP请求报文有语法错误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:// 初始化套接字地址,函数内部会调用私有方法initvoid init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname);// 关闭http连接void close_conn(bool real_close = true);// 处理HTTP请求的入口函数void process();// 读取浏览器端发来的全部数据bool read_once();// 响应报文写入函数bool write();// 地址sockaddr_in *get_address(){return &m_address;}// 同步线程初始化数据库读取表void initmysql_result(connection_pool *connPool);// 计时器标志int timer_flag;int improv;private:void init(); // 初始化HTTP_CODE process_read(); // 从m_read_buf读取,并处理请求报文bool process_write(HTTP_CODE ret); // 向m_write_buf写入响应报文数据HTTP_CODE parse_request_line(char *text); // 主状态机,解析http请求行HTTP_CODE parse_headers(char *text); // 主状态机,解析http请求头HTTP_CODE parse_content(char *text); // 主状态机,判断http请求内容HTTP_CODE do_request(); // 生成响应报文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, ...); // responsebool add_content(const char *content); // contentbool add_status_line(int status, const char *title); // status_linebool add_headers(int content_length); // headersbool add_content_type(); // content_typebool add_content_length(int content_length); // content_lengthbool add_linger(); // lingerbool add_blank_line(); // blank_linepublic:static int m_epollfd; // 最大文件描述符个数static int m_user_count; // 当前用户连接数MYSQL *mysql; // 数据库指针int m_state; // 读为0, 写为1private:int m_sockfd; // 当前fdsockaddr_in m_address; // 当前地址char m_read_buf[READ_BUFFER_SIZE]; // 存储读取的请求报文数据long m_read_idx; // 缓冲区中m_read_buf中数据的最后一个字节的下一个位置long m_checked_idx; // m_read_buf读取的位置m_checked_idxint m_start_line; // m_read_buf中已经解析的字符个数char m_write_buf[WRITE_BUFFER_SIZE]; // 存储发出的响应报文数据int m_write_idx; // 指示buffer中的长度CHECK_STATE m_check_state; // 主状态机状态METHOD m_method; // 请求方法// 以下为解析请求报文中对应的6个变量char m_real_file[FILENAME_LEN]; // 存储读取文件的名称char *m_url; // urlchar *m_version; // versionchar *m_host; // hostlong m_content_length; // content_lengthbool m_linger; // lingerchar *m_file_address; // 读取服务器上的文件地址struct stat m_file_stat; // 文件状态struct iovec m_iv[2]; // io向量机制iovec,标识两个缓存区int m_iv_count; // 表示缓存区个数int cgi; // 是否启用的POSTchar *m_string; // 存储请求头数据int bytes_to_send; // 待发送字节个数int bytes_have_send; // 已发送字节个数char *doc_root; // 文件根目录map<string, string> m_users; // 用户名密码对int m_TRIGMode; // 触发模式int m_close_log; // 是否关闭logchar sql_user[100]; // 用户名char sql_passwd[100]; // 用户密码char sql_name[100]; // 数据库名
};
2. 读取客户数据
// 循环读取客户数据,直到无数据可读或对方关闭连接
// 非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once()
{// 超出最大读缓存限制if (m_read_idx >= READ_BUFFER_SIZE){return false;}// 标志有多少字节int bytes_read = 0;// LT读取数据if (0 == m_TRIGMode){// 接收数据,保存到读缓存区bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);// 修改m_read_idx的读取字节数m_read_idx += bytes_read;// 未读到数据if (bytes_read <= 0){return false;}return true;}// ET读数据else{while (true){// 接收数据,保存到读缓存区bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);// 读取异常if (bytes_read == -1){// 判断errno是否为重试或未发送完残留数据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;}
}
2. epoll 事件相关
主要有四个:设置非阻塞模式、注册事件、删除事件、重置 EPOLLONESHOT 事件:
// 对文件描述符设置非阻塞
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, int TRIGMode)
{epoll_event event;event.data.fd = fd;// 触发组合模式:ET模式if (1 == TRIGMode)event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;// 默认模式:LT监听、连接elseevent.events = EPOLLIN | EPOLLRDHUP;// one shot模式,保证一个socket只有一个线程操作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, int TRIGMode)
{epoll_event event;event.data.fd = fd;if (1 == TRIGMode)event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;elseevent.events = ev | EPOLLONESHOT | EPOLLRDHUP;epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}
3. 接收 HTTP 请求
浏览器发出 HTTP 连接,主线程创建 HTTP 对象以接受请求,并将所有数据读入对应的缓存区,然后将该对象插入工作队列,工作线程从工作队列中取出一个任务进行处理:
//创建MAX_FD个http类对象
http_conn* users=new http_conn[MAX_FD];//创建内核事件表
epoll_event events[MAX_EVENT_NUMBER];
epollfd = epoll_create(5);
assert(epollfd != -1);//将listenfd放在epoll树上
addfd(epollfd, listenfd, false);//将上述epollfd赋值给http类对象的m_epollfd属性
http_conn::m_epollfd = epollfd;while (!stop_server)
{//等待所监控文件描述符上有事件的产生int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if (number < 0 && errno != EINTR){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);
//LT水平触发
#ifdef LTint connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){continue;}if (http_conn::m_user_count >= MAX_FD){show_error(connfd, "Internal server busy");continue;}users[connfd].init(connfd, client_address);
#endif//ET非阻塞边缘触发
#ifdef ET//需要循环接收数据while (1){int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){break;}if (http_conn::m_user_count >= MAX_FD){show_error(connfd, "Internal server busy");break;}users[connfd].init(connfd, client_address);}continue;
#endif}//处理异常事件else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){//服务器端关闭连接}//处理信号else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)){}//处理客户连接上接收到的数据else if (events[i].events & EPOLLIN){//读入对应缓冲区if (users[sockfd].read_once()){//若监测到读事件,将该事件放入请求队列pool->append(users + sockfd);}else{//服务器关闭连接}}}
}
4. HTTP 报文解析
HTTP 报文解析流程如下:
process_read
函数:通过 while 循环,将主从状态机进行封装,对报文的每一行进行处理:- 将从状态设为
LINE_OK
,作为循环的入口条件; - 首先在循环中会解析请求行;
- 然后解析请求头,若是 GET 请求则到此为止,若是 POST 请求则继续;
- 解析消息体,同时处理从状态防止再次解析;
- 循环的判断条件有特殊含义,注释中已给出说明;
- 将从状态设为
parse_line
函数:从状态机,负责解析一行数据:- 在 HTTP 报文中,每一行数据由 \r\n 作为结束,据此可以判断行;
- 每次找到并处理 \r\n 后,将其置为 \0\0 ;
- 读取中,若当前字符为 \r ,可能出现三种情况:
- 下一个字符为 \n ,将 m_checked_idx 指向下一行的开头,返回 LINE_OK ;
- 读到了缓存区末尾,标识还需要继续接收数据,返回 LINE_OPEN ;
- 其他,标识语法错误,返回 LINE_BAD ;
- 若当前字节为 \n ,则意味着中途没接收完整,对应上一种情况的第二条,此时判断前一个字符是否为 \r 即可;
- 若当前字符不为上述两种情况,则接收不完整,返回 LINE_OPEN ;
- 由于已将 \r\n 改为了 \0\0 ,因此主状态机可以直接进行字符串处理;
get_line
函数:用于将指针向后偏移,指向未处理的字符,即处理一行数据;parse_request_line
函数:主状态机,负责解析 HTTP 请求的请求行:- 初始状态为 CHECK_STATE_REQUESTLINE ;
- 从
m_read_buf
中解析 HTTP 请求行,获取请求方法、目标 URL 和 HTTP 版本号; - 将状态改为 CHECK_STATE_HEADER ;
parse_headers
函数:主状态机,负责解析 HTTP 请求的请求头:- 由于请求头和空行的处理使用的同一个函数,因此需要通过根据当前的 text 首位是不是 \0 来判断是空行还是请求头;
- 请求头有多行,因此需要根据具体含义进行解析;
- 如果是 GET 请求,则到此为止;如果是 POST 请求,则还需解析消息体(将状态改为 CHECK_STATE_CONTENT);
parse_content
函数:主状态机,负责解析 HTTP 请求的消息体;
// 解析+响应的入口函数
void http_conn::process()
{// 报文解析,获取状态码HTTP_CODE read_ret = process_read();if (read_ret == NO_REQUEST){// 注册并监听读事件modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);return;}// 报文响应,是否成功bool write_ret = process_write(read_ret);if (!write_ret){close_conn();}// 注册并监听写事件modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}// 用于将指针向后偏移,指向未处理的字符,即处理一行数据
char* http_conn::get_line() { return m_read_buf + m_start_line; }; // 解析http请求,调用了主状态机、从状态机
http_conn::HTTP_CODE http_conn::process_read()
{// 初始化从状态机状态、HTTP请求解析结果LINE_STATUS line_status = LINE_OK;HTTP_CODE ret = NO_REQUEST;char *text = 0;// 循环处理,由从状态机驱动// 前一部分判断条件用于处理请求数据(消息体),因为这部分最后面没有\r\n,无法用从状态机判断// 前一部分判断条件主要使用主状态机来判断消息体,而因为这部分处理完后状态并没有发生改变,因此还需要从状态机来标记只处理一次while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK)){// 指向读缓存区目标行的位置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);// 主状态机三种状态转换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;
}// 从状态机,用于分析出一行内容
// 返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
//m_read_idx指向缓冲区m_read_buf的数据末尾的下一个字节
//m_checked_idx指向从状态机当前正在分析的字节
http_conn::LINE_STATUS http_conn::parse_line()
{char temp;// 逐字节读for (; m_checked_idx < m_read_idx; ++m_checked_idx){//temp为将要分析的字节temp = m_read_buf[m_checked_idx];//如果当前是\r字符,则有可能会读取到完整行if (temp == '\r'){//下一个字符达到了buffer结尾,则接收不完整,需要继续接收if ((m_checked_idx + 1) == m_read_idx)return LINE_OPEN;//下一个字符是\n,将\r\n改为\0\0else if (m_read_buf[m_checked_idx + 1] == '\n'){// 标记缓存结束,返回LINE_OKm_read_buf[m_checked_idx++] = '\0';m_read_buf[m_checked_idx++] = '\0';return LINE_OK;}//如果都不符合,则返回语法错误return LINE_BAD;}//如果当前字符是\n,也有可能读取到完整行//一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况else if (temp == '\n'){//前一个字符是\r,则接收完整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;
}// 主状态机,解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{//在HTTP报文中,请求行用来说明请求类型,要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。//请求行中最先含有空格和\t任一字符的位置并返回m_url = strpbrk(text, " \t");//如果没有空格或\t,则报文格式有误if (!m_url){return BAD_REQUEST;}//将该位置改为\0,用于将前面数据取出*m_url++ = '\0';//取出数据,并通过与GET和POST比较,以确定请求方式char *method = text;if (strcasecmp(method, "GET") == 0)m_method = GET;else if (strcasecmp(method, "POST") == 0){m_method = POST;cgi = 1;}// 返回坏请求标志elsereturn BAD_REQUEST;//m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有//将m_url向后偏移,通过查找,继续跳过空格和\t字符,指向请求资源的第一个字符m_url += strspn(m_url, " \t");//使用与判断请求方式的相同逻辑,判断HTTP版本号m_version = strpbrk(m_url, " \t");if (!m_version)return BAD_REQUEST;*m_version++ = '\0';// 移动到版本的位置进行标记m_version += strspn(m_version, " \t");//仅支持HTTP/1.1if (strcasecmp(m_version, "HTTP/1.1") != 0)return BAD_REQUEST;//对请求资源前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)
{// 头部信息为空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标志设置为truem_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");// 保存hostm_host = text;}// 意外情况else{LOG_INFO("oop!unknow header: %s", text);}return NO_REQUEST;
}// 主状态机,处理content字段,判断http请求是否被完整读入
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;
}
5. HTTP 请求响应
HTTP 报文响应流程如下:
do_request
函数:对解析后的请求进行分析,并对 URL 进行处理,返回请求的状态:- / :GET 请求,跳转 judge.html ,即欢迎页面;
- /0 :POST 请求,跳转 register.html ,即注册页面;
- /1 :POST 请求,跳转 log.html ,即登录页面;
- /2 :POST 请求,进行登录校验,成功跳转 welcome.html ,即资源请求成功页面;失败跳转 logError.html ,即登陆失败页面;
- /3 :POST 请求,进行注册校验,跳转同上;
- /5 :POST 请求,跳转 picture.html ,即图片请求页面;
- /6 :POST 请求,跳转 video.html ,即视频请求页面;
- /7 :POST 请求,跳转 fans.html ,即关注页面;
- 若资源存在且访问正常,就将其映射到内存中准备发送;
add_response
函数:构造响应报文的公共接口,被各类消息报头构造函数调用;process_write
函数:向m_write_buf
中写入响应报文,响应报文分两种:- 一种是文件存在,通过 io 向量机制 iovec 声明 2 个 iovec ,第一个指向
m_write_buf
,第二个指向 mmap 的地址m_file_address
; - 第二种是请求出错,只申请一个 iovec ;
- 注册 EPOLLOUT 事件,服务器主线程监测到事件后调用
write
函数;
- 一种是文件存在,通过 io 向量机制 iovec 声明 2 个 iovec ,第一个指向
write
函数:将响应报文发送给浏览器端:- 根据已发送数据大小判断发送是否完成;
- 根据 EAGAIN 判断缓冲区是否已满;
- 每次发送数据后需要更新已发送字数;
- 发送完成后需要重置 HTTP 对象并重置 EPOLLONESHOT 事件。
// 响应http请求,检验、分配、响应请求所需的资源
http_conn::HTTP_CODE http_conn::do_request()
{//将初始化的m_real_file赋值为网站根目录strcpy(m_real_file, doc_root);// 记录文件路径长度int len = strlen(doc_root);// printf("m_url:%s\n", m_url);//找到m_url中/的位置const char *p = strrchr(m_url, '/');// //实现登录和注册校验,cgi=1启用post// *(p+1):0注册POST、1登录POST、2登录校验POST、3注册校验POST、5picture页面POST、6video页面POST、7fans页面POSTif (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3')){// 根据标志判断是登录检测还是注册检测,即/符号后的第一位char flag = m_url[1];// 申请url空间char *m_url_real = (char *)malloc(sizeof(char) * 200);// 存入/strcpy(m_url_real, "/");// 存入/后的第二位之后的urlstrcat(m_url_real, m_url + 2);// 文件存储区在文件路径之后存入真实urlstrncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);// 释放真实url存储空间free(m_url_real);// 将用户名和密码提取出来// user=123&passwd=123char 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, "')");// 没找到则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");elsestrcpy(m_url, "/registerError.html");}elsestrcpy(m_url, "/registerError.html");}// 如果是登录,直接判断// 若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0else if (*(p + 1) == '2'){if (users.find(name) != users.end() && users[name] == password)strcpy(m_url, "/welcome.html");elsestrcpy(m_url, "/logError.html");}}//如果请求资源为/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");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);}// 指向原始路径elsestrncpy(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;
}// 构造响应报文的接口
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);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)
{return 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);
}// 构造响应报文,处理好各缓存区的指针,为发送数据做准备
bool http_conn::process_write(HTTP_CODE ret)
{// 根据HTTP状态码构造响应头switch (ret){// 内部错误,500case INTERNAL_ERROR:{// 状态行add_status_line(500, error_500_title);// 消息报头add_headers(strlen(error_500_form));if (!add_content(error_500_form))return false;break;}// 报文语法有误,404case BAD_REQUEST:{add_status_line(404, error_404_title);add_headers(strlen(error_404_form));if (!add_content(error_404_form))return false;break;}// 资源没有访问权限,403case FORBIDDEN_REQUEST:{add_status_line(403, error_403_title);add_headers(strlen(error_403_form));if (!add_content(error_403_form))return false;break;}// 文件存在,200case 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_idxm_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;
}// 发送数据,即写入文件描述符
bool http_conn::write()
{int temp = 0;// 若要发送的数据长度为0// 表示响应报文为空,一般不会出现这种情况if (bytes_to_send == 0){// 将事件重置为EPOLLONESHOTmodfd(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;}// 正常发送,temp为发送的字节数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();// 在epoll树上重置EPOLLONESHOT事件modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);// 浏览器的请求为长连接if (m_linger){// 重新初始化HTTP对象init();return true;}// 不保持else{return false;}}}
}
参考文献
[1] 最新版Web服务器项目详解 - 04 http连接处理(上)
[2] 最新版Web服务器项目详解 - 05 http连接处理(中)
[3] 最新版Web服务器项目详解 - 06 http连接处理(下)