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.hqwc.cn/news/28517.html

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

相关文章

go语言中的string类型简介

在 Go 中&#xff0c;String 是一种不可变的类型&#xff0c;不能被修改。 在 Go 语言中&#xff0c;字符串由 Unicode 字符组成&#xff0c;每个字符都可以用一个或多个字节来表示。我们使用双引号或反引号来定义字符串&#xff0c;使用反引号定义的字符串不会对其内容进行任何…

win7,win10下删除HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\Root\报错

在调试虚拟网卡驱动时&#xff0c;由于修改错误&#xff0c;导致枚举顺序错乱&#xff0c;因此通过删除HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\Root\WINTUN下的所有项&#xff0c;即可&#xff0c;win10可用。 1、下载PStools&#xff1a; http://technet.microsoft.c…

(学习笔记-TCP基础知识)TCP与UDP区别

UDP UDP不提供复杂的控制机制&#xff0c;利用IP提供面向[无连接]的通信服务。 UDP协议非常简单&#xff0c;头部只有8个字节(位)&#xff0c;UDP的头部格式如下&#xff1a; 目标和源端口&#xff1a;主要是告诉UDP协议应该把报文发给哪个进程包长度&#xff1a;该字段保存了…

行为式验证码(成语点选)(C#版和Java版)

一、先看效果图 二、背景介绍 图形验证码网上有挺多&#xff0c;比如&#xff1a;网易易盾、腾讯防水墙、阿里云验证码等等。参考了一下&#xff0c;自己实现了一个简单的成语点选的模式。 三、实现思路 1.选择若干张图片&#xff08;这里使用的是320x160的尺寸&#xff09;…

HTPP入门教程||HTTP 状态码||HTTP content-type

HTTP 状态码 当浏览者访问一个网页时&#xff0c;浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前&#xff0c;此网页所在的服务器会返回一个包含 HTTP 状态码的信息头&#xff08;server header&#xff09;用以响应浏览器的请求。 HTTP 状态码的英文为…

Nacos服务注册和配置中心(Config,Eureka,Bus)2

Nacos数据模型 Nacos领域模型,Namespace命名空间、Group分组、集群这些都是为了进行归类管理&#xff0c;把服务和配置文件进行归类&#xff0c;归类之后就可以实现一定的效果&#xff0c;比如隔离。对于服务来说&#xff0c;不同命名空间中的服务不能够互相访问调用 N…

tql!一款Go编写的RAT主机管理工具

工具介绍 这是一款使用go编写的RAT主机群管理工具&#xff0c;已具备命令控制台、文件管理、屏幕截屏、开机启动服务、NPS代理等功能。 流量&#xff1a;支持TCP&#xff0c;UDP/KCP协议&#xff0c;通讯默认使用tls证明书进行加密 关注【Hack分享吧】公众号&#xff0c;回复…

Openlayers layer 基础及重点内容讲解

图层就像是含有文字或图形等元素的图片,一张张按顺序叠放在一起,组合起来形成页面的最终效果。 在 openlayers 中,图层是使用 layer 对象表示的,主要有 WebGLPoints Layer、热度图(HeatMap Layer)、图片图层(Image Layer)、切片图层(Tile Layer)和 矢量图层(Vector Layer…

Spring Boot环境配置Envirnoment

Srping Boot 中我们使用 EnvironmentAware 注入 Environment 对象后&#xff0c;可以在 Environment 中获得系统参数&#xff0c;命令行采参数&#xff0c;文件配置等信息。 Environment 是如何存储&#xff0c;管理这些值的呢&#xff1f;变量发生冲突怎么办呢&#xff1f;我…

Python生成exe文件运行出现黑框闪退如何查看运行bug?

cmd进行回车 第一&#xff1a;进入到可执行exe文件目录&#xff0c;如下图所示 第二&#xff1a;输入可执行文件名&#xff0c;然后就会出现报错提示

Java开发 - 深入理解Redis哨兵机制原理

前言 Redis的主从、哨兵模式、集群模式&#xff0c;在前文中都已经有了详细的搭建流程&#xff0c;可谓是手把手教程&#xff0c;也得到了很多朋友的喜欢。由于前文偏向于应用方面&#xff0c;就导致了理论知识的匮乏&#xff0c;我们可能会用了&#xff0c;但却不明所以&…

自定义实现list及其功能

#pragma once #include <iostream> #include <assert.h> using namespace std;namespace test {//******************************设置结点******************************template<class T>struct list_node{T _data;list_node<T>* _next;list_node&l…