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。
相关文章:
HttpServer模块 --- 封装TcpServer支持Http协议
目录 模块设计思想 模块代码实现 模块设计思想 本模块就是设计一个HttpServer模块,提供便携的搭建http协议的服务器的方法。 那么这个模块需要如何设计呢? 这还需要从Http请求说起。 首先http请求是分为静态资源请求和功能性请求的。 静态资源请求…...
蓝牙资讯|iOS 18.1 正式版下周推送,AirPods Pro 2耳机将带来助听器功能
苹果公司宣布将在下周发布 iOS 18.1 正式版,同时确认该更新将为 AirPods Pro 2 耳机带来新增“临床级”助听器功能。在启用功能后,用户首先需要使用 AirPods 和 iPhone 进行简短的听力测试,如果检测到听力损失,系统将创建一项“个…...
C语言之环形缓冲区概述及实现
在C语言中存在一种高效的数据结构,叫做环形缓存区,其被广泛用于处理数据流与缓存区的管理。如:数据的收发、程序层级之间的数据交换、硬件接收大量数据的场景,同时也可配合DMA实现通信协议收发数据,已确保流量控制、数…...
C++Socket通讯样例(服务端)
1. 创建Socket实例并开启。 private int OpenTcp(int port, string ip "") {//1. 开启服务端try{_tcpServer new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAddr IPAddress.Any;if (ip ! "" && i…...
【学术会议论文投稿】大数据治理:解锁数据价值,引领未来创新
第六届国际科技创新学术交流大会(IAECST 2024)_艾思科蓝_学术一站式服务平台 更多学术会议请看:https://ais.cn/u/nuyAF3 目录 引言 一、大数据治理的定义 二、大数据治理的重要性 三、大数据治理的核心组件 四、大数据治理的实践案例…...
location中href和replace的区别
1.有两种方式: a、使用 location.href:window.location.href“success.html”; b、使用location.replace:window.location.replace(“new_file.html”); 2.区别是什么? 结果:href相当于打开一个新页面,…...
基于Spring Boot的在线摄影工作室开发指南
1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及,互联网成为人们查找信息的重要场所,二十一世纪是信息的时代,所以信息的管理显得特别重要。因此,使用计算机来管理网上摄影工作室的相关信息成为必然。开发合…...
JDK源码系列(五)—— ConcurrentHashMap + CAS 原理解析
更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 ConcurrentHashMap 类 ConcurrentHashMap 1.7 在JDK1.7中ConcurrentHashMap采用了数组分段锁的方式实现。 Segment(分段锁)-减少锁的粒度 ConcurrentHashMap中的分段锁称为Segment,它即类似于…...
技术成神之路:二十三种设计模式(导航页)
设计原则/模式链接面向对象的六大设计原则技术成神之路:面向对象的六大设计原则创建型模式单例模式建造者模式原型模式工厂方法模式抽象工厂模式行为型模式策略模式状态模式责任链模式观察者模式备忘录模式迭代器模式模板方法模式访问者模式中介者模式命令模式解释器…...
Rust编程与项目实战-元组
【图书介绍】《Rust编程与项目实战》-CSDN博客 《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com) Rust编程与项目实战_夏天又到了的博客-CSDN博客 8.2.1 元组的定义 元组是Rust的内置复合数据类型。Rust支持元组,而且元…...
容性串扰和感性串扰
串扰根源在于耦合,电场耦合产生容性耦合电流,磁场耦合产生感性耦合电流 关于容性后向串扰电压与后向串扰系数推导...
windows Terminal 闪退 -- 捣蛋砖家
最近点击Windows 终端总是闪退。 日志提示: 错误应用程序名称: WindowsTerminal.exe,版本: 1.21.2410.17001,时间戳: 0x67118f02 错误模块名称: ucrtbase.dll,版本: 10.0.22621.3593,时间戳: 0x10c46e71 异常代码: 0xc0000409 错…...
java-web-day5
1.spring-boot-web入门 目标: 开始最基本的web应用的构建 使用浏览器访问后端, 后端给浏览器返回HelloController 流程: 1.创建springboot工程, 填写模块信息, 并勾选web开发的相关依赖 注意: 在新版idea中模块创建时java下拉框只能选17, 21, 23 这里选17, maven版本是3.6.3, 很…...
Python | Leetcode Python题解之第508题出现次数最多的子树元素和
题目: 题解: class Solution:def findFrequentTreeSum(self, root: TreeNode) -> List[int]:cnt Counter()def dfs(node: TreeNode) -> int:if node is None:return 0sum node.val dfs(node.left) dfs(node.right)cnt[sum] 1return sumdfs(r…...
Java 分布式缓存
在当今的大规模分布式系统中,缓存技术扮演着至关重要的角色。Java 作为一种广泛应用的编程语言,拥有丰富的工具和框架来实现分布式缓存。本文将深入探讨 Java 分布式缓存的概念、优势、常见技术以及实际应用案例,帮助读者更好地理解和应用这一…...
【MySQL】MySQL 使用全教程
MySQL 使用全教程 介绍 MySQL 是一种广泛使用的开源关系型数据库管理系统(Relational Database Management System),它基于 Structured Query Language(SQL)进行数据管理,允许用户存储、检索、更新和删除数据库中的数据。通过提供…...
油猴脚本-GPT问题导航侧边栏增强版
为 GPT官网和相关网站提供了一个便捷的侧边栏目录,能够自动搜集当前会话页面的问题,展示在侧边栏上,可快速导航到问题的位置。 安装使用地址:https://scriptcat.org/zh-CN/script-show-page/1972 安装前请确保浏览器有油猴,没有…...
Java Lock ConditionObject 总结
前言 相关系列 《Java & Lock & 目录》(持续更新)《Java & Lock & ConditionObject & 源码》(学习过程/多有漏误/仅作参考/不再更新)《Java & Lock & ConditionObject & 总结》(学习…...
模块化主动隔振系统市场规模:2023年全球市场规模大约为220.54百万美元
模块化主动隔振系统是一种用于精密设备和实验装置的隔振解决方案,通过主动控制技术消除振动干扰,提供稳定的环境。目前,随着微纳制造和精密测量技术的发展,对隔振系统的要求越来越高。模块化设计使得系统能够灵活适应不同负载和工…...
SpringAOP:对于同一个切入点,不同切面不同通知的执行顺序
目录 1. 问题描述2. 结论结论1:"对于同一个切入点,同一个切面不同类型的通知的执行顺序"结论2:"对于同一个切入点,不同切面不同类型通知的执行顺序" 3. 测试环境:SpringBoot 2.3.4.RELEASE测试集合…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
Web中间件--tomcat学习
Web中间件–tomcat Java虚拟机详解 什么是JAVA虚拟机 Java虚拟机是一个抽象的计算机,它可以执行Java字节码。Java虚拟机是Java平台的一部分,Java平台由Java语言、Java API和Java虚拟机组成。Java虚拟机的主要作用是将Java字节码转换为机器代码&#x…...
深入浅出Diffusion模型:从原理到实践的全方位教程
I. 引言:生成式AI的黎明 – Diffusion模型是什么? 近年来,生成式人工智能(Generative AI)领域取得了爆炸性的进展,模型能够根据简单的文本提示创作出逼真的图像、连贯的文本,乃至更多令人惊叹的…...
