cocos2d-x C++与Lua交互

Cocos版本: 3.10

Lua版本: 5.1.4

环境: window Visual Studio 2013


Lua


Lua作为一种脚本语言, 它的运行需要有宿主的存在,通过Lua虚拟栈进行数据交互。

它的底层实现是C语言,C语言封装了很多的API接口,使得C/C++与Lua之间可以很方便的通信交互。

Lua的官网: https://www.lua.org/

在cocos2dx中, Lua与C++的交互是通过**tolua++**进行的,**tolua++**实质上是对Lua C API的一层封装。

通过tolua++ 设定的接口,使得Lua很方便的调用C++提供的 cocos API接口。


Lua的运行需要有宿主的存在,在cocos2d-x中,C++可以作为Lua的宿主, 而Lua运行的时候需要通过虚拟栈进行数据交互,这个运行的环境,通常被称为Lua_State

Lua虚拟栈

栈的特点是先进后出的, 在Lua的虚拟栈中它有着如下的特点:

  • 栈中数据通过索引进行获取数据
  • 索引的数值可以为正数,也可为负数; 正数为1的永远表示栈底,负数为-1的永远表示栈顶

如下图所示(图片来源:Lua调用原理展示):

pic_1)

假设我们想使用C++访问Lua文件中的数据

-- test.lua
str = "Get Lua Data Sucess!!!"function Add(num1, num2)return num1 + num2
end 

C++获取Lua文件中的字符串为例,其数据交互的流程是:

  1. C/C++将参数str放入Lua栈的栈顶

  2. Lua从栈中获取参数str,并将栈顶置为空

  3. Lua从全局表中查找参数str对应的数据

  4. 全局表将参数str的数据反馈给Lua

  5. Lua将参数str的返回值放入堆栈中,此时返回值位于栈顶

  6. C++从栈中获取数据


C++调用Lua需要进行环境配置:

1. 新建项目,选择Empty Project,在项目的Source Files新增.cpp文件
2. 若有Lua的相关环境,可将Lua/5.1目录下的include,lib文件夹拷贝到与.cpp文件同目录下
若无,则推荐LuaForWindows
其网址为:http://files.luaforge.net/releases/luaforwindows/luaforwindows
它会自动配置lua的环境,并安装SciTE工具相关,以后就可以在控制台,SciTE输入lua相关代码进行调试属性配置,打开项目属性:
1. C/C++ -> General -> Additional Include Directories 将include目录添加进去
2. Linker -> General -> Additional Library Directories 将lib目录添加进去
3. 再通过Linker -> Input -> Additional Dependencies 添加lua5.1.lib, lua51.lib

示例代码:

#include <iostream>
#include <string.h>extern "C" {
// 提供了Lua的基本函数,在lua.h中的函数均已"lua_"为前缀
#include "lua.h"
// 定义lua的标准库函数,比如table, io, math等
#include "lualib.h"
// 提供了辅助库相关,以"luaL_"为前缀
#include "lauxlib.h"    
}void main(){// 创建lua环境,并加载标准库lua_State* pL = lua_open();luaL_openlibs(pL);// 加载lua文件,返回0表示成功int code = luaL_loadfile(pL, "test.lua");if (code != 0){return;}// 执行lua文件,参数分别为,lua环境,输入参数个数,返回值个数lua_call(pL, 0, 0);// 重置栈顶索引,设置为0表示栈清空lua_settop(pL, 0);// ------------- 读取变量 -------------//lua_getglobal 主要做了这么几件事: 将参数压入栈中,lua获取参数的值后再将返回的结果压入栈中lua_getglobal(pL, "str");// 判定栈顶值类型是否为string,返回1表示成功,0表示失败int isStr = lua_isstring(pL, 1);if (isStr == 1) {// 获取栈顶值,并将lua值转换为C++类型std::string str = lua_tostring(pL, 1);std::cout << "str = " << str.c_str() << std::endl;}// ------------- 读取函数 -------------lua_getglobal(pL, "Add");// 将函数所需要的参数入栈lua_pushnumber(pL, 1);            // 压入第一个参数lua_pushnumber(pL, 2);            // 压入第二个参数/*lua_pcall与lua_call类似,均用于执行lua文件,其方法分别为:void lua_call(lua_State *L, int nargs, int nresults);int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc);两者的区别在于:前者在出现错误,程序会崩溃。后者多了一个errfunc索引,用于准确定位错误处理函数。函数执行成功返回0,失败后可通过获取栈顶信息获取错误数据两者的共同之处在于:会根据nargs将参数按次序入栈,并根据nresults将返回值按次序填入栈中若返回值结果数目大于nresults时,多余的将被丢弃;若小于nresults时,则按照nil补齐。*/int result = lua_pcall(pL, 2, 1, 0);if (result != 0) {const char *pErrorMsg = lua_tostring(pL, -1);std::cout << "ERROR:" << pErrorMsg << std::endl;lua_close(pL);return;}/*此处的栈中情况:------------- 栈顶 -------------正索引 负索引   类型       返回值2     -1      number    31     -2      string    "Get Lua Data Sucess!!!"------------- 栈底 -------------因此如下的索引获取数字索引可以使用-1或者2*/int isNum = lua_isnumber(pL, -1);if (isNum == 1) {double num = lua_tonumber(pL, -1);std::cout << "num = " << num << std::endl;}// 关闭state环境,即销毁Lua_State对象,并释放Lua动态分配的空间lua_close(pL);system("pause");
}

