设为首页 收藏本站
查看: 987|回复: 0

[经验分享] http://www.ibm.com/developerworks/cn/linux/l-cn-spidermonkey/index.html

[复制链接]

尚未签到

发表于 2015-10-4 07:48:39 | 显示全部楼层 |阅读模式
基于 C 语言的 JavaScript 引擎探索
  使用 SpiderMonkey 脚本化您的应用
  邱 俊涛, 软件工程师, 云电同方研发中心
  邱俊涛,毕业于昆明理工大学计算机科学与技术专业,对机械控制、电子、人工智能、函数式编程等领域有浓厚的兴趣,对计算机科学的底层比较熟悉。喜欢 C/JAVA/Python/JavaScript 等语言。现就职于云电同方研发中心。
  简介: JavaScript 语言具有动态性,支持函数式编程,动态弱类型等等优点。作为一个脚本语言,可以很方便的脚本化需要高度可定制的应用程序。本文介绍基于 C 语言的 JavaScript 引擎 SpiderMonkey,详细讨论如何通过该引擎,使得 C 语言和 JavaScript 语言进行交互。
  标记本文!
  发布日期: 2011 年 2 月 17 日     
级别: 初级     
访问情况 7621 次浏览     
建议: 0 (添加评论)
DSC0000.gif DSC0001.gif 平均分 (共 7 个评分 )
  基础知识
  SpiderMonkey 简介
  和其他的 JavaScript 引擎一样,SpiderMonkey 不直接提供像 DOM 这样的对象,而是提供解析,执行 JavaSccript 代码,垃圾回收等机制。SpidlerMonkey 是一个在 Mozilla 之下的开源项目,要使用 SpiderMonkey,需要下载其源码,然后编译为静态 / 动态库使用。
  要在自己的应用程序中使用 SpiderMonkey,首先需要了解以下三个核心概念:
  运行时环境运行时环境是所有 JavaScript 变量,对象,脚本以及代码的上下文所存在的空间。每一个上下文对象,以及所有的对象均存在于此。一般应用仅需要一个运行时即可。
  上下文上下文即脚本执行的环境,在 SpiderMonkey 中,上下文可以编译执行脚本,可以存取对象的属性,调用 JavaScript 的函数,转换类型,创建 / 维护对象等。几乎所有的 SpiderMonkey 函数都需要上下文作为其第一个参数 (JSContext *)。
  上下文与线程密不可分,一般来讲,单线程应用可以使用一个上下文来完成所有的操作,每一个上下文每次只能完成一个操作,所有在多线程应用中,同一时刻只能有一个线程来使用上下文对象。一般而言,多线程应用中,每个线程对应一个上下文。
  全局对象全局对象包含 JavaScript 代码所用到的所有类,函数,变量。在 DOM 操作中,我们使用的:
alter("something");
  事实上使用的是全局变量 window 的一个属性 alter( 这个属性正好是一个函数 ),事实上上边的语句在执行时会别解释为:

window.alter("something");
  三者的关系如下图所示:
  图 1. 引擎内部结构依赖关系
DSC0002.png
  安装 SpiderMonkey
  首先从 SpiderMonkey 的代码库中下载其源码包 js-1.7.0.tar.gz 本文在 Linux 环境下编译,SpiderMonkey 的编译安装很容易:

# 解压缩
tar xvzf js-1.7.0.tar.gz
# 切换至源码目录
cd js-1.7.0/src
# 编译
make -f Makefile.ref
  编译完成之后,会生成一个新的目录,这个目录的名称依赖于平台,比如在 Linux 下,名称为:Linux_All_DBG.OBJ,其中包含静态链接库 libjs.a 和动态链接库 libjs.so 等。本文后续的编译环境就需要依赖于我们此处编译出来的库文件。应该注意的是,此处编译出来的库文件包含对调试的支持,体积较大,在应用程序发布时,可以去掉这些调试支持,使用下列重新编译库:

