定义一些常量
#define SOAP_SOCK_TIMEOUT (10) // socket超时时间(单秒秒)
#define SOAP_TO "urn:schemas-xmlsoap-org:ws:2005:04:discovery"
#define SOAP_ACTION "http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe"
#define SOAP_MCAST_ADDR "soap.udp://239.255.255.250:3702" // onvif规定的组播地址
#define SOAP_ITEM "" // 寻找的设备范围
#define SOAP_COMPAT_TYPES "dn:NetworkVideoTransmitter" // 寻找的设备类型
239.255..255.250:3702是一个基于UDP协议的网络服务,也被称为Simple Service Discovery Protocol(简称SSDP)。它的作用是为了发现网络中的设备和服务。在实际的应用场景中,239.255..255.250:3702通常是被智能设备、软件或者网页应用所使用的,例如智能音箱、智能家居、互联网电视等。当局域网中的设备连接上互联网时,也可以通过239.255..255.250:3702来搜索网络中可用的服务,包括设备名称、IP地址、端口号等。
在实际的应用场景中,239.255..255.250:3702采用了多播方式,即多个设备可以同时监听和发送消息。当一个设备发送一个搜索请求时,其它设备可以通过响应的方式向发起搜索请求的设备发送消息,同时发起广告宣传自己的服务。
dn:NetworkVideoTransmitter 是摄像头的设备类型
urn:schemas-xmlsoap-org:ws:2005:04:discovery 和 http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe 代表是一条Probe消息,是WS-Discovery协议规定的消息头。
定义一个Soap类型,用来管理上下文
class OnvifSoap {
public:
OnvifSoap(int timeout) {
// There is no need to call soap_init to initialize the context
// allocated with soap_new, since soap_new initializes the allocated
// context.
soap_ = soap_new();
soap_set_namespaces(soap_, namespaces); // 设置soap的namespaces
// 不正常数据设置成20s
if (timeout <= 0)
timeout = 20;
soap_->recv_timeout = timeout; // 设置超时(超过指定时间没有数据就退出)
soap_->send_timeout = timeout;
soap_->connect_timeout = timeout;
#if defined(__linux__) || \
defined(__linux) // 参考https://www.genivia.com/dev.html#client-c的修改:
soap_->socket_flags =
MSG_NOSIGNAL; // To prevent connection reset errors
#endif
soap_set_mode(
soap_,
SOAP_C_UTFSTRING); // 设置为UTF-8编码,否则叠加中文OSD会乱码
}
~OnvifSoap() {
soap_destroy(
soap_); // deletes data, array, and other managed C++ objects
soap_end(soap_); // delete managed memory。soap_malloc
// soap_done(soap_); // Reset, close communications, and remove
// callbacks
soap_free(soap_); /* we're done with the context */
}
struct soap *soap() {
return soap_;
}
void *Malloc(size_t n) {
if (!n) {
return nullptr;
}
// Allocate a block of heap memory managed by the specified soap context
// All such blocks allocated are deleted with a single call to soap_end.
auto p = soap_malloc(soap_, n);
assert(p);
return p;
}
const char *WsaRandUuid() { return soap_wsa_rand_uuid(soap_); }
void InitHeader() {
// T * soap_new_T(struct soap*) allocates and initializes data of type T
// in context-managed heap memory, managed data is deleted with
// soap_destroy (deletes C++ objects) and soap_end (deletes all other
// data), and you can also use soap_malloc to allocate uninitialized
// context-managed memory.
struct SOAP_ENV__Header *header = soap_new_SOAP_ENV__Header(soap_);
soap_default_SOAP_ENV__Header(soap_, header);
header->wsa__MessageID = (char *)this->WsaRandUuid();
header->wsa__To = (char *)this->Malloc(strlen(SOAP_TO) + 1);
header->wsa__Action = (char *)this->Malloc(strlen(SOAP_ACTION) + 1);
strcpy(header->wsa__To, SOAP_TO);
strcpy(header->wsa__Action, SOAP_ACTION);
soap_->header = header;
}
void InitProbeType(struct wsdd__ProbeType *probe) {
// 用于描述查找哪类的Web服务
struct wsdd__ScopesType *scope = soap_new_wsdd__ScopesType(soap_);
soap_default_wsdd__ScopesType(soap_, scope); // 设置寻找设备的范围
scope->__item = "";
// soap_default_wsdd__ProbeType(soap_, probe);
probe->Scopes = scope;
probe->Types = (char *)SOAP_COMPAT_TYPES; // 设置寻找设备的类型
}
int Error() { return soap_->error; }
private:
struct soap *soap_;
};
soap对象创建与销毁
构造函数中,使用soap_new()来创建 struct soap对象,在析构函数中销毁。对于上下文中管理的对象等其他东西,需要soap_destroy、soap_end和soap_free结合一起使用。
填充消息头
见InitHeader()
T * soap_new_T(struct soap*) 函数是一个通用的在堆上分配并初始化对象一种对象,管理在上下文中,这种对象可以使用soap_destroy和soap_end销毁。
有一个默认的设置函数soap_default_XXX需要调用。
soap_malloc创建的对象需要使用soap_end销毁。
本步骤构造的就是如下一个消息结构,uuid随机生成的。
<s:Header>
<a:MessageID>uuid:9328fd5b-4c83-4e4f-9850-66549c6d9136</a:MessageID>
<a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>
<a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Hello</a:Action>
</s:Header>
填充Probe
见InitProbeType()
需要填充寻找设备的范围,和设备的类型。
设备扫描
void ONVIF_DetectDevice(void (*cb)(char *DeviceXAddr)) {
int i;
int result = 0;
unsigned int count = 0; // 搜索到的设备个数
struct wsdd__ProbeType req; // 用于发送Probe消息
struct __wsdd__ProbeMatches rep; // 用于接收Probe应答
struct wsdd__ProbeMatchType *probeMatch;
OnvifSoap onvif_soap(SOAP_SOCK_TIMEOUT);
onvif_soap.InitHeader(); // 设置消息头描述
onvif_soap.InitProbeType(&req); // 设置寻找的设备的范围和类型
result = soap_send___wsdd__Probe(onvif_soap.soap(), SOAP_MCAST_ADDR, NULL,
&req); // 向组播地址广播Probe消息
while (SOAP_OK == result) // 开始循环接收设备发送过来的消息
{
soap_default___wsdd__ProbeMatches(onvif_soap.soap(), &rep);
result = soap_recv___wsdd__ProbeMatches(onvif_soap.soap(), &rep);
if (SOAP_OK == result) {
if (onvif_soap.Error()) {
soap_perror(onvif_soap.soap(), "ProbeMatches");
} else { // 成功接收到设备的应答消息
if (NULL != rep.wsdd__ProbeMatches) {
count += rep.wsdd__ProbeMatches->__sizeProbeMatch;
for (i = 0; i < rep.wsdd__ProbeMatches->__sizeProbeMatch;
i++) {
probeMatch = rep.wsdd__ProbeMatches->ProbeMatch + i;
std::cout << probeMatch->XAddrs << ", "
<< probeMatch->Types << std::endl;
}
}
}
} else if (onvif_soap.Error()) {
break;
}
}
SOAP_DBGLOG("\ndetect end! It has detected %d devices!\n", count);
return;
}
发送到soap.udp://239.255.255.250:3702 地址的格式尤为重要,根据前面所说这是一个广播地址,前面的协议头也是必须给的。
在发送广播之后,需要循环接收设备发过来的消息,所以是个循环。
对于onvif设备来说,我们最终得到的地址probeMatch->XAddrs 是最重要的部分,所有后续的接口请求都需要使用这个接口(或者使用下面细分的接口地址——但也必须从这个获取)。
源代码参考:onvif_demo/tests/scan_device.cpp at main · NoevilMe/onvif_demo · GitHub