摘要:本文从解决实际需要出发,通过采用Windows Socket API等网络编程技术实现了在局域网共享一条电话线的情况下,当服务器拨号上网时能及时通知各客户端通过代理服务器进行上网。本文还特别给出了基于Microsoft
Visual C++ 6.0的部分关键实现代码。
一、 问题提出的背景
笔者所使用的局域网拥有一个服务器及若干分布于各办公室的客户机,通过网卡相连。服务器不提供专线上网,但可以拨号上网,而各客户机可以通过装在服务器端的代理服务器共用一条电话线上网,但前提必须是服务器已经拨号连接。考虑到经济原因,服务器不可能长时间连在网上,因此经常出现由于分布于各办公室的客户机不能知道服务器是否处于连线状态而造成的想上网时服务器没有拨号,或是服务器已经拨号而客户机却并不知晓的情况,这无疑会在工作中带来极大的不便。而笔者作为一名程序设计人员,有必要利用自己的专业优势来解决实际工作中所遇到的一些问题。通过对实际情况的分析,可以归纳为一点:当服务器在进行拨号连接时能及时通知在网络上的各个客户机,而各客户机在收到服务器发来的消息后可以根据自己的情况来决定是否上网。这样就可以在同一时间内同时为较多的客户机提供上网服务,此举不仅提高了利用效率也大大节省了上网话费。
二、 程序主要设计思路及实现
由于本网络是通过网卡连接的局域网,因此可以首选Windows Socket API进行
套接字编程。整个系统分为两部分:服务端和客户端。服务端运行于服务器上负责监视服务器是否在进行拨号连接,一旦发现马上通过网络发送消息通知客户端;而客户端软件则只需完成同服务端软件的连接并能接收到从服务端发送来的通知消息即可。服务器端要完成比客户端更为繁重的任务。下面对这几部分的实现分别加以描述:
(一)监视拨号连接事件的发生
在采用拨号上网时,首先需要通过拨号连接通过电话线连接到ISP上,然后才能享受到ISP所提供的各种互联网服务。而要捕获拨号连接发生的事件不能依赖于消息通知,因为此时发出的消息同一个对话框出现在屏幕上时所产生的消息是一样的。唯一同其他对话框区别的是其标题是固定的"拨号连接",因此在无其他特殊情况下(如其他程序的标题也是"拨号连接"时)可以认定当桌面上的所有程序窗口出现以"拨号连接" 为标题的窗口时,即可认定此时正在进行拨号连接。因此可以通过搜寻并判断窗口标题的办法对拨号连接进行监视,具体可以
用CWnd类的FindWindows()函数来实现:
第一个参数为NULL,指定对当前所有窗口都进行搜索。第二个参数就是待搜寻的窗口标题,一旦找到将返回该窗口的窗口句柄。因此可以在窗口句柄不为空的情况下去通知客户端服务器现在正在拨号。由于一般的拨号连接都需要一段时间的连接应答后才能登录到ISP上,因此从提高程序运行效率角度出发可以通过定时器的使用来每间隔一段时间(如500毫秒)去搜寻一次,以确保能监视到每一次的拨号连接而又不致过分加重CPU的负担。
(二)服务器端网络通讯功能的实现
在此采用的是可靠的有连接的流式套接字,并且采用了多线程和异步通知机制能有效避免一些函数如accept()等的阻塞会引起整个程序的阻塞。由于套接字编程方面的书籍资料非常丰富,对其进行网络编程做了很详细的描述,故本文在此只针对一些关键部分做简要说明,有关套接字网络编程的详细内容请参阅相关资料。采用流式套接字的服务器端的主要设计流程可以归结为以下几步:
1. 创建套接字
sock=socket(AF_INET,SOCK_STREAM,0); |
该函数的第一个参数用于指定地址族,在Windows下仅支持AF_INET(TCP/IP地址);第二个参数用于描述套接字的类型,对于流式套接字提供有SOCK_STREAM;最后一个参数指定套接字使用的协议,一般为0。该函数的返回值保存了新套接字的句柄,在程序退出前可以用closesocket()函数来将其释放。
2. 绑定套接字
服务器方一旦获取了一个新的套接字后应通过bind()将该套接字与本机上的一个端口相关联。此时需要预先对一个指向包含有本机IP地址和端口信息的sockaddr_in结构填充一些必要的信息,如本地端口号和本地主机地址等。然后就可经过bind()将服务器进程在网络上标识出来。需要注意的是由于1024以内的埠号都是保留的端口号因此如无特别需要一般不能将sockin.sin_port的端口号设置为1024以内的值:
…… sockin.sin_family=AF_INET; sockin.sin_addr.s_addr=0; sockin.sin_port=htons(USERPORT); bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); …… |
3. 侦听套接字
4. 等待客户机的连接
这里需要通过accept()调用等待接收客户端的连接以完成连接的建立,由于该函数在没有客户端进行申请连接之前会处于阻塞状态,因此如果采取通常的单线程模式会导致整个程序一直处于阻塞状态而不能响应其他的外界消息,因此为该部分代码单独开辟一个线程,这样阻塞将被限制在该线程内而不会影响到程序整体。
AfxBeginThread(Server,NULL);//创建一个新的线程 …… UINT Server(LPVOID lpVoid)//线程的处理函数 { //获取当前视类的指针,以确保访问的是当前的实例对象。 CNetServerView* pView=((CNetServerView*)( (CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetActiveView()); while(pView->nNumConns<1)//当前的连接者个数 { int nLen=sizeof(SOCKADDR); pView->newskt= accept(pView->sock, (LPSOCKADDR)& pView->sockin,(LPINT)& nLen); WSAAsyncSelect(pView->newskt, pView->m_hWnd,WM_SOCKET_MSG,FD_CLOSE); pView->nNumConns++; } return 1; } |
这里在accept ()后使用了WSAAsyncSelect()异步选择函数。对于网络事件的响应最好采取异步选择机制,只有采取这种方式才可以在由网络对方所引起的不可预知的网络事件发生时能马上在进程中做出及时的响应处理,而在没有网络事件到达时则可以处理其他事件,这种效率是很高的,而且完全符合Windows所标榜的消息触发原则。WSAAsyncSelect()函数便是实现网络事件异步选择的核心函数。通过第四个参数FD_CLOSE注册了应用程序感兴取的网络事件是网络断开,当客户方端开连接时该事件会被检测到,同时会发出由第三个参数指定的自定义消息WM_SOCKET_MSG。
5. 发送/接收
当客户机同服务器建立好连接后就可以通过send()/recv()函数进行发送和接收数据了,对于本程序只需在监测到有拨号连接事件发生时向客户机发送通知消息即可:
char buffer[1]={'a'}; send(newskt,buffer,1,0);//向客户机发送字符a,表示现在服务器正在拨号。 |
6. 关闭套接字
在全部通讯完成之后,在退出程序之前需要调用closesocket();函数把创建的套接字关闭。
(三)客户机端的程序设计
客户机的编程要相对简单许多,全部通讯过程只需以下四步:
1. 创建套接字
2. 建立连接
3. 发送/接收
4. 关闭套接字
具体实现过程同服务器编程基本类似,只是由于需要接收数据,因此待监测的网络事件为FD_CLOSE和FD_READ,在消息响应函数中可以通过对消息参数的低位字节进行判断而区分出具体发生是何种网络事件,并对其做出响应的反应。下面结合部分主要实现代码对实现过程进行解释:
…… m_ServIP=SERVERIP; //指定服务器的IP地址 m_Port=htons(USERPORT); //指定服务器的端口号 if((IPaddr=inet_addr(m_ServIP))==INADDR_NONE) //转换成网络地址 return FALSE; else { sock=socket(AF_INET,SOCK_STREAM,0); //创建套接字 sockin.sin_family=AF_INET; //填充结构 sockin.sin_addr.S_un.S_addr=IPaddr; sockin.sin_port=m_Port; connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); //建立连接 //设定异步选择事件 WSAAsyncSelect(sock,m_hWnd,WM_SOCKET_MSG,FD_CLOSE|FD_READ); //在这里可以通过震铃、弹出对话框等方式通知客户已经连上服务器 } ……
//网络事件的消息处理函数 int message=lParam & 0x0000FFFF;//取消息参数的低位 switch(message) //判断发生的是何种网络事件 { case FD_READ: //读事件 AfxBeginThread(Read,NULL); break; case FD_CLOSE: //服务器关闭事件 …… break; }
|
在读事件的消息处理过程中,单独为读处理过程开辟了一个线程,在该线程中接收从服务器发送过来的信息,并通过震铃、弹出对话框等方式通知客户端现在服务器正在拨号:
…… int a=recv(pView->sock,cDataBuffer,1,0); //接收从服务器发送来的消息 if(a>0) AfxMessageBox("拨号连接已启动!"); //通知用户 …… |
三、必要的完善
前面只是介绍了程序设计的整体框架和设计思路,仅仅是一个雏形,有许多重要的细节没有完善,不能用于实际使用。下面就对一些完全必要的细节做适当的完善:
(一) 界面的隐藏
由于本程序系自动检测、自动通知,完全不需要人工干预,因此可以将其视为后台运行的服务程序,因此程序主界面现在已无存在的必要,可以在应用程序类的初始化实例函数InitInstance()中将ShowWindow();的参数SW_SHOW改成SW_HIDE即可。当需要有对话框弹出通知用户时仅对话框出现,主界面仍隐藏,因此是完全可行的。
(二) 自启动的实现
由于服务端软件需要时刻监视有无进行拨号连接,所以必须具备自启动的特性。而客户端软件由于接收消息和通知客户都可以自动完成,因此如果能具备自启动特性则可以完全脱离用户的干预而取得较高的自动化程度。设置自启动的特性,可以从以下几个途径加以考虑:
1. 在"启动"菜单上添加指向程序的快捷方式。
2. 在Autoexec.bat中添加启动程序的命令行。
3. 在Win.ini中的[windows]节的run项目后添加程序路径。
4. 修改注册表,添加键值的具体路径为:
"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run"
并将添加的键值修改为程序的存放路径即可。以上几种方法既可以手工添加,也可以通过编程使之自动完成。
(三) 自动续联
对于服务/客户模式的网络通讯程序普遍要求服务端要先于客户端运行,而本系统的客户、服务端均为自启动,不能保证服务器先于客户机启动,而且本系统要求只要客户机和服务器连接在网络上就要不间断保持连接,因此需要使客户和服务端都要具备自动续联的功能。
对于服务器端,当客户端断开时,需要关闭当前的套接字,并重新启动一个新的套接字以等待客户机的再次连接。这可以放在FD_CLOSE事件对应的消息WM_SOCKET_MSG的消息响应函数中来完成。而对于客户端,如果先于服务器而启动,则connect()函数将返回失败,因此可以在程序启动时用SetTimer()设置一个定时器,每隔一段时间(10秒)就试图连接服务器一次,当connect()函数返回成功即服务器已启动并与之连接上之后可以用KillTimer()函数将定时器关闭。另外当服务器关闭时需要再次开启定时器,以确保当服务器再次运行时能与之建立连接,可以通过响应FD_CLOSE事件来捕获该事件的发生。
小结:本文通过Windows Sockets API实现了基于TCP/IP协议的面向连接的流式套接字的网络通讯程序的设计,通过网络通讯程序的支持可以把服务器捕获到的拨号连接发生的事件及时通知给客户端,最后通过对一些必要的细节的完善很好解决了在局域网上能及时得到服务器拨号连接的消息通知。本文所述程序在Windows 98 SE下,由Microsoft Visual C++ 6.0编译通过;使用的代理服务器软件为WinGate 4.3.0;上网方式为拨号上网。