HttpServer模块 --- 封装TcpServer支持Http协议

目录

模块设计思想

模块代码实现


模块设计思想

本模块就是设计一个HttpServer模块,提供便携的搭建http协议的服务器的方法。

那么这个模块需要如何设计呢? 这还需要从Http请求说起。

首先http请求是分为静态资源请求和功能性请求的。

静态资源请求顾名思义就是用来获取服务器中的某些路径下的实体资源,比如文件的内容等,这一类请求中,url 中的资源路径必须是服务器中的一个有效的存在的文件路径。

而如果提取出来的资源路径并不是一个实体文件的路径,那么他大概率是一个功能性请求,这时候就有用户来决定如何处理这个请求了,也就是我们前面说过的 请求路径 和 处理方法的路由表。

但是还有一种特殊的情况就是资源路径是一个目录,比如 / ,这时候有可能是一个访问网站首页的请求,所以我们需要判断在这个路径后面加上 index.html (也可以是其他的文件名,取决于你的网站的首页的文件名) ,如果加上之后,路径有效且存在实体文件,那么就是一个静态资源请求,如果还是无效,那么就是一个功能性请求。

而功能性请求如何处理呢?这是由使用或者说搭建服务器的人来决定的。 用户未来想要提供某些功能,可以让他和某个虚拟的目录或者说特定的路径绑定起来。 比如提供一个登录功能,那么用户可以规定  /login 这个路径就代表登录的功能,未来如果收到了一个请求资源路径是 /login ,那么就不是请求实体资源,而是调用网站搭建者提供的登录的方法进行验证等操作。 一般来说这些虚拟路径不会和实体资源路径冲突。

同时,对于这种功能性请求对应的路径,他并不是说一个路径只能有一个功能,不同的请求方法,同一个路径,最终执行的方法也可以是不同的,这具体还是要看使用者的设定。

所以为了维护这样的功能性路径和需要执行的方法之间的映射关系,我们需要为每一种请求方法都维护一张路由表,路由表中其实就是保存了路径和所需要执行的方法之间的映射关系。

在我们这里,就只考虑常用的五种方法,get,post,delete,head,put,其他的暂时就不提供支持了。

    //五张路由表using Handler = std::function<void(const HttpRequest&,HttpResponse*)>; using HandlerTable = std::unordered_map<std::string,Handler>;HandlerTable _get_route; HandlerTable _post_route; HandlerTable _head_route; HandlerTable _put_route; HandlerTable _delete_route;

这是交给用户进行设置的,我们也会提供五个接口给用户用来添加处理方法。

但是,这样的表真的好吗? 

在实际的应用中,比如有以下的功能性请求的请求路径 , /login1213 , /login12124 , /login1213626 , /login12152 , /login1295 , /login1275 ,对于这样的一类路径,他们其实需要执行的是同一个方法,而并不需要为每一个类似的路径设置一个方法,而路径后半部分的数字其实后续可以当成参数来用。

那么综上所述,我们的路由表中作为 key 值的并不是 std::string ,而是只需要满足某一种匹配要求的路径,都可以执行某一方法,那么作为 key 值的其实是正则表达式。

    using HandlerTable = std::unordered_map<std::regex,Handler>;

但是如果我们编译一下就会发现,正则表达式是不能作为哈希的 key 值的,或者说不匹配默认的哈希函数。 

我们可以思考一下,我们用正则表达式作为 key 了,那么后面不管使用何种数据结构来存储正则表达式和操作方法的映射关系,我们都是要遍历整个路由表的,需要遍历表中的所有的正则表达式,然后拿着我们的路径来进行正则匹配,匹配上了就说明这是我们要找的方法,如果匹配不上就说明不是,不管怎么样,都是要进行遍历,那么其实我们直接用数组来存储也是一样的。

所以最终我们使用 vector 来存储用户方法。

    using HandlerTable = std::vector<std::pair<std::regex,Handler>>;

而HttpServer模块中除了五张路由表,还需要一个TcpServer对象,这是毋庸置疑的。 同时还需要保存一个网页根目录,这个根目录是要交给用户设置的,由使用者决定。

那么最终HttpServer的成员如下:


