VSOA Server
VSOA 服务器相关的所有 API 都包含在 vsoa_server.h
和 libvsoa-server.so
文件中。
libvsoa-hpserv.so
与 libvsoa-server.so
是二进制兼容的,libvsoa-hpserv.so
提供了一个并行处理服务器,在多核处理器上可以更加高效地运行。建议使用符号链接来指定使用的库,以便应用程序不需要重新修改函数接口。
Notice
在 VSOA 服务器成功创建之后,下列函数为线程安全:
- vsoa_server_count(),
- vsoa_server_is_subscribed(),
- vsoa_server_publish(),
- vsoa_server_quick_publish(),
- vsoa_server_cli_close(),
- vsoa_server_cli_is_subscribed(),
- vsoa_server_cli_address(),
- vsoa_server_cli_reply(),
- vsoa_server_cli_priority(),
- vsoa_server_cli_keepalive(),
- vsoa_server_cli_array(),
- vsoa_server_cli_datagram(),
- vsoa_server_cli_quick_datagram(),
- vsoa_server_cli_send_timeout(),
- vsoa_server_cli_set_authed(),
- vsoa_server_cli_authed(),
- vsoa_server_set_custom(),
- vsoa_server_custom(),
- vsoa_server_stream_create(),
- vsoa_server_stream_accept(),
- vsoa_server_stream_close().
所有回调函数都在 vsoa_server_input_fds()
中调用,因此需要注意多线程安全。
QoS 功能函数包括:
- vsoa_server_cli_priority(),
- vsoa_server_cli_send_timeout().
当与客户端的物理连接断开时,如果没有数据要发送,服务器无法知道客户端已断开连接,建议使用: vsoa_server_cli_keepalive()
进行客户端保活检测。
调用 vsoa_server_close()
后,服务器对象将不再可用,且只能在 vsoa_server_input_fds()
所在的事件循环中调用 vsoa_seerver_close()
,而不能在其他回调中调用。
快速通道用于高频数据更新通道。
由于数据更新频率高,对通信可靠性的要求并不严格。
使用下列函数接口创建一个 VSOA 服务器:
vsoa_server_t *vsoa_server_create(const char *info_json);
- 函数成功返回 server 句柄,失败返回
NULL
; - 参数
info_json
: 服务器的信息,采用 json 格式。此 JSON 信息必须包含name
字段;
使用示例
vsoa_server_t *s = vsoa_server_create("{\"name\":\"Test Server\"}");
使用下列函数接口关闭 VSOA 服务器:
void vsoa_server_close(vsoa_server_t *server);
- 参数
server
:VSOA 服务器对象句柄;
设置服务器连接密码:
bool vsoa_server_passwd(vsoa_server_t *server, const char *passwd);
- 函数成功返回
true
,失败返回false
; - 参数
server
: VSOA 服务器句柄; - 参数
passwd
: 配置的密码字符串;
服务器密码配置完成后,客户端必须提供正确的密码才能连接到此服务,否则,客户端连接请求将被服务器拒绝。
注意:密码是一个没有任何安全加密的字符串,在当前版本中只是一个相对简单的安全管理。
启动服务器:
bool vsoa_server_start(vsoa_server_t *server, const struct sockaddr *addr, socklen_t namelen);
- 函数成功返回
true
,失败返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
addr
:服务器地址,支持AF_INET(结构体sockaddr_in)
和AF_INET6(结构体sockaddr_in6)
; - 参数
namelen
:地址结构大小;
如果开始时绑定的端口为 0
,系统将自动分配一个端口。此时,VSOA 服务器的 TCP和 UDP 端口是不同的,客户端需要使用版本 1.0.1 或更高版本来提供支持。
通过以下函数获取服务器地址:
bool vsoa_server_address(vsoa_server_t *server, struct sockaddr *addr, socklen_t *namelen);
同时还可以为服务器绑定网络接口,它可以在 vsoa_server_start
之前或 vsoa_server_start
之后调用:
bool vsoa_server_bind_if(vsoa_server_t *server, const char *ifname)
在服务器成功启动后,可以进入事件循环。
Server Event Loop
VSOA 服务器运行时,会始终使用以下两个 API 循环监视和处理所有输入事件:
int vsoa_server_fds(vsoa_server_t *server, fd_set *rfds);
void vsoa_server_input_fds(vsoa_server_t *server, const fd_set *rfds);
- 参数
server
:VSOA 服务器句柄; - 参数
rfds
:需要监视的 VSOA 服务器中的所有事件;
使用示例
fd_set fds;
int custom_fd, max_fd, cnt;
while (1) {
FD_ZERO(&fds);
max_fd = vsoa_server_fds(my_server, &fds);
/* add custom event */
FD_SET(custom_fd, &fds);
if (max_fd < custom_fd) {
max_fd = custom_fd;
}
/* wait event */
cnt = pselect(max_fd + 1, &fds, NULL, NULL, &timeout, NULL);
if (cnt > 0) {
vsoa_server_input_fds(my_server, &fds);
}
/* do other things */
}
以上代码实现了一个标准的事件循环模板,可以将任何自定义事件添加到这个循环中,非常简单和方便!
Remote Client Management
在 VSOA 服务器端,每个连接的客户端都被分配一个唯一的客户端 id:
In VSOA server side, each connected client will be assigned a unique client id:
typedef uint32_t vsoa_cli_id_t;
服务器使用此 API 来监视客户端的连接状态:
void vsoa_server_on_cli(vsoa_server_t *server, vsoa_server_cli_func_t oncli, void *arg);
- 参数
server
:VSOA 服务器句柄; - 参数
oncli
:当客户端同服务器连接或断开时,将调用该回调函数; - 参数
arg
:可以使用这个回调参数来传递任何自定义信息;
回调函数原型 :
typedef void (*vsoa_server_cli_func_t)(void *arg, vsoa_server_t *server, vsoa_cli_id_t id, bool connect);
- 参数
arg
:回调参数来传递任何自定义信息; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端 id; - 参数
connect
:客户端连接状态;
可以获得连接到当前服务器的客户端总数:
int vsoa_server_count(vsoa_server_t *server);
还有一种方法可以获得所有客户端的 id:
int vsoa_server_cli_array(vsoa_server_t *server, vsoa_cli_id_t ids[], int max_cnt);
- 函数成功返回实际的客户端数量,失败返回 -1;
- 参数
server
:VSOA 服务器句柄; - 参数
ids
:存储客户端 id 的数组; - 参数
max_cnt
:ids[] 数组大小;
可使用以下 API 获取客户端的地址:
bool vsoa_server_cli_address(vsoa_server_t *server, vsoa_cli_id_t id, struct sockaddr *addr, socklen_t *namelen);
为远程客户端设置一个连接存活检测计时器:
bool vsoa_server_cli_keepalive(vsoa_server_t *server, vsoa_cli_id_t id, int keepalive);
- 函数成功返回
true
,失败返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
keepalive
:将被用于TCP_KEEPIDLE
和TCP_KEEPINTVL
;
服务器可以主动关闭客户端:
bool vsoa_server_cli_close(vsoa_server_t *server, vsoa_cli_id_t id);
- 函数成功返回
true
,失败返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端id;
服务器可以获取指定的客户端是否订阅了指定的主题:
bool vsoa_server_cli_is_subscribed(vsoa_server_t *server, vsoa_cli_id_t id, const vsoa_url_t *url);
- 客户端订阅时返回
true
,未订阅时返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端 id; - 参数
url
:指定的主题 url;
QoS
在VSOA中,有一个针对客户端的 QoS 机制:
bool vsoa_server_cli_priority(vsoa_server_t *server, vsoa_cli_id_t id, int new_prio);
- 函数成功返回
true
,失败返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端 id; - 参数
new_prio
:新的优先级值,范围为0(低)~5(高)
,默认值为0
;
注意:客户端的优先级可以动态更新。
设置客户端数据包发送超时时间(默认在网络被阻塞时等待发送):
bool vsoa_server_cli_send_timeout(vsoa_server_t *server, vsoa_cli_id_t id, const struct timespec *timeout);
- 函数成功返回
true
,失败返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端id。; - 参数
timeout
:设置的超时时间值;
如果timeout
值为NULL
,则表示使用默认的发送超时(100 ms)。如果连续三次发送超时,则连接将被断开。
配置或者获取客户端的授权状态:
bool vsoa_server_cli_set_authed(vsoa_server_t *server, vsoa_cli_id_t id, bool authed);
bool vsoa_server_cli_authed(vsoa_server_t *server, vsoa_cli_id_t id);
服务器可以设置并获取客户端的授权状态。当客户端连接到服务器并通过了密码检查时,客户端的身份验证状态为 true
,此时可以同时接收服务器的发布信息。当服务器仍然需要执行其他独立授权检查时,当客户端刚刚连接时,此状态可以设置为 false
,此时客户端将无法获取发布消息,直到客户端通过服务器的独立授权检查。
使用示例
vsoa_server_t *server;
...
void onclient(void *arg, vsoa_server_t *server, vsoa_cli_id_t id, bool connect)
{
if (connect) {
// We need independent authorization.
vsoa_server_cli_set_authed(server, id, false);
}
}
vsoa_server_on_cli(server, onclient, NULL);
/* Independent Authorization */
void onauth(void *arg, vsoa_server_t *server, vsoa_cli_id_t id, vsoa_header_t *vsoa_hdr, vsoa_url_t *url, vsoa_payload_t *payload)
{
if (/* Independent Authorization Verify */) {
vsoa_server_cli_set_authed(server, id, true);
/* The authorization verification is successful,
* and the server's publish information can be received */
vsoa_server_cli_reply(server, id, 0, vsoa_parser_get_seqno(vsoa_hdr), 0, NULL);
} else {
vsoa_server_cli_reply(server, id, AUTH ERROR CODE, vsoa_parser_get_seqno(vsoa_hdr), 0, NULL);
}
}
vsoa_url_t url = { .url: "/auth", .url_len: 5 };
vsoa_server_add_listener(server, &url, onauth, NULL);
此功能在 VSOA 1.0.7 及更高版本中可用。
Base Data Struct
基本数据结构的定义在 vsoa_parser.h
。
服务器和客户端之间的所有数据交换都需要唯一的标识: URL。
URL
typedef struct {
char *url;
size_t url_len;
} vsoa_url_t;
- 参数
url
:url字符串,必须从'/'
开头,例如"/res/a/b"
; - 参数
url_len
:url的字符串长度;
使用示例
vsoa_url_t url;
...
url.url = "/res/a/b"
url.url_len = strlen(url.url);
Payload
Payload 结构用于 VSOA 中除流外的所有数据传输。
typedef struct {
char *param;
size_t param_len;
void *data;
size_t data_len;
} vsoa_payload_t;
- 参数
param
:字符串类型,可选项; - 参数
param_len
:字符串长度,如果参数不存在需将其设置为0
; - 参数
data
:原始数据类型,可选项; - 参数
data_len
:数据的长度,如果参数不存在需将其设置为0
;
注意:我们强烈建议使用 JSON 格式作为 param
参数,并且可以轻松地使用 jstruct
工具来解析/组合参数 param
。对于这些数据,你可以使用 binary
格式(应考虑端序)或 base64
格式。
VSOA Packet Header
typedef struct {...} vsoa_header_t;
该结构体的成员一般不要直接访问,可以借助一些辅助程序宏:
#define vsoa_parser_get_seqno(vsoa_hdr) ...
#define vsoa_parser_get_type(vsoa_hdr) ...
#define vsoa_parser_get_flags(vsoa_hdr) ...
#define vsoa_parser_get_status(vsoa_hdr) ...
#define vsoa_parser_get_tunid(vsoa_hdr) ...
#define vsoa_parser_get_url_len(vsoa_hdr) ...
#define vsoa_parser_get_param_len(vsoa_hdr) ...
#define vsoa_parser_get_data_len(vsoa_hdr) ...
seqno
: 对于 RPC 来说是确保 RPC 相关业务完整性的一个非常重要的信息,因为 RPC 可以异步并行调用。type
: 由VSOA核心使用,用户不必关心;flags
: 可以从flags中获得的RPC方法:VSOA_FLAG_SET
;status
: RPC从服务器返回的代码,下表中数值为当前默认定义:
状态码 | 值 | 描述 |
---|---|---|
vsoa.code.SUCCESS | 0 | 调用成功 |
vsoa.code.PASSWORD | 1 | 密码错误 |
vsoa.code.ARGUMENTS | 2 | 参数错误 |
vsoa.code.INVALID_URL | 3 | 无效的URL |
vsoa.code.NO_RESPONDING | 4 | 服务器未响应 |
vsoa.code.NO_PERMISSIONS | 5 | 没有权限 |
vsoa.code.NO_MEMORY | 6 | 内存不足 |
vsoa.code.PROXY | 7 | 代理错误 (payload.param.error 指示错误消息) |
您还可以定义您自己的状态代码。用户定义的故障值建议为128
~ 254
。
tunid
: 隧道id,用于流传输,后面章节会有更多细节描述。url_len
: url的长度,由VSOA核心使用。param_len
: 参数的长度,由VSOA核心使用。data_len
: 数据的长度,由VSOA核心使用。
使用以下API打印标题信息:
void vsoa_parser_print_header(const vsoa_header_t *vsoa_hdr, bool data_detail);
- 参数
vsoa_hdr
:VSOA头指针; - 参数
data_detail
:以十六进制格式打印数据;
Server Publish
bool vsoa_server_publish(vsoa_server_t *server, const vsoa_url_t *url, const vsoa_payload_t *payload);
- 函数成功返回
true
,失败返回false
; - 参数
url
:当前 payload 对应的 url; - 参数
payload
:需要发布的 payload 数据;
如果需要大量且高频的数据发布,但对传输可靠性的要求并不严格,可以使用以下发布接口:
bool vsoa_server_quick_publish(vsoa_server_t *server, const vsoa_url_t *url, const vsoa_payload_t *payload);
在发布一个 url 之前,建议检查是否有客户端需要此 url 数据,如果没有,服务器则不需要进行发布:
bool vsoa_server_is_subscribed(vsoa_server_t *server, const vsoa_url_t *url);
- 有客户端订阅时,函数返回
true
,否则返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
url
:需要查询的 url;
使用示例
vsoa_server_t *server;
vsoa_url_t url;
vsoa_payload_t payload;
if (!vsoa_server_is_subscribed(server, &url)) {
return;
}
vsoa_server_publish(server, &url, &payload);
注意:URL匹配:URL使用 '/'
作为分隔符,例如:'/a/b/c'
。如果客户端订阅'/a/'
,服务器发布'/a'
、'/a/b'
或'/a/b/c'
消息,则客户端都将收到。
Server RPC
服务器应该首先为 RPC 的 URL 添加一个监听器:
bool vsoa_server_add_listener(vsoa_server_t *server, const vsoa_url_t *url, vsoa_server_cmd_func_t callback, void *arg);
- 有客户端订阅时,函数返回
true
,否则返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
url
:url 信息; - 参数
callback
:当服务器监测到 RPC 从客户端到达时,此回调将被调用; - 参数
arg
:回调函数的参数;
监听器回调原型:
typedef void (*vsoa_server_cmd_func_t)(void *arg, vsoa_server_t *server, vsoa_cli_id_t id, vsoa_header_t *vsoa_hdr, vsoa_url_t *url, vsoa_payload_t *payload);
- 参数
arg
:回调函数的参数; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端id; - 参数
vsoa_hdr
:VSOA 标准数据头; - 参数
url
:请求的 url 信息; - 参数
payload
:来自客户端的 payload 数据;
可以使用下列 API 动态删除 RPC 监听器:
void vsoa_server_remove_listener(vsoa_server_t *server, const vsoa_url_t *url);
当 RPC 数据到达时,服务器使用以下 API 来回复客户端:
bool vsoa_server_cli_reply(vsoa_server_t *server, vsoa_cli_id_t id, uint8_t status, uint32_t seqno, uint16_t tunid, const vsoa_payload_t *payload);
- 函数成功返回
true
,失败返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端 id; - 参数
status
:VSOA 状态码; - 参数
seqno
:与 RPC 请求相同; - 参数
tunid
:用于流传输; - 参数
payload
:对客户端的 payload,可以为NULL
;
通常,服务器在RPC监听器回调函数中回复客户端。
使用示例
vsoa_server_t *server;
vsoa_url_t url;
url.url = "/read";
url.url_len = strlen(url.url);
vsoa_server_add_listener(my_server.server, &url, command_read, NULL);
void command_read (void *arg, vsoa_server_t *server, vsoa_cli_id_t cid, vsoa_header_t *vsoa_hdr, vsoa_url_t *url, vsoa_payload_t *payload)
{
/* note: get the seqno from header */
uint32_t seqno = vsoa_parser_get_seqno(vsoa_hdr);
vsoa_payload_t payload_reply;
/* do somthing */
/* reply to client */
vsoa_server_cli_reply(server, cid, 0, seqno, 0, &payload_reply);
}
RPC URL 匹配规则
PATH | RPC URL 匹配规则 |
---|---|
"/" | 默认URL监听器 |
"/a/b/c" | 仅处理 "/a/b/c" 路径的调用 |
"/a/b/c/" | 处理 "/a/b/c" 和 "/a/b/c/..." 所有路径调用 |
注意:如果 "/a/b/c"
和 "/a/b/c/"
RPC处理程序同时存在,当客户端进行 "/a/b/c"
RPC调用时,"/a/b/c"
处理程序会在 "/a/b/c/"
处理程序之前完成匹配并进行处理。
Server Stream
一个流实际上是一个新的 TCP 连接通道:
vsoa_server_stream_t *vsoa_server_stream_create(vsoa_server_t *server);
- 函数成功返回流对象
- 参数
server
:VSOA 服务器句柄;
stream 结构体:
typedef struct vsoa_server_stream {
bool valid;
int listenfd;
int clifd;
uint16_t tunid;
void *custom;
} vsoa_server_stream_t;
- 参数
valid
:识别该流对象是否有效,由VSOA核心使用; - 参数
listenfd
:服务器使用此句柄来监听及接受客户端的连接; - 参数
clifd
:连接后的客户端句柄; - 参数
tunid
:新的TCP通道,服务器使用该通道回复客户端,客户端使用该通道连接到服务器; - 参数
custom
:自定义参数;
服务器使用以下API来接受客户端连接:
int vsoa_server_stream_accept(vsoa_server_stream_t *stream, struct sockaddr *addr, socklen_t *namelen, int keepalive);
- 函数成功返回 客户端句柄(服务器使用此句柄进行后续流传输),失败返回负数;
- 参数
stream
:流对象; - 参数
addr
:存储一个新的客户端连接地址,可以为NULL
; - 参数
namelen
:存储addr
的长度,可以为NULL
; - 参数
keepalive
:将被用于TCP_KEEPIDLE
和TCP_KEEPINTVL
(参考函数vsoa_server_cli_keepalive
);
完成流传输后,服务器应关闭该流:
void vsoa_server_stream_close(vsoa_server_stream_t *stream);
当流对象被关闭后,不允许再次被使用。
注意:我们强烈建议您为一个流分配一个独立的线程,以避免阻止正常的RPC请求。
更多细节,请参考 example/c/stream.c
。
Server Datagram
DATAGRAM(数据报)是另一种传输类型,通常这种类型的数据用于传输一些不需要确认的数据,例如,VSOA的数据报数据包可以用于构建VPN网络。
服务器向客户端发送数据报,这个API与vsoa_server_cli_reply()
非常相似,但是没有status
,seqno
,tunid
参数。
bool vsoa_server_cli_datagram(vsoa_server_t *server, vsoa_cli_id_t id, const vsoa_url_t *url, const vsoa_payload_t *payload);
- 函数成功返回
true
,失败返回false
; - 参数
server
:VSOA 服务器句柄; - 参数
id
:客户端 id; - 参数
url
:url 信息; - 参数
payload
:对客户端的 payload,可以为NULL
;
使用以下 API 可以通过快速通道向客户端发送数据报:
bool vsoa_server_cli_quick_datagram(vsoa_server_t *server, vsoa_cli_id_t id, const vsoa_url_t *url, const vsoa_payload_t *payload);
服务器接收来自客户端的数据报:
void vsoa_server_on_datagram(vsoa_server_t *server, vsoa_server_dat_func_t callback, void *arg);
- 参数
server
:VSOA 服务器句柄; - 参数
callback
:回调函数,当数据报到达服务器端该回调函数会被调用; - 参数
arg
:回调函数参数;
回调函数原型:
typedef void (*vsoa_server_dat_func_t)(void *arg, vsoa_server_t *server, vsoa_cli_id_t id, vsoa_url_t *url, vsoa_payload_t *payload, bool quick);
Custom Data Bind
使用下列两个函数可以绑定和扩展服务框架。
bool vsoa_server_cli_set_custom(vsoa_server_t *server, vsoa_cli_id_t id, void *custom);
void *vsoa_server_cli_custom(vsoa_server_t *server, vsoa_cli_id_t id);
VSOA Server Parameter Synchronizer
此功能可协助VSOA服务器同步数据并减少数据同步代码。在C版本中,它们被设计成宏,以便于适应不同的数据类型。它们在文件: libvsoa/vsoa_syncer.h
同步器返回值:
宏定义 | 值 |
---|---|
VSOA_SYNCER_PUB_OK | 0 |
VSOA_SYNCER_SET_OK | 1 |
VSOA_SYNCER_GET_OK | 2 |
VSOA_SYNCER_ERROR | -1 |
Publish Syncer
当有任何客户端订阅时,它将作为标准方式自动发布URL的数据。
vsoa_syncer_ret VSOA_PARAM_SYNCER_PUBLISH(server, url, module, struc, bin, bin_len);
- 函数返回状态代码,成功回复
VSOA_SYNCER_PUB_OK
,失败回复 “VSOA_SYNCER_ERROR”;+ 参数server
:VSOA 服务器句柄; - 参数
url
:URL 信息; - 参数
module
:数据类型模块前缀(由jstruct
自动生成); - 参数
struc
:数据类型的对象; - 参数
bin
:payload 数据; - 参数
bin_len
:payload 数据的长度;
RPC Syncer
vsoa_syncer_ret VSOA_PARAM_SYNCER_RPC(server, cli_id, module, struc, vsoa_header, payload, checker, arg);
如果获取请求方法,此函数将自动回复RPC请求。如果设置了该方法,此函数将自动解析和同步数据,并将最新的数据发布到所有其他客户端。
- 函数返回值可能是
VSOA_SYNCER_SET_OK
或VSOA_SYNCER_GET_OK
或VSOA SYNCER ERROR
; - 参数
server
:VSOA 服务器句柄; - 参数
cli_id
:客户端 id; - 参数
module
:数据类型模块前缀(由jstruct
自动生成); - 参数
struc
:数据类型的对象; - 参数
vsoa_header
:RPC 数据头; - 参数
payload
:payload 数据; - 参数
checker
:这是一个针对自定义有效检查的回调函数; - 参数
arg
:checker 的参数; - 该函数可通过 RPC(vsoa_server_cli_reply()函数) 回复客户端状态代码,成功回复
VSOA_STATUS_SUCCESS
,否则回复其他相应状态;
注意: checker
的函数原型是:
uint8_t (*checker) (void *arg, void *struc, vsoa_payload_t *payload);