Sofia nua模块--高层UA库
nua 模块包含UA库的实现代码,它关心SIP UA的基本功能。它的功能包括呼叫话务管理、消息,及事件检索。
概述
NUA API为高层应用程序提供了一个透明的,完全控制的SIP协议引擎。NUA在现有的事务语义之(nta模块的实现)上提供呼叫语义。通过NUA可以搭建不同的SIP UA,比如说终端、网关,或MCU。
nua 引擎为应用程序员隐藏了许多底层信令和媒体管理细节。它可以使用不同的媒体接口,甚至可以是远程的,完全透明的方式处理媒体。
UA库内的协议引擎和应用程序可以运行在不同的线程中。协议引擎与应用层线程通过事件(events)机制通信,通过回调函数传递给应用层。回调函数由应用层的上下文相关线程调用,通过一个su_root_t 对象体现。
NUA用户的Sofia 概念
引言
Sofia 软件套件是基于目前的基础观点和概念建立的,这些概念在Sofia软件的所有层级中通用。里面的许多内容都由Sofia工具库(su)实现,并对多数主流OS提供了统一的接口封装。
这面章节介绍这些概念,NUA用户必须理解它们才能让Sofia程序正常工作。其它工具(SU库提供的,或Sofia 软件套件的其它库提供的)可能对应用程序开发者也是有帮助的,但是使用时务必小心,因为它们有可能改变Sofia软件套件的行为,从而导致NUA库工作异常。更多细节可以参考[su]模块的相关文档。
事件循环Event loop 与根对象 root object
NUA以事件反应器模式(也称为分发及通知模式)驱动事件系统(请参考[Using Design Patterns to Develop Reusable Object-oriented Communication Software, D.C. Schmidt, CACM October '95, 38(10): 65-74]一书)。Sofia以任务作为编程模型的基本执行单元。根据编程模型,程序可以请求事件循环在特定事件触发时调用回调函数。具体事件包括I/O激活,定时器或其它任务传递的异步消息。
root 对象是应用软件中描述一个任务的句柄。透视事件的另一种方式是:root对象描述任务的主事件循环。通过root对象,任务代码可以访问它的上下文信息(magic)和线程同步,比如说等待对象、定时器,消息。
使用NUA服务的应用必须创建一个root对象,并设置处理NUA事件的回调函数。调用 su_root_create() 创建root对象,调用nua_create() 函数注册回调函数。root对象的数据类型是su_root_t。
Magic
magic 是一种描述上下文指针的术语,应用程序可以把它绑定到Sofia栈的各种对象上(比如说root对象和操作句柄)。当主事件循环调用注册的回调函数时,上下文指针会回传给应用层代码。Sofia栈保留回调函数调用之间的上下文信息。应用层可以利用上下文信息存储处理事件所需要的任何信息。
内存处理
给定的任务需要分配许多小块内存时,使用home-based内存管理机制会很便利。内存通过home对象分配,它维护所有内存块的引用信息。当home对象释放时,所有相关内存一并释放。这个机制简化了应用逻辑,因为应用层代码不需要跟踪内存块的分配记录,也不需要独立释放每一个内存块。
使用NUA服务的应用程序可以使用SU库提供的内存管理服务,但这不是强制要求的。
Tags
Tagging 是Sofia软件所使用的一种机制,它用于向函数打包参数。它可以传递非固定类型的可变长度的参数。对应用层程序员来说,tagging是一个可见的宏,可用它封装传递的参数。评估时,tagging宏创建一个包含tag的结构(记录参数的类型),和一个值(指向不透明数据)。通过检查tag,Sofia软件的各层可以检查它们是否能够处理参数,或者简单地把它传递给下一层处理。
有一些具有特殊含义的tag:
- TAG_NULL() (与TAG_END()同义) ,tag列表的结束
- TAG_SKIP() 空tag 项
- TAG_NEXT() 指向另一个tag列表的tag项,结束当前tag列表
- TAG_ANY() 接受任何tag的的筛选tag
- TAG_IF() tag 项的条件包含
如果NUA函数的参数表末尾是以下这样的,那么可以通过tag列表调用:
tag_type_t tag,
tag_value_t value,
...);
参数表的最后一个tag值必须为TAG_NULL() (或 TAG_END(), 它等价于 TAG_NULL()).
每个tag都有两个版本:
NUTAG_<tagname>
它带有一个值参数;
NUTAG_<tagname>_REF
它带有一个引用参数。后者使用tl_gets()函数从tag列表中检查tag值。
对于SIP头域来说,还有另一种tag的版本:
SIPTAG_<tagname>_STR
这个版本以一个C语言字符串作为参数。不带_STR 后缀的相应tag将解析的值结构作为参数。
下面是一个调用实例:
nua_unregister(op->op_handle,TAG_IF(use_registrar, NUTAG_REGISTRAR(registrar)),SIPTAG_CONTACT_STR("*"),SIPTAG_EXPIRES_STR("0"),TAG_NULL());
使用NUA服务的应用程序必须使用tag参数为函数传参。nua_invite() 表现了从tag构建SIP消息的过程。
调试与日志
Sofia 栈的模块包含可配置的调试及日志功能,它们的服务接口在<sofia-sip/su_log.h>里定义。调试及日志细节(比如说日志等级,输出文件名)可以通过环境变量配置,或者通过配置文件指定,还有代码层的指定。
定义的环境变量及含义:
- SOFIA_DEBUG 默认的调试等级 (0..9)
- NUA_DEBUG NUA 调试等级 (0..9)
- NTA_DEBUG 事务引擎调试等级 (0..9)
- TPORT_DEBUG 传输层事件调试等级 (0..9)
- TPORT_LOG 如果设置,在传输层打印所有解析出来的SIP消息
- TPORT_DUMP 指定一个文件名,用于转储传输层未解析的消息
预定义的调试输出等级:
- 0 致命性错误,崩溃级的
- 1 严重错误,至少是子系统层的问题
- 2 非严重的错误信息
- 3 告警,进展信息
- 5 信令协议动作 (收到包, ...)
- 7 媒体协议动作 (收到包, ...)
- 9 进入/退出函数,非常细致的进展信息
使用NUA服务的应用程序可以使用Sofia栈提供的调试和日志服务,但不强求。
NUA Concepts
NUA 栈对象
栈对象描述SIP栈和媒体引擎的实例。它包含指向栈root对象的引用,UA指定的设置,还有SIP事务引擎的引用。
NUA栈对象调用nua_create() 创建,调用nua_destroy()销毁。nua 引擎可以用nua_shutdown() 函数优雅地释放会话。
NUA栈对象的类型为nua_t。
NUA 操作句柄
操作句柄描述一个抽象的SIP call/session。它包含SIP dialog的信息和媒体session,呼叫的状态机,高层SDPoffer-answer协议,注册,订阅,发布和简单的SIP事务。操作句柄还可能包含用于NUA构建SIP消息的tag列表,比如说From和To头域。
应用程序用NUA显式创建操作句柄来发送消息(nua_handle()函数),或者栈处理收入呼时隐式创建(INVITE 或MESSAGE)。句柄通过NUA调用销毁(nua_handle_destroy())。
指示及响应事件与操作句柄关联。
NUA操作句柄的类型是nua_handle_t。
栈线程及消息传递概念
栈线程与应用程序分离,它提供实时的协议栈操作,因此应用程序线程可以自己堵塞处理,比如重绘UI。
栈线程与应用线程之间的通信是异步的。很多NUA API函数都会发消息给栈线程处理,类似的,某些事件发生后,栈线程也会向应用线程发消息。当应用程序调用su_root_run() 或 su_root_step()函数时,向应用线程发的消息作为它的回调进行传递。
SIP 消息和头域处理
SIP消息通过类型安全的SIPTAG_ tag处理。每个SIPtag有三种版本:
- SIPTAG_<tagname>() 以解析后的值作为参数。
- SIPTAG_<tagname>_STR() 以未解析的字符串为参数
- SIPTAG_<tagname>_REF() 以引用为参数,与tl_gets() 函数一同使用,以从tag列表中检索tag值。
- SIPTAG_<tagname>__STR_REF() 以引用为参数,与tl_gets() 函数一同使用,以从tag列表中检索字符串tag值。
For example a header named "Example" would have tags names SIPTAG_EXAMPLE(), SIPTAG_EXAMPLE_STR(), and SIPTAG_EXAMPLE_REF().
比如说,有个头域名为"Example",它将拥有这几个tag:SIPTAG_EXAMPLE(), SIPTAG_EXAMPLE_STR(), 和SIPTAG_EXAMPLE_REF()。
当在NUA呼叫中使用tag时,会将相应的头域添加到消息中。如果头域只能在消息中出现一次,并且消息中已经存在一个相应的头域,那么tag提供的值取代现有的头域值。传递tag值NULL对头域没有影响。传递tag值 (void *)-1将相应的头域从消息中移除。
例如:
- 发送一条带有一个Event:头域和两个 Accept:头域的SUBSCRIBE 消息:
nua_subscribe(nh,SIPTAG_EVENT_STR("presence"),SIPTAG_ACCEPT(accept1),SIPTAG_ACCEPT(accept2),TAG_END());
- 处理nua_r_subscribe事件时提取tag值:
sip_accept_t *ac = NULL;sip_event_t *o = NULL;tl_gets(tl,SIPTAG_EVENT_REF(o), /* _REF takes a reference! */SIPTAG_ACCEPT_REF(ac),TAG_END());
SIP/NUA 教程
本节使用消息序列图描述NUA/Sofia栈的基本使用场景。
外呼
入呼
基本外发操作
基本消息接收操作
带鉴权的外发操作
简单应用实例
接下来,将以代码形式给出一个实例,展示如何利用NUA服务构建应用。实例代码并不完整,但能够体现NUA使用的所有相关细节。
在sourceforge上有一个可用的实例程序 sofisip_cli.c ,可以作为更完整的示例研究。
数据结构定义
使用NUA提供服务的程序通常定义一些用于存储上下文信息(比如说"magic")的数据域。指向这些上下文信息数据域的指针通过类型定义传递给NUA。
/* type for application context data */
typedef struct application application;
#define NUA_MAGIC_T application/* type for operation context data */
typedef union oper_ctx_u oper_ctx_t;
#define NUA_HMAGIC_T oper_ctx_t
信息域内容本身可以定义为C结构体或联合体:
/* example of application context information structure */
typedef struct application
{su_home_t home[1]; /* memory home */su_root_t *root; /* root object */nua_t *nua; /* NUA stack object *//* other data as needed ... */
} application;/* Example of operation handle context information structure */
typedef union operation
{nua_handle_t *handle; /* operation handle /struct{nua_handle_t *handle; /* operation handle /... /* call-related information */} call;struct{nua_handle_t *handle; /* operation handle /... /* subscription-related information */} subscription;/* other data as needed ... */} operation;
NUA栈对象和句柄对应用层程序员来说是不透明的。同样,应用层的上下文对NUA栈模块也是完全不透明的。NUA函数传入一个指针,然后这个指针在回调函数的参数中回传给应用层。在这种场景下,应用层上下文信息结构还可以存储root对象和home内存基址。此外,应用层上下文信息中还包含NUA栈对象信息。
初始化与清除
以下代码展示应用层初始化系统,进入主循环处理消息,消息 处理结束,系统清除的过程。
如果应用程序不仅响应收到的SIP消息,还必须有向NUA发送消息的方法。这可以通过单独的线程处理,调用NUA函数来发消息,或建立socket连接向其它应用发命令(参考su_wait_create()和su_root_register())。
/* Application context structure */
application appl[1] = {{{{(sizeof appl)}}}};/* initialize system utilities */
su_init();/* initialize memory handling */
su_home_init(appl->home);/* initialize root object */
appl->root = su_root_create(appl);if (appl->root != NULL) {
/* create NUA stack */
appl->nua = nua_create(appl->root,
app_callback,
appl,
/* tags as necessary ...*/
TAG_NULL());if (appl->nua != NULL) {
/* set necessary parameters */
nua_set_params(appl->nua,
/* tags as necessary ... */
TAG_NULL());/* enter main loop for processing of messages */
su_root_run(appl->root);/* destroy NUA stack */
nua_destroy(appl->nua);
}/* deinit root object */
su_root_destroy(appl->root);
appl->root = NULL;
}/* deinitialize memory handling */
su_home_deinit(appl->home);/* deinitialize system utilities */
su_deinit();
事件处理
在应用初始化时,通过nua_create()函数向NUA栈注册事件的相关回调函数,NUA栈捕获事件时,就会调用回调函数处理事件。回调函数的内容是一组最简单的 switch/case语句,把捕获的事件分发给具体的处理函数。
void app_callback(nua_event_t event,int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{switch (event) {case nua_i_invite:app_i_invite(status, phrase, nua, magic, nh, hmagic, sip, tags);break;case nua_r_invite:app_r_invite(status, phrase, nua, magic, nh, hmagic, sip, tags);break;/* and so on ... */default:/* unknown event -> print out error message */if (status > 100) {printf("unknown event %d: %03d %s\n",event,status,phrase);}else {printf("unknown event %d\n", event);}tl_print(stdout, "", tags);break;}
} /* app_callback */
发起呼叫
以下三个函数展示基本的SIP呼叫是怎样发起的。
place_a_call()函数创建一个句柄并调用SIP INVITE方法:
operation *place_a_call(char const *name, url_t const *url)
{operation *op;sip_to_t *to;/* create operation context information */op = su_zalloc(appl->home, (sizeof *op));if (!op)return NULL;/* Destination address */to = sip_to_create(NULL, url);if (!to)return NULL;to->a_display = name;/* create operation handle */op->handle = nua_handle(appl->nua, op, SIPTAG_TO(to), TAG_END());if (op->handle == NULL) {printf("cannot create operation handle\n");return NULL;}nua_invite(op->handle,/* other tags as needed ... */TAG_END());} /* place_a_call */
app_r_invite() 函数在收到INVITE消息的应答时,由协议栈的回调函数调用。这里假设没有激活automatic acknowledge,因此必须显式地发送ACK消息。
void app_r_invite(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{if (status == 200) {nua_ack(nh, TAG_END());}else {printf("response to INVITE: %03d %s\n", status, phrase);}
} /* app_r_invite */
在呼叫状态变化时,发出nua_i_state event事件,回调函数调用app_i_state()函数:
void app_i_state(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{nua_callstate_t state = nua_callstate_init;tl_gets(tags,NUTAG_CALLSTATE_REF(state),NUTAG__REF(state),state = (nua_callstate_t)t->t_value;printf("call %s\n", nua_callstate_name(state));} /* app_i_state */
接收来电
收到INVITE消息时,回调函数调用app_i_invite()函数。这里假设没有开启自动应答,因此必须显示发送应答消息:
void app_i_invite(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{printf("incoming call\n");nua_respond(nh, 200, "OK", SOA_USER_SDP(magic->sdp), TAG_END());} /* app_i_invite */
呼叫成功建立,媒体激活时,回调函数调用app_i_state() 函数:
void app_i_active(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{printf("call active\n");} /* app_i_active */
拆线
以下三个函数展示一通SIP呼叫是如何终止的。
erminate_call() 函数发出SIP BYE消息。
void terminate_call(void)
{nua_bye(op->handle, TAG_END());} /* terminate call */
在收到BYE消息的应答消息时,回调函数调用app_r_bye()函数。它销毁呼叫句柄并释放存储操作上下文信息的相关内存。
void app_r_bye(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{if (status < 200)return;printf("call released\n");/* release operation handle */nua_handle_destroy(hmagic->handle);op->handle = NULL;/* release operation context information */su_free(appl->home, hmagic);} /* app_r_bye */
收到BYE消息时,回调函数调用app_i_bye()函数。它销毁呼叫句柄并释放存储操作上下文信息的相关内存。
void app_i_bye(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{printf("call released\n");/* release operation handle */nua_handle_destroy(hmagic->handle);op->handle = NULL;/* release operation context information */su_free(appl->home, hmagic);} /* app_i_bye */
发一条消息
以下代码展示如何发一条SIP MESSAGE 消息。
send_message() 函数发出SIP MESSAGE。
void send_message(void)
{op_t *op;/* create operation context information */op = su_zalloc(appl->home, sizeof(op_t));if (op = NULL) {printf("cannot create operation context information\n");return;}/* how we create destination_address? *//* create operation handle */op->handle = nua_handle(appl->nua,op,NUTAG_URL(destination_address),TAG_END());if (op->handle == NULL) {printf("cannot create operation handle\n");return;}/* send MESSAGE */nua_message(op->handle,SIPTAG_CONTENT_TYPE_STR("text/plain"),SIPTAG_PAYLOAD_STR("Hello, world!"),/* other tags as needed ... */TAG_END());} /* send_message */
收到MESSAGE消息的应答时,回调函数调用app_r_message()函数:
void app_r_message(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{printf("response to MESSAGE: %03d %s\n", status, phrase);
} /* app_r_message */
接收消息
以下代码展示收到SIP MESSAGE时是怎样处理的。
收到SIP MESSAGE时,回调函数调用 app_i_message():
void app_i_message(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
printf("received MESSAGE: %03d %s\n", status, phrase);printf("From: %s%s" URL_PRINT_FORMAT "\n",
sip->sip_from->a_display ? sip->sip_from->a_display : "",
sip->sip_from->a_display ? " " : "",
URL_PRINT_ARGS(sip->sip_from->a_url));if (sip->sip_subject) {
printf("Subject: %s\n", sip->sip_subject->g_value);
}if (sip->sip_payload) {
fwrite(sip->sip_payload->pl_data, sip->sip_payload->pl_len, 1, stdout);
fputs("\n", stdout);
}
} /* app_i_message */
建立Presence Server
...application_t *app;operation_t *oper;...oper->app = app;app->nua = nua_create(ssip->s_root,app_callback,app,TAG_NULL());
...oper->handle = nua_handle(app->nua, app,NUTAG_URL(to->a_url),SIPTAG_TO(to),ta_tags(ta));
...nua_notifier(oper->handle,SIPTAG_EXPIRES_STR("3600"),SIPTAG_EVENT_STR("presence"),SIPTAG_CONTENT_TYPE_STR("application/pidf-partial+xml"),NUTAG_SUBSTATE(nua_substate_pending),TAG_END());
在 nua_notifier对象,即presence server建立之后,返回一个nua_r_notifier。app_callback函数的状态及phrase值显示创建成功。
收到订阅消息时的鉴权可以由回调函数处理:
void app_callback(nua_event_t event,
int status, char const *phrase,
nua_t *nua, application_t *app,
nua_handle_t *nh, oper_t *op,
sip_t const *sip, tagi_t tags[])
{nea_sub_t *subscriber = NULL;switch (event) {case nua_i_subscription:tl_gets(tags,NEATAG_SUB_REF(subscriber),TAG_END());nua_authorize(nua_substate_active);default:break;
}
关停
以下函数展示应用程序是如何终止NUA栈的工作的。
shutdown()函数开始关停操作:
void shutdown(void)
{nua_shutdown(appl->nua);} /* shutdown */
无论NUA栈的停止是成功或失败,回调函数调用app_r_shutdown()函数。
void app_r_shutdown(int status,char const *phrase,nua_t *nua,nua_magic_t *magic,nua_handle_t *nh,nua_hmagic_t *hmagic,sip_t const *sip,tagi_t tags[])
{printf("shutdown: %d %s\n", status, phrase);if (status < 200) {/* shutdown in progress -> return */return;}/* end the event loop. su_root_run() will return */su_root_break(magic->root);} /* app_r_shutdown */
Sofia nua模块--高层UA库相关推荐
- 天津市职高高一计算机试题及答案,职高(中职)数学(基础模块)上册题库.doc
职高(中职)数学(基础模块)上册题库.doc 文档编号:388585 文档页数:13 上传时间: 2018-08-30 文档级别:精品资源 文档类型:doc 文档大小:653.00KB 宁波至达教育 ...
- 手机微信ua大全,最新不重复的UA库
手机微信ua大全,最新不重复的UA库,最新微信ua标识 有各种不同品牌的手机型号 微信内置浏览器和原生浏览器UA (手机微信 UA)常用UserAgent列表(去重共12833条) 类型 系统 设备 ...
- matlab对象浏览器模块隐,MATLAB 添加自定义的模块到simulink库浏览器
EDA365欢迎您登录! 您需要 登录 才可以下载或查看,没有帐号?注册 x MATLAB 添加自定义的模块到simulink库浏览器' X) d- E" x' b; y" Y% ...
- Python—— 组合数据类型(模块5: jieba库的使用)(实例:基本统计值计算文本词频统计)
前言 本篇主要介绍组合数据类型,以基本统计值计算为例,介绍函数使用和各种类型定义.以文本词频统计为例,介绍Jieba库的使用. (从本篇开始,出现的一些库中函数介绍以及部分简单代码都将以图片形式呈现) ...
- python使用osgeo库_Python使用内置urllib模块或第三方库requests访问网络资源
前言 更多内容,请访问我的 个人博客. Python 访问网络资源有很多方法,urllib, urllib2, urllib3, httplib, httplib2, requests ,现介绍如下两 ...
- MATLAB 添加自定义的模块到simulink库浏览器
在MATLAB开发环境中,Simulink仿真平 台可以建立用户自定义的库文件,并将它们显示在Library Browser窗口下,方便用户进行模块的操作.用户可以将一些平时使用比较频繁,或者自己建立 ...
- 03-requests模块携带UA请求头,携带参数,携带cookie,持久化存储
reques模块的基础使用方法 chrome浏览器使用方法了解新建隐身窗口的目的,了解chrome中network的使用 1,新建隐身窗口(无痕窗口) 浏览器中直接打开网站,会自动带上之前网站保存的c ...
- Python学习笔记011_模块_标准库_第三方库的安装
容器 -> 数据的封装 函数 -> 语句的封装 类 -> 方法和属性的封装 模块 -> 模块就是程序 , 保存每个.py文件 # 创建了一个hello.py的文件,它的内容如下 ...
- Android开发之ApiCloud模块开发之模块引用第三方库的问题
因为现在第三方库比较多,所以很多人为了快速开发导致库用烂大街了,但是在模块开发中本人不建议使用第三方库的依赖会有很多问题,要么是资源图片找不到,要么是布局找不到啥的,但是有的需求只有第三方库怎么办呢? ...
最新文章
- amp 保留指定位c语言,C语言位运算符学习
- 随着计算机多媒体技术的产生和发展,多媒体技术及其应用与发展论文(本科)10...
- 怎么样设置关闭网页再次登录网页是正常登录状态_学籍系统出现“该账号已登录,不能重复登录”怎么办?...
- python微信跳一跳小游戏刷分
- Linux驱动小技巧 | 利用DRIVER_ATTR实现调用内核函数
- electron增加导航按钮_Electron发布6.0 Released版本
- github-仓库基本-下载-上传
- JS模块化开发_思维导图
- RabbitMQ学习之spring配置文件rabbit标签的使用
- java中import的使用
- html生成小窗口,用JS制作9种弹出小窗口(HTML)
- Vue插槽(solt)简单案例
- 最简单的基于FFMPEG的图像编码器(YUV编码为JPEG)
- C#中创建线程的四种方式
- flashplayer 10 的 p2p 基础
- android模拟器横屏显示,安卓模拟器bluestacks怎么横屏显示
- 【群晖】VMM安装ubuntu虚拟机+Jenkins
- 试题 算法提高 盾神与条状项链
- 【随笔】小记2014年东北四省赛
- REPL module
热门文章
- Python3+Selenium 实现自动登录淘宝+清空购物车
- 黑马程序员——黑马诗歌
- 绿色红色荧光标记PLGA,PLA,PVP,PVA,明胶纳米纤维膜定制
- PHP echo ‘\n‘ 无效 无法不能换行的 一个简单解决方法
- 蓝桥杯真题:九宫幻方
- 《真香编辑器—CSDN编辑器|CSDN编辑器测评》
- 2023青岛电博会启幕 京东方艺云创新显示科技大放异彩
- Git的简介与Git详细操作流程
- 建设银行app流水申请
- Kubernetes 开源负载均衡器 OpenELB 使用指南