基础知识
?HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能被浏览器端识别。
?其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。
请求报文
?HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
其中,请求分为两种,GET和POST,具体的:
●GET
1 2 3 4 5 6 7 8 9 10 | 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
1 2 3 4 5 6 7 8 | 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版本。
●请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
?HOST,给出请求资源所在服务器的域名。
?User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
?Accept,说明用户代理可处理的媒体类型。
?Accept-Encoding,说明用户代理支持的内容编码。
?Accept-Language,说明用户代理能够处理的自然语言集。
?Content-Type,说明实现主体的媒体类型。
?Content-Length,说明实现主体的大小。
?Connection,连接管理,可以是Keep-Alive或close。
●空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
●请求数据也叫主体,可以添加任意的其他数据。
响应报文
?HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
1 2 3 4 5 6 7 8 9 10 | 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状态码
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:服务器在执行请求时出现错误。
有限状态机
?有限状态机,是一种抽象的理论模型,它能够把有限个变量描述的状态变化过程,以可构造可验证的方式呈现出来。比如,封闭的有向图。
?有限状态机可以通过if-else,switch-case和函数指针来实现,从软件工程的角度看,主要是为了封装逻辑。
?带有状态转移的有限状态机示例代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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; } } } |
?该状态机包含三种状态:type_A,type_B和type_C。其中,type_A是初始状态,type_C是结束状态。
?状态机的当前状态记录在cur_State变量中,逻辑处理时,状态机先通过getNewPackage获取数据包,然后根据当前状态对数据进行处理,处理完后,状态机通过改变cur_State完成状态转移。
?有限状态机一种逻辑单元内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清晰易懂。
http_conn类 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | class http_conn{ public: //设置读取文件的名称m_real_file大小 static const int FILENAME_LEN= ; //设置读缓冲区m_read_buf大小 static const int READ_BUFFER_SIZE= ; //设置写缓冲区m_write_buf大小 static const int WRITE_BUFFER_SIZE= ; //报文的请求方法,本项目只用到GET和POST enum METHOD{GET= ,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATH}; //主状态机的状态 enum CHECK_STATE{CHECK_STATE_REQUESTLINE= ,CHECK_STATE_HEADER,CHECK_STATE_CONTENT}; //报文解析的结果 enum HTTP_CODE{NO_REQUEST,GET_REQUEST,BAD_REQUEST,NO_RESOURCE,FORBIDDEN_REQUEST,FILE_REQUEST,INTERNAL_ERROR,CLOSED_CONNECTION}; //从状态机的状态 enum LINE_STATUS{LINE_OK= ,LINE_BAD,LINE_OPEN}; public: http_conn(){} ~http_conn(){} public: //初始化套接字地址,函数内部会调用私有方法init void init(int sockfd,const sockaddr_in &addr); //关闭http连接 void close_conn(bool real_close=true); void process(); //读取浏览器端一次请求的全部数据 bool read_once(); //响应报文写入函数 bool write(); sockaddr_in *get_address(){ return &m_address; } //同步线程初始化数据库读取表 void initmysql_result(); //CGI使用线程池初始化数据库表 void initresultFile(connection_pool *connPool); private: void init(); //从m_read_buf读取,并处理请求报文 HTTP_CODE process_read(); //向m_write_buf写入响应报文数据 bool process_write(HTTP_CODE ret); //主状态机解析报文中的请求行数据 HTTP_CODE parse_request_line(char *text); //主状态机解析报文中的请求头数据 HTTP_CODE parse_headers(char *text); //主状态机解析报文中的请求内容 HTTP_CODE parse_content(char *text); //生成响应报文 HTTP_CODE do_request(); //m_start_line是已经解析的字符 //get_line用于将指针向后偏移,指向未处理的字符 char* get_line(){return m_read_buf+m_start_line;}; //从状态机读取出一行 LINE_STATUS parse_line(); void unmap(); //根据响应报文格式,生成对应 个部分,以下函数均由do_request调用 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: static int m_epollfd; static int m_user_count; MYSQL *mysql; private: int m_sockfd; sockaddr_in m_address; //存储读取的请求报文数据 char m_read_buf[READ_BUFFER_SIZE]; //缓冲区中m_read_buf中数据的最后一个字节的下一个位置 int m_read_idx; //m_read_buf读取的位置m_checked_idx int m_checked_idx; //m_read_buf中已经解析的字符个数 int m_start_line; //存储发出的响应报文数据 char m_write_buf[WRITE_BUFFER_SIZE]; //指示buffer中的长度 int m_write_idx; //主状态机的状态 CHECK_STATE m_check_state; //请求方法 METHOD m_method; //以下为解析请求报文中对应的 个变量 //存储读取文件的名称 char m_real_file[FILENAME_LEN]; char *m_url; char *m_version; char *m_host; int m_content_length; bool m_linger; char *m_file_address; //读取服务器上的文件地址 struct stat m_file_stat; struct iovec m_iv[ ]; //io向量机制iovec int m_iv_count; int cgi; //是否启用的POST char *m_string; //存储请求头数据 int bytes_to_send; //剩余发送字节数 int bytes_have_send; //已发送字节数 }; |
解析请求报文
流程图与状态机
?从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。

?主状态机:三种状态,标识解析位置
●CHECK_STATE_REQUESTLINE,解析请求行
●CHECK_STATE_HEADER,解析请求头部
●CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求
?从状态机:三种状态,标识解析一行的读取状态。
●LINE_OK,完整读取一行
●LINE_BAD,报文语法有误
●LINE_OPEN,读取的行不完整
http_conn::read_once():读入客户一次请求到读缓冲区并判断是否加入请求队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //循环读取客户数据,直到无数据可读或对方关闭连接 bool http_conn::read_once() { if(m_read_idx>=READ_BUFFER_SIZE) { return false; } int bytes_read=0; while(true) { //从套接字接收数据,存储在m_read_buf缓冲区 bytes_read=recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE-m_read_idx,0); if(bytes_read==-1) { //非阻塞ET模式下,需要一次性将数据读完,EAGAIN即缓冲区无数据可读(读完) 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; } |
解析请求中的HTTP_CODE含义
表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析时只涉及到四种。
●NO_REQUEST
?请求不完整,需要继续读取请求报文数据
●GET_REQUEST
?获得了完整的HTTP请求
●BAD_REQUEST
?HTTP请求报文有语法错误
●INTERNAL_ERROR
?服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发
process()处理请求:解析+响应
?具体流程:浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列后,工作线程从任务队列中取出一个任务调用请求类的process函数进行处理。
?process函数:各子线程通过调用请求类的process函数处理请求,process函数调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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(); } //触发EPOLLOUT事件让主线程处理 modfd(m_epollfd,m_sockfd,EPOLLOUT); } |
process_read():解析报文
?整体流程:通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。如果请求读取完整,调用do_request()执行请求,将相应的文件映射到内存准备写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | //m_start_line是行在buffer中的起始位置,将该位置赋给text //此时从状态机已提前将一行的末尾字符\r\n变为\0\0 ,所以text可以直接取出完整的行进行解析 char* get_line(){ return m_read_buf+m_start_line; } 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)) { text=get_line(); //m_start_line是每一个数据行在m_read_buf中的起始位置 //m_checked_idx表示从状态机在m_read_buf中读取的位置 m_start_line=m_checked_idx; //主状态机的三种状态转移逻辑 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_status=LINE_OPEN; break; } default: return INTERNAL_ERROR; } } return NO_REQUEST; } |
?为什么while判断条件为
1 2 | 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请求报文中,消息体的末尾没有任何字符,不能从\r\n来判断,所以不能使用从状态机的状态,不能使用parse_line来处理,这里转而使用主状态机的状态CHECK_STATE_CONTENT作为循环入口条件。
●后面的&& line_status==LINE_OK:解析完消息体后,报文的完整解析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT,还是会再次进入循环,所以增加该句,在完成消息体解析后,将line_status改为LINE_OPEN,就可以跳出循环,完成报文解析
从状态机即parse_line():提取一行内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //从状态机,用于分析出一行内容 //返回值为行的读取状态,有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\0 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; } //如果当前字符是\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; } } //并没有找到\r\n,需要继续接收 return LINE_OPEN; } |
主状态机:parse_request_line/headers/content
●CHECK_STATE_REQUESTLINE
?主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。
?初始状态,调用parse_request_line函数解析请求行
?解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
?解析完成后主状态机的状态变为CHECK_STATE_HEADER
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | //解析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; } else return 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.1 if(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; } |
●CHECK_STATE_HEADER
?解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。
?调用parse_headers函数解析请求头部信息
?判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。
?若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。
?connection字段判断是keep-alive还是close,决定是长连接还是短连接
?content-length字段,这里用于读取post请求的消息体长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | //解析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标志设置为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); } return NO_REQUEST; } |
●CHECK_STATE_CONTENT
?仅用于解析POST请求,调用parse_content函数解析消息体
?用于保存post请求消息体,为后面的登录和注册做准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //判断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; } |
响应请求报文
基础API
●stat
?stat函数用于取得指定文件的文件属性,并将文件属性存储在结构体stat里,这里仅对其中用到的成员进行介绍。
1 2 3 4 5 6 7 8 9 10 11 12 | #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
?用于将一个文件或其他对象映射到内存,提高文件的访问速度。
1 2 3 4 5 6 7 8 9 10 11 | 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 表示页内容可以被读取 //flags:指定映射对象的类型,映射选项和映射页是否可以共享 MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件 //fd:有效的文件描述符,一般是由open()函数返回 //off_toffset:被映射对象内容的起点 |
●iovec
?定义了一个向量元素,通常,这个结构用作一个多元素的数组
1 2 3 4 5 6 | struct iovec { void *iov_base; /* starting address of buffer */ size_t iov_len; /* size of buffer */ }; //iov_base指向数据的地址 //iov_len表示数据的长度 |
●writev
?writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。
1 2 3 4 5 6 | #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返回输出的字节总数,通常,它应等于所有缓冲区长度之和。 |
?循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然需要通过遍历iovec来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界iovec的io_base和io_len。
流程
?浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,process_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():得到完整请求后响应请求文件
?m_url为请求报文中解析出的请求资源,以/开头,也就是/xxx,项目中解析后的m_url有8种情况。得到对应的html文件后,将其映射到内存中,并加入iovec用于主线程
writev
●/
?GET请求,跳转到judge.html,即欢迎访问页面
●/0
?POST请求,跳转到register.html,即注册页面
●/1
?POST请求,跳转到log.html,即登录页面
●/2CGISQL.cgi
?POST请求,进行登录校验
?验证成功跳转到welcome.html,即资源请求成功页面
?验证失败跳转到logError.html,即登录失败页面
●/3CGISQL.cgi
?POST请求,进行注册校验
?注册成功跳转到log.html,即登录页面
?注册失败跳转到registerError.html,即注册失败页面
●/5
?POST请求,跳转到picture.html,即图片请求页面
●/6
?POST请求,跳转到video.html,即视频请求页面
●/7
?POST请求,跳转到fans.html,即关注页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | //网站根目录,文件夹内存放请求的资源和跳转的html文件 const char* doc_root="/home/qgy/github/ini_tinywebserver/root"; 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, '/'); //实现登录和注册校验 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"); //将网站目录和/register.html进行拼接,更新到m_real_file中 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); } 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; } |
add_response()等:被process_write调用将响应报文格式写入写缓冲区
?下面的函数,均是内部调用add_response函数更新m_write_idx指针和缓冲区m_write_buf中的内容。
●add_status_line函数,添加状态行:http/1.1 状态码 状态消息
●add_headers函数添加消息报头,内部调用add_content_length和add_linger函数
?content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据
?connection记录连接状态,用于告诉浏览器端保持长连接
●add_blank_line添加空行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 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); 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); } |
process_read():写响应报文到写缓冲区
?响应报文分为两种,一种是请求文件的存在,通过io向量机制iovec,声明两个iovec,第一个指向m_write_buf,第二个指向mmap的地址m_file_address;一种是不能响应请求文件(报文错误/内部错误/无权限等等),这时候只申请一个iovec,指向m_write_buf。
?iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据。
?成员iov_len表示实际写入的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 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; return true; } |
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单次发送不成功,判断是否是socket写缓冲区满了。
?若不是因为缓冲区满了而失败,取消mmap映射,关闭连接
?若eagain则缓冲区满了,更新iovec结构体的指针和长度,并注册写事件(oneshot需要充值),等待下一次写事件触发(当写缓冲区从不可写变为可写,触发epollout),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | bool http_conn::write() { int temp = 0; int newadd = 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); //正常发送,temp为发送的字节数 if (temp > 0) { //更新已发送字节 bytes_have_send += temp; //偏移文件iovec的指针 newadd = bytes_have_send - m_write_idx; } if (temp <= -1) { //判断缓冲区是否满了 if (errno == EAGAIN) { //第一个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 + newadd; m_iv[1].iov_len = bytes_to_send; } //继续发送第一个iovec头部信息的数据 else { m_iv[0].iov_base = m_write_buf + bytes_to_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树上重置EPOLLONESHOT事件 modfd(m_epollfd,m_sockfd,EPOLLIN); //浏览器的请求为长连接 if(m_linger) { //重新初始化HTTP对象 init(); return true; } else { return false; } } } } |