注意:

  • C++在获取不同文件下的方法时,在通过include引用后,然后就直接调用
  • Lua通过luaL_loadfile进行加载,然后等 lua_call/lua_pcall执行后才能获取数据

Lua这么处理的原因在于: 全局变量表中是不会存储相关数据的


Lua C API


这里简要说明下常用的API相关,有助于对了解后面的**tolua++**有帮助。 主要方法有:

/*
获取栈顶索引即栈中元素的个数,因为栈底为1,所以栈顶索引为多少,就代表有多少个元素
*/
int lua_gettop(lua_State *L);/*
将栈顶索引设置为指定的数值
若设置的index比原栈顶高,则以nil补足。若index比原栈顶低,高出的部分舍弃。
比如: 栈中有8个元素,若index为7,则表示删除了一个栈顶的元素。若index为0,表示清空栈
注意: index为正数表示相对于栈底设置的,若为负数则相对于栈顶而设置的
*/
void lua_settop(lua_State *L, int index);/*
将栈中索引元素的副本压入栈顶
比如:从栈底到栈顶,元素状态为10,20,30,40;若索引为3则元素状态为:10,20,30,40,30
类似的还有:
lua_pushnil: 压入一个nil值
lua_pushboolean: 压入一个bool值
lua_pushnumber: 压入一个number值
*/
void lua_pushvalue(lua_State *L, int index);/*
删除指定索引元素,并将该索引之上的元素填补空缺
比如:从栈底到栈顶,元素状态为10,20,30,40;若索引为-3则元素状态为10,30,40
*/
void lua_remove(lua_State *L, int index);/*
将栈顶元素替换索引位置的的元素
比如:从栈底到栈顶,元素状态为10,20,30,40,50;若索引为2则,元素状态为10,50,30,40
即索引为2的元素20被栈顶元素50替换
*/
void lua_replace(lua_State *L, int index);/*
获取栈中指定索引元素的类型,若失败返回类型LUA_TNONE。其它类型有:
LUA_TBOOLEAN, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE
LUA_TFUNCTION, LUA_USERDATA等
*/
int lua_type(lua_State *L, int idx);/*
检测栈中元素是否为某个类型,成功返回1,失败返回0; 类似的还有:
lua_isnumber, lua_isstring, lua_iscfunction, lua_isuserdata
*/
int lua_isXXX(lua_State *L, int index);// 将栈中元素转换为C语言指定类型
lua_Number lua_tonumber(lua_State *L, int idx);
lua_Integer lua_tointeger(lua_State *L, int idx);
int lua_toboolean(lua_State *L, int idx);
const char* lua_tolstring(lua_State *L, int idx, size_t *len);
lua_CFunction lua_tocfunction(lua_State *L, int idx);
void* lua_touserdata(lua_State *L, int idx);

下面我们说下cocos2d-x 对Lua的封装相关。


cocos Lua框架

Lua在cocos引擎封装相关,它主要被放在cocos引擎的libluacocos2d

  • auto: 使用tolua++工具自动生成的C++代码相关

  • **manual:**放置了cocos扩展的一些功能,比如LuaEngine, LuaStack, LuaBridge等

  • luajit: 高效版的lua库,额外添加了lua没有的cocos库,并在对浮点计算,循环等进行了优化

  • luasocket: 网络库相关

  • tolua: tolua++库相关,实质是对Lua C库进行的再封装

  • xxtea: cocos2d-x 自带的加密相关,目前项目使用的较少,很容易被破解

