20.添加HTTP模块

添加一个简单的静态HTTP。

这里默认读者是熟悉http协议的。

来看看http请求Request的例子

客户端发送一个HTTP请求到服务器的请求消息,其包括:请求行、请求头部、空行、请求数据。

HTTP之响应消息Response 

服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息,其包括:状态行、消息报头、空行和响应正文。

 前面所说的就是http的请求和响应答复。那我们可以封装出两个类。

HttpRequest:http请求类封装

HttpResponse:http响应类封装

注意:这里会使用到我们之前写的Buffer类。因为服务器是把读到的数据存储在Buffer中的,所以大家要熟悉Buffer类的一些用法

1、HttpRequest 类

该类的主要作用是客户端发送请求,服务端收到的数据存放于Buffer中,之后解析成HttpRequest请求对象,调用成员函数设置请求头、请求体等。

首先会有请求方式method_,http版本version_,请求头headers_(用map管理)。请求的路径path_(即是url),还有请求体query_

请求体有可能是在url中的"?"后面,也可能是在请求头后面的。

class HttpRequest
{
public:enum class Method{kInvalid, kGet, kPost, kHead, kPut, kDelete};enum class Version{kUnknown, kHttp10, kHttp11};HttpRequest():method_(Method::kInvalid),version_(Version::kUnknown){}void setVersion(Version v) { version_ = v; }Version getVersion()const { return version_; }bool setMethod(const char* start, const char* end){string m(start, end);if (m == "GET") {method_ = Method::kGet;}else if (m == "POST") {method_ = Method::kPost;}//省略"HEAD","DELETE"等等方式......return method_ != Method::kInvalid;}Method getMothod()const { return method_; }const char* methodString()const {const char* result = "UNKNOWN";switch (method_) {case Method::kGet:result = "GET";break;case Method::kPost:result = "POST";break;//省略"HEAD","DELETE"等等方式......}return result;}void setPath(const char* start, const char* end) {path_.assign(start, end);}const string& path()const { return path_; }void setQuery(const char* start, const char* end) {query_.assign(start, end);}const string& query()const { return query_; }void addHeader(const char* start, const char* colon, const char* end){//isspace(int c)函数判断字符c是否为空白符//说明:当c为空白符时,返回非零值,否则返回零。(空白符指空格、水平制表、垂直制表、换页、回车和换行符。// 要求冒号前无空格string field(start, colon);++colon;while (colon < end && isspace(*colon))// 过滤冒号后的空格++colon;string value(colon, end);while (!value.empty() && isspace(value[value.size() - 1]))//过滤value中的空格value.resize(value.size() - 1);headers_[field] = value;}string getHeader(const string& field)const{string result;auto it = headers_.find(field);if (it != headers_.end()) {return it->second;}return result;}const std::unordered_map<string, string>& headers()const { return headers_; }private:Method method_;Version version_;string path_;	//请求路径string query_;	//请求体std::unordered_map<string, string> headers_;
};

注意:添加请求头时,函数addHeader需要删除键值对的字符串左侧和右侧的空字符,保证解析正常。因为解析请求头时,对一行字符串用冒号“:”进行分割解析。

2、HttpResponse 类

服务器端得到的客户的请求信息后,再创建一个HttpResponse响应对象,也是会调用成员函数设置响应头部、响应体,并格式化到Buffer中,回复给客户端。

按照上面的响应例子,那应该有响应头headers_,响应的状态码statusCode_,状态码的文字描述statusMessage_,响应体body_等等。

成员函数就是一些设置状态码,设置响应头等操作。

class HttpResponse
{
public:enum class HttpStatusCode{kUnknown,k200Ok = 200,k301MovedPermanently = 301,k400BadRequest = 400,k404NotFound = 404,};explicit HttpResponse(bool close):statusCode_(HttpStatusCode::kUnknown),closeConnection_(close){}void setStatusCode(HttpStatusCode code) { statusCode_ = code; }void setstatusMessage(const string& message) { statusMessage_ = message; }void setCloseConnection(bool on) { closeConnection_ = on; }bool closeConnection()const { return closeConnection_; }void setContentType(const string& contentType) { addHeader("Content-Type", contentType); }void addHeader(const string& key, const string& value) {headers_[key] = value;}void setBody(const string& body) { body_ = body; }void appendToBuffer(Buffer* output)const;private:std::unordered_map<string, string> headers_;HttpStatusCode  statusCode_;    //状态码string statusMessage_;    //响应行中的状态码文字描述bool closeConnection_;    //是否关闭连接string body_;        //响应体
};

这里特别值得一说的是如何把响应消息格式化的操作格式化appendToBuffer(Buffer* output)

该函数默认使用HTTP1.1版本,按照HTTP协议对HttpResponse对象进行格式化输出到Buffer中。

按照要求添加响应行,响应头,空行,响应体。

void HttpResponse::appendToBuffer(Buffer* output) const
{//响应行string buf = "HTTP/1.1 " + std::to_string(static_cast<int>(statusCode_));output->append(buf);output->append(statusMessage_);output->append("\r\n");//响应头部if (closeConnection_) {output->append("Connection: close\r\n");}else {output->append("Connection: Keep-Alive\r\n");buf = "Content-Length:" + std::to_string(body_.size()) + "\r\n";output->append(buf);}for (const auto& header : headers_) {buf = header.first + ": " + header.second + "\r\n";output->append(buf);}output->append("\r\n");	//空行output->append(body_);	//响应体
}

3、HttpContext 类

服务端接收客户请求,存在Buffer中,那怎么从Buffer中解析得到我们想要的信息呢这时,需要一个解析类HttpContext,解析后数据封装到回复HttpRequest中。

其成员有处理的状态state_,响应request_。

class HttpContext
{
public:enum class HttpRequestPaseState{kExpectRequestLine,	//请求行kExpectHeaders,    // 请求头kExpectBody,        // 请求体kGotAll,            //表示都处理完全};HttpContext():state_(HttpRequestPaseState::kExpectRequestLine)//默认从请求行开始解析{}bool parseRequest(Buffer* buf);// 解析请求Bufferbool gotAll()const { return state_ == HttpRequestPaseState::kGotAll; }void reset()// 为了复用HttpContext{state_ = HttpRequestPaseState::kExpectRequestLine;HttpRequest dumy;request_.swap(dumy);}const HttpRequest& request() const{ return request_; }HttpRequest& request(){ return request_; }private:bool processRequestLine(const char* begin, const char* end);HttpRequestPaseState state_;	//需要处理的状态,状态机HttpRequest request_;
};

一个正常的请求,一般至少是有请求行的,默认解析状态为kExpectRequestLine。

这里就主要关注是如何解析Buffer的。

3.1、请求解析 parseRequest(Buffer* buf)

这里为了方便找到buf中的"\r\n",添加了Buffer::findCRLF()函数。

const char Buffer::kCRLF[] = "\r\n";//为了方便解析http "\r\n"位置
const char* findCRLF()const {const char* crlf = std::search(peek(), beginWirte(), kCRLF, kCRLF + 2);return crlf == beginWirte() ? nullptr : crlf;
}

传入需要解析的Buffer对象,根据期望解析的部分(即是状态state_)进行处理。

处理就三种情况:请求行,请求头,请求体。具体的流程可以看代码

bool HttpContext::parseRequest(Buffer* buf)
{bool ok = true;bool hasMore = true;while (hasMore) {if (state_ == HttpRequestPaseState::kExpectRequestLine) {	//解析请求行//查找出buf中第一次出现"\r\n"位置const char* crlf = buf->findCRLF();if (crlf) {//若是找到"\r\n",说明至少有一行数据,可以进行解析//buf->peek()为数据开始部分ok = processRequestLine(buf->peek(), crlf);if (ok) {//解析成功buf->retrieveUntil(crlf + 2);//buf->peek()向后移动2字节,到下一行state_ = HttpRequestPaseState::kExpectHeaders;}else {hasMore = false;}}else {hasMore = false;}}else if (state_ == HttpRequestPaseState::kExpectHeaders) {const char* crlf = buf->findCRLF();	//找到"\r\n"位置if (crlf) {const char* colon = std::find(buf->peek(), crlf, ':');//定位分隔符if (colon != crlf) {request_.addHeader(buf->peek(), colon, crlf);	//添加键值对}else {/*state_ = HttpRequestPaseState::kGotAll;hasMore = false;*/state_ = HttpRequestPaseState::kExpectBody;//这样就可以解析body}buf->retrieveUntil(crlf + 2);	//后移动2字节}else {hasMore = false;}}else if (state_ == HttpRequestPaseState::kExpectBody) {//解析请求体if (buf->readableBytes()) {//表明还有数据,那就是请求体request_.setQuery(buf->peek(), buf->beginWirte());}state_ = HttpRequestPaseState::kGotAll;hasMore = false;}}return ok;
}

3.1、请求行的解析 processRequestLine()

请求行有固定格式Method URL Version \r\n,URL中可能带有请求参数。根据空格符进行分割成三段字符。URL可能带有请求参数,使用"?”分割解析。

bool HttpContext::processRequestLine(const char* begin, const char* end)
{bool succeed = true;const char* start = begin;const char* space = std::find(start, end, ' ');//第一个空格前的字符串是请求方法 例如:postif (space != end && request_.setMethod(start, space)) {start = space + 1;space = std::find(start, end, ' ');//寻找第二个空格 urlif (space != end) {const char* question = std::find(start, space, '?');if (question != space) {// 有"?",分割成path和请求参数request_.setPath(start, question);request_.setQuery(question, space);}else {request_.setPath(start, space);//只有path}//最后一部分,解析http协议版本string version(space + 1, end);if (version == "HTTP/1.0")request_.setVersion(HttpRequest::Version::kHttp10);else if (version == "HTTP/1.1")request_.setVersion(HttpRequest::Version::kHttp11);elsesucceed = false;}}return succeed;
}

这样解析就完成了。

4、HttpServer类

为了可以方便使用,封装个HttpServer类。

该类内部会有Server类型成员,并提供了一个回调函数的接口,当服务器收到http请求时,调用客户端的处理函数进行处理。

HttpServer支持多线程,也可以使用单线程。

class HttpServer
{
public:using HttpCallback = std::function<void(const HttpRequest&, HttpResponse*)>;HttpServer(EventLoop* loop, const InetAddr& listenAddr);void setHttpCallback(const HttpCallback& cb) { httpCallback_ = cb; }void start(int numThreads);private:void onConnetion(const ConnectionPtr& conn);	//连接到来的回调函数void onMessage(const ConnectionPtr& conn, Buffer* buf);	//消息到来的回调函数void onRequest(const ConnectionPtr& conn, const HttpRequest&);Server server_;HttpCallback httpCallback_;};

函数setHttpCallback就是设置用户的业务处理回调函数的。

4.1HttpServer构造函数

//默认的回调函数
void defaultHttpCallback(const HttpRequest& req, HttpResponse* resp)
{resp->setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);resp->setstatusMessage("Not Found");resp->setCloseConnection(true);
}
//构造函数
HttpServer::HttpServer(EventLoop* loop, const InetAddr& listenAddr):server_(listenAddr,loop), httpCallback_(defaultHttpCallback)
{//新连接到来回调该函数server_.setConnectionCallback([this](const ConnectionPtr& conn) {onConnetion(conn); });//消息到来回调该函数	server_.setMessageCallback([this](const ConnectionPtr& conn, Buffer* buf) {onMessage(conn, buf); });
}

这里就是初始化Server,并将HttpServer的回调函数传给Server。主要有两个函数。

前面的HttpResponse类和HttpRequest类已经在HttpServer使用了,但是解析类HttpContext还没有使用。

很容易想到是在回调函数中使用。在有消息到来的时候,就会进行解析数据,这时就会使用到HttpContext。可以在每次调用函数onMessage中创建HttpContext对象。这在短连接中使用是合适的。但是在长连接的情况下,这样可能效率不高

那么就可以在有新连接到来的时刻,就设置好HttpContext。

那就说到onConnetion函数

4.2 连接到来的回调函数onConnetion

//这里绑定一个HttpContext主要是为了长连接中仅分配一次对象,提高效率。
void HttpServer::onConnetion(const ConnectionPtr& conn)
{if (conn->connected()) {//conn->setContext(std::make_shared<HttpContext>()); //c++11的std::shared_ptr<void>conn->setContext(HttpContext());    //c++17的std::any}
}

该函数为一个新的Connection绑定一个HttpContext对象,绑定之后,HttpContext就相当于Connection的成员,TcpConection在MessageCallback中就可以随意的使用该HttpContext对象了。
这里绑定一个HttpContext主要是为了长连接中仅分配一次对象,提高效率

这里绑定使用的是c++17的std::any。std::any表示可以接受任意类型的变量。

来看看Conntection类中需要添加的变量

#include<any>
class Connection:public std::enable_shared_from_this<Connection>
{
public://省略之前的变量和函数//void setContext(std::shared_ptr<void> context) { context_ = context; }//std::shared_ptr<void> getConntext()const { return context_; }void setContext(const std::any& context) { context_ = context; }std::any* getMutableContext() { return &context_; }
private://std::shared_ptr<void> context_;	//c++11做法std::any context_;	//用来解析http或者websocket或者其他协议的
};

首先我们要明确为什么要的是接收任意类型的变量这总做法,为什么不是直接就是用HttpContext类替代std::any。

因为我们后续可能还需要解析其他协议的,例如websockte协议(下一节会讲解)。要是直接写HttpContext的话,那要解析websocket协议的时候,Connection类中还需要添加websocketContext类成员变量,这就很麻烦的。所以用std::any来就可以绑定所有的解析类。

那又有疑惑,为什么不直接用void*?简单点说是,它类型不安全,还需要用户手动去delete。

std::shared_ptr和void*一样不能解决类型安全的问题。详细的了解可以查看该文章https://www.cnblogs.com/gnivor/p/12793239.html

那说完std::any和回调函数onConnetion,那就到函数onMessage。

4.3 新消息到来的回调函数onMessage

void HttpServer::onMessage(const ConnectionPtr& conn, Buffer* buf)
{//HttpContext* context = reinterpret_cast<HttpContext*>(conn->getConntext().get());	//c++11做法auto context = std::any_cast<HttpContext>(conn->getMutableContext());	//c++117if (!context) {LOG_ERROR<<"context is bad\n";return;}if (!context->parseRequest(buf)) {conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");conn->shutdown();}if (context->gotAll()) {onRequest(conn, context->request());context->reset();//一旦请求处理完毕,重置context,因为HttpContext和Connection绑定了,我们需要解绑重复使用}
}

当Connection中所拥有的连接有新消息到来时,会调用它的messageCallback_函数,其实就是调用HttpServer的onMessage()函数。而之前在函数onConnection()中把HttpContext利用std::any绑定给了Connection,那在该函数中就可以对Connection使用HttpContext类来解析数据包了。

onMessage()函数首先调用HttpContext的parserRequset()函数解析请求,判断请求是否合法,进而选择关闭连接,或者处理请求(函数onRequest)。

4.4处理请求的函数onRequest

void HttpServer::onRequest(const ConnectionPtr& conn, const HttpRequest& req)
{const std::string& connetion = req.getHeader("Connection");bool close = connetion == "close" || (req.getVersion() == HttpRequest::Version::kHttp10 && connetion != "Keep-Alive");HttpResponse response(close);//执行用户注册的回调函数httpCallback_(req, &response);Buffer buf;response.appendToBuffer(&buf);conn->send(&buf);//发送数据if (response.closeConnection()) {conn->shutdown();}
}

先判断是长连接还是短连接。接着使用close构造一个HttpResponse对象。之后很重要的是执行用户注册的回调函数,这个就是用户的业务函数。

5.HtttpServer的用法

#include"src/Server.h"
//省略一些其他头文件//用户的业务处理的函数
void onRequest(const HttpRequest& req, HttpResponse* resp)
{if (req.path() == "/") {// 根目录请求resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);resp->setstatusMessage("OK");resp->setContentType("text/html");resp->addHeader("Server", "li");resp->setBody("<html><head><title>This is title</title></head>""<body><h1>Hello</h1>Now is hello" "</body></html>");}else if (req.path() == "/hello") {resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);resp->setstatusMessage("OK");resp->setContentType("text/plain");resp->setBody("hello, world!\n");}else {resp->setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);resp->setstatusMessage("Not Found");resp->setCloseConnection(true);}
}int main(int argc, char* argv[])
{EventLoop loop;HttpServer server(&loop, InetAddr(9999));server.setHttpCallback(onRequest);        //比普通的server添加了这行server.start(0);    //副io线程数量为0,单线程运行loop.loop();return 0;
}

主要就是用户自写的一个业务处理函数,之后调用HttpServer类的函数setHttpCallback来进行注册即可。

这里例子是创建了端口是9999的HTTPServer,提供访问的是/,/hello。

在浏览器输入 http://localhost:9999或者http://localhost:9999/hello即可访问成功。(localhost可以改成是自己linux的ip)

HTTP调用的流程图

HTTP服务器基本就是结束了,这里的是简单静态web服务器,我们没有解析客户发送过来的body。需要其他功能可以在这基础上进行完善或添加,比如支持fcgi。

完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v20

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

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

相关文章

android studio 的 adb配置

首先在 Android Studio 中 打开 File -> Settings: 下载 “Google USB Driver” 这个插件 (真机调试的时候要用到), 并且记一下上面的SDK路径: 右键桌面上的 “我的电脑”, 点击 “高级系统设置”, 配置计算机的高级属性, 有两步: 添加一个新的环境变量 ANDROID_HOME, 变量…

【C++】C++面向对象编程三大特性之一——继承

❤️前言 本篇博客主要是关于C面向对象编程中的三大特性之一的继承&#xff0c;希望大家能和我一起共同学习进步&#xff01; 正文 我们刚刚学习一块全新的知识&#xff0c;首先简单关注一下它的概念和简单的使用方法。 继承的概念及定义 继承的概念 继承的概念&#xff1a;…

WordPress Page Builder KingComposer 2.9.6 Open Redirection

WordPress Page Builder KingComposer 2.9.6 Open Redirection WordPress 插件 KingComposer 版本2.9.6 以及以前版本受到开放重定向漏洞的影响。该漏洞在packetstorm网站披露于2023年7月24日&#xff0c;除了该漏洞&#xff0c;该版本的插件还存在XSS攻击的漏洞风险 图1.来自…

AI「反腐」,德国马普所结合 NLP 和 DNN 开发抗蚀合金

内容一览&#xff1a;在被不锈钢包围的世界中&#xff0c;我们可能都快忘记了腐蚀的存在。然而&#xff0c;腐蚀存在于生活中的方方面面。无论是锈迹斑斑的钢钉&#xff0c;老化漏液的电线&#xff0c;还是失去光泽的汽车&#xff0c;这一切的发生都与腐蚀有关。据统计&#xf…

通信笔记:RSRP、RSRQ、RSNNR

0 基础概念&#xff1a;RE、RS和RB RE (Resource Element)&#xff1a;资源元素是 LTE 和 5G 网络中的最小物理资源单位。一个资源元素对应于一个子载波的一个符号周期。 RS (Reference Signal)&#xff1a;参考信号是在 LTE 和 5G 网络中用于多种目的的特定类型的信号。它们可…

数学建模--三维图像绘制的Python实现

目录 1.绘制三维坐标轴的方法 2.绘制三维函数的样例1 3.绘制三维函数的样例2 4.绘制三维函数的样例3 5.绘制三维函数的样例4 6.绘制三维函数的样例5 1.绘制三维坐标轴的方法 #%% #1.绘制三维坐标轴的方法 from matplotlib import pyplot as plt from mpl_toolkits.mplot3…

【算法系列篇】分冶-快排

文章目录 前言什么是分冶1.颜色分类1.1 题目要求1.2 做题思路1.3 Java代码实现 2. 排序数组2.1 题目要求2.2 做题思路2.3 Java代码实现 3.数组中的第k个最大元素3.1 题目要求3.2 做题思路3.3 Java代码实现 4. 最小的k个数4.1 题目要求4.2 做题思路4.3 Java代码实现 总结 前言 …

Python Tkinter Multiple Windows 教程

一、说明 在这个Python Tkinter教程中&#xff0c;我们将学习如何在Python Tkinter中创建多个窗口&#xff0c;我们还将介绍与多个窗口相关的不同示例。而且&#xff0c;我们将介绍这些主题。 Python Tkinter multiple windows使用多个窗口的 Python Tkinter 用户注册Python Tk…

【聚类】DBCAN聚类

OPTICS是基于DBSCAN改进的一种密度聚类算法&#xff0c;对参数不敏感。当需要用到基于密度的聚类算法时&#xff0c;可以作为DBSCAN的一种替代的优化方案&#xff0c;以实现更优的效果。 原理 基于密度的聚类算法&#xff08;1&#xff09;——DBSCAN详解_dbscan聚类_root-ca…

跨源资源共享(CORS)Access-Control-Allow-Origin

1、浏览器的同源安全策略 没错&#xff0c;就是这家伙干的&#xff0c;浏览器只允许请求当前域的资源&#xff0c;而对其他域的资源表示不信任。那怎么才算跨域呢&#xff1f; 请求协议http,https的不同域domain的不同端口port的不同 好好好&#xff0c;大概就是这么回事啦&…

【权限提升-Windows提权】-UAC提权之MSF模块和UACME项目-DLL劫持-不带引号服务路径-不安全的服务权限

权限提升基础信息 1、具体有哪些权限需要我们了解掌握的&#xff1f; 后台权限&#xff0c;网站权限&#xff0c;数据库权限&#xff0c;接口权限&#xff0c;系统权限&#xff0c;域控权限等 2、以上常见权限获取方法简要归类说明&#xff1f; 后台权限&#xff1a;SQL注入,数…

1780_添加鼠标右键空白打开命令窗功能

全部学习汇总&#xff1a; GitHub - GreyZhang/windows_skills: some skills when using windows system. 经常执行各种脚本&#xff0c;常常需要切换到命令窗口中输入相关的命令。从开始位置打开cmd然后切换目录是个很糟糕的选择&#xff0c;费时费力。其实Windows 7以及Windo…

经管博士科研基础【19】齐次线性方程组

1. 线性方程组 2. 非线性方程组 非线性方程,就是因变量与自变量之间的关系不是线性的关系,这类方程很多,例如平方关系、对数关系、指数关系、三角函数关系等等。求解此类方程往往很难得到精确解,经常需要求近似解问题。相应的求近似解的方法也逐渐得到大家的重视。 3. 线…

vue3 封装千分位分隔符自定义指令

toLocaleString作用&#xff1a;在没有指定区域的基本使用时&#xff0c;返回使用默认的语言环境和默认选项格式化的字符串。可点击进入MDN查看 // 千分位分隔符指令 import { Directive, DirectiveBinding } from vueconst thousandSeparator: Directive {mounted(el: any, …

Win10搭建VisualSvn Server

Win10搭建VisualSvn Server 目录 Win10搭建VisualSvn Server一、下载VisualSvn Server安装包二、安装VisualSvn Server三、配置和使用VisualSVN Server四、添加用户及权限设定方法五、创建目录及配置权限 1、服务端&#xff1a;有集成了Subversion和Apache、安装使用非常简单且…

Redis从基础到进阶篇(三)----架构原理与集群演变

目录 一、缓存淘汰策略 1.1 LRU原理 1.2 案例分析 1.3 Redis缓存淘汰策略 1.3.1 设置最⼤缓存 1.3.2 淘汰策略 二、Redis事务 2.1 Redis事务典型应⽤—Redis乐观锁 2.2 Redis事务介绍 2.3 事务命令 2.3.1 MULTI 2.3.2 EXEC 2.3.3 DISCARD 2.3.4 WATCH 2.3.5 UNW…

【GitLab私有仓库】在Linux上用Gitlab搭建自己的私有库并配置cpolar内网穿透

文章目录 前言1. 下载Gitlab2. 安装Gitlab3. 启动Gitlab4. 安装cpolar5. 创建隧道配置访问地址6. 固定GitLab访问地址6.1 保留二级子域名6.2 配置二级子域名 7. 测试访问二级子域名 前言 GitLab 是一个用于仓库管理系统的开源项目&#xff0c;使用Git作为代码管理工具&#xf…

视频汇聚/视频云存储/视频监控管理平台EasyCVR部署后无法正常启用是什么问题?该如何解决?

安防监控/视频监控/视频汇聚平台EasyCVR能在复杂的网络环境中&#xff0c;将分散的各类视频资源进行统一汇聚、整合、集中管理&#xff0c;在视频监控播放上&#xff0c;视频云存储/安防监控汇聚平台EasyCVR支持多种播放协议&#xff0c;包括&#xff1a;HLS、HTTP-FLV、WebSoc…

MavenCentral库发布记录

最近发布了 Android 路由库 URouter&#xff0c;支持 AGP8、ActivityResult启动等特性。 把提交到 Maven Central 过程记录一下。 一、注册 Sonatype 账号&#xff0c;新建项目 注册 https://​​issues.sonatype.org 登录后&#xff0c;新建项目&#xff1a; 相关选项&…

Stable Diffusion 多视图实践

此教程是基于秋叶的webui启动器 1.Stable Diffsuion 使用多视图需要准备一个多角度open pose 图 我给大家提供一个可使用的。 2.需要添加图片到到controlnet当中,不要选择预处理器,选择模型为openpose的模型,然后需要点选同步图片尺寸。 3.然后填写关键字可以参照一下这个…