//支持Http协议的服务器
class HttpServer
{
private:TcpServer _server;std::string _base_path; //网页根目录//五张路由表using Handler = std::function<void(const HttpRequest&,HttpResponse*)>; using HandlerTable = std::vector<std::pair<std::regex,Handler>>;HandlerTable _get_route; HandlerTable _post_route; HandlerTable _head_route; HandlerTable _put_route; HandlerTable _delete_route; public:
};

后续我们都不需要写构造函数。

那么需要哪些接口呢?

然后就是提供给用户的五个设置功能方法的接口,以及设置网页根目录和服务器线程数的接口。

还需要提供给用户是否开启超时释放,以及启动服务器的接口。

提供给用户的接口就这么多,其实都很简单,难的是私有的一些接口:

首先,未来拿到一个完整请求之后,我们需要能够判断这个请求是静态资源请求还是功能性请求。如果是资源性请求我们需要怎么做? 如果是功能性请求我们有需要怎么做?

最后还需要将相应组织成一个tcp报文进行回复。

同时还需要提供未来设置给TcpServer的连接建立和新数据到来的回调方法,这两个方法是必需的,其他的三个倒是无所谓。因为在连接建立时我们必须要设置上下文,在新数据到来时必须要有逻辑来决定怎么处理。

至于具体的实现,我们一步一步慢慢来。

模块代码实现

首先实现几个简单的提供给用户的接口:当然这里的Start或者说构造还没有完全实现,因为我们还没有设置连接建立回调和新数据回调这两个回调方法。

public:void SetBasePath(const std::string& basedir){_base_path = basedir;}void Get(const std::regex& e , const Handler& cb)  //设置GET{_get_route.push_back(std::make_pair(e,cb));}void Post(const std::regex& e , const Handler& cb)  //设置POST{_post_route.push_back(std::make_pair(e,cb));}void Put(const std::regex& e , const Handler& cb)  //设置PUT{_put_route.push_back(std::make_pair(e,cb));} void Head(const std::regex& e , const Handler& cb)  //设置HEAD{_head_route.push_back(std::make_pair(e,cb));}void Delete(const std::regex& e , const Handler& cb)  //设置DELETE{_delete_route.push_back(std::make_pair(e,cb));}        void EnableInactiveRelease(int delay = 30) //启动非活跃销毁   {_server.EnableInactiveRelease(delay);}void SetThreadCount(int cnt)    //设置线程数量{_server.SetThreadCount(cnt);}void Start()                    //启动服务器{_server.Start();}

那么剩下的就是连接建立回调以及新数据回调的逻辑了,