# 创建非 debug 模式的库
make BUILD_OPT=1 -f Makefile.ref
  Windows 及其他平台的编译此处不再赘述,读者可以自行参考 SpiderMonkey 的官方文档。
  JavaScript 对象与 C 对象间的转换关系
  JavaScript 是一门弱类型的语言,变量的值的类型在运行时才确定,而且可以在运行时被修改为其他类型的变量;而 C 语言,是一门静态类型的语言,变量类型在编译时就已经确定。因此,这两者之间变量的互访就有了一定的难度,SpiderMonkey 提供了一个通用的数据类型 jsval 来完成两者之间的交互。
  事实上,在 C 代码中定义的 jsval 类型的变量可以是 JavaScript 中的字符串,数字,对象,布尔值,以及 null 或者 undefined。基于这个类型,SpiderMonkey 提供了大量的类型判断及类型转换的宏和函数。可以参看下表:
  表 1. JavaScript 对象与 C 对象转换表
  JavaScript 类型
jsval 类型判断
jsval 常量
jsval 转化
  null
JSVAL_IS_NULL(v)
JSVAL_NULL
  Undefined
JSVAL_IS_VOID(v)
JSVAL_VOID
  Boolean
JSVAL_IS_BOOLEAN(v)
JSVAL_TRUE,
JSVAL_FALSE,
BOOLEAN_TO_JSVAL(b)
JSVAL_TO_BOOLEAN(v)
  number
JSVAL_IS_NUMBER(v),
JSVAL_IS_INT(v),
JSVAL_IS_DOUBLE(v)
INT_TO_JSVAL(i),
DOUBLE_TO_JSVAL(d)
JSVAL_TO_INT(v),
JSVAL_TO_DOUBLE(v)
  string
JSVAL_IS_STRING(v)
STRING_TO_JSVAL(s)
JSVAL_TO_STRING(v),
JS_GetStringChars(s),
JS_GetStringLength(s)
  object
JSVAL_IS_OBJECT(v)
&& JSVAL_IS_NULL(v)
OBJECT_TO_JSVAL(o)
JSVAL_TO_OBJECT(v)
  应该注意的是,jsval 有一定的缺陷:


  • jsval 并非完全的类型安全,在进行类型转换之前,你需要明确被转换的对象的真正类型,比如一个变量的值为 number 类型,而对其做向字符串的转化,则可能引起程序崩溃。解决方法是,在转换之前,先做判断。
  • jsval 是 SpiderMonkey 垃圾回收机制的主要目标,如果 jsval 引用一个 JavaScript 对象,但是垃圾收集器无法得知这一点,一旦该对象被释放,jsval 就会引用到一个悬空指针。这样很容易使得程序崩溃。解决方法是,在引用了 JavaScript 对象之后,需要显式的告知垃圾收集器,不引用时,再次通知垃圾收集器。

  回页首
  简单示例
  基本代码模板
  基本流程
  使用 SpiderMonkey,一般来讲会使用以下流程:


  • 创建运行时环境
  • 创建一个 / 多个上下文对象
  • 初始化全局对象
  • 执行脚本,处理结果
  • 释放引擎资源
  在下一小节详细说明每个流程
  代码模板
  使用 SpiderMonkey,有部分代码是几乎每个应用程序都会使用的,比如错误报告,初始化运行时环境,上下文,全局变量,实例化全局变量等操作。这里是一个典型的模板:
  清单 1. 必须包含的头文件


#include "jsapi.h"
  引入 jsapi.h,声明引擎中的所用到的记号,结构体,函数签名等,这是使用 SpiderMonkey 所需的唯一一个接口文件 ( 当然,jsapi.h 中不可能定义所有的接口,这些文件在 jsapi.h 头部引入 jsapi.h,如果对 C 语言的接口,头文件引入方式不熟悉的读者,请参阅相关资料 )。
  清单 2. 全局变量声明


