2010年10月6日星期三

onvif设备搜索小结

Onvif是新提出的一种监控协议框架标准,它利用了现有的一些成熟的web service技术,组成了一个完整的监控协议。其中,设备搜索功能就使用到了ws-discoverysoap技术。下面就主要对在研究onvif设备搜索时遇到的问题,做一个总结。

Onvif的设备搜索机制和我们目前前端设备的搜索基本类似,都是基于的组播搜索:发送一个搜索请求(Probe)到一个指定的组播地址,然后等待前端的单播搜索应答(PM: Probe Match)。关于前端的搜索应答,我们目前采用的是组播应答,可以搜索到不同网段内的前端设备;而Onvif则仅使用单播来做应答;这样,Onvif只能搜索到同一网段内的设备(Onvif还另外提供了一种DP: Discovery Proxy机制来实现搜索不同网段内的设备)。
此外,Onvif还提供了Hello(前端上线通告),Bye(前端下线通告)和Resolve消息(为了兼容ws-discovery,局域网内搜索基本用不到)。


了解了Onvif的搜索机制后,我们就可以具体实现了。首先获得标准的ws-discoverywsdl和一个gsoap开发环境,利用gsoap,我们可以很轻松的生成包含所有搜索接口的代码文件。(具体生产方法可参考之前同事的一篇案例《gSOAP开发WebService程序》)
生成的相关函数声明如下:


我们最关心的是这两个搜索函数:
负责发送搜索请求(Probe)的soap_send___tns__ProbeOp和负责接收搜索应答的(Probe Match)的soap_recv___tns__ProbeMatchOp,如下:



好,万事俱备,我们只需调用这两个接口就能实现搜索了。可惜,事实证明前途是光明的,道路是曲折的,在我们通过搜索获取到设备基本信息前,还得越过几道坎:
1.      Uuid
我们的程序第一次运行后,并没有出现我们期望的结果。不慌,首先抓包,看看我们的搜索请求有没有发出去,ok,抓包数据如下:

仔细分析了下我们的数据和Onvif官方提供的测试工具发出的搜索请求包,发现并没有少哪一个字段啊?并且所有涉及的命名空间也都一致,可就是我们发出的搜索请求后,一个前端应答都没有。那是不是消息中的哪个字段内容不对呢?回头再读文档,终于找到了这一段:

其中提到,为了防止被攻击,设备应该忽略带有重复messageID的请求,而对照我们的代码,其中的messageID字段是我们自己构造的,写死的,每次的请求都是一样的,自然就被前端给忽略掉了(也许有人说了,那第一次呢?对,我们第一次发请求时,前端其实是响应的,可惜,我们第一次搜索时没抓包啊!并且,可以想见,这个措施是有时效性的,只要我们一直发送请求,原来前端“记忆”的ID失效了,那就会认为是一个正常的请求而响应了)。

2.      AppSequence
问题定位了,那就好办了,网上找了下uuid的生成代码,其实就是一个时间和空间上全局唯一的号,每次发搜索请求时,生成一个uuid,然后填充到消息了。重新编译,再次运行程序,这次抓包终于看到响应了,前端回复了应答;可是再看我们的程序,还是没显示搜到的前端。应答包是收到了,可程序并没有返回结果,应该是程序内部出了问题,可这部分的工作其实是gsoap生成的代码完成的,难道只能单步跟踪吗?幸好,gsoap还提供了日志功能,先看看日志里能不能提供些有用的信息:
Unexpected element 'wsa:AppSequence' in input (level=2, 1)
比较明显的一处非正常提示,通过提示,找到了具体代码,看来是解析消息出错了,不认识“AppSequence”这个元素;再来看我们抓到的应答包,里面的确有这项数据:

代码是gsoap生成的,是生成代码有误,还是我们提供的源文件有问题?google了下,同时又看了下文档,才知道AppSequencesoap:Envelope::Head中的一个可选元素,但在标准的ws-discovery中的soap:Envelope::Head中是没有显式提供的,所以导致gsoap生成代码时没有包含对该元素的解析。我们只需要在soap:Envelope的定义中包含AppSequence即可:

#ifndef SOAP_TYPE_SOAP_ENV__Header
#define SOAP_TYPE_SOAP_ENV__Header (29)
/* SOAP Header: */
struct SOAP_ENV__Header
{
        char *wsa__MessageID; /* optional element of type wsa:MessageID */
        struct wsa__Relationship *wsa__RelatesTo;   /* optional element of type wsa:RelatesTo */
        struct wsa__EndpointReferenceType *wsa__From;   /* optional element of type wsa:From */
        struct wsa__EndpointReferenceType *wsa__ReplyTo;     /* mustUnderstand */
        struct wsa__EndpointReferenceType *wsa__FaultTo;     /* mustUnderstand */
        char *wsa__To;    /* mustUnderstand */
        char *wsa__Action;    /* mustUnderstand */
        struct tns__AppSequenceType tns__AppSequence;  /* mustUnderstand */
};
#endif
终于,经过修改的代码运行后,屏幕上出现了期望的打印:

