基础知识:epoll、http报文格式、状态码和有限状态机
代码:对服务端处理http请求的全部流程进行简要介绍,然后结合代码对http类及请求接收进行详细分析。
#include
int epoll_create(int size)
创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。
#include
int epoll_ctl(int epfd,int op,struct epoll_event *event)
该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除
struct epoll_event{
__unit32_t events;
epoll_data_t data;
};
#include
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)
该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数
HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能浏览器端识别。其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。
HTTP请求报文由请求行、请求头部、空行和请求数据四个部分组成。
请求分为GET和POST两种。
1 POST / HTTP1.1
2 Host:www.wrox.com
3 User-Agent:Mozilla/4.0 (compatible;MSIE 6.0; Windowa 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
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指定了MME类型的HTML(text/html),编码类型是UTF-8
有限状态机,是一种抽象的理论模型,它能够把有限个变量描述的状态变化过程,以可构造可验证的方式呈现出来。比如,封闭的有向图。
有限状态机可以通过if-else,switch-case和函数指针来实现,从软件工程的角度看,主要是为了封装逻辑。
带有状态转移的有限状态机示例代码。
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报文处理的流程进行简要介绍,然后具体介绍http类的定义和服务器接收http请求的具体过程。
http_conn.h中,主要是http类的定义。
class http_conn{
public:
//设置读取文件的名称m_real_file大小
static const int FILENAME_LEN=200;
//设置读缓冲区m_read_buf大小
static const int READ_BUFFER_SIZE=2048;
//设置写缓冲区m_write_buf大小
static const int WRITE_BUFFER_SIZE=1024;
//报文的请求方法,本项目只用到GET和POST
enum METHOD{GET=0,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATH};
//主状态机的状态
enum CHECK_STATE{CHEACK_STATE_QEQUESTLINE=0,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=0,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();
//根据响应报文格式,生成对应8个部分,以下函数均由do_request调用
bool add_reponse(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;
//以下为解析请求报文中对应的6个变量
//存储读取文件的名称
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[2];//io向量机制iovec
int m_iv_count;
int cgi;//是否启用的POST
char *m_string;//存储请求头数据
int bytes_to_send;//剩余发送字节数
int bytes_have_send;//已发送字节数
};
在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化,不用过多讲解。
read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。
//循环读取客户数据,直到无数据可读或对方关闭连接
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模式下,需要一次性将数据读完
if(errno==EAGAIN||errno==EWOULDBLOCK){
break;
return false;
}
}else if(bytes_read==0){
return false;
}
//修改m_read_idx的读取字节数1
m_read_idx+=bytes_read;
}
return true;
}
项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种。
//对文件描述符设置非阻塞模式
int setnoblocking(int fd){
int old_option=fcntl(fd,F_GETFL);//获取文件fd的当前状态
int new_option=old_option|O_NONBLOCK;
fcntl(fd,F_SETFL,new_option);
return old_option;
}
void addfd(int epollfd,int fd,bool one_shot)
{
epoll_event event;
event.data.fd=fd;
#ifdef ET
event.events=EPOLLIN|EPOLLET|EPOLLRDHUP;
#endif
#ifdef LT
event.events=EPOLLIN|EPOLLRDHUP;
#endif
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);
}
void modfd(int epollfd,int fd,int ev){
epoll_event event;
event.data.fd=fd;
#ifdef ET
event.events=ev|EPOLLET|EPOLLONESHOT|EPOLLRDHUP;
#endif
#ifdef LT
event.events=ev|EPOLLONESHOT|EPOLLRDHUP;
#endif
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}
浏览器端发出http连接请求,主线程创建http对象接收请求并所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列取出一个任务进行处理。
//创建MAX_FD个http类对象
http_conn* user=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 LT
int 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&(EPOLLRDRDHUP|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{
//服务器关闭连接
}
}
}
}