VC编程轻松获取局域网连接通知
[文章信息] 作者:信息产业部电子第二十二研究所青岛分所郎锐时间:2003-06-23出处:yesky责任编辑:方舟 [文章导读] 在局域网共享一条电话线的情况下,当服务器拨号上网时能及时通知各客户端通过代理服务器进行上网
摘要: 本文从解决实际需要出发,通过采用Windows Socket API等网络编程技术实现了在局域网共享一条电话线的情况下,当服务器拨号上网时能及时通知各客户端通过代理服务器进行上网。本文还特别给出了基于Microsoft Visual C++ 6.0的部分关键实现代码。
一、 问题提出的背景
笔者所使用的局域网拥有一个服务器及若干分布于各办公室的客户机,通过网卡相连。服务器不提供专线上网,但可以拨号上网,而各客户机可以通过装在服务器端的代理服务器共用一条电话线上网,但前提必须是服务器已经拨号连接。考虑到经济原因,服务器不可能长时间连在网上,因此经常出现由于分布于各办公室的客户机不能知道服务器是否处于连线状态而造成的想上网时服务器没有拨号,或是服务器已经拨号而客户机却并不知晓的情况,这无疑会在工作中带来极大的不便。而笔者作为一名程序设计人员,有必要利用自己的专业优势来解决实际工作中所遇到的一些问题。通过对实际情况的分析,可以归纳为一点:当服务器在进行拨号连接时能及时通知在网络上的各个客户机,而各客户机在收到服务器发来的消息后可以根据自己的情况来决定是否上网。这样就可以在同一时间内同时为较多的客户机提供上网服务,此举不仅提高了利用效率也大大节省了上网话费。
二、 程序主要设计思路及实现
由于本网络是通过网卡连接的局域网,因此可以首选Windows Socket API进行套接字编程。整个系统分为两部分:服务端和客户端。服务端运行于服务器上负责监视服务器是否在进行拨号连接,一旦发现马上通过网络发送消息通知客户端;而客户端软件则只需完成同服务端软件的连接并能接收到从服务端发送来的通知消息即可。服务器端要完成比客户端更为繁重的任务。下面对这几部分的实现分别加以描述:
(一)监视拨号连接事件的发生
在采用拨号上网时,首先需要通过拨号连接通过电话线连接到ISP上,然后才能享受到ISP所提供的各种互联网服务。而要捕获拨号连接发生的事件不能依赖于消息通知,因为此时发出的消息同一个对话框出现在屏幕上时所产生的消息是一样的。唯一同其他对话框区别的是其标题是固定的"拨号连接",因此在无其他特殊情况下(如其他程序的标题也是"拨号连接"时)可以认定当桌面上的所有程序窗口出现以"拨号连接" 为标题的窗口时,即可认定此时正在进行拨号连接。因此可以通过搜寻并判断窗口标题的办法对拨号连接进行监视,具体可以用CWnd类的FindWindows()函数来实现:
CWnd *pWnd=CWnd::FindWindow(NULL,"拨号连接");
第一个参数为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. 侦听套接字
listen(sock,1);
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,cDa补充:软件开发 , Vc ,