说明下,由于Onvif搜索采用的ws-discovery技术,所以搜索到的不都是Onvif设备;只要是支持ws-discovery的设备都可以搜到,譬如上图中的172.16.200.75就是一台WIN7系统的PC

3.      又是AppSequence
平静没几天,又出问题了:上次测的是一款AXIS设备,这次换成了另一款SONY的设备,发现搜不到了,抓包看了下,没有搜索应答包;又是我们的搜索请求内容不符合规范吗?可究竟是哪儿呢?恰巧,Onvif协议的设备通信部分也基本做好了,可以获取前端的一些其他信息了,于是试了下这款SONY设备,看基于TCPHttp交互信息能否成功;结果是:获取同样失败了,不过,这次tcp交互却把错误原因显示了出来:
<SOAP-ENV:Fault SOAP-ENV:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><SOAP-ENV:Code><SOAP-ENV:Value>SOAP-ENV:MustUnderstand</SOAP-ENV:Value></SOAP-ENV:Code><SOAP-ENV:Reason><SOAP-ENV:Text xml:lang="en">The data in element 'wsa:AppSequence' must be understood but cannot be handled</SOAP-ENV:Text></SOAP-ENV:Reason></SOAP-ENV:Fault>
找到问题所在了,又是AppSequence惹得祸;分析了下错误信息:原来,AppSequence是一个可选项(optional),而我们在上次改动时,却把AppSequence设置成了必须项(mustUnderstand),导致发给前端后,前端发现解析不了这个mustUnderstand项,所以导致后续的失败(也就是说SONY的设备并不关心这个AppSequence)。修改也很简单,把mustUnderstand去掉即可,即变成:
struct tns__AppSequenceType tns__AppSequence; /* optional element of type tns:AppSequence */

1.      Onvif提供的搜索机制和我们原先的搜索机制有很大的不同,其中消息内容中的很多字段都有一定用途,需要很好的理解。
2.      gsoap还是很强大的,不仅体现在快速开发上,一些错误处理机制,也使开发人员能比较容易定位问题。
3.      Onvif除了搜索机制外,还涉及了许多其他web service服务,特别是安全机制,对比我们目前的协议,可以说基本没有考虑安全,所以,值得好好研究。

2010年8月29日星期日

IPv6小结

1. 关于sockaddr_in6
在windowsXP SP3+vc6.0下,该结构体为:

1
2
3
4
5
6
struct sockaddr_in6 {
short sin6_family;    /* AF_INET6 */
u_short sin6_port; /* Transport level port number */
u_long sin6_flowinfo; /* IPv6 flow information */
struct in_addr6 sin6_addr; /* IPv6 address */
};

大小为24个字节;在使用WSAStringToAddress转换函数时,使用该结构体会失败,原因就是结构体大小小于函数所需要的28个字节。

查看了同一台机器上的visual studio 2005的头文件(ws2tcpip.h),上述结构体被命名为old,而同样名字则使用了另一个结构体,大小为28字节:

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* Old IPv6 socket address structure (retained for sockaddr_gen definition below) */
 
struct sockaddr_in6_old {
   short   sin6_family;        /* AF_INET6 */
   u_short sin6_port;          /* Transport level port number */
   u_long  sin6_flowinfo;      /* IPv6 flow information */
   struct in6_addr sin6_addr;  /* IPv6 address */
};
 
/* IPv6 socket address structure, RFC 2553 */
 
struct sockaddr_in6 {
   short   sin6_family;        /* AF_INET6 */
   u_short sin6_port;          /* Transport level port number */
   u_long  sin6_flowinfo;      /* IPv6 flow information */
   struct in6_addr sin6_addr;  /* IPv6 address */
   u_long sin6_scope_id;       /* set of interfaces for a scope */
};



关于这个多出来的scope_id变量,具体应用和意义不是很清楚(link-local地址会有所涉及)。

网上搜了下,发现另一个系统建议的同时支持IPv4和IPv6的结构体:


1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct sockaddr_storage {
   short ss_family;               // Address family.
   char __ss_pad1[_SS_PAD1SIZE];  // 6 byte pad, this is to make
                                  // implementation specific pad up to
                                  // alignment field that follows  explicit
                                  // in the data structure.
   __int64 __ss_align;            // Field to force desired structure.
   char __ss_pad2[_SS_PAD2SIZE];  // 112 byte pad to achieve desired size;
                                  // _SS_MAXSIZE value minus size  of
                                  // ss_family, __ss_pad1, and
                                  // __ss_align fields is 112.
};

此结构体大小为128字节,足够大了(大到目前还不知道多出来的空间用于存放什么数据)。
而linux下,sockaddr_in6结构体和windows下最新的结构体一致,sockaddr_storage大小也是128;所以对于windows下vc6.0以下,采用sockaddr_storage结构体,而其他则一律采用sockaddr_in6结构体。


1
2
3
4
5
6
7
8
typedef union tagSockAddr
{
   struct sockaddr_in ip4;
   struct sockaddr_in6 ip6;
#if defined(_WIN32) && _MSC_VER <= 1200
   struct sockaddr_storage ip;
#endif
} USockAddr;
(vc6.0对应的_MSC_VER为1200)