/* 全局变量的类声明 */
static JSClass global_class = {
"global",
JSCLASS_GLOBAL_FLAGS,
JS_PropertyStub,
JS_PropertyStub,
JS_PropertyStub,
JS_PropertyStub,
JS_EnumerateStub,
JS_ResolveStub,
JS_ConvertStub,
JS_FinalizeStub,
JSCLASS_NO_OPTIONAL_MEMBERS
};
  JSClass 是一个较为重要的数据结构,定义了 JavaScript 对象的基本结构 ---“类”,这个类可以通过 SpiderMonkey 引擎来实例化为对象。JS_PropertyStub 是 JS_PropertyOp 类型的变量,这里的 JS_PropertyStub 是为了提供一个默认值。JS_PropertyOp 可以用做对象的 setter/getter 等的,这些内容我们将在后边的章节详细讨论。
  清单 3. 错误处理函数


/* 错误处理函数,用于回调,打印详细信息 */
void report_error(JSContext *cx,  const char *message, JSErrorReport *report){
fprintf(stderr, "%s:%u:%s\n",
report->filename ? report->filename : &quot;<no filename>&quot;,
(unsigned int) report->lineno,
message);
}
  定义好这些结构之后,我们需要实例化这些结构,使之成为内存对象,流程如下:
  清单 4. 主流程


int main(int argc, char *argv[]){
JSRuntime *runtime;
JSContext *context;
JSObject *global;
// 创建新的运行时 8M
runtime = JS_NewRuntime(8L * 1024L * 1024L);
if (runtime == NULL){
return -1;
}
// 创建新的上下文
context = JS_NewContext(runtime, 8*1024);
if (context == NULL){
return -1;
}
//
JS_SetOptions(context, JSOPTION_VAROBJFIX);
// 设置错误回调函数 , report_error 函数定义如上
JS_SetErrorReporter(context, report_error);
// 创建一个新的 JavaScript 对象
global = JS_NewObject(context, &global_class, NULL, NULL);
if (global == NULL){
return -1;
}
// 实例化 global, 加入对象,数组等支持
if (!JS_InitStandardClasses(context, global)){
return -1;
}
//
// 使用 global, context 等来完成其他操作,用户定制代码由此开始
//
// 释放上下文对象
JS_DestroyContext(context);
// 释放运行时环境
JS_DestroyRuntime(runtime);
// 停止 JS 虚拟机
JS_ShutDown();
return 0;
}
  用户自己的代码从上边代码中部注释部分开始,用户代码可以使用此处的 context 对象及预设过一定属性,方法的 global 对象。
  执行 JavaScript 代码
  执行 JavaScript 代码片段
  执行 JS 最简单的方式,是将脚本作为字符串交给引擎来解释执行,执行完成之后释放临时的脚本对象等。SpiderMonkey 提供一个 JS_EvaluateScript 函数,原型如下:
  清单 5. 执行 JS 代码的函数原型


JSBool JS_EvaluateScript(JSContext *cx, JSObject *obj,
const char *src, uintN length, const
char *filename,
uintN lineno, jsval *rval);
  使用这个函数,需要提供上下文,全局变量,字符串形式的脚本,脚本长度及返回值指针,脚本名和行号参数可以填空值 ( 分别为 NULL 和 0)。如果函数返回 JS_TRUE,表示执行成功,执行结果存放在 rval 参数中,否则执行失败,rval 中的值为 undefined。我们可以具体来看一个例子:
  清单 6. 执行 JS 代码片段


char *script = &quot;(function(a, b){return a * b;})(15, 6);&quot;;
jsval rval;
status = JS_EvaluateScript(context, global, script, strlen(script)\
, NULL, 0, &rval);
if (status == JS_TRUE){
jsdouble d;
JS_ValueToNumber(context, rval, &d);
printf(&quot;eval result = %f\n&quot;, d);
}
  执行结果为:

eval result = 90.000000
  编译 JavaScript 代码
  通常,我们可能会多次执行一段脚本,SpiderMonkey 可以将脚本编译成 JSScript 对象,然后可以供后续的多次调用。现在来看一个例子,使用 C 代码编译一个 JavaScript 脚本,然后运行这个脚本。
  清单 7. 从文件加载并执行脚本


