第4章 总体设计
4.1体系结构设计
通常的通信工具,都采用客户机/服务器(C/S)体系结构,C/S结构是这样的一种结构:它包括一个客户机(或前端),一个服务器(或称后端),客户机的作用是访问和处理远程服务器上的数据,服务器的作用是接收和处理客户机的数据请求。有时,可能有多个客户向同一个服务器同时请求服务,这就需要服务器决定怎样处理这些请求。Client/Server结构是当前数据库应用程序中极为流行的一种方式。尤其是网络技术的发展,使得当前很多系统都采用这种方式进行构造,其最大的优点是将计算机工作任务分别由客户端和服务器端来共同完成,这样有利于充分合理的利用系统资源。另外它的服务器端还可以将信息集中起来,任何客户机都可以通过访问服务器而获得所需的信息。Client/Server模型最终可归结为一种“请求/应答”关系。一个请求总是首先被客户发出,然后服务器总是被动地接收请求,返回客户需要的结果。在客户发出一个请求之前,服务进程一直处于休眠状态。一个客户提出请求后,服务进程被“唤醒”并且为客户提供服务,对客户的请求做出所需要的应答。如下图所示:
图4-1客户机/服务器通信结构示图
但是具体到现在这个项目,如果要做成C/S结构,需要在局域网内架设一个
服务器,而在一个局域网中,如果网络结构不是集中式的而是分散式的,那么可能没有一台机器来充当服务器这个角色,比如我们的校园宿舍网络就是如此。所以说,我的设计是每个程序自己既充当客户机又充当服务器,自己维护网络上的客户机列表,每两个客户端要进行连结时都直接连通而不用通过服务器来进行信息的中转,这样虽然损失了一些性能各带宽资源,但是对通信量不是很大的局域网,这点损失可以由换得的专门架设服务器的优点来弥补。所以,系统结构将设计成如下方式:
图4-2客户机直接通信结构示图
当然,上述图示只是通信数据的一个流向示意图,不是真正的网络拓扑结构,也就是说其中的服务器可能只是装了服务器软件的一台普通工作站。在底层的数据流向中,也就是网络的物理连结,并非我们这个软件所能决定的,我们的所做都是在一个透明的数据通信层之上的。
4.2功能模块划分
因为在本系统中,每一个客户端系统都是相同的也就是具体到通信双方而言其实是对称的,所以只需要考虑一个系统就可以了。
根据以上的系统需求分析,以及体系结构设计,可以对系统进行如下的功能模块划分如图4-3所示。
其中主线程模块完成对网络的初始化,然后启动两个子线程:服务端监听线程以及网络扫描模块线程,然后由网络扫描模块得到当前的网络用户分布情况,并填充相关的数据结构,然后生成用户列表界面显示给用户。
通信模块又包括两个子模块:数据接收模块和数据发送模块,这两个模块都由系统定义的网络事件来触发。
输入/输出模块用来响应用户双击用户列表的某一项要准备发送信息时的消息,以及当系统接收到某个网络用户发送来的消息,要将其显示给用户的时候。
网络扫描模块是由主线程模块启动,进行网络扫描,确定哪些用户当前处于可到达状态,以及哪些可到达状态的用户安装有相应的通信软件,并启动之可以与之进行通信。
图4-3 功能模块划分示图
我觉得这样的模块划分设计符合强内聚,弱耦合的原则,并且易于实现。
4.3数据结构设计
由于系统首先要维护一个网络用户列表,所以要设计一个数组。这个数组具体可为255行2列数组,每一行代表局域网中的一台主机,然后第一列代表其是否处于可到达状态,第二列代表其是否处于可通信状态。
因为程序刚启动时的网络扫描阶段耗时较久,所以应当在程序启动后以一个人性化的界面提示用户不要以为程序无法响应了,暂时的缓慢是正常反应。因此我决定设计一个启动画面,这个启动画面可由VC++6.0提供的Splash screen组件来实现,它将向工程中插入一个类-CSplashWnd.我只需要在合适的地方调用该类,并进行具体的定制即可。
其它的相关类皆由MFC的应用程序向导自动生成,我只需要在其中进行具体的功能代码添加即可。
4.4用户界面设计
4.4.1启动画面设计
用户界面将由MFC框架的基于对话框的应用程序模板生成,然后在模板的基础上做具体的设计,美化修改。当用户双击程序图标时应该正常启动程序,并显示启动画面,显示版本及作者信息,启动画面由VC 组件 SplashScreen 来实现。实际效果如图4-4所示:
图4-4程序启动画面
4.4.2主显示界面
当扫描完毕时后,在主显示界面中就会显示出局域网内所有当前在线的用户列表,并且显示哪些主机可以进行聊天。并且由于显示是即时的,也就是扫描完一台马上显示出来,并且用户可以看到当前正在扫描哪一台机器,这样的设计也是比较合理的,如下图4-5所示。本程序还有一个附加非常方便的功能就是上线提示,当局域网内某用户打开了对应的程序,那么就会弹出相应的提示,并即时更新用户列表。
图4-5程序主显示界面
4.4.3用户聊天及接收信息界面
当双击列表中的某一项时,会弹出聊天对话框,来实现消息发送功能,或者当程序接收到某个网内用户发来的信息时,也要弹出对话框向用户进行显示,实际效果如下图所示:
图4-6程序聊天界面
第5章 详细设计及编码实现
5.1主框架及用户界面模块详细设计
由于该系统基本架构为基于对话框的MFC WIN32应用程序,所以以主框架模块由两个类构成,它们都是由MFC应用程序向导所生成:CNetTestWithAPIApp类以及CNetTestWithAPIDlg类,类结构如下图所示:
图5-1两个主框架类结构示图
其中前面为红色方块的为类成员函数,浅蓝色方块的为类成员数据。下面对这两个类的设计进行详细的说明。
CNetTestWithAPIAPP类为应用程序主框架类,它在后台完成了一个基于MFC的应用程序的所有基本的初始化工作,如果用户需要在程序的初始化时加入一些自定义的操作,只需在其中的InitInstance()函数中加入就可以了。在本程序中,只需要将WIN SOCKETS的网络初始化工作完成。代码为:
if(!AfxSocketInit()){
AfxMessageBox("Load socket library failed!");
return FALSE;}
在每一个基于MFC的WIN32程序中,它都是通过一个由全局对象启动整个初始化过程的机制,因为根据C++的设计,一个全局对象的生成即其构造函数的调用要先于程序的入口函数。所以,在这个对象的构造函数中可以做很多准备工作,这个对象就是程序主框架类的对象,具体到本程序中,即是上面讲到的CNetTestWithAPIAPP类。
CNetTestWithAPIDlg类为应用程序主显示对话框类,它是由MFC的内部机制生成的一个类,这个类将被做为程序的主显示对话框类,它在整个系统的消息循环以及程序处理中起着非常重要的作用,绝大多数的用户交互都将在这个类的成员函数中定义并完成。首先我们做的当然是用户界面的设计,按照VC6.0的控件开发方法,只需要找到与该类关联的那个对话框资源,然后在其上摆放正确的控件,如图所示:
图5-2 界面控件摆放示意图
界面上面其实只摆放了三个控件,第一个为StaticEdit控件,用来显示:局域网内主机信息,中间为一个ListCtrl控件,用来显示扫描得到的网络主机列表,最下面其实还有一个StaticEdit控件,它暂时没有显示文字,但在程序启动时,会动态地显示当前的网络扫描状态。
在以上的三个控件中,需要用代码做初始化的只有中间的ListCtrl控件,初始化代码应当加在CNetTestWithAPIDlg类的OnInitDialog()之中,在做初始化之前要先为该控件关联一个成员变量,通过它才能来操作控件。这一步要通过VC的ClassWizard工具完成,在MemberVariable属性页中找到ListCtrl控件的控件ID,选中之然后单击右边的Add Variable 按钮,接着在弹出的对话框中进行如下图所示的填写:
图5-3 界面控件变量关联示意图
然后就可以进行代码添加了,代码如下:
m_pcList.SetExtendedStyle(LVS_EX_FULLROWSELECT);
m_pcList.InsertColumn(0,"网内主机",LVCFMT_CENTER,120,NULL);
m_pcList.InsertColumn(1,"主机名",LVCFMT_CENTER,120,NULL);
m_pcList.InsertColumn(2,"状态",LVCFMT_CENTER,50,NULL);
m_pcList.InsertColumn(3,"聊天",LVCFMT_CENTER,150,NULL);
具体的列表项的显示将由网络扫描模块扫描时即时进行填充显示。
然后在这个函数之中还有做的事情包括启动服务器监听线程,以及网络扫描模块线程。代码分别如下:
ServerParam * sp=new ServerParam; //Create server thread!
sp->ServerSocket=ServerSocket;
sp->tempdlg=this;
hSevThread=CreateThread(NULL,0,
(LPTHREAD_START_ROUTINE)SevThreadFunc,sp,0,&SevThreadID);
服务端线程的具体作就是首先创建一个SOCKETS,然后将该套接字绑定到本地主机的某一个固定的端口上,在本程序中选择了41786端口。接着将该套接字设置为异步非阻塞模式,并为它注册各种网络异步事件,最后开始监听。具体代码如下:
void CNetTestWithAPIDlg::SevThreadFunc(LPVOID lpParam)
{
CNetTestWithAPIDlg* TempObject=(ServerParam*)lpParam->tempdlg;
SOCKET tempServer=((ServerParam*)lpParam)->ServerSocket;
sockaddr_in localaddr;//*//Bind to a local port
localaddr.sin_family=AF_INET;
localaddr.sin_port=htons(41786);
localaddr.sin_addr.s_addr=0;
if(bind(tempServer,(struct sockaddr*)&localaddr,sizeof(sockaddr))==SOCKET_ERROR){
AfxMessageBox("绑定地址失败!");
closesocket(tempServer);
WSACleanup();
return ;
}
//将SeverSock设置为异步非阻塞模式,并为它注册各种网络异步事件
if(WSAAsyncSelect(tempServer, TempObject->m_hWnd,NETWORK_EVENT,FD_ACCEPT | FD_CLOSE | FD_READ | FD_WRITE) == SOCKET_ERROR){
AfxMessageBox("注册网络异步事件失败!");
WSACleanup();
return;}
listen(tempServer, 5);//设置侦听模式
}
然后启动扫描线程:
SearchDisplay(); //Serach Lan and display the result to the user!
上面调用了SearchDisplay()来启动网络扫描线程,该函数的实现如下:
void CNetTestWithAPIDlg::SearchDisplay(){
hScanThread=CreateThread(NULL,0,
(LPTHREAD_START_ROUTINE)ScanThreadFunc,this,0,&ScanThreadID);
CloseHandle(hScanThread);
}
具体的扫描过程在网络扫描模块详细设计中阐述。
5.2网络扫描模块详细设计
网络扫描基于ICMP协议,所以需要先定义两个数据结构:
typedef struct {
unsigned char Ttl; // Time To Live
unsigned char Tos; // Type Of Service
unsigned char Flags; // IP header flags
unsigned char OptionsSize; //Size in bytes of options data
unsigned char *OptionsData; // Pointer to options data
} IP_OPTION_INFORMATION, * PIP_OPTION_INFORMATION;
typedef struct {
DWORD Address; // Replying address
unsigned long Status; // Reply status
unsigned long RoundTripTime; // RTT in milliseconds
unsigned short DataSize; // Echo data size
unsigned short Reserved; // Reserved for system use
void *Data; // Pointer to the echo data
IP_OPTION_INFORMATION Options; // Reply options
} IP_ECHO_REPLY, * PIP_ECHO_REPLY;
然后在网络扫描线程的线程函数中加入以下代码,完成扫描网络并显示用户列表的功能:
char HostName[255];
PHOSTENT HostInfo;
gethostname(HostName,sizeof(HostName));//get localhost info
HostInfo=gethostbyname(HostName);
LPCSTRip=inet_ntoa(*(structin_addr *)*HostInfo->h_addr_list);
in_addr* pip=(struct in_addr *)*HostInfo->h_addr_list;
CString temp=ip;
int n=0;
int a=temp.Find('.');
a=temp.Find('.',a+1);
a=temp.Find('.',a+1);
a=temp.GetLength()-a;
CString temp2=temp.Right(a-1);
n=atoi(temp2);
HINSTANCE hIcmp = LoadLibrary("ICMP.DLL");
if (hIcmp == 0){
AfxMessageBox("Unable to locate ICMP.DLL!");
return ;
}// 装载ICMP.DLL连接库
typedef HANDLE (WINAPI* pfnHV)(VOID);//定义函数三个指针类型
typedef BOOL (WINAPI* pfnBH)(HANDLE);
typedef DWORD (WINAPI* pfnDHDPWPipPDD)(HANDLE, DWORD, LPVOID, WORD,
PIP_OPTION_INFORMATION, LPVOID, DWORD, DWORD); // evil, no?
pfnHV pIcmpCreateFile;//定义三个指针函数
pfnBH pIcmpCloseHandle;
pfnDHDPWPipPDD pIcmpSendEcho;
pIcmpCreateFile = (pfnHV)GetProcAddress(hIcmp,"IcmpCreateFile");
pIcmpCloseHandle=(pfnBH)GetProcAddress(hIcmp,"IcmpCloseHandle");
pIcmpSendEcho=(pfnDHDPWPipPDD)GetProcAddress(hIcmp,"IcmpSendEcho"); //从ICMP.DLL中得到函数入口地址
if((pIcmpCreateFile == 0)||(pIcmpCloseHandle == 0)||(pIcmpSendEcho == 0)){
AfxMessageBox("Failed to get proc addr for function.");
return ;
}
HANDLE hIP = pIcmpCreateFile();// 打开ping服务
if (hIP == INVALID_HANDLE_VALUE){
AfxMessageBox("Unable to open ping service.");
return ;}
char acPingBuffer[64];// 构造ping数据包
memset(acPingBuffer, '/xAA', sizeof(acPingBuffer));
PIP_ECHO_REPLY pIpe=(PIP_ECHO_REPLY)GlobalAlloc(GMEM_FIXED|GMEM_ZEROINIT,sizeof(IP_ECHO_REPLY)+sizeof(acPingBuffer));
if(pIpe == 0){
AfxMessageBox("Failed to allocate global ping packet buffer.");
return ;}
pIpe->Data = acPingBuffer;
pIpe->DataSize = sizeof(acPingBuffer);
//构造探测41786端口的连接Socket
SOCKET DetectSock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
sockaddr_in remoteaddr;
remoteaddr.sin_family=AF_INET;
remoteaddr.sin_port=htons(41786);
int n_item=0;
CNetTestWithAPIDlg* TempObject=(CNetTestWithAPIDlg*)lpParam;
CString ClientHostName;// 发送ping数据包
for(int i=0,row_num=0;i<255;i++)
{//do ping here
if(i==n)
continue;
pip->S_un.S_un_b.s_b4=i;
char status[100]={0};
sprintf(status,"Scan %d.%d.%d.%d ing...",pip->S_un.S_un_b.s_b1,pip->S_un.S_un_b.s_b2,pip->S_un.S_un_b.s_b3,pip->S_un.S_un_b.s_b4);
TempObject->GetDlgItem(IDC_STATIC_STATUS)->SetWindowText(status);
DWORD dwStatus = pIcmpSendEcho(hIP, *((DWORD*)pip),
acPingBuffer, sizeof(acPingBuffer), NULL, pIpe,
sizeof(IP_ECHO_REPLY)+sizeof(acPingBuffer),100);
if (dwStatus!=0){ //the host is awaken !
TempObject->hostes[i][0]=1;
hostent * host=gethostbyaddr((const char*)(pip),4,AF_INET);
if(host!=NULL){
ClientHostName=host->h_name;}
CString temp;
temp=inet_ntoa(*pip);
n_item=TempObject->m_pcList.InsertItem(row_num,"row",NULL);
TempObject->m_pcList.SetItemText(n_item,0,temp);
TempObject->m_pcList.SetItemText(n_item,1,ClientHostNme);
TempObject->m_pcList.SetItemText(n_item,2,"已开机");
remoteaddr.sin_addr=*pip;
int result=connect(DetectSock,(struct sockaddr *)&remoteaddr,sizeof(sockaddr));
if(result==0){
TempObject->hostes[i][1]=1;
TempObject->m_pcList.SetItemText(n_item,3,"可聊天");}
else{
TempObject->hostes[i][1]=0;
TempObject->m_pcList.SetItemText(n_item,3,"不可聊天");}
TempObject->m_pcList.SetItemText(n_item,4,"传文件");
row_num++;}}
char finishscan[40]="Scan finished!";//扫描完毕。
TempObject->GetDlgItem(IDC_STATIC_STATUS)->SetWindowText(finishscan);
closesocket(DetectSock);//Close the detect socket!
GlobalFree(pIpe);// 关闭,回收资源
5.3信息发送模块详细设计
信息发送事件是由用户在双击位于ListCtrl控件中的某一个用户列表的时候发生的,所以对ListCtrl的列表项双击事件添加事件响应函数如下:
void CNetTestWithAPIDlg::OnDblclkPclist(NMHDR* pNMHDR, LRESULT* pResult) {
if(dlg.m_hWnd!=0){
return; }
int nItem=-1;
nItem=m_pcList.GetNextItem(nItem,LVNI_SELECTED);
if(nItem==-1) return;
CString strSelectedItem;
strSelectedItem=m_pcList.GetItemText(nItem,0);
CString temp=strSelectedItem;
int n=0;
int a=temp.Find('.');
a=temp.Find('.',a+1);
a=temp.Find('.',a+1);
a=temp.GetLength()-a;
CString temp2=temp.Right(a-1);
n=atoi(temp2);
if(hostes[n][1]==1){//If the host cliked can be talked
dlg.ClientHost.S_un.S_addr=inet_addr(temp);
dlg.Create(IDD_SEND_DIALOG,this);
dlg.ShowWindow(SW_SHOW);}
else{
AfxMessageBox("There's no client in goal host!");}
*pResult = 0;
}
5.4信息接收模块详细设计
信息接收由前面注册的网络事件被系统所触发,当
void CNetTestWithAPIDlg::OnAccept(SOCKET CurSock){
SOCKADDR_IN addrClient;
int len=sizeof(SOCKADDR);//为新的socket注册异步事件
CurrentConn=accept(ServerSocket,(sockaddr*)&addrClient,&len);
int n=addrClient.sin_addr.S_un.S_un_b.s_b4;
dlg.ClientHost=addrClient.sin_addr;
if(hostes[n][1]==1){hostes[n][1]=1;
CString ClientIp=inet_ntoa(addrClient.sin_addr);
ClientIp+=" is online!";
CString HostName;//get the hostname!
int n_item=0;int row_num=0;
else{
hostes[n][0]=1;
hostent * host=gethostbyaddr((const char*)(&(addrClient.sin_addr)),4,AF_INET);
if(host!=NULL){
HostName=host->h_name;}
else{
HostName="";}}
for(int i=0;i<255;i++){
if(hostes[i][0]==1){
row_num++;}}}
void CNetTestWithAPIDlg::OnReceive(SOCKET CurSock){
char recvbuff[512]={0};
int flag=recv(CurSock,recvbuff,512,0);//读出网络缓冲区中的数据包
if(flag==SOCKET_ERROR){
AfxMessageBox("Recv Error!");}
if(recvbuff[0]!=0){
if(dlg.m_hWnd!=0){
dlg.m_History_Message+="/r/nHe said:";
dlg.m_History_Message+=recvbuff;
dlg.UpdateData(FALSE);}
else{
dlg.m_History_Message="He said:";
dlg.m_History_Message+=recvbuff;
dlg.Create(IDD_SEND_DIALOG,this);
dlg.ShowWindow(SW_SHOW);}}}
以上即各个模块的详细设计的主要实现过程。