2. 关于IPv6服务器同时支持IPv4和IPv6客户端
按照UNPv3上说,可以在服务器上仅创建一个PF_INET6的socket,bind在IPv6的通配地址,此时该监听socket可以同时支持IPv4和IPv6的客户端接入(当然,前提条件是该服务器同时需具有IPv4地址和IPv6地址),而IPv4客户端接入进来后显示的地址为一个IPv4映射的IPv6地址(可以通过IN6_IS_ADDR_V4MAPPED宏辨别)。

同时,如果不需要这种特性,而强制只是支持IPv6客户端接入,可以通过设置socket属性:IPV6_V6ONLY来修改。

linux上一切ok,然而到了windows上,同样的情况下,windows默认只支持接入IPv6客户端(可恶的默认值),即IPV6_V6ONLY属性值为非0,

而要想在windows上设置该属性,需要vista或者更新的windows版本(完全忽视了windowsXP+vc6.0的用户)。

摘自MSDN:
Indicates if a socket created for the AF_INET6 address family is restricted to IPv6 communications only. Sockets created for the AF_INET6 address family may be used for both IPv6 and IPv4 communications. Some applications may want to restrict their use of a socket created for the AF_INET6 address family to IPv6 communications only.
When this value is non-zero (the default on Windows), a socket created for the AF_INET6 address family can be used to send and receive IPv6 packets only.
When this value is zero, a socket created for the AF_INET6 address family can be used to send and receive packets to and from an IPv6 address or an IPv4 address. Note that the ability to interact with an IPv4 address requires the use of IPv4 mapped addresses.
This socket option is supported on Windows Vista or later.

3. 关于WSAStringToAddress和WSAAddressToString函数
windows提供的这两个函数提供了sockaddr结构和字符串之间的转换,linux下类似的为inet_ntop和inet_pton。比较特殊的是windows这两个函数中的字符串格式不仅仅是ip地址,还包含了端口和网络接口信息,如:[fe80::21e:c9ff:fe45:832c%4]:55392,把sockaddr中所有有效的信息都返回了。同样的,在WSAStringToAddress中,可以把类似结构的字符串传入,得到对应的sockaddr结构体。

个人意见:KISS原则;按照这种做法,本来通过一次调用就可以获得的ip地址,现在还要额外做一次分析工作(不知到有没有提供相应的宏),其实这些函数的需求是将不可读的ip地址变为可读,其他信息,直接在需要时从sockaddr结构体中就可获得。

4. 关于link-local地址
实践下来,link-local地址是可以用于局域网内的通信(不过路由器),需注意的是,利用link-local地址进行通信,需指定本地网络接口。例如ping:

windows:ping6 fe80::21c:c0ff:feeb:5cc3%4 (本地网络接口id为4)
linux:ping6 -Ieth0 fe80::21e:c9ff:fe34:5112 (本地网络接口eth0)

而在具体编程中,通过strace上述命令,可以看到实际该接口id是设给了sockaddr_in6.sin6_scope_id = if_nametoindex("eth0");

但为什么link-local地址通信时,需要针对具体网络接口,还不知道原因?而相对的global地址,却不需要。
这个问题被我在迷糊中想到了一个解释(很灵异):之前,如果有多个网络接口的话,那么接口上的ip地址不能为同一网段,否则就会冲突;而现在,IPv6给每个网络接口都默认分配了一个link-local地址,即FE80开头的地址,显然都是同一网段的;此时如果和该网段的另一个地址通信,系统会不知道源地址采用哪一个接口上的地址,因为无论哪个其实都可以和目标进行通信,所以,必须人为的指定具体接口。

5. 关于组播组
介绍两个特别的组播组的用法。FF01::1和FF02::1,分别ping这两个组播组,凡是响应第一个的,都是支持IPv6的局域网内的机子(包括linux和windows);而响应第二个的,则仅是windows主机。原理是所有支持IPv6的主机都会加入FF01::1这个组播组,而windows主机还会加入FF02::1这个组播组。

2010年8月15日星期日

开始IPv6

开始接触IPv6了,在linux的机子上配了下,发现了link-local这个地址,可以直接从MAC地址转换过来(EUI-64),但在局域网内的通信,貌似用这个地址还不行,仅能ping通;后来,自己配了个global地址,还加了下路由,通信ok了。

初步有一个设想:在支持IPv6的机子上,监听某个端口,地址为IPv6的::(unspecified address), 如果当前为IPv6的网络,那一切通信以IPv6进行;反之,如果当前是IPv4的网络,且本机配置了IPv4的地址,那其他IPv4的主机连接时,会以IPv4映射IPv6的地址形式在netstat里显示,如::ffff:172:16:197:80,那tcp的话,之间的通信应该没问题,内核会进行这之间的地址转化,那同样的,udp应该也ok吧。当然,只考虑了单播,组播应该更复杂些,同时兼容IPv4和IPv6,How? 进一步,如果要实现基于链路的PF_PACK接收组播,又该如何兼容呢?

恩,环境也很重要;没环境,只能纸上谈兵了。