JSBool evalScriptFromFile(JSContext *cntext, const
char *file){
JSScript *script;
JSString *jss;
JSBool status;
jsval value;
//get the global object
JSObject *global = JS_GetGlobalObject(context);
//compile the script for further using
script = JS_CompileFile(context, global, file);
if (script == NULL){
return JS_FALSE;
}
//execute it once
status = JS_ExecuteScript(context, global, script, &value);
jss = JS_ValueToString(context, value);
printf(&quot;eval script result is : %s\n&quot;, JS_GetStringBytes(jss));
//destory the script object
JS_DestroyScript(context, script);
return status;
}
  这里传递给函数 evalScriptFromFile的 JSContext* 参数为外部创建好的 Context 对象,创建的方法参看上一节。
  清单 8. 执行


JSBool status = evalScriptFromFile(context, &quot;jstest.js&quot;);
if (status == JS_FALSE){
fprintf(stderr, &quot;error while evaluate the script\n&quot;);
}
  假设我们将如下脚本内容保存进一个脚本 jstest.js:
  清单 9. jstest.js 脚本内容


varPerson = function(name){
var _name_ = name;
this.getName = function(){
return _name_;
}
this.setName = function(newname){
_name_ = newname;
}
}
varjack = new Person(&quot;jack&quot;);
jack.setName(&quot;john&quot;);
// 最后一句将作为脚本的执行结果返回给 C 代码
jack.getName();
  jack 对象的名字现在设置为了”john”, 脚本的最后一条语句的值将作为脚本的返回值返回到 C 代码处,并打印出来:

eval script result is : john
  回页首
  C 与 JavaScript 的交互
  C 程序调用 JavaScript 函数
  由于两者的数据类型上有较大的差异,因此无法直接从 C 代码中调用 JavaScript 代码,需要通过一定的转化,将 C 的变量转换为 JavaScript 可以设别的变量类型,然后进行参数的传递,返回值的处理也同样要经过转换。
  我们在 JavaScript 中定义一个函数 add,这个函数接受两个参数然后返回传入的两个参数的和。定义如下:
  清单 10. JavaScript 版本的 add


function add(x, y){
return x + y;
}
  然后,我们在 C 语言中根据名称调用这个 JS 函数:
  清单 11. 从 C 代码中调用 JavaScript 函数


JSBool func_test(JSContext *context){
jsval res;
JSObject *global = JS_GetGlobalObject(context);
jsval argv[2];
//new 2 number to pass into the function &quot;add&quot; in script
JS_NewNumberValue(context, 18.5, &res);
argv[0] = res;
JS_NewNumberValue(context, 23.1, &res);
argv[1] = res;
JS_CallFunctionName(context, global, &quot;add&quot;, 2, argv, &res);
jsdouble d;
//convert the result to jsdouble
JS_ValueToNumber(context, res, &d);
printf(&quot;add result = %f\n&quot;, d);
return JS_TRUE;
}
  这里需要注意的是,JS_CallFunctionName 函数的参数列表:
  清单 12. JS_CallFunctionName 原型


JSBool  JS_CallFunctionName(JSContext *cx, JSObject *obj,
const char *name, uintN argc, jsval *argv, jsval *rval);
  表 2. JS_CallFunctionName 参数列表含义
  名称
类型
类型描述
  cx
JSContext *
上下文定义
  obj
JSObject *
调用该方法的对象
  name
const char *
函数名
  argc
uintN
函数参数个数
  argv
jsval *
函数实际参数形成的数组
  rval
jsval *
返回值
  参数中的 argv 是一个 jsval 形成的数组,如果直接传递 C 类型的值,则很容易出现 core dump(Linux 下的段错误所导致 ),因此,需要 JS_NewNumberValue 函数转换 C 语言的 double 到 number( 原因见对象转换小节 )。
  JavaScript 程序调用 C 函数
  从 JS 中调用 C 函数较上一节为复杂,我们来看一个较为有趣的例子:SpiderMonkey 中原生的 JavaScript 的全局变量中没有 print 函数,我们可以使用 C 的 printf 来实现这个功能。我们定义了一个函数 print, print 使用 logging 函数,而 logging 函数是定义在 C 语言中的,接受一个字符串作为参数,打印这个字符串到标准输出上 :
  清单 13. JavaScript 调用 C 函数