Lua的在引擎中的封装,主要是:

  • LuaEngine 封装的对Lua的管理引擎类
  • Lua_Stack 对Lua运行环境Lua_State的封装,LuaEngine主要管理的就是Lua_Stack

项目启动

关于LuaEngine的初始化,主要在项目启动的时候初始化的,它仅仅是项目启动的一个小的环节。

如果想了解引擎的整个启动流程,可参考博客:cocos2dx 的启动和结束流程

而对于LuaEngine的启动,可以简单的分为三个步骤:

  1. 初始化LuaEngine,通过 LuaStack 获取 LuaState 运行环境
  2. 通过**tolua++**提供的接口,将C++ 不同的模块进行注册
  3. 执行Lua脚本

其主要代码实现在:

bool AppDelegate::applicationDidFinishLaunching() {// 初始化LuaEngine// 在getInstance中会初始化LuaStack,LuaStack初始化Lua环境相关auto engine = LuaEngine::getInstance();// 将LuaEngine添加到脚本引擎管理器ScriptEngineManager中ScriptEngineManager::getInstance()->setScriptEngine(engine);// 获取Lua环境lua_State* L = engine->getLuaStack()->getLuaState();// 注册额外的=C++提供的API模块相关lua_module_register(L);register_all_packages();// 设置cocos自带的加密相关LuaStack* stack = engine->getLuaStack();stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));// 执行Lua脚本文件if (engine->executeScriptFile("main.lua")) {return false;}return true;
}

LuaEngine初始化

通过LuaEngine::getInstance(),我们了解下**LuaStack::init()**的相关实现

extern "C" {
#include "lua.h"             
#include "tolua++.h"    
#include "lualib.h"         
#include "lauxlib.h"
}bool LuaStack::init(void)
{// 初始化Lua环境并打开标准库_state = lua_open();     luaL_openlibs(_state);toluafix_open(_state);// 注册全局函数print到lua中,它会覆盖lua库中的print方法const luaL_reg global_functions [] = {{"print", lua_print},{"release_print",lua_release_print},{nullptr, nullptr}};// 注册全局变量luaL_register(_state, "_G", global_functions);// 注册cocos2d-x引擎的API到lua环境中g_luaType.clear();register_all_cocos2dx(_state);// ...#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)// 导入android下调用java相关APILuaJavaBridge::luaopen_luaj(_state);
#endifaddLuaLoader(cocos2dx_lua_loader);return true;
} 

针对于addLuaLoader,它是Lua的加载器,主要将 cocos2dx_lua_loader 方法添加到Lua全局变量package下的loaders成员中。

当Lua通过 requires加载脚本时,Lua会借助package下的loaders中的加载器 cocos2dx_lua_loader 加载。

该加载器支持我们自定义设置搜索路径相关,且拓展实现对脚本的加密解密相关。看下它的实现:

