之前学习了有关线程池、数据库连接池的相关知识,接下来进行重头戏——HTTP连接的相关知识。
这段时间在准备暑期实习的面试,参加了腾讯、阿里、CVTE、深信服等公司的面试发现自己对之前的这个开源项目还有很多细节不够了解,特此回来学习一波,如果有什么纰漏还请大家谅解、指正。
一、http的详细连接过程
http需要结合epoll/select/poll来理解,所以可以点此跳转回学习笔记2,复习相关知识。
1.1 Http报文
有关HTTP报文格式,HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能被浏览器端识别。
其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。
·请求报文=请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。又分为两种,GET/POST。
请求行,用来说明请求类型,要访问的资源以及所使用的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协议版本号,状态码,状态消息 三部分组成。第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
·消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/xml),编码类型是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:服务器在执行请求时出现错误。
1.2 http报文处理流程
·浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。 参见1.3节
·工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。参见1.4节
·解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。
1.3 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};
1.3.1 read_once
1 //循环读取客户数据,直到无数据可读或对方关闭连接 2bool 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}
1.3.2 epoll部分
① 文件描述符的设置
② epoll的epollshot注册
//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT void addfd(int epollfd, int fd, bool one_shot, int TRIGMode) { epoll_event event; event.data.fd = fd; if (1 == TRIGMode) event.events = EPOLLIN | EPOLLET | EPOLLRDHUP; else event.events = EPOLLIN | EPOLLRDHUP; if (one_shot) event.events |= EPOLLONESHOT; epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); setnonblocking(fd); }
PS:epoll_onesheot主要是为了防止同一socket连续接收到数据,造成解析异常,详见博客:https://blog.csdn.net/liuhengxiao/article/details/46911129。
③ 重置epollshot事件
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; else event.events = ev | EPOLLONESHOT | EPOLLRDHUP; epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event); }
1.3.3 服务器接收http请求
主要由函数://事件回环(即服务器主线程)
void WebServer::eventLoop()进行处理。
1.4 http的状态机与解析请求报文
从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。流程图如下:(该部分最好的理解途径就是借助流程图进行理解)
主状态机有三种状态: ·CHECK_STATE_REQUESTLINE,解析请求行 ·CHECK_STATE_HEADER,解析请求头 ·CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求 从状态机的三种状态: ·LINE_OK,完整读取一行 ·LINE_BAD,报文语法有误 ·LINE_OPEN,读取的行不完整
解析报文整体流程
process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。
1.5 服务器相应报文
浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,进入相应的逻辑和模块。其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_once和http_conn::write完成数据的读取与发送。(参考:https://mp.weixin.qq.com/s/451xNaSFHxcxfKlPBV3OCg)
PS:关于这个部分的http资源请求和登陆,如果还想进一步了解其细节,可以直接参考博客:https://mp.weixin.qq.com/s/451xNaSFHxcxfKlPBV3Ocg以及https://mp.weixin.qq.com/s/wAQHU-QZiRt1VACMZZjNlw。