//log user log in information
logging(&quot;user jack login on 2010/7/6&quot;);
//user do nothing else
nothing();
//log user log out information
logging(&quot;user jack logout on 2010/7/7&quot;);
function print(){
for (vari = 0; i < arguments.length; i++){
logging(arguments);
}
}
print(&quot;hello&quot;, &quot;all&quot;, &quot;my&quot;, &quot;friend&quot;);
  在 C 语言中,我们定义 logging 函数和 nothing 函数的原型如下:
  清单 14. C 函数的实现


/**
* define an exposed function to be used in scripts
* print out all the incoming arguments as string.
*/
static JSBool  logging(JSContext *context, JSObject *object, uintN argc,
jsval *argv, jsval *value){
int i = 0;
JSString *jss;
for(i = 0; i < argc; i++){
jss = JS_ValueToString(context, argv);
printf(&quot;message from script environment : %s\n&quot;, \
JS_GetStringBytes(jss));
}
return JS_TRUE;
}
/**
* define an exposed function to be used in scripts
* do nothing but print out a single line.
*/
static JSBool  nothing(JSContext *context,
JSObject *object, uintN argc, jsval *argv, jsval *value)
{
printf(&quot;got nothing to do at all\n&quot;);
return JS_TRUE;
}
  从函数的签名上可以看出,C 中暴露给 JS 使用的函数,参数的个数,及对应位置上的类型,返回值都是固定的。所有的从 C 中暴露给 JS 的函数都需要“实现这个接口”。
  定义好了函数之后,还需要一些设置才能在 JS 中使用这些函数。首先定义一个 JSFunctionSpec 类型的数组,然后通过 JS_DefineFunctions 将这些函数放到 global 对象上,然后在 JS 代码中就可以访问上边列出的 C 函数了。具体步骤如下:
  清单 15. 注册函数列表


static JSFunctionSpec functions[] = {
{&quot;logging&quot;, logging, LOG_MINARGS, 0, 0},
{&quot;nothing&quot;, nothing, NOT_MINARGS, 0, 0},
{0, 0, 0, 0, 0}
};
//define function list here
if (!JS_DefineFunctions(context, global, functions)){
return -1;
}
  运行结果如下:

message from script environment : user jack login on 2010/7/6
got nothing to do at all
message from script environment : user jack logout on 2010/7/7
message from script environment : hello
message from script environment : all
message from script environment : my
message from script environment : friend
  在 C 程序中定义 JavaScript 对象
  在 SpiderMonkey 中,在 JavaScript 中使用由 C 语言定义的对象较为复杂,一旦我们可以定义对象,使得两个世界通过 JS 交互就变得非常简单而有趣,很容易使用这样的方式来定制我们的应用,在系统发布之后仍然可以轻松的修改系统的行为。
  首先,我们要定义好基本的数据结构,即我们要暴露给 JS 世界的对象的属性,结构;然后,使用 JSAPI 定义这个对象的属性;然后,使用 JSAPI 定义对象的方法;最后,创佳这个对象,并绑定其属性表和方法列表,放入全局对象。
  假设我们有这样一个数据结构,用来表述一个人的简单信息 :
  清单 16. PersionInfo 结构


typedef struct{
char name[32];
char addr[128];
}PersonInfo;
  定义属性表为枚举类型:
  清单 17. 属性表


enum person{
NAME,
ADDRESS
};
  我们需要将 C 语言原生的数据结构定义为 JSAPI 所识别的那样:
  清单 18. 属性定义


//define person properties
JSPropertySpec pprops[] = {
{&quot;name&quot;, NAME, JSPROP_ENUMERATE},
{&quot;address&quot;, ADDRESS, JSPROP_ENUMERATE},
{0}
};
  清单 19. 方法定义