extern "C" {int cocos2dx_lua_loader(lua_State *L) {// 后缀为luac和luastatic const std::string BYTECODE_FILE_EXT    = ".luac";static const std::string NOT_BYTECODE_FILE_EXT = ".lua";// require传入的要加载的文件名,比如:require "cocos.init" 下的"cocos.init"std::string filename(luaL_checkstring(L, 1));// 去掉后缀名".luac"或“.lua”size_t pos = filename.rfind(BYTECODE_FILE_EXT);if (pos != std::string::npos) {filename = filename.substr(0, pos);} else { pos = filename.rfind(NOT_BYTECODE_FILE_EXT);if (pos == filename.length() - NOT_BYTECODE_FILE_EXT.length()) {filename = filename.substr(0, pos);}}// 将"."替换为"/"pos = filename.find_first_of(".");while (pos != std::string::npos) {filename.replace(pos, 1, "/");pos = filename.find_first_of(".");}Data chunk;std::string chunkName;FileUtils* utils = FileUtils::getInstance();// 获取package.path的变量lua_getglobal(L, "package");lua_getfield(L, -1, "path");// 通过package.path获取搜索路径相关,该路径为模版路径,格式类似于:// ?; ?.lua; c:\windows\?;  /usr/local/lua/lua/?/?.lua 以“;”作为分割符std::string searchpath(lua_tostring(L, -1));lua_pop(L, 1);size_t begin = 0;size_t next = searchpath.find_first_of(";", 0);// 遍历package.path中的所有路径,查找文件是否存在,存在则通过getDataFromFile读取数据do {if (next == std::string::npos)next = searchpath.length();std::string prefix = searchpath.substr(begin, next);if (prefix[0] == '.' && prefix[1] == '/') {prefix = prefix.substr(2);}pos = prefix.find("?.lua");// 将?替换为文件名,获取搜索路径名,比如:?.lua替换为cocos/init.luachunkName = prefix.substr(0, pos) + filename + BYTECODE_FILE_EXT;if (utils->isFileExist(chunkName)) {chunk = utils->getDataFromFile(chunkName);break;} else {chunkName = prefix.substr(0, pos) + filename + NOT_BYTECODE_FILE_EXT;if (utils->isFileExist(chunkName)) {chunk = utils->getDataFromFile(chunkName);break;}}// 指定搜素路径下不存在该文件,则下一个begin = next + 1;next = searchpath.find_first_of(";", begin);} while (begin < (int)searchpath.length());// 判定文件内容是否获取成功if (chunk.getSize() > 0) {// 加载文件LuaStack* stack = LuaEngine::getInstance()->getLuaStack();stack->luaLoadBuffer(L, reinterpret_cast<const char*>(chunk.getBytes()),static_cast<int>(chunk.getSize()), chunkName.c_str());} else {CCLOG("can not get file data of %s", chunkName.c_str());return 0;}return 1;}
}    

这个的实现其实就是Lua语言关于require实现的本质,比如:

  • Cocos2dx 是如何实现搜索指定的Lua文件的
  • Lua通过require是如何检索引用文件的
  • 关于Lua文件查找不到,报错的路径信息是如何获取的

想了解更多内容,参考:lua 之 require

继续代码研究,看下luaLoadBuffer的实现:

nt LuaStack::luaLoadBuffer(lua_State *L, const char *chunk, int chunkSize, const char *chunkName) {int r = 0;// 判定是否加密,若lua脚本加密,则解密后在加载脚本文件// luaL_loadbuffer 用于加载并编译Lua代码,并将其压入栈中if (_xxteaEnabled && strncmp(chunk, _xxteaSign, _xxteaSignLen) == 0) {// decrypt XXTEAxxtea_long len = 0;unsigned char* result = xxtea_decrypt((unsigned char*)chunk + _xxteaSignLen,(xxtea_long)chunkSize - _xxteaSignLen,(unsigned char*)_xxteaKey,(xxtea_long)_xxteaKeyLen,&len);skipBOM((const char*&)result, (int&)len);r = luaL_loadbuffer(L, (char*)result, len, chunkName);free(result);} else {skipBOM(chunk, chunkSize);r = luaL_loadbuffer(L, chunk, chunkSize, chunkName);}// 判定内容是否存在错误#if defined(COCOS2D_DEBUG) && COCOS2D_DEBUG > 0if (r) {switch (r) {case LUA_ERRSYNTAX: // 语法错误CCLOG("[LUA ERROR] load \"%s\", error: syntax error during pre-compilation.", chunkName);break;case LUA_ERRMEM:// 内存分配错误CCLOG("[LUA ERROR] load \"%s\", error: memory allocation error.", chunkName);break;case LUA_ERRRUN: // 运行错误CCLOG("[LUA ERROR] load \"%s\", error: run error.", chunkName);break;case LUA_ERRFILE:// 文件错误CCLOG("[LUA ERROR] load \"%s\", error: cannot open/read file.", chunkName);break;case LUA_ERRERR:           // 运行错误处理函数时发生错误CCLOG("[LUA ERROR] load \"%s\", while running the error handler function.", chunkName);default:// 未知错误CCLOG("[LUA ERROR] load \"%s\", error: unknown.", chunkName);}// 通过lua的堆栈,获取栈顶的错误信息,将错误日志打印出来(-1永远表示栈顶)const char* error = lua_tostring(L, -1);CCLOG("[LUA ERROR] Error Result: %s", error);lua_pop(L, 1);}#endifreturn r;
}

这里对Lua做的事情主要是:

  • 检测Lua是否加密,如果是,则进行解密; 否则加载并编译Lua代码
  • 如果是测试版本,会对Lua的内容进行安全检测。

最后我们梳理下关于LuaEngine对Lua的操作流程:

  • 获取LuaEngine的接口,调用LuaStack对Lua所需要的环境进行初始化
  • 通过addLuaLoader将Lua的变量等信息添加到加载器中进行解析
  • 解析文件后,通过loadBuffer加载Lua文件数据并进行安全检测。

到这里Lua的文件数据相关算是初始化成功了。


tolua++

**tolua++**是cocos官方提供的一个将C++代码相关转换为指定格式文件的工具,用于实现Lua对C++的调用, 简单的看下引擎在项目启动中关于tolua++封装的接口相关:

TOLUA_API int register_all_cocos2dx(lua_State* tolua_S)
{tolua_open(tolua_S);tolua_module(tolua_S,"cc",0);tolua_beginmodule(tolua_S,"cc");lua_register_cocos2dx_Ref(tolua_S);lua_register_cocos2dx_Node(tolua_S);// 省略...tolua_endmodule(tolua_S);return 1;
}int lua_register_cocos2dx_Ref(lua_State* tolua_S)
{tolua_usertype(tolua_S,"cc.Ref");tolua_cclass(tolua_S,"Ref","cc.Ref","",nullptr);// tolua_function 表示对应的Ref所持有的public接口相关tolua_beginmodule(tolua_S,"Ref");tolua_function(tolua_S,"release",lua_cocos2dx_Ref_release);tolua_function(tolua_S,"retain",lua_cocos2dx_Ref_retain);tolua_function(tolua_S,"getReferenceCount",lua_cocos2dx_Ref_getReferenceCount);tolua_endmodule(tolua_S);std::string typeName = typeid(cocos2d::Ref).name();g_luaType[typeName] = "cc.Ref";g_typeCast["Ref"] = "cc.Ref";return 1;
}

**tolua++**的特点就是开头必带前缀: tolua_

  • tolua_usertype 用于声明一个自定义的类型,将其注册到Lua中
  • tolua_cclass 声明一个C++类,并将其注册到Lua中。参数中的第一个是Lua中的类名,第二个是C++类名,第三个是父类名,第四个是模板参数(可选),第五个是模板名(可选)
  • tolua_beginmodule/tolua_endmodule 用于定义一个模块,并将接口函数注册到该模块中
  • tolua_function 将一个C++类的成员函数或静态函数注册为Lua中的函数, 方便调用

任意看一段函数的实现:

int lua_cocos2dx_Ref_getReferenceCount(lua_State* tolua_S) {int argc = 0;cocos2d::Ref* cobj = nullptr;bool ok  = true;#if COCOS2D_DEBUG >= 1tolua_Error tolua_err;#endif// 从Lua栈中获取cocos对象类型,是否为cc.Ref#if COCOS2D_DEBUG >= 1if (!tolua_isusertype(tolua_S,1,"cc.Ref",0,&tolua_err)) goto tolua_lerror;#endif// 将数据转换为Ref对象,若失败则提示:无效的对象cobj = (cocos2d::Ref*)tolua_tousertype(tolua_S,1,0);#if COCOS2D_DEBUG >= 1if (!cobj) {tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_Ref_getReferenceCount'", nullptr);return 0;}#endif// 获取参数数目,-1的原因在于对象类型Ref也在栈中argc = lua_gettop(tolua_S)-1;if (argc == 0) {if(!ok){tolua_error(tolua_S,"invalid arguments in function 'lua_cocos2dx_Ref_getReferenceCount'", nullptr);return 0;}unsigned int ret = cobj->getReferenceCount();tolua_pushnumber(tolua_S,(lua_Number)ret);return 1;}luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d \n", "cc.Ref:getReferenceCount",argc, 0);return 0;
}

其他的cocos2d-x提供的Lua可调用方法不再赘述,与之类似。


结语

前面的内容将cocos2dx对Lua的封装,以及tolua++的使用,说了很多,主要原因在于:

  • 官方提供的一些接口,仅支持在C++中使用,不支持在Lua中使用,比如骨骼动画中的一些复杂逻辑处理
  • 项目的拓展需要有底层的支持
  • 项目如果将cocosStudio替换为FairyGUI,需要了解这些。

最后祝大家学习生活愉快!

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

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

相关文章

pycharm连接gitlab

1、下载安装gitlab 下载地址&#xff1a;Git - Downloading Package 下载后傻瓜式安装&#xff0c;注意勾选配置环境变量 未配置自己配置&#xff0c;电脑-属性-高级系统配置-环境变量 系统变量path&#xff1a;添加git安装目录下bin目录 2、检验安装完成 桌面右键git-open…

XXE漏洞复现实操

文章目录 一、漏洞原理二、验证payload三、没有回显时的验证四、漏洞特征五、读取文件六、Base64加密读取七、端口检测八、使用php检测端口九、dtd外部实体读取文件十、Xxe漏洞防御 一、漏洞原理 (1)XXE漏洞全称XML External Entity Injection&#xff0c;即xmI外部实体注入漏…

webpack 解决:Cannot use import statement outside a module 的问题

1、问题描述&#xff1a; 其一、报错为&#xff1a; Uncaught SyntaxError: Cannot use import statement outside a module; 中文为&#xff1a; 未捕获的语法错误&#xff1a;无法在模块外部使用 import 语句; 其二、问题描述为&#xff1a; 在项目打包的时候 npm run …

TensorFlow入门(十八、激活函数)

激活函数是什么? 单个神经元的网络模型: 用计算公式表达如下: 即在神经元中,输入的x通过与权重w相乘,与偏置量b求和后,还被作用了一个函数,这个函数就是激活函数。 激活函数的作用 如果没有激活函数,整个神经元模型就是一个简单的线性方程。而在现实生活中,线性方程能解决的事…

使用 Apache Camel 和 Quarkus 的微服务(五)

【squids.cn】 全网zui低价RDS&#xff0c;免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 在本系列的第三部分中&#xff0c;我们了解了如何在 Minikube 中部署基于 Quarkus/Camel 的微服务&#xff0c;这是最常用的 Kubernetes 本地实现之一。虽然这样的本地…

Nacos 小bug: application.properties配置未生效,导致端口未生效

最近用了下nacos 1.4.6 ,发现windows 中修改配置中的启动端口未生效&#xff0c;其实就是配置文件没读取到。 去github 逛了一下issue ,参考这个&#xff1a;https://github.com/alibaba/nacos/issues/10217 这哥们儿是nacos 1.4.5 Linux系统下的相同问题&#xff0c;shell 中…

格式工厂怎么把两个视频合并在一起

免费的工具谁不喜欢呢&#xff0c;今天为大家介绍的是格式工厂这款多功能视频转换软件&#xff0c;然而今天主要为大家介绍的是格式工厂的视频合并功能。 是的&#xff0c;你没有听错&#xff0c;格式工厂除了转换之外&#xff0c;还可以视频合适、视频剪辑、视频分割、去水印…

声音克隆,定制自己的声音,使用最新版Bert-VITS2的云端训练+推理记录

说明 本次训练服务器使用Google Colab T4 GPUBert-VITS2库为&#xff1a;https://github.com/fishaudio/Bert-VITS2&#xff0c;其更新较为频繁&#xff0c;使用其2023.10.12的commit版本&#xff1a;主要参考&#xff1a;B站诸多大佬视频&#xff0c;CSDN:https://blog.csdn.…

Talk | SIGGRAPH‘23 Best Paper 秦颖思:分罗曼三维显示器—各点独立变焦显示技术

本期为TechBeat人工智能社区第537期线上Talk。 北京时间10月12日&#xff08;周四&#xff09;20:00&#xff0c;卡耐基梅隆大学博士生—秦颖思的Talk已准时在TechBeat人工智能社区开播&#xff01; 她与大家分享的主题是: “分罗曼三维显示器—各点独立变焦显示技术”&#xf…

keepalived高可用

keepalived高可用 文章目录 keepalived高可用1.简介&#xff1a;2.优缺点&#xff1a;3.工作原理&#xff1a;4.工作流程&#xff1a;5. keepalived实现nginx负载均衡机高可用环境说明&#xff1a;haproxy部署http负载均衡前提&#xff08;部署两台RS主机&#xff09; 5.1.keep…

win10取消ie浏览器自动跳转edge浏览器

建议大家看完整篇文章再作操作 随着windows10 日渐更新&#xff0c;各种不同的操作&#xff0c;规避IE浏览器跳转Edge浏览器的问题 算了&#xff0c;找了台云机装的server 有自带的IE 1.&#xff08;失败&#xff09;思路 协助Edge浏览器 管理员身份打开 PowerShell 一般e…

# Web server failed to start. Port 9793 was already in use

Web server failed to start. Port 9793 was already in use. 文章目录 Web server failed to start. Port 9793 was already in use.报错描述报错原因解决方法Spring Boot 修改默认端口号关闭占用某一端口号的进程关闭该进程 报错描述 Springboot项目启动控制台报错 Error st…