TinyWebServer项目笔记 ——03 http连接处理(上)
目录
1.介绍
2.基础知识
(1)epoll
1.epoll_create函数
2.epoll_ctl函数
3.eopll_wait函数
4.select/poll/epoll
5. LT/ET/EPOLLONESHOT
(2)HTTP报文格式
1.请求报文
2.响应报文
(3)HTTP状态码
(4)有限状态机
3.http处理流程
(1)http报文处理流程
(2)http类
(3)epoll相关代码
(4)服务器接收http请求
1.介绍
在服务器项目中,http请求的处理和响应至关重要,关系到用户界面的跳转和反馈。
基础知识方面包括epoll、HTTP报文格式、状态码和有限状态机。
代码分析方面,对服务器端处理http请求的全部流程进行简要介绍,然后结合代码对http类及请求接收进行详细分析。
2.基础知识
(1)epoll
epoll涉及的知识比较多,仅对API和基础知识做介绍。更多资料可以查看——Linux高性能服务器编程这本书。
1.epoll_create函数
1 #include 2 int epoll_create(int size)
创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。
2.epoll_ctl函数
1 #include 2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于操作内核事件表健康的文件描述符上的事件:注册、修改和删除。
-
epfd:为epoll_creat的句柄
-
op:表示动作,用3个宏来表示:
-
EPOLL_CTL_ADD (注册新的fd到epfd),
-
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
-
EPOLL_CTL_DEL (从epfd删除一个fd);
-
event:告诉内核需要监听的事件
上述event是epoll_event结构体指针类型,表示内核所监听的事件,具体定义如下:
-
events描述事件类型,其中epoll事件类型有以下几种
-
EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
-
EPOLLOUT:表示对应的文件描述符可以写
-
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
-
EPOLLERR:表示对应的文件描述符发生错误
-
EPOLLHUP:表示对应的文件描述符被挂断;
-
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
-
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3.eopll_wait函数
1 #include 2 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
该函数用于等待所监控文件描述符上事件的产生,返回就绪的文件描述符个数
-
events:用来存内核得到事件的集合,
-
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
-
timeout:是超时时间
-
-1:阻塞
-
0:立即返回,非阻塞
-
>0:指定毫秒
-
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
4.select/poll/epoll
-
调用函数
-
select和poll都是一个函数,epoll是一组函数
-
文件描述符数量
-
select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
-
poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目
-
epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效
-
将文件描述符从用户传给内核
-
select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝
-
epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上
-
内核判断就绪的文件描述符
-
select和poll通过遍历文件描述符集合,判断哪个文件描述符上有事件发生
-
epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
-
epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list
-
应用程序索引就绪文件描述符
-
select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历
-
epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可
-
工作模式
-
select和poll都只能工作在相对低效的LT模式下
-
epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。
-
应用场景
-
当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll
-
当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll
-
当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能。
5. LT/ET/EPOLLONESHOT
-
LT水平触发模式
-
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。
-
当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理
-
ET边缘触发模式
-
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件
-
必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
-
EPOLLONESHOT
-
一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
-
我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件。
(2)HTTP报文格式
HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能被浏览器端识别。其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。
1.请求报文
HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。其中,请求分为两种,GET和POST,具体的:
GET:
1 GET /562f25980001b1b106000338.jpg HTTP/1.1 2 Host:img.mukewang.com 3 User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) 4 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 5 Accept:image/webp,image/*,*/*;q=0.8 6 Referer:http://www.imooc.com/ 7 Accept-Encoding:gzip, deflate, sdch 8 Accept-Language:zh-CN,zh;q=0.8 9 空行 10 请求数据为空
POST:
1 POST / HTTP1.1 2 Host:www.wrox.com 3 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) 4 Content-Type:application/x-www-form-urlencoded 5 Content-Length:40 6 Connection: Keep-Alive 7 空行 8 name=Professional%20Ajax&publisher=Wiley
-
请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。
-
请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
-
HOST,给出请求资源所在服务器的域名。
-
User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
-
Accept,说明用户代理可处理的媒体类型。
-
Accept-Encoding,说明用户代理支持的内容编码。
-
Accept-Language,说明用户代理能够处理的自然语言集。
-
Content-Type,说明实现主体的媒体类型。
-
Content-Length,说明实现主体的大小。
-
Connection,连接管理,可以是Keep-Alive或close。
-
空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
-
请求数据也叫主体,可以添加任意的其他数据。
2.响应报文
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
1HTTP/1.1 200 OK 2Date: Fri, 22 May 2009 06:07:21 GMT 3Content-Type: text/html; charset=UTF-8 4空行 5 6 7 8 9 10
-
状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
-
消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。
-
空行,消息报头后面的空行是必须的。
-
响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。
(3)HTTP状态码
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:服务器在执行请求时出现错误。
(4)有限状态机
有限状态机,是一种抽象的理论模型,它能够把有限个变量描述的状态变化过程,以可构造可验证的方式呈现出来。比如,封闭的有向图。
有限状态机可以通过if-else,switch-case和函数指针来实现,从软件工程的角度看,主要是为了封装逻辑。
带有状态转移的有限状态机示例代码。
1 STATE_MACHINE(){ 2 State cur_State = type_A; 3 while(cur_State != type_C){ 4 Package _pack = getNewPackage(); 5 switch(){ 6 case type_A: 7 process_pkg_state_A(_pack); 8 cur_State = type_B; 9 break; 10 case type_B: 11 process_pkg_state_B(_pack); 12 cur_State = type_C; 13 break; 14 } 15 } 16}
该状态机包含三种状态:type_A,type_B和type_C。其中,type_A是初始状态,type_C是结束状态。
状态机的当前状态记录在cur_State变量中,逻辑处理时,状态机先通过getNewPackage获取数据包,然后根据当前状态对数据进行处理,处理完后,状态机通过改变cur_State完成状态转移。
有限状态机一种逻辑单元内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清晰易懂。
3.http处理流程
首先对http报文处理的流程进行简要介绍,然后具体介绍http类的定义和服务器接收http请求的具体过程。
(1)http报文处理流程
-
浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。(本篇)
-
工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。(中篇)
-
解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。(下篇)
(2)http类
这一部分代码在TinyWebServer/http/http_conn.h中,主要是http类的定义。
1class http_conn{ 2 public: 3 //设置读取文件的名称m_real_file大小 4 static const int FILENAME_LEN=200; 5 //设置读缓冲区m_read_buf大小 6 static const int READ_BUFFER_SIZE=2048; 7 //设置写缓冲区m_write_buf大小 8 static const int WRITE_BUFFER_SIZE=1024; 9 //报文的请求方法,本项目只用到GET和POST 10 enum METHOD{GET=0,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATH}; 11 //主状态机的状态 12 enum CHECK_STATE{CHECK_STATE_REQUESTLINE=0, CHECK_STATE_HEADER,CHECK_STATE_CONTENT}; 13 //报文解析的结果 14 enum HTTP_CODE{NO_REQUEST,GET_REQUEST,BAD_REQUEST, NO_RESOURCE,FORBIDDEN_REQUEST,FILE_REQUEST, INTERNAL_ERROR,CLOSED_CONNECTION}; 15 //从状态机的状态 16 enum LINE_STATUS{LINE_OK=0,LINE_BAD,LINE_OPEN}; 17 18 public: 19 http_conn(){} 20 ~http_conn(){} 21 22 public: 23 //初始化套接字地址,函数内部会调用私有方法init 24 void init(int sockfd,const sockaddr_in &addr); 25 //关闭http连接 26 void close_conn(bool real_close=true); 27 void process(); 28 //读取浏览器端发来的全部数据 29 bool read_once(); 30 //响应报文写入函数 31 bool write(); 32 sockaddr_in *get_address(){ 33 return &m_address; 34 } 35 //同步线程初始化数据库读取表 36 void initmysql_result(); 37 //CGI使用线程池初始化数据库表 38 void initresultFile(connection_pool *connPool); 39 40 private: 41 void init(); 42 //从m_read_buf读取,并处理请求报文 43 HTTP_CODE process_read(); 44 //向m_write_buf写入响应报文数据 45 bool process_write(HTTP_CODE ret); 46 //主状态机解析报文中的请求行数据 47 HTTP_CODE parse_request_line(char *text); 48 //主状态机解析报文中的请求头数据 49 HTTP_CODE parse_headers(char *text); 50 //主状态机解析报文中的请求内容 51 HTTP_CODE parse_content(char *text); 52 //生成响应报文 53 HTTP_CODE do_request(); 54 55 //m_start_line是已经解析的字符 56 //get_line用于将指针向后偏移,指向未处理的字符 57 char* get_line(){return m_read_buf+m_start_line;}; 58 59 //从状态机读取一行,分析是请求报文的哪一部分 60 LINE_STATUS parse_line(); 61 62 void unmap(); 63 64 //根据响应报文格式,生成对应8个部分,以下函数均由do_request调用 65 bool add_response(const char* format,...); 66 bool add_content(const char* content); 67 bool add_status_line(int status,const char* title); 68 bool add_headers(int content_length); 69 bool add_content_type(); 70 bool add_content_length(int content_length); 71 bool add_linger(); 72 bool add_blank_line(); 73 74 public: 75 static int m_epollfd; 76 static int m_user_count; 77 MYSQL *mysql; 78 79 private: 80 int m_sockfd; 81 sockaddr_in m_address; 82 83 //存储读取的请求报文数据 84 char m_read_buf[READ_BUFFER_SIZE]; 85 //缓冲区中m_read_buf中数据的最后一个字节的下一个位置 86 int m_read_idx; 87 //m_read_buf读取的位置m_checked_idx 88 int m_checked_idx; 89 //m_read_buf中已经解析的字符个数 90 int m_start_line; 91 92 //存储发出的响应报文数据 93 char m_write_buf[WRITE_BUFFER_SIZE]; 94 //指示buffer中的长度 95 int m_write_idx; 96 97 //主状态机的状态 98 CHECK_STATE m_check_state; 99 //请求方法 100 METHOD m_method; 101 102 //以下为解析请求报文中对应的6个变量 103 //存储读取文件的名称 104 char m_real_file[FILENAME_LEN]; 105 char *m_url; 106 char *m_version; 107 char *m_host; 108 int m_content_length; 109 bool m_linger; 110 111 char *m_file_address; //读取服务器上的文件地址 112 struct stat m_file_stat; 113 struct iovec m_iv[2]; //io向量机制iovec 114 int m_iv_count; 115 int cgi; //是否启用的POST 116 char *m_string; //存储请求头数据 117 int bytes_to_send; //剩余发送字节数 118 int bytes_have_send; //已发送字节数 119};
在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化,不用过多讲解。
这里,对read_once进行介绍。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。
1 //循环读取客户数据,直到无数据可读或对方关闭连接 2 bool http_conn::read_once() 3 { 4 if(m_read_idx>=READ_BUFFER_SIZE) 5 { 6 return false; 7 } 8 int bytes_read=0; 9 while(true) 10 { 11 //从套接字接收数据,存储在m_read_buf缓冲区 12 bytes_read=recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE- m_read_idx,0); 13 if(bytes_read==-1) 14 { 15 //非阻塞ET模式下,需要一次性将数据读完 16 if(errno==EAGAIN||errno==EWOULDBLOCK) 17 break; 18 return false; 19 } 20 else if(bytes_read==0) 21 { 22 return false; 23 } 24 //修改m_read_idx的读取字节数 25 m_read_idx+=bytes_read; 26 } 27 return true; 28}
(3)epoll相关代码
项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种。
非阻塞模式
1 //对文件描述符设置非阻塞 2 int setnonblocking(int fd) 3 { 4 int old_option = fcntl(fd, F_GETFL); 5 int new_option = old_option | O_NONBLOCK; 6 fcntl(fd, F_SETFL, new_option); 7 return old_option; 8}
内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,listenfd不用开启。
1 void addfd(int epollfd, int fd, bool one_shot) 2 { 3 epoll_event event; 4 event.data.fd = fd; 5 6 #ifdef ET 7 event.events = EPOLLIN | EPOLLET | EPOLLRDHUP; 8 #endif 9 10 #ifdef LT 11 event.events = EPOLLIN | EPOLLRDHUP; 12 #endif 13 14 if (one_shot) 15 event.events |= EPOLLONESHOT; 16 epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); 17 setnonblocking(fd); 18}
内核事件表删除事件
1 void removefd(int epollfd, int fd) 2 { 3 epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0); 4 close(fd); 5 }
重置EPOLLONESHOT事件
1 void modfd(int epollfd, int fd, int ev) 2{ 3 epoll_event event; 4 event.data.fd = fd; 5 6 #ifdef ET 7 event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP; 8 #endif 9 10 #ifdef LT 11 event.events = ev | EPOLLONESHOT | EPOLLRDHUP; 12 #endif 13 14 epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event); 15}
(4)服务器接收http请求
浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
1 //创建MAX_FD个http类对象 2 http_conn* users=new http_conn[MAX_FD]; 3 4 //创建内核事件表 5 epoll_event events[MAX_EVENT_NUMBER]; 6 epollfd = epoll_create(5); 7 assert(epollfd != -1); 8 9 //将listenfd放在epoll树上 10 addfd(epollfd, listenfd, false); 11 12 //将上述epollfd赋值给http类对象的m_epollfd属性 13 http_conn::m_epollfd = epollfd; 14 15 while (!stop_server) 16{ 17 //等待所监控文件描述符上有事件的产生 18 int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1); 19 if (number = MAX_FD) 41 { 42 show_error(connfd, "Internal server busy"); 43 continue; 44 } 45 users[connfd].init(connfd, client_address); 46#endif 47 48//ET非阻塞边缘触发 49#ifdef ET 50 //需要循环接收数据 51 while (1) 52 { 53 int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength); 54 if (connfd = MAX_FD) 59 { 60 show_error(connfd, "Internal server busy"); 61 break; 62 } 63 users[connfd].init(connfd, client_address); 64 } 65 continue; 66#endif 67 } 68 69 //处理异常事件 70 else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) 71 { 72 //服务器端关闭连接 73 } 74 75 //处理信号 76 else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)) 77 { 78 } 79 80 //处理客户连接上接收到的数据 81 else if (events[i].events & EPOLLIN) 82 { 83 //读入对应缓冲区 84 if (users[sockfd].read_once()) 85 { 86 //若监测到读事件,将该事件放入请求队列 87 pool->append(users + sockfd); 88 } 89 else 90 { 91 //服务器关闭连接 92 } 93 } 94 95 } 96}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-