热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

从零实现Web服务器(三):日志优化,压力测试,实战接收HTTP请求,实战响应HTTP请求

文章目录一、日志系统的运行流程1.1异步日志和同步日志的不同点1.2缓冲区的实现二、基于Webbench的压力测试三、HTTP请求报文解析http报文处理流程epoll相关代码服务

文章目录


  • 一、日志系统的运行流程
    • 1.1 异步日志和同步日志的不同点
    • 1.2 缓冲区的实现

  • 二、基于Webbench的压力测试
  • 三、HTTP请求报文解析
    • http报文处理流程
    • epoll相关代码
    • 服务器接收http请求

  • 四、HTTP请求报文响应




一、日志系统的运行流程

步骤:

  1. 单例模式(局部静态变量懒汉方法)获取实例。
  2. 主程序一开始Log::get_instance()->init()初始化实例。
    初始化后:服务器启动按当前时刻创建日志(前缀为时间,后缀为自定义log文件名,并记录创建日志的时间day和行数count)。如果是异步(通过是否设置队列大小判断是否异步,0为同步), 工作线程将要写的内容放进阻塞队列,还创建了写线程用于在阻塞队列里取出一个内容(指针),写入日志。
  3. 其他功能模块调用write_log()函数写日志。(write_log:实现日志分级、分文件、按天分类,超行分类的格式化输出内容。)

1.1 异步日志和同步日志的不同点

因为同步日志的,日志写入函数与工作线程串行执行,由于涉及到I/O操作,在单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。

而异步日志采用生产者-消费者模型,工作线程将所写的日志内容先存入缓冲区,写线程从缓冲区中取出内容,写入日志。并发能力比较高。

1.2 缓冲区的实现

单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图。
在这里插入图片描述

在实际项目中,使用循环数组实现队列,作为两者共享的缓冲区。

二、基于Webbench的压力测试

父进程fork若干个子进程,每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求,父子进程通过管道进行通信,子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息,父进程通过管道读端读取子进程发来的相关信息,子进程在时间到后结束,父进程在所有子进程退出后统计并给用户显示最后的测试结果,然后退出。

三、HTTP请求报文解析

http报文处理流程


  1. 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。

  2. 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。(中篇讲)

  3. 解析完之后,跳转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和POST
enum 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(){}
//初始化套接字地址,函数内部会调用私有方法init
void 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_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; //已发送字节数
};

这里,对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事件四种。

  1. 非阻塞模式

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;
}

  1. 内核事件表注册新事件,开启EPOLL ONESHOT,针对客户端连接的描述符,listenfd不用开启

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(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);
}

  1. 重置EPOLLONESHOT事件

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_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 !&#61; EINTR)break;
for(int i&#61;0;i<number;i&#43;&#43;){
int sockfd &#61; events[i].data.fd;
if(sockfd &#61;&#61; listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength &#61; sizeof(client_address);
#ifdef LT //水平触发
int connfd&#61;accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
if(connfd < 0)continue;
if(http_conn::m_user_count >&#61; max_fd)
{
show_error(connfd,"Internal Server Busy");
continue;
}
users[connfd].init(connfd,client_address);
#endif
#ifdef ET
while(1){//需要不断接收数据
int connfd&#61;accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
if(connfd < 0)break;
if(http_conn::m_user_count >&#61; 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&#61;&#61;pipefd[0]) && (events[i].events & EPOLLIN) ){
}
//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
//读入对应缓冲区
if (users[sockfd].read_once())
{
//若监测到读事件&#xff0c;将该事件放入请求队列
pool->append(users &#43; sockfd);
}
else
{
//服务器关闭连接
}
}
}
}

四、HTTP请求报文响应








推荐阅读
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
author-avatar
曹彩节
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有