//define person methods
JSFunctionSpec pfuncs[] = {
{&quot;print&quot;, printing, 0},
{&quot;getName&quot;, getName, 0},
{&quot;setName&quot;, setName, 0},
{&quot;getAddress&quot;, getAddress, 0},
{&quot;setAddress&quot;, setAddress, 0},
{0}
};
  清单 20. 类定义


//define person class (JSClass)
JSClass pclass = {
&quot;person&quot;, 0,
JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub
};
  一旦这些基本信息定义好 (pfuncs 数组中的 getter/setter 的实现比较简单,这里由于篇幅不列出代码,感兴趣的朋友可以参看附录 ),我们就可以实例化它,并将其放入上下文中,使得 JS 代码可以访问。
  清单 21. 定义对象及对象的属性,方法


JSObject *person;
//define the object
person = JS_DefineObject(\
context, global, &quot;person&quot;, &pclass, 0, JSPROP_ENUMERATE);
//install the properties and methods on the person object
JS_DefineProperties(context, person, pprops);
JS_DefineFunctions(context, person, pfuncs);
  这样,在 JavaScript 代码中,我们就可以通过 person 这个标识符来访问 person 这个对象了:
  清单 22. 测试脚本


//undefined of course
person.print();
//person.name = &quot;abruzzi&quot;;
//person.address = &quot;Huang Quan Road&quot;;
person.setName(&quot;Desmond&quot;);
person.setAddress(&quot;HuangQuan Road&quot;);
//print is global function, access properties directly
print(&quot;person name = &quot; + person.name);
print(&quot;person address = &quot; + person.address);
person.print();
(function(){
//using getter/setter to access properties
return person.getName() + &quot; : &quot; + person.getAddress();
})();
  对运行结果如下:

name : undefined
address : undefined
person name = Desmond
person address = HuangQuan Road
name : Desmond
address : HuangQuan Road
eval script result is : Desmond : HuangQuan Road
  回页首
  结束语
  本文中详细讨论了如何使用基于 C 的 JavaScript 引擎:SpiderMonkey 的用法。包括最基本的代码模板,C 代码与 JavaScript 代码之间的交互,以及在 C 代码中定义 JavaScript 对象等内容,使用这些基本概念,很容易将实现应用程序的脚本化。在实际的应用中,可以将应用程序的部分组件 ( 提供给用户自定义的组件 ) 暴露给 JavaScript 访问,或者在 JavaScript 脚本中提供函数的存根 ( 仅仅定义函数的原型 ),用户可以通过实现这些函数的具体逻辑,来实现脚本化。

  回页首
  下载
  描述
名字
大小
下载方法
  样例代码
jsfun.zip
8KB
HTTP
  关于下载方法的信息
  参考资料
  学习


  • SpiderMonkey 官方网站,包括 API 函数签名,完整的文档等。
  • w3c 的一个简易的 JavaScript教程,介绍客户端的 JavaScript 的基本用法。
  • 一篇介绍 SpiderMonkey 的文章,介绍 SpiderMonkey 基本的用法。
  • 在 developerWorks Linux 专区寻找为 Linux 开发人员(包括 Linux 新手入门)准备的更多参考资料,查阅我们 最受欢迎的文章和教程。
  • 在 developerWorks 上查阅所有 Linux 技巧和 Linux 教程。
  • 随时关注 developerWorks 技术活动和 网络广播。
  讨论


  • 欢迎加入 developerWorks 中文社区。
  关于作者
  邱俊涛,毕业于昆明理工大学计算机科学与技术专业,对机械控制、电子、人工智能、函数式编程等领域有浓厚的兴趣,对计算机科学的底层比较熟悉。喜欢 C/JAVA/Python/JavaScript 等语言。现就职于云电同方研发中心。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-122329-1-1.html 上篇帖子: Configure mutiple IBM HTTP Server / Other Apache based WEB server on 1 physical 下篇帖子: IBM Tivoli Storage Manager
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表