从零实现Web服务器(三):日志优化,压力测试,实战接收HTTP请求,实战响应HTTP请求
文章目录
- 一、日志系统的运行流程
- 1.1 异步日志和同步日志的不同点
- 1.2 缓冲区的实现
- 二、基于Webbench的压力测试
- 三、HTTP请求报文解析
- http报文处理流程
- epoll相关代码
- 服务器接收http请求
- 四、HTTP请求报文响应
一、日志系统的运行流程
步骤:
- 单例模式(局部静态变量懒汉方法)获取实例。
- 主程序一开始Log::get_instance()->init()初始化实例。
初始化后:服务器启动按当前时刻创建日志(前缀为时间,后缀为自定义log文件名,并记录创建日志的时间day和行数count)。如果是异步(通过是否设置队列大小判断是否异步,0为同步), 工作线程将要写的内容放进阻塞队列,还创建了写线程用于在阻塞队列里取出一个内容(指针),写入日志。 - 其他功能模块调用write_log()函数写日志。(write_log:实现日志分级、分文件、按天分类,超行分类的格式化输出内容。)
1.1 异步日志和同步日志的不同点
因为同步日志的,日志写入函数与工作线程串行执行,由于涉及到I/O操作,在单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
而异步日志采用生产者-消费者模型,工作线程将所写的日志内容先存入缓冲区,写线程从缓冲区中取出内容,写入日志。并发能力比较高。
1.2 缓冲区的实现
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图。
在实际项目中,使用循环数组实现队列,作为两者共享的缓冲区。
二、基于Webbench的压力测试
父进程fork若干个子进程,每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求,父子进程通过管道进行通信,子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息,父进程通过管道读端读取子进程发来的相关信息,子进程在时间到后结束,父进程在所有子进程退出后统计并给用户显示最后的测试结果,然后退出。
三、HTTP请求报文解析
http报文处理流程
-
浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
-
工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。(中篇讲)
-
解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。
这一部分代码在TinyWebServer/http/http_conn.h中,主要是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;//报文的请求方法,本项目只用到GET和POSTenum METHOD{GET=0,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATH};enum CHECK_STATE{CHECK_STATE_REQUESTLINE=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(){}//初始化套接字地址,函数内部会调用私有方法initvoid init(int sockfd,const sockaddr_in &addr);void close_conn(bool real_close=true);void process();//读取浏览器发来的所有数据void read_once();bool write();sockaddr_in *get_address(){return &m_address;}void initmysql_result();//CGI使用线程池初始化数据库表void initresultFile(connection_pool *connPool);private:void init();HTTP_CODE process_read();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_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_buffer[read_buffer_size];//缓冲区中m_read_buffer中的长度int m_read_idx;//m_read_buf读取的位置m_checked_idxint 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向量机制iovecint m_iv_count;int cgi; //是否启用的POSTchar *m_string; //存储请求头数据int bytes_to_send; //剩余发送字节数int bytes_have_send; //已发送字节数};
这里,对read_once进行介绍。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。
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}
epoll相关代码
项目中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;
}
- 内核事件表注册新事件,开启EPOLL ONESHOT,针对客户端连接的描述符,listenfd不用开启
void addfd(int epollfd,int fd, bool one_shot)
{epoll_event event;event.data.fd = fd;#ifdef ETevent.events = EPOLLIN | EPOLLET | EPOLLRDHUP;#endif #ifdef LTevent.events = EPOLLIN | EPOLLRDHUP;#endif if(ont_shot) event.events |= EPOLLONESHOT;epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);setnonblocking(fd);
}
3.内核事件表删除事件
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)
{epoll_event event;event.data.fd = fd;#ifdef ETevent.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;#endif #ifdef LTevent.events = ev | EPOLLONESHOT | EPOLLRDHUP;#endif epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}
服务器接收http请求
http_conn* users = new http_conn[max_fd];//创建内核事件表
epoll_event events[max_event_number];
epollfd = epoll_create(5);
assert(epoll_fd != -1);//添加listenfd
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);#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#ifdef ETwhile(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)){//强制关闭连接}//pipefd[0]即读端文件描述符,pipefd[1]即写端文件描述符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{//服务器关闭连接}}}
}