06 HTTP(下)

06 HTTP(下)

介绍服务器如何响应请求报文,并将该报文发送给浏览器端。介绍一些基础API,然后结合流程图和代码对服务器响应请求报文进行详解。
基础API部分,介绍stat、mmap、iovec、writev。
流程图部分,描述服务端响应请求报文的逻辑,各模块间的关系。
代码部分,结合代码对服务器响应请求报文进行详解。

基础API

stat

stat函数用于取得指定文件的文件属性,并将文件属性存储在结构体stat里,这里仅对其中用到的成员进行介绍。

#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

用于将一个文件或其他对象映射到内存,提高文件的访问速度

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:有效的文件描述符,一般是由opne()函数返回
  • off_t offset:被映射对象内容的起点

iovec

定义了一个向量元素,通常这个结构体用作一个多元素的数组。

struct iovec{
	void *iov_base;//指向数据的地址
	size_t iov_len; //数据的长度
};

writev

writev函数用于在一次函数调用中写多个非连续缓冲区,聚集写。

#include <sys/uio.h>
ssize_t writev(int fileds,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,进入相应的逻辑和模块。
其中,服务器子线程完成报文的解析与响应;主线程检测读写事件,调用read_once和http_conn::write完成数据的读取与发送。
在这里插入图片描述

HTTP_CODE含义

表示HTTP请求的处理结果,在头文件中初始化了8种情形,在报文解析与响应中只用到了7种。

NO_REQUEST

  • 请求不完整,需要继续读取请求报文数据
  • 跳转主线程继续监测读事件

GET_REQUEST

  • 获得了完整的HTTP请求
  • 调用do_request完成请求资源映射

NO_RESOURCE

  • 请求资源不存在
  • 跳转process_write完成响应报文

BAD_REQUEST

  • HTTP请求报文有语法错误或请求资源为目录
  • 跳转process_write完成响应报文

FILE_REQUEST

  • 请求资源可以正常访问
  • 跳转process_write完成响应报文

INTERNAL_ERROR

  • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

代码分析

do_request

process_read函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的BAD_REQUEST,一部分是do_request的返回结果。该函数将网站根目录和url文件拼接,然后通过stat判断该文件属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。
为了更好的理解请求资源的访问流程,这里对各种页面跳转机制进行简要介绍。浏览器网址栏中的字符,即url,可以将其抽象成ip:port/xxx/,xxx通过html文件的action属性进行设置。
m_url为请求报文解析出的请求资源,以/开头,也就是/xxx,项目中解析后的m_url有8种情况。

/

  • GET请求,跳转到judge.html,即欢迎访问页面

/0

  • POST请求,跳转到register.html,即注册页面

/1

  • POST请求,跳转到log.thml,即登录页面

/2CGISQL.cgi

  • POST请求,进行登录校验
  • 验证成功跳转到welcome.html,即资源请求成功页面
  • 验证失败跳转到logError.html,即登录失败页面

/3CGISQL.cgi

  • POST请求,进行注册校验
  • 注册成功跳转到log.html,即登录页面
  • 注册失败跳转到registerError.html,即注册失败页面

/5

  • POST请求,跳转到picture.thml,即图片请求页面

/6

  • POST请求,跳转到video.html,即视频请求页面

/7

  • POST请求,跳转到fans.html,即关注页面

具体的登录和注册校验功能会在第12节进行详解。

//网站根目录,文件夹内存放请求的资源和跳转的html文件
const char* doc_root="/home/qgy/github/ini_tinywebserver/root";

http_conn::HTTP_CODE http::do_request()
{
	//将初始化的m_real_file赋值为网站根目录
	strcpy(m_real_file,doc_root);
	int len=strlen(doc_root);

	//找到m_url中/的位置
	const char *p=strchr(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,表示跳转登录页面
	if(*(p+1)=='1'){
		char *m_url_real=(char *)malloc(sizeof(char)*200);
		strcpy(m_url_real,"/log.html");

		//将网站目录和/register.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,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;	
}

process_write

根据do_request的返回状态,服务器子线程调用process_write向m_write_buf中写如响应报文。

  • add_status_line函数,添加状态行:http/1.1 状态码 状态消息
  • add_headers函数添加消息报头,内部调用add_content_length和add_linger函数;content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据;connection记录连接状态,用于告诉浏览器端保持长连接。
  • add_blank_line添加空行

上述涉及的5个函数,均是内部调用add_response函数更新m_write_idx指针和缓冲区m_write_buf中的内容。

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

响应报文分为两种,一种是请求文件的存在,通过io向量机制iovec,声明两个iovec,第一个指向m_write_buf,第二个指向mmap的地址m_file_address;一种是请求出错,这时候只申请一个iovec,指向m_write_buf.

  • iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是write将要发送的数据
  • 成员iov_len表示实际写入的长度
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_header(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].iob_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单次发送不成功,判断是否是写缓冲区满了
    若不是因为缓冲区满了而失败,取消mmap映射,关闭连接
    若eagain则满了,更新iovec结构体的指针和长度,并注册写事件,等待下一次写事件触发(当写缓冲区从不可写变为可写,触发epollout),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性。
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-=send;

		//判断条件,数据已全部发送完
		if(bytes_to_send<=0)
		{
			unmap();

			//在epoll树上重置EPOLLIN事件
			modfd(m_epollfd,m_sockfd,EPOLLIN);

			//浏览器的请求为长连接
			if(m_linger)
			{
				//重新初始化HTTP对象
				init();
				return true;
			}
			else
			{
				return false;
			}
		}
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/60172.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

写材料使用恰当的词汇和专业术语,不要使用生僻或不恰当的词汇

注意使用恰当的词汇和专业术语是公文写作中的关键&#xff0c;不要使用过于生僻或不恰当的词汇。 首先&#xff0c;在选择词汇和专业术语时&#xff0c;需要了解公文所涉及的领域和专业知识。对于不同领域和专业的公文&#xff0c;需要选择恰当的词汇和术语&#xff0c;以确保公…

Akuity Certified ArgoCD课程学习与认证

今天是「DevOps云学堂」与你共同进步的第 48天 第⑦期DevOps实战训练营 7月15日已开营 实践环境升级基于K8s和ArgoCD 本文主要分享&#xff0c;如何免费地参与由Akuity Academy提供的ArgoCD GitOps 培训课程并取得认证证书。 目前Akuity Academy只发布了Introduction to Contin…

王道计网 第四章笔记

4.1 生活在网络层的“工人”是路由器,他负责各种异构网络的连接,但是因为他只生活在前三层所以从网络层之上的东西他不能管理,所以网路层之上的数据对于路由器来说必须是相同的、透明的。 常见的网络层协议有IP 和 ICMPTCP IP传输层协议FTP应用层协议一句话区分IP和MAC地址…

GO语言的垃圾回收机制

内存垃圾的产生 程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。对于C等早期编程语言栈上的内存回由编译器负责管理回收&#xff0c;而堆上的内存空间需要编程人员负责申请和释放。在Go中栈上内存仍由编译器负责管理回收&#xff0c;而堆上的内存由编译器…

snap xxx has “install-snap“ change in progress

error description * 系重复安装&#xff0c;进程冲突 solution 展示snap的改变 然后sudo snap abort 22即可终止该进程 之后重新运行install command&#xff5e;&#xff5e; PS: ubuntu有时候加载不出来&#xff0c;执行resolvectl flush-caches&#xff0c;清除dns缓存…

ChatGPT即将取代程序员

W...Y的主页 相信ChatGPT大家已经都不陌生&#xff0c;我们经常会在工作和学习中应用。但是ChatGPT的发展速度飞快。功能也越来越全面。ChatGPT的文章也是层次不穷的出现&#xff0c;ChatGPT即将取代程序员的消息也铺天盖地。那ChatGPT真的会取代程序员吗&#xff1f;我们是否…

【深度学习_TensorFlow】梯度下降

写在前面 一直不太理解梯度下降算法是什么意思&#xff0c;今天我们就解开它神秘的面纱 写在中间 线性回归方程 如果要求出一条直线&#xff0c;我们只需知道直线上的两个不重合的点&#xff0c;就可以通过解方程组来求出直线 但是&#xff0c;如果我们选取的这两个点不在直…

GD32F103VE外部中断

GD32F103VE外部中断线线0~15&#xff0c;对应外部IO口的输入中断。它有7个中断向量&#xff0c;外部中断线0 ~ 4分别对应EXTI0_IRQn ~ EXTI4_IRQn中断向量&#xff1b;外部中断线 5 ~ 9 共用一个 EXTI9_5_IRQn中断向量&#xff1b;外部中断线10~15 共用一个 EXTI15_10_IRQn中断…

MySQL数据库:表的约束

表的约束&#xff0c;实质上就是用数据类型去约束字段&#xff0c;但是数据类型的约束手法很单一&#xff0c;比如&#xff0c;我们在设置身份证号这个字段&#xff0c;数据类型唯一起的约束是它属于char类型或者varchar类型&#xff0c;不能是浮点型也不能是日期时间类型&…

.net 6 efcore一个model映射到多张表(非使用IEntityTypeConfiguration)

现在有两张表&#xff0c;结构一模一样&#xff0c;我又不想创建两个一模一样的model&#xff0c;就想一个model映射到两张表 废话不多说直接上代码 安装依赖包 创建model namespace oneModelMultiTable.Model {public class Test{public int id { get; set; }public string…

Linux服务器大量日志如何快速定位

Linux服务器大量日志如何快速定位 在生产环境&#xff0c;定位问题&#xff0c;经常会遇到日志文件特别多的情况&#xff0c;经常会遇到日志比较难拿的情况&#xff0c;所以有什么方法可以快速拿日志&#xff1f;除了在代码里很好的打印关键日志信息外&#xff0c;也需要掌握L…

RabbitMQ 教程 | 第10章 网络分区

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…

第20节 R语言医学分析:某保险医疗事故赔偿因素分析

文章目录 某保险医疗事故赔偿因素分析源码源文件下载某保险医疗事故赔偿因素分析 我们分析数据集“诉讼”的第一个方法是确定样本数量、变量类型、缩放/编码约定(如果有)用于验证数据清理。 接下来,数据集看起来很干净,没有缺失值,并且对于分类变量,将编码约定替换为实际…

智慧工地云平台源码,基于微服务+Java+Spring Cloud +UniApp +MySql开发

智慧工地可视化系统利用物联网、人工智能、云计算、大数据、移动互联网等新一代信息技术&#xff0c;通过工地中台、三维建模服务、视频AI分析服务等技术支撑&#xff0c;实现智慧工地高精度动态仿真&#xff0c;趋势分析、预测、模拟&#xff0c;建设智能化、标准化的智慧工地…

LeetCode151.反转字符串中的单词

151.反转字符串中的单词 目录 151.反转字符串中的单词题目描述解法一&#xff1a;调用API解法二&#xff1a;原生函数编写 题目描述 给你一个字符串s&#xff0c;请你反转字符串中单词的顺序。 单词是由非空格字符组成的字符串&#xff0c;s中使用至少一个空格将字符串中的单…

嵌入式一开始该怎么学?学习单片机

学习单片机&#xff1a; 模电数电肯定必须的&#xff0c;玩单片机大概率这两门课都学过&#xff0c;学过微机原理更好。 直接看野火的文档&#xff0c;芯片手册&#xff0c;外设手册。 学单片机不要纠结于某个型号&#xff0c;我认为stm32就OK&#xff0c;主要是原理和感觉。…

IPSEC VPN知识点总结

具体的实验&#xff1a;使用IPSEC VPN实现隧道通信 使用IPSEC VPN在有防火墙和NAT地址转换的场景下实现隧道通信 DS VPN实验 目录 1.什么是数据认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段? 2.什么是身份认证&#xff0c;有什么作用&#xff0c;有哪些实现…

SAP标准搜索帮助(Search Help)改造之标准增强点

1. 搜索帮助加载前 包含程序&#xff1a;LWDTMO01 行&#xff1a;40 标准搜索帮助输出前的控制&#xff08;影响标准Search Help CDS View Search Help&#xff08;如果在标准Search Help搜索帮助出口函数上修改控制参数&#xff0c;则不会影响 CDS View Search Help&#xf…

一百四十五、Kettle——查看Kettle在Windows本地和在Linux上生成的.kettle文件夹位置

&#xff08;一&#xff09;目的 查看kettle连数据库后自动生成的.kettle文件夹在Windows本地和在Linux中的位置&#xff0c; 这个文件很重要&#xff01;&#xff01;&#xff01; &#xff08;二&#xff09;.kettle文件夹在Windows本地的位置 C:\Users\Administrator\.k…

轻松搭建酒店小程序

酒店小程序的制作并不需要编程经验&#xff0c;只需要按照以下步骤进行操作&#xff0c;就能很快地搭建自己的小程序商城。 第一步&#xff0c;注册登录账号进入操作后台&#xff0c;找到并点击【商城】中的【去管理】进入商城的后台管理页面&#xff0c;然后再点击【小程序商城…