http连接处理(下)(四)

1.结合代码分析请求报文响应

下面我们将介绍服务器如何响应请求报文,并将该报文发送给浏览器端。首先介绍一些基础API,然后结合流程图和代码对服务器响应请求报文进行详解。

基础API部分,介绍stat、mmap、iovecwritev

流程图部分,描述服务器端响应请求报文的逻辑,各模块间的关系。

代码部分,结合代码对服务器响应请求报文进行详解。

1.1 基础API

为了更好的源码阅读体验,这里提前对代码中使用的一些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);
  1. start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址
  2. length:映射区的长度
  3. prot:期望的内存保护标志,不能与文件的打开模式冲突
    • PROT_READ 表示页内容可以被读取
  4. flags:指定映射对象的类型,映射选项和映射页是否可以共享
    • MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件
  5. fd:有效的文件描述符,一般是由open()函数返回
  1. off_toffset:被映射对象内容的起点

iovec

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

struct iovec {void      *iov_base;      /* starting address of buffer */size_t    iov_len;        /* size of buffer */
};
  1. iov_base指向数据的地址
  2. iov_len表示数据的长度

writev

writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。

#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
  1. filedes表示文件描述符
  2. iov为前述io向量机制结构体iovec
  3. iovcnt为结构体的个数

若成功则返回已写的字节数,若出错则返回-1。

writev以顺序iov[0]iov[1]iov[iovcnt-1]从缓冲区中聚集输出数据。

writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。

特别注意: 循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然需要通过遍历iovec来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界iovec的io_base和io_len。

1.2 流程图

浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,进入相应的逻辑和模块。

其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_oncehttp_conn::write完成数据的读取与发送。

HTTP_CODE含义

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

  1. NO_REQUEST
    • 请求不完整,需要继续读取请求报文数据
    • 跳转主线程继续监测读事件
  2. GET_REQUEST
    • 获得了完整的HTTP请求
    • 调用do_request完成请求资源映射
  3. NO_RESOURCE
    • 请求资源不存在
    • 跳转process_write完成响应报文
  4. BAD_REQUEST
    • HTTP请求报文有语法错误或请求资源为目录
    • 跳转process_write完成响应报文
  5. FORBIDDEN_REQUEST
    • 请求资源禁止访问,没有读取权限
    • 跳转process_write完成响应报文
  6. FILE_REQUEST
    • 请求资源可以正常访问
    • 跳转process_write完成响应报文
  7. INTERNAL_ERROR
    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

1.3 代码分析

do_request

process_read函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的BAD_REQUEST,一部分是do_request的返回结果.该函数将网站根目录和url文件拼接,然后通过stat判断该文件属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。

为了更好的理解请求资源的访问流程,这里对各种各页面跳转机制进行简要介绍。其中,浏览器网址栏中的字符,即url,可以将其抽象成ip:port/xxxxxx通过html文件的action属性进行设置。

m_url为请求报文中解析出的请求资源,以/开头,也就是xxx,项目中解析后的m_url有8种情况。

  1. /
    • GET请求,跳转到judge.html,即欢迎访问页面
  2. /0
    • POST请求,跳转到register.html,即注册页面
  3. /1
    • POST请求,跳转到log.html,即登录页面
  4. /2CGISQL.cgi
    • POST请求,进行登录校验
    • 验证成功跳转到welcome.html,即资源请求成功页面
    • 验证失败跳转到logError.html,即登录失败页面
  5. /3CGISQL.cgi
    • POST请求,进行注册校验
    • 注册成功跳转到log.html,即登录页面
    • 注册失败跳转到registerError.html,即注册失败页面
  6. /5
    • POST请求,跳转到picture.html,即图片请求页面
  7. /6
    • POST请求,跳转到video.html,即视频请求页面
  8. /7
    • POST请求,跳转到fans.html,即关注页面

如果大家对上述设置方式不理解,不用担心。具体的登录和注册校验功能会在第1下面进行详解,到时候还会针对html进行介绍。