首先连接建立的时候,我们需要设置一个上下文给Connection对象。

    void OnConnect(const PtrConnection& conn){//设置一个上下文HttpContext ctx;conn->SetContext(ctx);}

剩下的就是最复杂的新数据回调了。

    void OnMessage(const PtrConnection& conn,Buffer* buf)   //获取新数据回调{}

首先第一步需要将上下文获取出来。

        // 1 获取上下文Any* context = conn->GetContext();HttpContext* pctx = context->GetData<HttpContext>();

然后就需要通过上下文对缓冲区数据进行解析,也就是调用HttpContext的接口进行处理,但是我们要看处理结果是什么来判断下一步怎么做。

        // 2 解析缓冲区数据pctx->RecvHttpRequest(buf);HttpRequest& req = pctx->GetRequest();HttpResponse resp;//判断解析是否出错if(pctx->RespStatu() >= 400)  //请求解析出错,此时的_recv_statu 也一定是RECV_ERR{HandlerError(req,resp);         //调用错误处理方法WriteResponse(conn,req,resp);   //返回响应conn->ShutDown();               //发生错误就关闭连接return;}if(pctx->RecvStatu() != RECV_OVER)      //还没收到一个完整请求return;//走到这里说明req是一个完整的请求
    void HandlerError(HttpRequest& req , HttpResponse& resp);void WriteResponse(const PtrConnection& conn , HttpRequest& req , HttpResponse& resp);

这里用到的两个接口我们一会再来实现。

接受到一个请求之后,其实我们就需要进行方法的路由了,那么我们直接再封装成一个接口。

        // 3 数据处理,路由Route(req,resp);    //进行方法路由,判断是不是静态资源请求。
    void Route(HttpRequest& req , HttpResponse& resp);

那么路由的过程中会填充好我们的响应的关键信息。 

处理完之后,我们就需要将响应发回给客户端。

        // 4 返回给客户端WriteResponse(conn,req,resp);

最后我们需要判断需不需要关闭连接,因为Http协议是请求应答式的服务,一般来说,只处理一个请求之后就会关闭连接。但是我们不要忘了长连接这个技术,也就是说,如果对方支持长连接,那么我们就不需要关闭连接,而是重置上下文之后进行下一个请求的处理。

        // 5 处理完之后重置上下文pctx->Reset();// 6 判断长短连接if(resp.Close())    //如果是短连接就直接关闭{conn->ShutDown();return;}//如果是长连接就需要搞成循环,读取下一个报文

如果是长连接的话,那么我们上面的处理的流程就应该是循环式的。

    void OnMessage(const PtrConnection& conn,Buffer* buf)   //获取新数据回调{while(buf->ReadSize() > 0)      //从逻辑上来说 while(1) 也是一样的{// 1 获取上下文Any* context = conn->GetContext();HttpContext* pctx = context->GetData<HttpContext>();// 2 解析缓冲区数据pctx->RecvHttpRequest(buf);HttpRequest& req = pctx->GetRequest();HttpResponse resp;//判断解析是否出错if(pctx->RespStatu() >= 400)  //请求解析出错,此时的_recv_statu 也一定是RECV_ERR{HandlerError(req,resp);         //调用错误处理方法WriteResponse(conn,req,resp);   //返回响应conn->ShutDown();               //发生错误就关闭连接return;}if(pctx->RecvStatu() != RECV_OVER)      //还没收到一个完整请求return;//走到这里说明req是一个完整的请求// 3 数据处理,路由Route(req,resp);    //进行方法路由,判断是不是静态资源请求。// 4 返回给客户端WriteResponse(conn,req,resp);// 5 处理完之后重置上下文pctx->Reset();// 6 判断长短连接if(resp.Close())    //如果是短连接就直接关闭{conn->ShutDown();return;}//如果是长连接就需要搞成循环,读取下一个报文}}

那么接下来就是里面用到的接口的实现了。

我们先来完成Route接口,在路由的接口中,首先我们需要判断资源路径是不是静态资源,如果是,那么就需要读取文件,如果不是,那么就需要进行任务的路由或者说派发。

    void Route(HttpRequest& req , HttpResponse& resp){if(IsFileResquest(req,resp))       //判断是否是静态资源请求return FileHandler(req,resp);//否则就需要到几个方法表中进行路由if(req._method == "GET")return Dispatcher(req,resp,_get_route);if(req._method == "POST")return Dispatcher(req,resp,_post_route);if(req._method == "PUT")return Dispatcher(req,resp,_put_route); if(req._method == "HEAD")return Dispatcher(req,resp,_head_route);if(req._method == "DELETE")return Dispatcher(req,resp,_delete_route);         //如果走到了这里,说明前面的处理方法都不行,那么一定是请求出问题了resp._statu = 405;      //Method Not AllowedHandlerError(req,resp,resp->_statu);}

那么静态资源如何判断处理呢?下面是判断的方法:

    bool IsFileResquest(HttpRequest& req , HttpResponse& resp) //判断以及处理静态资源{// 1 首先需要判断有没有设置资源根目录if(_base_path.empty()) return false;    //肯定不是静态资源请求// 2 静态资源请求的方法必须是 GET 或者 HEAD ,因为其他的方法不是用来获取资源的if(!(req._method == "GET" || req._method == "HEAD")) return false;//然后静态资源请求的路径必须是一个合法的路径if(Util::IsValid(req._path) == false) return false;//最后就需要判断请求的路径的资源是否存在//但是我们需要考虑路径是目录的时候,给它加上一个 index.htmlstd::string path = req._path;if(path.back() == '/') path += "index.html";//判断文件是否存在DEBUG_LOG("path:%s",path.c_str());std::string real_path = _base_path+path; if(Util::IsRegular(real_path) == false) return false;return true;    //走到这里才算是一个静态资源请求}

静态资源方法如何处理? 其实很简单,将文件读取出来放到响应的正文就行了,不过读取完之后还需要设置一些响应的Content相关的头部字段。

    void HandlerFile(HttpRequest& req , HttpResponse& resp) //处理静态资源请求{std::string path = _base_path+req._path;if(path.back() == '/') path +="index.html";Util::ReadFile(path,&resp._body);//然后设置响应头部字段//在这里我们可以只设置 Content-Type 字段,Content-Length可以交给WriteResponse接口来设置std::string mime = Util::GetMime(path);resp.AddHeader("Content-Type",mime);}

然后就是功能性请求的路由,其实就是遍历方法表进行匹配就行了。

    void Dispatcher(HttpRequest& req , HttpResponse& resp , const HandlerTable& table){for(std::pair<const std::regex& , Handler> p: table){const std::regex& e = p.first;const Handler& cb = p.second;std::smatch matches;bool ret = std::regex_match(req._path,matches,e);if(ret) return cb(req,&resp);}//走到这里说明路由表中没有对应的方法resp._statu = 404;  //Not FoundHandlerError(req,resp,resp->_statu);}

那么到此为止,路由的方法就解决了。

剩下的就是错误处理以及响应的格式化了。

错误的处理我们可以返回一个错误的展示界面

    void HandlerError(HttpRequest& req , HttpResponse& resp ,int statu){std::string body;body += "<!DOCTYPE html>";body += "<html><head><title>";body += std::to_string(statu);body += Util::StatuDesc(statu);body += "</title></head><body><h1>抱歉,该页面无法找到。</h1>";body += "<p>请检查您输入的网址是否正确,或者 <a href=\"/\">返回首页</a>。</p>";body += "</body></html>";resp._body = body;resp.AddHeader("Content-Type","text/html");}

最后就是处理一下我们的WriteResponse接口,

第一步需要完善响应的报头字段:

        if(req.Close()) resp.AddHeader("Connection","close");else  resp.AddHeader("Connection","keep-alive");if(resp._body.size()&&!resp.HasHeader("Content-Length")) resp.AddHeader("Content-Length",std::to_string(resp._body.size()));if(resp._body.size() && !resp.HasHeader("Content-Type")) resp.AddHeader("Content-Type","application/octet-stream");//重定向信息if(resp._redirect_flag) resp.AddHeader("Location",resp._redirect_url);

然后需要按指定格式组织响应,我们可以使用 osstream 这个字符流对象

        // 2 组织响应std::ostringstream out;//响应行  HTTP/1.0 404 NotFound\r\nout<<req._version<<" "<<std::to_string(resp._statu)<<" "<<Util::StatuDesc(resp._statu)<<"\r\n";//头部字段for(auto& p : resp._headers){out<<p.first<<": "<<p.second<<"\r\n";} //空行out<<"\r\n";//正文out<<resp._body;

最后就是发送出去

        // 3 发送conn->Send(out.str().c_str(),out.str().size());

那么WriteResponse的总体的代码:

    void WriteResponse(const PtrConnection& conn , HttpRequest& req , HttpResponse& resp){// 1 先把响应的头部字段完善了if(req.Close()) resp.AddHeader("Connection","close");else  resp.AddHeader("Connection","keep-alive");if(resp._body.size()&&!resp.HasHeader("Content-Length")) resp.AddHeader("Content-Length",std::to_string(resp._body.size()));if(resp._body.size() && !resp.HasHeader("Content-Type")) resp.AddHeader("Content-Type","application/octet-stream");//重定向信息if(resp._redirect_flag) resp.AddHeader("Location",resp._redirect_url);// 2 组织响应std::ostringstream out;//响应行  HTTP/1.0 404 NotFound\r\nout<<req._version<<" "<<std::to_string(resp._statu)<<" "<<Util::StatuDesc(resp._statu)<<"\r\n";//头部字段for(auto& p : resp._headers){out<<p.first<<": "<<p.second<<"\r\n";} //空行out<<"\r\n";//正文out<<resp._body;// 3 发送conn->Send(out.str().c_str(),out.str().size());}

那么最后我们再完善一下构造函数,需要传入一个端口号来对我们内部的TcpServer对象进行初始化,以及绑定两个回调函数,

    HttpServer(int port ,int delay = 30):_server(port) {_server.EnableInactiveRelease(delay);   //我们的http服务器默认是开启超时释放的_server.SetConnectCallBack(std::bind(&HttpServer::OnConnect,this,std::placeholders::_1));_server.SetMessageCallBack(std::bind(&HttpServer::OnMessage,this,std::placeholders::_1,std::placeholders::_2));}

那么我们也会注意到一个问题,就是新数据到来进行处理的时候,解析失败会调用 HandlerError和WriteResponse,而WriteResponse中会用到 req 的version ,但是我们实际上可能并没有读取到,所以我们可以给version一个初始值,可以给HttpRequest增加一个构造函数。

    HttpRequest():_version("HTTP/1.0"){}

其他的倒是没什么大问题了。

那么我们的http服务器的设计也就设计完了。

为了防止头文件重复包含,我们也需要加上条件编译。

#ifndef __HTTP__MUDUO__SERVER
#define __HTTP__MUDUO__SERVER
//  头文件内容
#endif

我们的服务器的代码编译是没有问题的,后续我们会对其进行测试,来修正项目中的一些没有注意到的bug。

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

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

相关文章

音视频入门基础:AAC专题(11)——AudioSpecificConfig简介

音视频入门基础&#xff1a;AAC专题系列文章&#xff1a; 音视频入门基础&#xff1a;AAC专题&#xff08;1&#xff09;——AAC官方文档下载 音视频入门基础&#xff1a;AAC专题&#xff08;2&#xff09;——使用FFmpeg命令生成AAC裸流文件 音视频入门基础&#xff1a;AAC…

java-web-day5

1.spring-boot-web入门 目标: 开始最基本的web应用的构建 使用浏览器访问后端, 后端给浏览器返回HelloController 流程: 1.创建springboot工程, 填写模块信息, 并勾选web开发的相关依赖 注意: 在新版idea中模块创建时java下拉框只能选17, 21, 23 这里选17, maven版本是3.6.3, 很…

基于SSM的智能台球厅系统

基于SSM的智能台球厅系统设计与实现 摘要 智能台球厅系统是一个以用户便捷体验为核心的管理系统&#xff0c;结合SSM&#xff08;Spring、Spring MVC、MyBatis&#xff09;框架来实现台球厅日常业务流程的自动化和智能化管理。系统主要包含用户预约、场地管理、设备状态监控、支…

String的长度有限,而我对你的思念却无限延伸

公主请阅 1. 为什么学习string类&#xff1f;2. string类的常用接口2.1 string类对象的常见构造2.1.1 string 2.2 operator[]2.3 迭代器2.4 auto自动推导数据类型2.5 范围for2.6 迭代器第二层2.7 size和length获取字符串的长度2.8 max_size 获取这个字符串能设置的最大长度2.9 …

spring-第十一章 注解开发

spring 文章目录 spring前言1.注解回顾1.1原理1.2springIOC注解扫描原理1.2.1解释1.2.2案例 2.声明bean的注解补充&#xff1a;Bean注解&#xff0c;管理三方包对象 3.spring注解的使用3.1加入aop依赖3.2配置文件中添加context命名空间3.3配置文件中指定要扫描的包3.4在Bean上使…

Linux 之 文件属性与目录、字符串处理、系统信息获取

学习任务&#xff1a; 1、 文件属性与目录&#xff1a;Linux 文件类型、stat、chmod、链接文件、目录文件 2、 字符串处理&#xff1a;字符串输入/输出、strlen、strcat、strcpy、memset、atoi()、atol()、atoll() 3、 系统信息&#xff1a;proc 虚拟文件系统&#xff08;重点&…

搜索引擎算法更新对网站优化的影响与应对策略

内容概要 随着互联网的不断发展&#xff0c;搜索引擎算法也在不断地进行更新和优化。了解这些算法更新的背景与意义&#xff0c;对于网站管理者和优化人员而言&#xff0c;具有重要的指导意义。不仅因为算法更新可能影响到网站的排名&#xff0c;还因为这些变化也可能为网站带…

省域经济高质量发展水平测算及数据2000-2021年

经济高质量发展水平测算&#xff0c;是通过一系列科学的方法和指标&#xff0c;对经济活动的各个方面进行评估和量化的过程。这不仅涉及到经济增长的速度&#xff0c;更涵盖了效益、效率、可持续性等多个维度。包含了2000年至2021年期间&#xff0c;全国31个省份、自治区、直辖…

MacOS/Macbook用户自定义字体安装教程

Mac本自定义字体 示例机型一、下载相关字体文件到本地二、打开启动台三、选择其他四、选择字体册五、添加字体六、选择字体七、安装字体八、安装完成 MacOS官网安装教程 示例机型 系统&#xff1a;MacOS12.6&#xff0c;芯片&#xff1a;M1Pro 一、下载相关字体文件到本地 二…

【undefined reference to xxx】zookeeper库编译和安装 / sylar项目ubuntu20系统编译

最近学习sylar项目&#xff0c;编译项目时遇到链接库不匹配的问题&#xff0c;记录下自己解决问题过程&#xff0c;虽然过程很艰难&#xff0c;但还是解决了&#xff0c;以下内容供大家参考&#xff01; undefined reference to 问题分析 项目编译报错 /usr/bin/ld: ../lib/lib…

sql进阶篇

1.更新记录 AC&#xff1a; update examination_info set tag replace(tag, "PYTHON", "Python") where tag "PYTHON";2.删除记录 AC&#xff1a; DELETE FROM exam_record WHERE timestampdiff(minute, start_time, submit_time) < 5AND…

【每日刷题】Day145

【每日刷题】Day145 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 375. 猜数字大小 II - 力扣&#xff08;LeetCode&#xff09; 2. LCR 112. 矩阵中的最长递增路径 …

【tomcat系列漏洞利用】

Tomcat 服务器是一个开源的轻量级Web应用服务器&#xff0c;在中小型系统和并发量小的场合下被普遍使用。主要组件&#xff1a;服务器Server&#xff0c;服务Service&#xff0c;连接器Connector、容器Container。连接器Connector和容器Container是Tomcat的核心。一个Container…

WPF的行为(Behavior)

WPF&#xff08;Windows Presentation Foundation&#xff09;是微软.NET框架中用于构建Windows客户端应用程序的UI框架。它提供了一种声明性的方式来定义用户界面&#xff0c;并且支持MVVM&#xff08;Model-View-ViewModel&#xff09;设计模式。 在WPF中&#xff0c;“行为…

【LeetCode】两数之和、大数相加

主页&#xff1a;HABUO&#x1f341;主页&#xff1a;HABUO 1.两数之和 题目&#xff1a;给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一…

React核心思维模型(一)

一、数据和视图分离&#xff0c;数据改变驱动视图更新 <div>Tom</div>如果我们想修改上述div盒子中的Tom为Jerry&#xff0c;应该怎样修改呢 在jquery中我们直接把界面元素抓过来修改 document.getElementsByTagName(div).item(0) Jerry 但在react中&#xf…

DataSophon集成ApacheImpala的过程

注意: 本次安装操作系统环境为Anolis8.9(Centos7和Centos8应该也一样) DataSophon版本为DDP-1.2.1 整合的安装包我放网盘了: 通过网盘分享的文件&#xff1a;impala-4.4.1.tar.gz等2个文件 链接: https://pan.baidu.com/s/18KfkO_BEFa5gVcc16I-Yew?pwdza4k 提取码: za4k 1…

使用 Kibana 将地理空间数据导入 Elasticsearch 以供 ES|QL 使用

作者&#xff1a;来自 Elastic Craig Taverner 如何使用 Kibana 和 csv 采集处理器将地理空间数据采集到 Elasticsearch 中&#xff0c;以便在 Elasticsearch 查询语言 (ES|QL) 中进行搜索。Elasticsearch 具有强大的地理空间搜索功能&#xff0c;现在 ES|QL 也具备这些功能&am…

ffmpeg视频滤镜:定向模糊-dblur

滤镜简述 dblur 官网链接 > https://ffmpeg.org/ffmpeg-filters.html#dblur 有一个模糊滤镜&#xff0c;我试了一下&#xff0c;没有感觉到它的特殊之处, 这里简单介绍一下。 滤镜使用 滤镜的参数 angle <float> ..FV.....T. set angle (from 0 t…

浏览器HTTP缓存解读(HTTP Status:200 304)

为什么要有浏览器缓存&#xff1f; 浏览器缓存(Brower Caching)是浏览器对之前请求过的文件进行缓存&#xff0c;以便下一次访问时重复使用&#xff0c;节省带宽&#xff0c;提高访问速度&#xff0c;降低服务器压力 http缓存机制主要在http响应头中设定&#xff0c;响应头中…