在通信领域,用户在网管界面操作,通过TCP/IP协议给电信设备发送报文,从而配置、维护电信设备。电信设备一般都不具备可视化终端,当电信设备从网管接收到了命令报文后,用户不方便了解电信设备接收到了哪些命令报文、以及处理报文过程是否正常。为了监视电信设备的运行情况,可以在电信设备上运行一个 socket服务器,在PC机上运行一个socket客户端,称为命令报文监视器,所有通过网管发给电信设备的命令报文都会通过socket服务器发送给报文监视器,请实现一个这样的命令报文监视器。
初赛要求:
1、监视器程序是一个可视化的GUI程序,刚运行时提供输入框让用户输入需要监视的电信设备的IP地址和端口号,然后与电信设备服务器建立socket连接。比如用户输入的IP地址为:192.192.192.1,端口号为8000,用户点击“连接”按钮即可与电信设备服务器建立连接。
2、监视器程序仅仅从socket连接中读取电信设备服务器发来的数据,这些数据是一串连续的ASCII码流。监视程序每次从socket中接收到数据后在数据前面加上时间信息,然后在GUI界面中显示。比如从服务器接收到的报文为“Ncp Send Message To Mcu: nMcuAdrs=0x10301 CmdCode = 0x11ab,lParamLen = 0x12.”,那么在GUI界面中的显示则如下:
[10/05/19 03:30:17] Ncp Send Message To Mcu: nMcuAdrs=0x10301 CmdCode = 0x11ab,lParamLen = 0x12.
其中[]里面的是显示接收到的PC机本地时间,其余ASCII字符为报文的内容。
监视器程序只管从socket中接收报文,无须通过socket发送报文。
3、当报文内容很多时为了方便用户选择查看有意义的报文,可以对报文进行过滤。支持用户输入需要过滤的字符的关键字。比如输入要过滤的关键字为 “nMcuAdrs=0x10301”,那么对于nMcuAdrs=0x10301的报文则不显示,仅显示满足关键字过滤规则的报文。当需要有多个要过滤的关键字时,需要支持一下的规则:
(1)与 key1 and key2, 表示既要满足关键字key1也要满足关键字key2
(2)或 key1 or key2, 表示只要满足key1或者kye2任一即可
(3)支持与和或的组合,与的优先级高于或。比如 key1 and key2 or key3,表示只要满足key1、key2,或者key3即可
(4)括号的优先级高于and。比如 key1 and (key2 or key3),表示满足key1并且满足key2、key3中任一即可。
每一个关键字都用双引号括起来,比如输入过滤的字符为“nMcuAdrs=0x10301” and “CmdCode = 0x11ab” ,表示既要满足nMcuAdrs=0x10301也要满足CmdCode = 0x11ab
4、支持监视器收到的所有报文保存到文件中,需要支持用户设置文件所能保存的最大报文数目。比如用户设置最大可以保存100条报文,当超过100报文时新接收到的报文可以覆盖时间最长的报文,例如附件 中的文件。
复赛要求
1、由于电信设备较多,需要在1个监视器程序中同时监视多个不同的电信设备。比如监视程序的窗体1显示1个电信设备的命令报文,窗体2显示另外1个电信设备的命令报文。需要注意的是窗体1和窗体2属于同一个监视器程序而不是两个不同的监视器程序。
2、由于电信设备的资源有限,仅允许1个监视器程序连接上电信设备。而测试人员有多人需要同时运行监视器程序来查看同一个电信设备的情况,多个监视器程序需要在同一台PC机上监视同一个电信设备。由于只有1个监视器程序可以连接上电信设备,其余的监视器程序需要从已经连接上的监视程序获得报文。当已经连接上的监视器程序退出时,其余的监视器程序要能争先连接电信设备,无须用户干预即能继续监视电信设备的报文。
3、为了提高监视器程序的友好性,用户可以通过按键选择显示满足过滤条件或者显示不满足过滤条件的报文。当选择显示满足过滤条件报文时,匹配的关键字需要以红色字体来显示。比如关键字为CmdCode,接收到的报文内容中包含字符串CmdCode则红色显示,其余字符以默认颜色显示。
4、当用户输入的关键字不正确时需要提示,比如用户输入了 key1 an key2,需要提示用户 an 不是正确的关键字,是否要补充上d。当输入(key1 and key时,需要提示用户是否要补充上)等。
下载地址
我主要工作是网络部分
图2.6 监视器端功能模块示意图
在LAN(局域网)环境中,一台报文监视器(CMM)启动起来后,首先进行局域网广播,然后将局域网内设备IP列表显示在主窗口设备列表内。用户选择希望连接的电信设备IP进行连接。在接收报文的过程中,将报文在子窗口显示,并显示当前网络环境下的连接情况,并支持关键字过滤与报文的存储与查看。
系统总共分为三层,如图2. 6:界面层、数据处理层与数据访问层。
l 界面层只包括界面模块,界面模块内涉及主界面与子界面的设计。主界面需要有平台示意图,以显示当前网络环境下的连接情况。设备信息表显示当前环境下的电信设备基本信息。子界面包括报文的操作,如存储、查看等,过滤设置可以支持匹配不显示、区分大小写与全字匹配等各项需求,以及报文显示。
l 数据处理层分为三个模块,分别是存储模块、过滤模块、网络模块。
储存模块主要与QSQLITE做交互,以存储报文。
过滤模块响应用户界面的各种过滤要求。
网络模块主要负责操作套接字,从网络获取及发送报文
l 数据访问层
数据连接,与QSQLITE连接,并存储、读取报文。
套接字与网络连接,接收、发送报文。
为了能与报文电信设备通信,需要知道报文电信设备的IP地址,在主窗口设计中已经有电信设备IP地址手动输入窗口。但是我们认为这样设计非常不方便,因为很多时候我们并不确定电信设备的IP地址。所以我们设计了基于UDP的广播搜索功能,能实时搜索局域网内的电信设备主机的各种信息,比如IP地址、主机号、TCP发送报文所使用的端口。同时当局域网中有多台电信设备的时候,能实时更新电信设备的加入与退出信息。
由于设备只能接受一个命令报文监视器的TCP连接请求,当此监视器收到报文后,需要把报文发送给其他需要此报文的其他监视器程序。此时一个监视器程序可能同时从多个电信设备获得报文,也需要将报文发送给不同的监视器程序。这样采用传统的TCP服务器/客户端传输架构就不能满足此要求。我们在此采用IP组播技术。
当需要将信息发送给同一子网的多个IP地址,可以采用如上的UDP广播技术,但是当发送的内容比较多,而且频繁的时候,采用广播技术(Broadcasting),极易造成网络带宽的大幅占用,影响整个网络的通信效率。而组播的出现却很好地弥补了此问题。“组播”也称为“多点传送”(Multicasting)或者多播技术,是一种让数据从一个成员送出,然后复制给其他多个成员的技术。采用这种技术,可有效减轻网络通信的负担,避免资源的无谓浪费。对一个网络内的工作站来说,只有在上面运行的进程表示自己“有兴趣”,多播数据才会复制给它们。我们在win32平台上采用IP组播技术实现。
如图2. 14,命令报文监视器(192.168.1.2)从电信设备(192.168.1.201)处获得了报文,它加入多播组一(233.0.1.201)并向多播组发送报文,此多播组地址的最后一段201采用相应电信设备IP地址的最后一段。其他对电信设备(192.168.1.201)报文感兴趣的监视器程序(192.168.1.3、192.168.1.8)在连接设备时发现电信设备被占用,即转而从多播组获取报文。同时我们注意监视器程序(192.168.1.8)可以同时加入两个多播(233.0.1.201、233.0.1.202)组接收报文,并在相应的子窗口显示报文。
当组播源(192.168.1.2)退出监视程序时,其多播组的其他成员(192.168.1.3、192.168.1.8)将自动竞争连接电信设备获取报文。
图2. 14 组播实现示意图
为此,我们提出基于UDP的搜索、基于TCP的报文接收、基于IP组播的报文发送与接收功能,如图2. 15,在主窗口线程(线程1)初始化的时候启动UDP广播守护线程(线程2),每隔2秒中进行一次局域网广播,同时更新主窗口界面中的设备列表,同时创建多播接收守护线程(线程5),不断接收来自于多播组的报文。当用户选择指定电信设备并点击连接后,创建报文监视子窗口(线程3),子窗口线程创建报文传输线程(线程4)进行电信设备连接与报文接收。
图2. 15 命令报文监视器端网络收发设计
l 功能介绍
当系统启动起来后,进行基于UDP的局域网广播,此线程为守护线程,在整个系统工作过程中不断进行广播,广播间隔时间为3秒。
l 程序设计
当系统初始化的时候,调用Broadcast()函数进入广播守护线程,如图3. 5。线程1完成初始化后创建广播线程(线程1),广播线程将SOCKET设置为广播模式然后发送广播包。为了防止在发送过程中出线不可预料的错误而出线线程僵死,主线程1中对其进行超时检验,超时60秒钟终止线程2。广播线程完成后创建IP列表记录线程(线程3)。记录线程调用堵塞函数recvfrom()进行UDP响应包的接收,并将电信设备的IP地址记录下来。在接收的同时进行超时检验,如果超时2秒钟没有收到UDP响应包,我们认为局域网电信设备已经全部应答完毕,退出线程3。
由于在广播前并不指定局域网内电信设备数量,无法估计电信设备IP列表信息的存储空间,如果此次存储空间的估计偏小,当电信设备数量增多的时候将直接促发程序的内存溢出。为了自适应电信设备数量,此处采用容器存储IP列表信息。信息内容如表表3. 1。
表3. 1 电信设备IP列表存储结构体说明
变量名 |
存储的信息 |
dcIp |
电信设备IP地址 |
dcName |
电信设备主机名 |
dcPort |
电信设备TCP报文发送端口号 |
图3. 5 基于UDP的广播守护线程工作流程图
具体函数实现请参照 |
软件代码设计(命令报文监视器端).chm |
/** @file udp.cxx * @brief 实现监视器UDP广播 * 实时得到局域网内各个报文电信设备的IP列表 * @author 曾凡平、陆丹峰、李祥平 * @version 3.0 * @date 2010-7-9 */ #pragma comment(lib, "Ws2_32.lib") #include <winsock2.h> #include "udp.h" #include "cmmwindow.h" ///UDP接收应答、发送缓冲区大小 #define BufferLen 4096 ///UDP应答端口 #define UDPport 8000 ///存储电信设备IP列表信息的容器声明 vector<struct DeviceInfo> deviceInfo; extern cmmWindow *m_window; /** @brief 广播线程 * 将SOCKET设置成广播模式,然后发送一段字符串。 * @param LPVOID lpParam * 参数传递的是SOCKET套接字 * @return 返回DWORD * - 1 表示成功 * - 0 表示失败 */ DWORD WINAPI BroadcastPthread(LPVOID lpParam) { BOOL bBroadcast; char broadcastBuff[]="idle?"; SOCKADDR_IN reciver; int i=1,ret; int iPort ; SOCKET b=(SOCKET)lpParam; DWORD dwLength=BufferLen; iPort=UDPport; reciver.sin_addr.S_un.S_addr=INADDR_BROADCAST; reciver.sin_family=AF_INET; reciver.sin_port=htons((short)iPort); dwLength=sizeof(broadcastBuff); bBroadcast=TRUE; setsockopt(b,SOL_SOCKET,SO_BROADCAST,(char*)&bBroadcast,sizeof(bBroadcast)); ret=sendto(b,broadcastBuff,dwLength,0,(SOCKADDR*)&reciver,sizeof(reciver)); if(ret == SOCKET_ERROR) { printf("sendto() failed; %d/n", WSAGetLastError()); return FALSE; } else if (ret == 0) return FALSE; bBroadcast=FALSE; setsockopt(b,SOL_SOCKET,SO_BROADCAST,(char*)&bBroadcast,sizeof(bBroadcast)); return 1; } /** @brief 记录响应局域网广播的IP列表线程 * 调用阻塞函数recvfrom(),一直等待接收各个电信设备的响应,并将IP添加到存放IP列表的全局变量IP链表中。 * @param LPVOID lpParam * 参数传递的是SOCKET套接字 * @return 返回DWORD * - 1 表示成功 * - 0 表示失败 */ DWORD WINAPI IPrecordPthread(LPVOID lpParam) { SOCKADDR_IN seed; int iPort,ret; char *recvBuff; DWORD dwSenderSize; struct hostent *pHost; SOCKET recordSocket=(SOCKET)lpParam; DWORD dwLength=BufferLen; iPort=UDPport; recvBuff=(char *)new char[dwLength]; dwSenderSize = sizeof(seed); fd_set readfds; struct timeval authtime; authtime.tv_usec =0L; authtime.tv_sec =(long) 2; while(1) { FD_ZERO (&readfds);// FD_SET (recordSocket, &readfds);// if(select(0,&readfds,NULL,NULL,&authtime) < 0) { return 1; } if (FD_ISSET(recordSocket, &readfds)) ret = recvfrom(recordSocket, recvBuff, dwLength, 0, (SOCKADDR *)&seed, (int *)&dwSenderSize); else return 1; if (ret == SOCKET_ERROR) { return 0; } else if (ret == 0) { printf("receive none!/n"); return 0; } else { recvBuff[ret] = '/0'; } //pHost=gethostbyaddr((char *)&seed.sin_addr,4,AF_INET); struct DeviceInfo temp; unsigned long ddd = inet_addr(inet_ntoa(seed.sin_addr)); pHost=gethostbyaddr((char *)&ddd,4,AF_INET); if(pHost==NULL) { // printf("Get the host name by address error!/n"); temp.dcName="unknown"; } else temp.dcName = string(pHost->h_name); temp.dcIp = string(inet_ntoa(seed.sin_addr)); temp.dcPort = ntohs(seed.sin_port ); deviceInfo.push_back(temp); } delete recvBuff; return 1; } /** @brief 广播守护线程 * 首先启动广播线程,对整个局域网进行广播 * 然后启动IP列表记录线程,记录响应局域网广播的IP列表 * 一个广播和IP列表的记录在5S内完成,然后进行同样运作,直到整个系统退出 * @param LPVOID lpParam * 参数传递的是SOCKET套接字 * @return 返回DWORD * - 1 表示成功 * - 0 表示失败 */ DWORD WINAPI SuperiorPthread(LPVOID lpParam) { HANDLE hThread; DWORD dwThreadId; SOCKET b; struct IPlist* ipList=NULL; b=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); if (b == INVALID_SOCKET) { printf("socket() failed; %d/n", WSAGetLastError()); return 0; } while(1) { hThread = CreateThread(NULL, 0, BroadcastPthread, (LPVOID)b, 0, &dwThreadId); if (hThread == NULL) { printf("CreateThread() failed: %d/n", GetLastError()); return 0; } WaitForSingleObject(hThread,60000); deviceInfo.clear(); hThread = CreateThread(NULL, 0, IPrecordPthread, (LPVOID)b, 0, &dwThreadId); if (hThread == NULL) { printf("CreateThread() failed: %d/n", GetLastError()); return 0; } WaitForSingleObject(hThread,-1); m_window->updateDeviceInfo(deviceInfo); } closesocket(b); return 1; } /** @brief 启动广播守护线程 * 启动后直接返回整型值。广播守护线程在整个系统运行中一直进行广播,以对电信设备IP列表进行不断更新。 * @param 无 * @return 返回整型值 * - 1 表示成功 * - 0 表示失败 */ int BroadcastDaemon() { HANDLE hThread; DWORD dwThreadId; hThread = CreateThread(NULL, 0, SuperiorPthread, (LPVOID)NULL, 0, &dwThreadId); printf("Superior Pthread=%d/n",dwThreadId); return 1; }
l 功能介绍
实现与用户指定电信设备进行TCP连接,并不断接收报文显示。
l 程序设计
当用户对指定IP地址的设备进行连接时,调用connectServer()函数,创建报文接收线程并加入多播组,多播组的地址由”233.0.1.1”为基准,最后一段IP由对应设备IP的最后一段构成。当连接电信设备失败后,加入接收从别的已经连接上的监视器获取报文,如果连接成功,收到报文后,将报文放入多播组,供对此电信设备报文感兴趣的监视器获取。具体工作如图3. 6,其中绿色部分为循环部分。
图3. 6 TCP的报文接收工作流程图
l 功能介绍
由于在本系统中,对各节点的控制工作几乎没有要求,所以我们采用IP组播方式,在控制层和数据层,采用“无根”方式,所有节点都是叶子节点,加入与退出多播组都非常容易实现。同时,组播源发送的信息,不需要对每个多播组成员发送,信息可以通过各个节点“蔓延”到所有节点,减少了组播源的网络负载。
l 程序设计
当系统初始化过程中,启动InitalSocket(),在此函数中初始化winsock 2.0服务并初始化组播套接字,并创建组播接收线程。在组播接收线程中,收到报文并查询对应的子窗口并显示,当收到组播源退出的信息后,监视器立即重新连接电信设备,竞争连接上电信设备并称为组播源,将从电信设备收到的报文放入多播组,具体工作如图3. 7。
图3. 7 IP组播守护线程流程图
/** * @file multicast.cxx * @brief 实现监视器端基于组播技术的报文接收与传送 * @author 曾凡平、陆丹峰、李祥平 * @version 3.0 * @date 2010-7-9 * */ #pragma comment(lib, "Ws2_32.lib") #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include "multicast.h" #include "tcp.h" ///试图连接过的设备列表结构体 struct EnvLinkedDevice{ ///电信设备的IP地址 string device; ///此电信设备对应的组播接收套接字 SOCKET socket; }; ///试图连接过的电信设备列表 vector<EnvLinkedDevice> linkedDevice; extern CRITICAL_SECTION criticalVector; extern SOCKET multiCastSock; extern vector<EnvLink> envLink; /** @brief 设置组播地址 * @param * - char *multiCastAddr 组播IP地址 * - char *deviceIP 试图连接的电信设备IP地址 * @return 返回整型值 * - 1 表示成功 * - 0 表示失败 */ int setMultiCastAddress(char *multiCastAddr,char *deviceIP) { strcpy_s(multiCastAddr,strlen(MCASTADDR)+1,MCASTADDR); char *mcp,*dp; int i; mcp=multiCastAddr; for(i=0;i<2 && *mcp;mcp++) { if(*mcp=='.') i++; } dp=deviceIP; for(i=0;i<2 && *dp;dp++) { if(*dp=='.') i++; } strcpy_s(mcp,strlen(dp)+1,dp); return 1; } /** @brief 等待从多播(组播)组接收报文 * @param * - char *deviceIP 试图连接的电信设备IP * @return 返回整型值 * - 0 表示组播发送端退出 * - -1 表示子窗体关闭 */ int multiBroadcastWaiting(char *ip) { multiBroadcastReceiverInit(ip); while(1) { vector<EnvLink>::iterator ip_pos; for(ip_pos = envLink.begin(); ip_pos != envLink.end(); ip_pos++) if(strcmp(ip_pos->deviceIP,ip)==0) { break; } if(ip_pos==envLink.end()) break; if(ip_pos->multicastOver==-1) { return -1; } if(ip_pos->disconncetFlag ) return -1; else if(ip_pos->multicastOver==1) { EnterCriticalSection(&criticalVector); ip_pos->TCPconnected=true; ip_pos->initalUI=false; ip_pos->multicastOver=0; LeaveCriticalSection(&criticalVector); return 0; } Sleep(500); } return 0; } /** @brief 组播接收守护线程 * @param 无 * @return 返回整型值 * - 1 表示成功 * - 0 表示失败 */ DWORD WINAPI multiBroadcastDamonPthread(LPVOID lpParam) { struct sockaddr_in from; SOCKET sockMulti; char recvbuf[BUFSIZE]; char recvbufBef[BUFSIZE]; bool firstFlag=true; int len = sizeof( struct sockaddr_in); int ret; vector<EnvLink>::iterator ip_pos; time_t rawtime; struct tm * timeinfo; char *receiveTime=(char *)new char[32],*receiveTimeBefore=(char *)new char[32]; memset(receiveTimeBefore,0,32); memset(recvbufBef,0,BUFSIZE); while(1) { fd_set readfds; struct timeval authtime; authtime.tv_usec =0L; authtime.tv_sec =(long) 1; while(1) { FD_ZERO (&readfds); FD_SET (multiCastSock, &readfds); if(select(0,&readfds,NULL,NULL,&authtime) < 0) { return 0; } if (FD_ISSET(multiCastSock, &readfds)) { ret = recvfrom(multiCastSock,recvbuf,BUFSIZE,0, (struct sockaddr*)&from,&len); recvbuf[ret]='/0'; printf("multi boradcast,RECV:' %s ' FROM