//网站根目录,文件夹内存放请求的资源和跳转的html文件
const char* doc_root="/home/wfc/obj/MyWebserver/root";http_conn::HTTP_CODE http_conn::do_request()
{//将初始化的m_real_file赋值为网站根目录strcpy(m_real_file,doc_root);int len=strlen(doc_root);//找到m_url中/的位置const char *p = strrchr(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,表示跳转登录界面else if( *(p+1) == '1'){char *m_url_real = (char *)malloc(sizeof(char) * 200);strcpy(m_url_real,"/log.html");//将网站目录和/log.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+len,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_writem_write_buf中写入响应报文。

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

  1. iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据。
  2. 成员iov_len表示实际写入的长度
bool http_conn::process_write(HTTP_CODE ret)
{switch(ret){//内部错误,500case INTERNAL_ERROR:{//状态行add_status_line(500,error_500_title);//消息报头add_headers(strlen(error_500_form));if(!add_content(error_500_form))return false;break;}//报文语法有误,404case BAD_REQUEST:{add_status_line(404,error_404_title);add_headers(strlen(error_404_form));if(!add_content(error_404_form))return false;break;}//资源没有访问权限,403case FORBIDDEN_REQUEST:{add_status_line(403,error_403_title);add_headers(strlen(error_403_form));if(!add_content(error_403_form))return false;break;}//文件存在,200case 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_idxm_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].iov_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结构体的指针和长度,并判断响应报文整体是否发送成功。

  1. 若writev单次发送成功,更新byte_to_send和byte_have_send的大小,若响应报文整体发送成功,则取消mmap映射,并判断是否是长连接.
    • 长连接重置http类实例,注册读事件,不关闭连接,
    • 短连接直接关闭连接
  2. 若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 -= temp;//判断条件,数据已全部发送完if (bytes_to_send <= 0){unmap();//在epoll树上重置EPOLLONESHOT事件modfd(m_epollfd,m_sockfd,EPOLLIN);//浏览器的请求为长连接if(m_linger){//重新初始化HTTP对象init();return true;}else{return false;}}}
}

书中原代码的write函数不严谨,这里对其中的Bug进行了修复,可以正常传输大文件。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Ubuntu 20.04LTS系统】安装CUDA11.8、cuDNN,可进行CUDA版本切换

Ubuntu 20.04LTS系统安装CUDA11.8、cuDNN&#xff0c;可进行CUDA版本切换 1. 更改为清华源并更新软件列表和依赖项2. 安装CUDA3. 安装cuDNN4. CUDA版本切换 1. 更改为清华源并更新软件列表和依赖项 https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/ # 默认注释了源码镜像以提…

二维码识别 OCR 原理及如何应用于物流和仓储管理中

摘要 在传统的物流和仓储管理中&#xff0c;人工操作容易出现错误和低效率。然而&#xff0c;随着二维码技术的普及和二维码识别OCR接口的应用&#xff0c;物流和仓储管理实现了更高水平的自动化和智能化。通过扫描和解码二维码&#xff0c;物流和仓储管理系统可以实现货物跟踪…

【JavaEE】JavaEE进阶:框架的学习 - Spring的初步认识

JavaEE进阶首章 文章目录 【JavaEE】JavaEE进阶&#xff1a;框架的学习 - Spring的初步认识1. JavaEE初阶 与 JavaEE进阶 开发上的区别1.1 Servlet VS Spring Boot1.2 Spring Boot的 “hello world”代码演示1.2.1 Spring Boot项目的创建1.2.2 hello world1.2.3 发布 2. 框架的…

分区类型ID一键变身!快速改变分区类型ID的简单方法

分区类型ID是什么&#xff1f; 想要改变分区类型ID&#xff0c;先得明白分区类型ID是什么。大多数电脑用户可能只熟悉分区和分区类型&#xff0c;实际上有5种分区类型&#xff1a;主分区、可扩展固件接口&#xff08;EFI&#xff09;、扩展分区、逻辑分区和Microsoft保留分…

idea 自定义类注释模板和方法模板,无警告

背景&#xff1a;idea&#xff1a;IntelliJ IDEA 2023.1.3 (Ultimate Edition) 效果&#xff1a;&#xff08;主要是没无参&#xff0c;不会换行&#xff09; 类&#xff1a; /** * author sss* date ${DATE} on ${TIME}* desc $NAME*/# 完全复制上面的&#xff0c;删除这一行…

ES6标准下在if中进行函数声明

ES5中规定&#xff0c;函数只能在顶层作用域或函数作用域之中声明&#xff0c;不能在块级作用域声明。 // 情况一 if (true) {function f() {} }// 情况二 try {function f() {} } catch(e) {// ... }上面两种函数声明&#xff0c;根据 ES5 的规定都是非法的。但是&#xff0c…

【C++】将类对象转换成基本数据类型

2023年7月19日&#xff0c;周三晚上&#xff1a; 今天晚上看源码的时候&#xff0c;突然在某个类里面看到了“operator bool() const;”&#xff0c;我完全想不起来这是啥意思了&#xff0c;于是今晚重新学习了一下 目录 类型转换函数的定义类型转换函数的作用 类型转换函数的…

王道计算机网络学习笔记(5)——传输层和应用层

前言 文章中的内容来自B站王道考研计算机网络课程&#xff0c;想要完整学习的可以到B站官方看完整版。 五&#xff1a;传输层 5.1&#xff1a;传输层基本概述 传输层的功能&#xff1a; 1传输层提供进程和进程之间的逻辑通信 2复用和分用 微信和QQ都使用传输层的协议进行发…

Vue3基础知识

文章目录 第一章 vue3 安装1.1安装1.2开启服务器1.3 使用图形化界面1.4 Vite1.5 vue3项目打包1.6 vue3 创建项目1.6.1 vue create命令1.6.2 创建一个项目 第二章 vue3 基础2.1 vue3 的目录结构2.2 vue3 的起步2.2.2 data 选项2,2.3 methods 2.3 Vue3 指令2.4 vue3 模板语法2.4.…

vue3和tauri直接下载Binary 数组的二进制文件内容到本地

通过发送url请求&#xff0c;直接获取到一个文件的Binary 数组内容&#xff0c;然后通过tauri的api&#xff1a;writeBinaryFile保存文件到本地电脑。 发送请求的时候&#xff0c;要加上响应类型&#xff1a;responseType: ResponseType.Binary 然后等返回的响应内容&#xf…

Chat GPT是什么,初学者怎么使用Chat GPT,需要注意些什么

目录 Chat GPT是什么 初学者怎么使用Chat GPT 使用Chat GPT需要注意什么 一些简单的prompt示例 Chat GPT是什么 Chat GPT是由OpenAI开发的一种大型语言模型&#xff0c;它基于GPT&#xff08;Generative Pre-trained Transformer&#xff09;架构。GPT是一种基于深度学习的…

应对突发流量,如何快速为自建 K8s 添加云上弹性能力

作者&#xff1a;庄宇 以 Kubernetes 为代表的容器技术带来的是一种应用交付模式的变革&#xff0c;其正迅速成为全世界数据中心的统一 API。 为了保证业务持续稳定、用户访问不中断&#xff0c;高可用、高弹性等能力是应用架构设计不变的追求&#xff0c;多集群架构天然具备…

使用 uiautomator2+pytest+allure 进行 Android 的 UI 自动化测试

目录 前言&#xff1a; 介绍 pytest uiautomator2 allure 环境搭建 pytest uiautomator2 allure pytest 插件 实例 初始化 driver fixture 机制 数据共享 测试类 参数化 指定顺序 运行指定级别 重试 hook 函数 断言 运行 运行某个文件夹下的用例 运行某…

【软件测试面试】腾讯数据平台笔试题-接口-自动化-数据库

数据库题 答案&#xff1a; Python编程题 答案&#xff1a; 接口参数化题 答案&#xff1a; 接口自动化题 答案&#xff1a; 以下是我收集到的比较好的学习教程资源&#xff0c;虽然不是什么很值钱的东西&#xff0c;如果你刚好需要&#xff0c;可以评论区&#…

高斯误差线性单元激活ReLU以外的神经网络

高斯误差线性单位&#xff08;GELU&#xff09;激活函数由加州大学伯克利分校的Dan Hendrycks和芝加哥丰田技术研究所的Kevin Gimpel于2018年引入。激活函数是触发神经元输出的“开关”&#xff0c;随着网络的深入&#xff0c;其重要性也随之增加。最近几周&#xff0c;机器学习…

create database创建数据库失败

瀚高数据库 目录 环境 症状 问题原因 解决方案 环境 系统平台&#xff1a;Linux x86-64 Red Hat Enterprise Linux 7 版本&#xff1a;4.5.7 症状 1、执行如下sql语句创建数据库报错。 create database printcdburn with encodingUTF8 OWNERprintcdburn LC_COLLATEzh_CN.UTF-…

github 最简单的使用步骤(个人学习记录~)

github 使用步骤&#xff1a; (11条消息) github新手用法详解&#xff08;建议收藏&#xff01;&#xff01;&#xff01;&#xff09;_github详解_怪 咖的博客-CSDN博客 1.获取ssh密钥 打开输入&#xff1a;ssh-keygen -t rsa -C “git账号” 输入之后一路Enter&#xff08…

谈谈VPN是什么、类型、使用场景、工作原理

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 前言 本文将讲解VPN是什么、以及它的类型、使用场景、工作原理。 目录 一、VPN是什么&#xff1f; 二、VPN的类型 1、站点对站点VPN 2、…

labview 弹窗(子vi)

如果你遇到了需要在主vi运行时需要弹窗某个窗口(或者称为子vi,子画面)&#xff0c;而且要主画面和子画面能独立运行各自的循环程序&#xff0c;本文能给你帮助。 本文的精髓在于: wait until Donefalse,表示子VI运行的同时&#xff0c;主vi也继续运行后面的代码&#xff0c;主…

win10下解决git报错 Permission denied(publickey)

今天在csdn的GitCode新建了一个项目&#xff0c;然后在windows下git clone时出现错误 gitgitcode.net: Permission denied (publickey). fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 完整…