目录
4.2、使用string类对象给函数传递字符串参数要加上.c_str()
4.5、调用API函数IcmpSendEcho发送ping包,该如何判断有没有ping通对方
今天将以前做过的一个小工具项目的经历与总结,分享给大家,希望能给大家提供一个借鉴或参考!小项目中主要涉及两大功能点,一是通过ping检测IPC监控摄像头是否在线,二是使用libcurl开源库发送设备不在线的告警邮件。下面将讲述了整个项目的开发调试过程,并对遇到的问题进行了详细地总结。
1、项目背景与项目需求
项目背景是这样的,客户使用了一个国外厂商的视频监控系统,视频管理平台可以监控到前端IPC摄像头的在线状态(使用软件间的心跳检测),但检测到IPC摄像头异常后,视频管理平台无法将告警信息发送到客户的163企业邮箱中,即视频管理平台的发送邮件功能有问题,调试了很久始终没有解决。
客户坚持要使用他们的163企业邮箱接收告警信息,并且客户要求尽快解决这个问题,这个问题解决后才能对整个项目进行验收!如果将这个需求反馈给国外的开发人员,来来回回会折腾很久,问题能不能彻底解决还不好说。
后来项目方公司领导想到一个临时替代办法,可以采用ping IPC摄像头的方式,简单地判断设备是否出网络故障。如果IPC摄像头ping不通,可能IPC摄像头设备是出问题了,然后将故障告警信息发送到指定的163企业邮箱中。
于是找到我,帮他们开发这个小工具,具体需求点如下:
(1)IPC设备管理
支持对IPC摄像头设备的管理,添加IPC摄像头的设备名称、设备ip,支持设备的增删改;这些设备信息保存在本地即可。
(2)对IPC设备的ping检测
对添加的IPC设备开启多线程进行检测,通过ping设备的IP地址,检测设备是否出网络故障,ping失败次数达到设定上限后,则认为设备出现网络故障了。
(3)发送IPC设备异常告警的邮件
检测出设备网络异常,通过公用的smtp服务器或者客户自己的邮箱系统自带的smtp服务器将故障信息邮件发到指定的163企业邮箱中进行告警。该软件中要支持配置smtp邮件服务器地址、发送告警信息的邮箱地址,以及接收告警邮件的邮箱地址。要支持发送测试邮件,判断当前的配置能否正常发送邮件。
2、项目技术实现思路
(1)ping功能实现
最开始想到使用裸socket,调用sendto和recvfrom,自己构造ping包的数据头,但是这个裸socket在windows上需要管理员权限,在win7及以上系统上会弹出UAC提示框,客户使用起来会比较麻烦。
因为windows上裸socket需要管理员权限,而linux上没有这个问题,所以研究了一下windows上有没有替代方案,找到了IcmpSendEcho API函数,可以实现ping包的发送与回复检测。但是发ping包,ping包在公网上跑可能会被路由器等网络设备拦截掉,所以发ping包的方法也不太保险。后来还是决定使用这个方法,因为管理软件和前端IPC摄像头运行在同一个局域网中,网络环境比较简单,不会走公网,所以不会有数据包被拦截的问题。
(2)发送邮件功能实现
到网上搜索到发邮件常用的smtp协议,找到了一些代码,但这些代码是直接使用socket api实现的,比较简单,虽然包含了smtp的基本协议,但都是先send马上recv,缺少对网络及协议交互的异常处理,很不可靠,很不稳定,根本没法用的。经过测试发现,邮件有的成功有的失败,会经常发送不成功。
后来搜索发现,可以使用开源库libcurl中支持smtp协议的邮件发送功能,编写了相关测试代码测试了一下,确实是有效果的。使用朋友自己的webmail smtp服务器进行测试,每次都能发送成功,比之前网上搜到的简单代码要靠谱的多。所以决定使用开源库libcurl来实现发邮件的功能。
3、libcurl开源库的编译
3.1、尝试使用现成的libcurl库
本来这个小项目急着用,以关键字curl到平时做过的代码目录中搜索了一下,看看之前的项目中有没有libcurl库的工程,可以直接拿来使用,就不用花时间去研究开源库libcurl的编译了。因为直接到libcurl官网上下载源码,编译和配置估计比较麻烦,会比较耗时。
在之前的项目中搜到了现成的libcurl库后,将之拿到当前的工程中测试,结果调用发送邮件的接口curl_easy_perform返回失败了,返回的错误码为CURLE_UNSUPPORTED_PROTOCOL,难道是当前使用的版本库不支持SMTP协议?
按讲不应该啊,SMTP协议是libcurl开源库官方支持的啊!查看代码才发现,原来是之前有人使用CURL_DISABLE_SMTP和HTTP_ONLY宏将SMTP、FTP、telnet等不用的协议都裁剪掉了,只保留了主要的HTTP功能,其他协议的.c文件也从项目工程中删除了,但磁盘上还是有的。
于是尝试着将SMTP等协议功能添加进来,将DISABLE宏注释掉,编译libcurl,提示链接不到SMTP模块的接口。后来发现smtp.c没有添加到工程中,即smtp.c没有编译,所以链接不到SMTP模块的接口。
但加进去后编译还是有问题,来回试了好几次还是不行,所以不得不到官网上下载新版本的libcurl,搜索libcurl的编译配置方法,只能使用官方的代码编译出libcurl库文件了。
3.2、7.32.0版本 libcurl库的编译
当下只能去libcurl的官网下载最新版本的源码,自己来编译了。官网最新版为7.54.0,下载到源码包,但这个压缩包的curl-7.54.0\projects\Windows路径下VC6-VC14各个版本的VS解决方案,在打开后编译,会提示找不到openssl相关的头文件:
fatal error C1083: 无法打开包括文件:“openssl/ssl.h”: No such file or directory
即libcurl库依赖到了openssl,但本地找不到openssl的相关文件,所以报出上面的错误。
于是需要下载openssl来手动安装配置一下。配置openssl有两种方法,一种是下载安装包直接安装(就像使用WindowsSDK包一样),一种是将openssl的代码下载下来编译配置。当然是前者最简单,可以去搜索Windows下openssl的下载安装和使用说明。但是因为时间比较紧,要抓紧实现并测试效果,所以就没时间去配置安装openssl并编译新版本的libcurl。正好看到相关文章,得知7.32.0版本(某个阶段的老版本,不是最新版本)是可以直接拿来用VS2010编译的,于是到官网上去下载7.32.0版本。根据文中的说明,可以很快将libcurl编译出来并加以使用。
3.3、7.54.0版本 libcurl库的编译
后来有时间的时候,研究了一下最新版7.54.0 libcurl库的编译。7.54新版本的libcurl新增了openssl库的依赖,需要事先配置openssl才能编译通过。另外,还需要libssl,也需要事先部署一下。
因为编译openssl之前要下载perl脚本工具,要处理makefile文件,比较复杂,可以选择下载openssl的安装包,手动安装。libcurl包含了openssl的头文件,引入了openssl的lib和dll库,所以要从openssl安装目录下拷贝头文件和库文件到我们的工程目录下。对于依赖的libssl库,则比较简单,直接到官网上将开源代码下载下来编译即可。
4、调试中遇到的问题
4.1、调试退出程序时的内存泄漏
代码写好后,对代码进行调试,发现退出时有较多的内存泄露。按讲不应该啊,代码中基本没有new和malloc,不应该有内存泄露。更为奇怪的是,竟然提示stl的string类有泄露,这个类的内存应该自管理的,是Visual Studio(后面简称VS)官方的类,肯定是没有问题的。
内存泄露对于长时间运行的监控程序是致命的,必须要解决掉,但是通过现有现象看感觉很诡异。根据退出时VS的提示,很多泄露发生在发送邮件的类CSmtpSendMail中,这个类中的成员都是string类型的变量,将发送邮件的代码注释掉,重新测试了下,就没有泄漏了。
后来想到,可能是监测线程的代码中sleep了30分钟(配置中配置了扫描间隔为30分钟),但是中途尝试退出程序时会先将启动的监测线程退出来,具体的做法是:将线程中的表示变量m_bStart置为FALSE,让监测线程函数自己退出,然后wait 5秒中,如果没有自动退出,则手动调用TerminateThread API函数将线程直接terminate掉。
因为sleep 了30分钟(软件中支持设置ping设备的间隔时间,间隔时间设置为30分钟),所以监测线程没有自动退掉,所以被我们强行terminate掉了,而此时可能正在运行发邮件的代码,导致发邮件的对象类没有正常析构,所以导致成员string类对象中的内存没有正常释放,从而导致了内存泄漏。
其实,是代码写的有问题,即便是sleep 30分钟,也要保证在m_bStart设置为FALSE时能及时退出线程,具体做法将间隔时间分成比如50ms因子若干倍m,然后for循环将50ms因子的sleep执行m次,每次执行sleep 50ms之前,检查一下m_bStart是否为FALSE,是FALSE说明当前要退出线程,直接退出sleep循环,让线程函数尽快退出,线程函数退出了,线程就结束了。
4.2、使用string类对象给函数传递字符串参数要加上.c_str()
调用CSmtpSendMail::SendMail函数发送邮件失败,调试发现是curl_easy_perform函数返回失败,返回CURLE_COULDNT_RESOLVE_HOST错误码,即不能解析出smtp服务器域名地址。一般smtp邮件服务器的地址都写成域名,所以libcurl库内部在连接之前需要进行域名解析。后来发现,需要将传递给curl_easy_setopt函数的参数由tmp改成tmp.c_str(),因为传递的应该是一个字符串。有问题的代码如下:
std::string tmp = "smtp://";
tmp += m_strServerName;
curl_easy_setopt(curl, CURLOPT_URL, tmp);
将tmp改成tmp.c_str()就正常了。
4.3、当前运行调试程序的PC的IP被邮箱服务器锁死了
用同学自己搭建的开源的webmail邮件系统测试邮件发送功能。上午测试的还好好,每封邮件都能发出来,下午测试的时候就连不上服务器了,这个就奇怪了,上午还好好的,下午怎么就有问题了呢?难道是邮件服务器挂掉了?调试程序连不上,使用同学给的web邮箱服务器主页地址,在浏览器中也打不开。
与同学沟通后,猜测可能是ip被锁住了,禁止登录了。用手机移动数据访问是能打开的,应该是电脑ip被锁住了。于是登录到路由器上,断开网络连接,重新连接后宽带运营商给路由器重新分配了ip地址就能发送邮件了。
这个IP限制,在某些视频网站上也有,比如网站限制当前PC只能看几个视频,超过数目限制,就不能观看了。此时,可以试试登录路由器挂断拨号,重新连接,运营商重新分配了一个IP就好了。有时,可能换IP还不行,还要清理浏览器缓存,然后用360清理一下系统才行。
至于为什么IP被锁死,不是很确定,可能是我们发送测试邮件的某些行为触发了邮箱服务器上设定某些安全规则,服务器将从PC的IP地址上发过来的数据包直接拒绝掉,比如多次密码输入错误,服务器会锁死ip一段时间,因为系统要将穷举法盗取密码的行为遏制住。邮箱有很多安全策略,比如短时间内发大量的邮件会触发规则。
4.4、用搜狐邮箱的邮件服务器就有问题
开源的libcurl要好很多,使用同学的webmail给163发送邮件,每次都能发送成功。但使用搜狐的邮箱服务器(注册了一个搜狐邮箱,登录后在设置中启用了smtp服务)给163邮箱发,却是时而成功,时而失败。
后来和海康威视的监控客户端对比了一下(占据视频监控市场份额全球第一的海康威视,他的客户端是可以在其官方下载,免费登录,免费使用的),海康的客户端设置中有设置邮件的功能,支持发送测试邮件,如果使用搜狐的smtp邮件服务器也有类似的情况。这样对比下来看,可能搜狐邮箱系统的smtp服务是有问题的。
后来使用126的服务器给163的邮箱发送测试,海康能发送成功,但我们的有问题,查看返回的错误码是向对端发送数据失败。这有点奇怪,难道海康用的是libcurl的multi接口,回头可以用api monitor监测一下。
4.5、调用API函数IcmpSendEcho发送ping包,该如何判断有没有ping通对方
从msdn上查看IcmpSendEcho系统API函数的说明:
DWORD WINAPI IcmpSendEcho(
HANDLE IcmpHandle,
IPAddr DestinationAddress,
LPVOID RequestData,
WORD RequestSize,
PIP_OPTION_INFORMATION RequestOptions,
LPVOID ReplyBuffer,
DWORD ReplySize,
DWORD Timeout
);
Return Value:
The number of replies received and stored in the reply buffer indicates success. Zero indicates failure. Extended error information is available through GetLastError.
查看该函数返回值的说明,返回值为0,则表示失败。通过MSDN上给出的示例代码也能看出来是这样的:
dwRetVal = IcmpSendEcho(hIcmpFile, ipaddr, SendData, sizeof(SendData),
NULL, ReplyBuffer, ReplySize, 1000);
if (dwRetVal != 0) {
PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)ReplyBuffer;
struct in_addr ReplyAddr;
ReplyAddr.S_un.S_addr = pEchoReply->Address;
printf("\tSent icmp message to %s\n", argv[1]);
if (dwRetVal > 1) {
printf("\tReceived %ld icmp message responses\n", dwRetVal);
printf("\tInformation from the first response:\n");
}
else {
printf("\tReceived %ld icmp message response\n", dwRetVal);
printf("\tInformation from this response:\n");
}
printf("\t Received from %s\n", inet_ntoa( ReplyAddr ) );
printf("\t Status = %ld\n",
pEchoReply->Status);
printf("\t Roundtrip time = %ld milliseconds\n",
pEchoReply->RoundTripTime);
}
else {
printf("\tCall to IcmpSendEcho failed.\n");
printf("\tIcmpSendEcho returned error: %ld\n", GetLastError() );
return 1;
}
但测试发现,添加一个不存在的IP地址,IcmpSendEcho有时会返回0,有时会返回非0,所以不能通过IcmpSendEcho的返回值来判断是否是能ping同对方了。
于是百度了一下IcmpSendEcho函数,在百度百科中找到了一个很有用的说明:
void Ping(char *pIPAddr)
{
HANDLE iHwnd;
iHwnd=IcmpCreateFile();
IPAddr pAddr;
pAddr=(IPAddr)inet_addr (pIPAddr);
icmp_echo_reply pData;
for(int i=1;i<=LoopSend;i++)
{
IcmpSendEcho(iHwnd,pAddr,NULL,0,NULL,(LPVOID)&pData,sizeof(icmp_echo_reply),0);
if (pData.Status==0)
{
printf("Ping测试返回的结果: Time=%dms TTL=%d \n",(int)pData.RoundTripTime,
(int)pData.Options.Ttl);
}
else
{
printf("Ping测试失败...\n");
}
}
if (!IcmpCloseHandle(iHwnd)) printf("Close handle has Error!\n");
}
即IcmpSendEcho函数返回0,不一定是函数返回错误,还要通过ICMP_ECHO_REPLY结构体中的Status成员来判断,如果Status为IP_SUCCESS,才表示ping成功了,其他值都表示是ping失败。这点微软官方msdn上竟然没有说明,所以有时候看百度百科可能也是很有用的。
5、发布的程序到客户的机器上运行报错
5.1、报错情况
将release版的发布程序发给客户后,客户解压出来,双击运行,弹出如下的错误:
即程序运行不起来。应该不是缺少库的问题,如果是缺少库则应该是弹出缺少库的提示框,而不是上图的提示框。程序的主工程已经设置为使用静态mfc库了,肯定不是缺少mfc的库。
客户电脑是64位的win7英文系统,按讲跟英语语言没关系,语言顶多显示乱码,但是不会引起无法启动的问题;应该与64位系统也没关系,64系统上可以运行32位程序,系统是能保证兼的。
5.2、报错分析
客户在出问题的机器上安装了teamviewer客户端,我这边直接远程到客户的系统中分析问题。
在征求对方同意后,在客户机器上安装了windbg,在windbg中启动DevMonitor程序,结果发现加载libcurl库之后,进程就退出了:
应该是libcurl.dll库的问题,是不是应该用64位版本的?不应该啊,因为我们的主程序是32位程序,不能使用64位的libcurl.dll,不能混用,除非要编译x64 64位主程序时,才会使用到64位的libcurl.dll。
Windbg也看看不出什么名堂,于是考虑下载Dependency Walker工具,看看是不是有库缺失,查看了主程序DevMonitor.exe,果然看到了有缺失的库,如下所示:
怎么release版本的libcurl.dll库还依赖debug版本的运行时库MSVCR90D.dll?难道libcurl.dll本身就是debug版本的?哈哈,问题就在于此了,libcurl.dll是我从下载下来的代码编译出来的,好像没有编译release版本的,所以发布用的还是debug版本的libcurl.dll库。
此处有两个问题:
1)应该使用release版本的libcurl.dll库;
2)应该将运行时库MSVCR90.dll也带上。
因为我本机上安装了VS2008,在安装VS2018时已经将MSVCR90D.dll和MSVCR90.dll库安装到我电脑的系统中了,所以在我机器上运行是没问题的。
5.3、编译release版本的libcurl
刚启动编译就提示如下的错误:
找不到curl/curlbuild.h头文件,感觉不应该啊,使用makefile编译应该也有这个问题,想到了之前在网上搜到的一篇关于libcurl库编译的文章,说到了用VS编译时的注意点:
即源代码目录中是有curlbuild.h头文件的,如下:
只不过VS工程文件的路径在curl-7.32.0\vs\vc8\lib路径下,所以为了不影响makefile的相对路径,对于VS编译的,则要自己在工程中将..\include改成..\..\..\include,估计是当时添加VS工程文件的人处理的有问题。
于是改后再编译,结果又报出如下错误:
上面是链接不到网络库的函数,下面是链接不到ldap目录访问协议:
所以在libcurl的编译文章中提到,要引入如下的两个lib库:
关于LDAP的说明:LDAP是轻量目录访问协议,英文全称是Lightweight Directory Access Protocol,一般都简称为LDAP。它是基于X.500标准的,但是简单多了并且可以根据需要定制。与X.500不同,LDAP支持TCP/IP,这对访问Internet是必须的。LDAP的核心规范在RFC中都有定义,所有与LDAP相关的RFC都可以在LDAPman RFC网页中找到。
6、告警邮件发送到客户的163企业邮箱有问题
之前听客户反馈,说提示告警邮件是发送成功了,但是收件箱中并没有邮件,于是又用team viewer远程登录到客户的机器上亲自测试了一下,发送邮件的接口确实返回成功了,登录163企业邮箱查看发件箱,发现邮件确实发出来了,但是收到的内容却是空的。
但是海康的监控客户端是发送测试邮件是没问题的。海康的产品确实很好,各项功能都做的很好,海康的邮件配置及测试界面如下:
接收邮箱中收到的邮件是:
客户这个163企业邮箱,海康客户端也是能发送成功的,于是想是不是我们发出去的数据包的格式,163服务器认为不合法,服务器直接将邮件内容丢弃了?
按讲不应该啊,之前用同学的webmail服务器测试,发给126和163邮箱都是没问题的。对比了一下,只有服务器不同,最有可能的是被163邮件服务器拦截了,因为服务器是不一样的。这个问题解决不了,这个项目就无法验收通过啊!必须继续研究!
其实最开始视频监控产品中是带有告警邮件发送功能的,国外的用户也用过这个功能,但是对163企业邮箱一直调不通,所以要开发这个设备网络实时监测工具,就要解决这个邮件发送问题。
尝试着用项目方公司的邮件系统,看看能不能测试通过,如果能测试通过,则直接给他们开个账户,直接使用项目方公司的邮箱。但是用管理员账户登录到服务器后台管理页面,但始终没有找到开启smtp服务的入口,没有smtp服务器就没法拿来发送邮件。
后来还发现了个问题,邮件接收方显示的邮件发送时间是1970年1月1号,这个特殊的时间,相信开发人员都比较熟悉。估计是发送数据包中没有带上时间戳引起的,但是使用其他服务器测试时时间是没问题的,估计是服务器发现没有时间会自动打上时间戳的。
最后想到实在不行,就直接让客户使用同学公司的webmail开源邮件系统,但是可能同学那边也有顾虑,因为毕竟是他们自己公司用的简易邮件系统,也要考虑安全,随便给外面的人使用可能会引起一系列安全问题,比如短时间内产生大量的邮件可能会有隐患。
问题转了一圈,还是转回来了,后来客户那边明确说了,要用他们自己的163企业邮箱,所以收到告警邮件时内容为空的问题必须解决。
想到海康的客户端可以发送到163邮箱,那能不能用wireshark抓包工具看一下海康是如何和163服务器进行交互的,数据包的格式是什么样的,看看我们能不能拿过来参考一下(这样的家假想太关键了!!!)。于是赶紧尝试了一下,果然可以抓到,海康的邮件内容使用标准的MEMI格式,使用utf8编码:
肯定是为了支持多国语言的,于是修改我们代码,使用utf8编码(之前使用的是GB2312,是中文本地编码,不支持多国语言),并且将邮件内容按海康的MEMI格式重新构建了,改好了马上进行测试,测试成功了,有点小激动啊!从没感觉过wireshark网络抓包工具是如此的给力,帮我们解决了这样的难题!
另外,从海康的抓包数据可以看出,数据包中是带走有DATE时间节点的,即邮件的时间戳,而我们的数据包中没加时间戳,这就能解释为什么使用项目方公司的邮件服务器测试时,收到的邮件都是1970年1月1号,估计是项目方公司的邮件服务器没有自动给邮件加上时间戳。如果要兼容所有的邮件系统,我们可以在构建邮件数据时加上DATE时间戳节点信息。
7、最后
虽然这是一个很小很简单的项目,但也能学习到一些细节上的东西,也会有一点收获。在此将这个项目分享给大家,希望能对大家有所帮助!