Winsock套接字有两种工作模式,即阻塞模式(同步模式)和非阻塞模式(异步模式)。阻塞模式下Winsock函数将程序的某个线程(如果程序中只有一个主线程,则整个程序将处于“等待”状态)处于“等待”状态。非阻塞模式Winsock函数不需要等待。在异步模式下,函数执行后立即返回,即使操作未完成;当函数执行完成时,应用程序将以某种方式通知。显然,异步模式更适合Windows下一步的开发。本文介绍了异步模式Winsock编程。
当套接字通过时socket()函数创建后,默认工作在阻塞模式下。为了使套接字在非阻塞模式下工作,需要将套接字设置为非阻塞模式。为了基于Windows该应用程序的消息驱动机制只介绍了常用的函数来改变套接字。这个函数是WSAAsyncSelect()函数的定义如下:
intWSAAsyncSelect(SOCKETs,HWNDhWnd,unsignedintwMsg,longlEvent);WSAAsyncSelect()函数将套接字设置为非阻塞模式,将指定套接字绑定到窗口。当网络事件发生时,相应的信息将发送到绑定窗口。函数的参数含义如下。
S:指定将工作模式改为非阻塞模式的套接字。
hWnd:指定网络事件发生时接收信息的窗口。
wMsg:指定网络事件发生时向窗口发送的信息。这个消息是一个自定义消息,定义自定义消息的方法是 WM_USER在 的基础上添加一个值,例如(WM_USER 1)。
lEvent:指定对应用程序感兴趣的通知码。它可以指定为多个通知码的组合。常用的通知码是 FD_READ(套接字收到对端发送的数据包),FD_ACCEPT(监听中的套接字有连接请求),FD_CONNECT(套接字成功连接到对方)和 FD_CLOSE(套接字对应的连接关闭)。指定通知码时不需要全部指定。对于基于 的TCP 协议客户端,FD_ACCEPT 是没有意义的;对于基于 TCP 对于服务端,FD_CONNECT 是没有意义的;对于基于 UDP对于客户端和服务器端器端,FD_ACCEPT、FD_CONNECT 和 FD_CLOSE 都没有意义。
在了解如何将套接字设置为非阻塞模式后,在这里完成一个简单的远程控制工具。远程控制工具是基于C/S该模是客户端/服务器端模式的架构。客户端在接到控制命令后,通过发送控制命令来响应相应的事件,完成特定的功能。
远程控制服务器端只实现以下功能。
- 向客户端发送帮助信息。
- 向客户端发送服务器信息。
- 交换鼠标的左右键和恢复鼠标的左右键。
- 打开和关闭光驱。
1. 远程控制软件框架设计
远程控制分为控制端和被控制端,通常是客户端,而被控制端通常是服务器端。客户端需要三个通知码,即FD_CONNECT、FD_CLOSE和FD_READ。对务器端需要三个通知码,即FD_ACCEPT、FD_CLOSE和FD_READ,如图1所示。
图1 服务器端和客户端通信
这里解释图1,并补充其框架设计。服务器端(Server对于端),它需要在监控状态下等待客户端(Client端)发起的连接(FD_ACCEPT),连接后,将等待接收客户端发送的控制命令(FD_READ),当客户端断开连接时,通信可以结束(FD_CLOSE)。对于客户端,需要等待确认连接是否成功(FD_CONNET);连接成功后,可将控制命令发送到服务器端,并等待接收命令响应结果(FD_READ);当服务器端关闭时,强制结束通信(FD_CLOSE)。因此,服务器端需要通知码FD_ACCEPT、FD_READ和FD_CLOSE,客户端需要的通知码有FD_CONNECT、FD_READ和FD_CLOSE。
客户端向服务器端发送的命令是“字符串”类型数据。服务器收到客户端发送的命令后,需要判断命令,然后执行相应的功能。
服务器反馈给客户端的执行结果可能是字符串或其他数据结构类型的内容。由于无法确定反馈数据的格式,必须对服务器反馈给客户端的信息进行特殊标记,并通过标记来判断发送的数据格式。客户端收到服务器端发送的数据后,必须分析格式,以便正确读取服务器端返回的命令反馈结果。服务器端的反馈数据协议格式如图2所示。
图2
从图2可以看出,服务器对客户端的反馈数据协议格式有三部分,第一部分bType第二部分用于区分文本数据和特定数据结构的数据bClass第三部分用于区分不同的特定数据结构szValue是真实的数据部分。对于服务器反馈的数据,如果是文本数据,客户端将直接szValue字符串显示输出;如果反馈是特定的数据结构,则必须区分数据结构,最后根据直接数据结构进行分析szValue中间的数据。将协议格式定义为数据结构,如下:
#defineTEXTMSG't表示文本信息#defineBINARYMSG'b表示特定的数据结构typedefstruct_DATA_MSG{BYTEbType;///数据类型BYTEbClass;//补充数据类型charszValue[0x200];//数据的信息}DATA_MSG,*PDATA_MSG;2. 软件代码远程控制要点
前面介绍了WSAAsyncSelect()函数原型和参数的含义如下WSAAsyncSelect()函数的使用。WSAAsyncSelect()函数将指定的套接字、窗口句柄、自定义信息和通知码连接在一起,使用如下:
//初始化Winsock库WSADATAwsaData;WSAStartup(MAKEWORD(2,2),&wsaData);//创建套接字,并将其设置为非阻塞模式m_ListenSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);WSAAsyncSelect(m_ListenSock,GetSafeHwnd(),UM_SERVER,FD_ACCEPT);在代码的WSAAsyncSelect()函数中,第一个参数是新创建的监控套接字m_ListenSock,使用第二个参数MFC的成员函数GetSafeHwnd()获得当前窗体的句柄,第三个参数UM_SERVER是一个自定义的类型,最后一个参数FD_ACCEPT这是一套接收字符的通知码。函数中的第三个参数是一个自定义的信息。该消息的定义如下:
#defineUM_SERVER(WM_USER 200)当有客户端与服务器端连接时,系统会发送UM_SERVER消息到与监听套接字相关的句柄指定的窗口。窗口收到消息后,需要处理消息。处理函数也需要手动添加,有三个地方可以添加。
第一个是添加到类定义中,代码如下:
////生成的信息映射函数//{{AFX_MSG(CServerDlg)virtualBOOLOnInitDialog();afx_msgvoidOnSysCommand(UINTnID,LPARAMlParam);afx_msgvoidOnPaint();afx_msgHCURSOROnQueryDragIcon();afx_msgVOIDOnSock(WPARAMwParam,LPARAMlParam);afx_msgvoidOnClose();//}}AFX_MSGDECLARE_MESSAGE_MAP()在这里添加afx_msg VOID OnSock(WPARAM wParam,LPARAM lParam);
在类实现中添加相应的函数实现代码,如下:
VOIDCServerDlg::OnSock(WPARAMwParam,LPARAMlParam){}三是添加信息映射,代码如下:
BEGIN_MESSAGE_MAP(CServerDlg,CDialog)//{{AFX_MSG_MAP(CServerDlg)ON_WM_SYSCOMMAND()ON_WM_PAINT()ON_WM_QUERYDRAGICON()ON_MESSAGE(UM_SERVER,OnSock)ON_WM_CLOSE()//}}AFX_MSG_MAPEND_MESSAGE_MAP()在这里添加ON_MESSAGE(UM_SERVER,OnSock)。
通过以上三个步骤,可以在程序中接收和响应UM_SERVER处理消息。
3. 远程控制界面布局
首先来看远程控制客户端与服务器端的窗口界面,如图3所示。
图3
在图3中,SERVER表示服务器端,Client表示客户端。服务器端。(Server)在虚拟机中运行,客户端(Client)在物理机器中运行。从图3可以看出,物理机器中的客户端和服务器端可以正常通信。
只有一个编辑框用于显示多行文本。界面相对简单。
客户端软件在IP将服务器端输入到地址后的编辑框中IP单击地址“连接”按钮,客户端将连接到远端服务器。当连接成功时,输入IP地址的编辑框会处于只读状态,“连接”按钮变为“断开连接”按钮。将编辑框发送命令后变为可用状态,“发送”按钮也变成了可用状态。
您可以调整软件界面的布局。
4. 实现服务器端代码
服务器启动时,需要创建套接字,并将套接字设置为异步模式,绑定IP代码如下:
BOOLCServerDlg::OnInitDialog(){……//添加其他初始化代码//初始化Winsock库WSADATAwsaData;WSAStartup(MAKEWORD(2,2),&wsaData);//创建套接字,并将其设置为非阻塞模式m_ListenSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);WSAAsyncSelect(m_ListenSock,GetSafeHwnd(),UM_SERVER,FD_ACCEPT);sockaddr_inaddr;addr.sin_family=AF_INET;addr.sin_addr.S_un.S_addr=ADDR_ANY;addr.sin_port=htons(5555);//绑定IP地址和5555端口处于监控状态bind(m_ListenSock,(SOCKADDR*)&addr,sizeof(addr));listen(m_ListenSock,1);returnTRUE;//returnTRUEunlessyousetthefocustoacontrol}当客户端与服务器端连接时,需要处理通知码FD_ACCEPT,并创建与客户端通信的新套接字。新套接字还需要设置为异步模式和设置FD_READ和FD_CLOSE两个通知码。代码如下:
VOIDCServerDlg::OnSock(WPARAMwParam,LPARAMlParam){if(WSAGETSELECTERROR(lParam)){return;}switch(WSAGETSELECTEVENT(lParam)){//处理FD_ACCEPTcaseFD_ACCEPT:{sockaddr_inClientAddr;intnSize=sizeof(ClientAddr);m_ClientSock=accept(m_ListenSock,(SOCKADDR*)&ClientAddr,&nSize);WSAAsyncSelect(m_ClientSock,GetSafeHwnd(),UM_SERVER,FD_READ|FD_CLOSE);m_StrMsg.Format("请求地址为%s:%d",inet_ntoa(ClientAddr.sin_addr),ntohs(ClientAddr.sin_port));DATA_MSGDataMsg;DataMsg.bType=TEXTMSG;DataMsg.bClass=0;lstrcpy(DataMsg.szValue,HELPMSG);send(m_ClientSock,(constchar*)&DataMsg,sizeof(DataMsg),0);break;}//处理FD_READcaseFD_READ:{charszBuf[MAXBYTE]={0};recv(m_ClientSock,szBuf,MAXBYTE,0);DispatchMsg(szBuf);m_StrMsg="对方发出命令:";m_StrMsg =szBuf;break;}//处理FD_CLOSEcaseFD_CLOSE:{closesocket(m_ClientSock);m_StrMsg="对方关闭连接";break;}}InsertMsg();}在代码中,当响应FD_READ通知码将接收客户端发送的命令,并通过DispatchMsg()处理客户端发送的命令函数。OnSock()函数的最后一个InsertMsg()函数用于在界面上相应的信息编辑框中显示接收到的命令。
DispatchMsg()用于处理客户端发送的命令的函数,代码如下:
VOIDCServerDlg::DispatchMsg(char*szBuf){DATA_MSGDataMsg;ZeroMemory((void*)&DataMsg,sizeof(DataMsg));if(!strcmp(szBuf,"help")){DataMsg.bType=TEXTMSG;DataMsg.bClass=0;lstrcpy(DataMsg.szValue,HELPMSG);}elseif(!strcmp(szBuf,"getsysinfo")){SYS_INFOSysInfo;GetSysInfo(&SysInfo);DataMsg.bType=BINARYMSG;DataMsg.bClass=SYSINFO;memcpy((void*)DataMsg.szValue,(constchar*)&SysInfo,sizeof(DataMsg));}elseif(!strcmp(szBuf,"open")){SetCdaudio(TRUE);DataMsg.bType=TEXTMSG;DataMsg.bClass=0;lstrcpy(DataMsg.szValue,"open完成命令执行");}elseif(!strcmp(szBuf,"close")){SetCdaudio(FALSE);DataMsg.bType=TEXTMSG;DataMsg.bClass=0;lstrcpy(DataMsg.szValue,"close命令执行完成");}elseif(!strcmp(szBuf,"swap")){SetMouseButton(TRUE);DataMsg.bType=TEXTMSG;DataMsg.bClass=0;lstrcpy(DataMsg.szValue,"swap完成命令执行");}elseif(!strcmp(szBuf,"restore")){SetMouseButton(FALSE);DataMsg.bType=TEXTMSG;DataMsg.bClass=0;lstrcpy(DataMsg.szValue,"restore完成命令执行");}else{DataMsg.bType=TEXTMSG;DataMsg.bClass=0;lstrcpy(DataMsg.szValue,"无效的指令");}////向客户端发送执行命令send(m_ClientSock,(constchar*)&DataMsg,sizeof(DataMsg),0);}在DispatchMsg()函数中,通过if()…else if()…else()比较客户端发送的命令执行相应的功能,并将执行结果发送给客户端。
实现命令功能的函数如下:
VOIDCServerDlg::GetSysInfo(PSYS_INFOSysInfo){unsignedlongnSize=0;SysInfo->OsVer.dwOSVersionInfoSize=sizeof(OSVERSIONINFO);GetVersionEx(&SysInfo->OsVer);nSize=NAME_LEN;GetComputerName(SysInfo->szComputerName,&nSize);nSize=NAME_LEN;GetUserName(SysInfo->szUserName,&nSize);}VOIDCServerDlg::SetCdaudio(BOOLbOpen){if(bOpen){///打开光驱mciSendString("setcdaudiodooropen",NULL,NULL,NULL);}else{///关闭光驱mciSendString("setcdaudiodoorclosed",NULL,NULL,NULL);}}VOIDCServerDlg::SetMouseButton(BOOLbSwap){if(bSwap){//交换SwapMouseButton(TRUE);}else{//恢复SwapMouseButton(FALSE);}}这里面对于getsysinfo具体如下:
#defineHELPMSG"帮助信息:\r\n"\"\thelp:显示帮助菜单\r\n"\"\tgetsysinfo:获取对方主机的信息\r\n"\"\topen:打开光驱\r\n"\"\tclose:关闭光驱\r\n"\"\tswap:交换鼠标左右键\r\n"\"\trestore:恢复鼠标左右键"\#defineNAME_LEN20typedefstruct_SYS_INFO{OSVERSIONINFOOsVer;////保存操作系统信息charszComputerName[NAME_LEN];///保存计算机名称charszUserName[NAME_LEN];//保存当前登录名}SYS_INFO,*PSYS_INFO;该结构不是文本数据,需要在反馈协议中填写bClass字段。对于getsysinfo命令,该bClass填充字段的内容是“SYSINFO”。SYSINFO定义如下:
#defineSYSINFO0x01L调用mciSendString()函数需要添加头文件和库文件,具体如下:
#include<mmsystem.h>#pragmacomment(lib,"Winmm")到目前为止,已经介绍了服务器端的主要功能,最后还没有列出两个函数,即InsertMsg()函数和释放Winsock代码如下:
voidCServerDlg::OnClose(){///添加处理程序代码或调用默认方法///关闭监听套接字,释放Winsock库closesocket(m_ClientSock);closesocket(m_ListenSock);WSACleanup();CDialog::OnClose();}VOIDCServerDlg::InsertMsg(){CStringstrMsg;GetDlgItemText(IDC_MSG,strMsg);m_StrMsg ="\r\n";m_StrMsg ="----------------------------------------\r\n";m_StrMsg =strMsg;SetDlgItemText(IDC_MSG,m_StrMsg);m_StrMsg="";}5.实现客户端代码
客户代码与服务代码基本相似,这里不再解释。
连接远程服务器的代码如下:
voidCClientDlg::OnBtnConnect(){///添加处理程序代码charszBtnName[10]={0};GetDlgItemText(IDC_BTN_CONNECT,szBtnName,10);//断开连接if(!lstrcmp(szBtnName,"断开连接")){SetDlgItemText(IDC_BTN_CONNECT,"连接");(GetDlgItem(IDC_SZCMD))->EnableWindow(FALSE);(GetDlgItem(IDC_BTN_SEND))->EnableWindow(FALSE);(GetDlgItem(IDC_IPADDR))->EnableWindow(TRUE);closesocket(m_Socket);m_StrMsg="主动断开连接";InsertMsg();return;}///连接远程服务器终端charszIpAddr[MAXBYTE]={0};GetDlgItemText(IDC_IPADDR,szIpAddr,MAXBYTE);m_Socket=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);WSAAsyncSelect(m_Socket,GetSafeHwnd(),UM_CLIENT,FD_READ|FD_CONNECT|FD_CLOSE);sockaddr_inServerAddr;ServerAddr.sin_family=AF_INET;ServerAddr.sin_addr.S_un.S_addr=inet_addr(szIpAddr);ServerAddr.sin_port=htons(5555);connect(m_Socket,(SOCKADDR*)&ServerAddr,sizeof(ServerAddr));}响应通知码函数如下:
VOIDCClientDlg::OnSock(WPARAMwParam,LPARAMlParam){if(WSAGETSELECTERROR(lParam)){return;}switch(WSAGETSELECTEVENT(lParam)){//处理FD_ACCEPTcaseFD_CONNECT:{(GetDlgItem(IDC_SZCMD))->EnableWindow(TRUE);(GetDlgItem(IDC_BTN_SEND))->EnableWindow(TRUE);(GetDlgItem(IDC_IPADDR))->EnableWindow(FALSE);SetDlgItemText(IDC_BTN_CONNECT,"断开连接");m_StrMsg="连接成功";break;}//处理FD_READcaseFD_READ:{DATA_MSGDataMsg;recv(m_Socket,(char*)&DataMsg,sizeof(DataMsg),0);DispatchMsg((char*)&DataMsg);break;}//处理FD_CLOSEcaseFD_CLOSE:{(GetDlgItem(IDC_SZCMD))->EnableWindow(FALSE);(GetDlgItem(IDC_BTN_SEND))->EnableWindow(FALSE);(GetDlgItem(IDC_IPADDR))->EnableWindow(TRUE);closesocket(m_Socket);m_StrMsg="对方关闭连接";break;}}InsertMsg();}将命令发送到远程服务器端的代码如下:
voidCClientDlg::OnBtnSend(){///添加处理程序代码charszBuf[MAXBYTE]={0};GetDlgItemText(IDC_SZCMD,szBuf,MAXBYTE);send(m_Socket,szBuf,MAXBYTE,0);}处理服务器端反馈结果的代码如下:
VOIDCClientDlg::DispatchMsg(char*szBuf){DATA_MSGDataMsg;memcpy((void*)&DataMsg,(constvoid*)szBuf,sizeof(DATA_MSG));if(DataMsg.bType==TEXTMSG){m_StrMsg=DataMsg.szValue;}else{if(DataMsg.bClass==SYSTEMINFO){ParseSysInfo((PSYS_INFO)&DataMsg.szValue);}}}解析服务器端信息的代码如下:
VOIDCClientDlg::ParseSysInfo(PSYS_INFOSysInfo){if(SysInfo->OsVer.dwPlatformId==VER_PLATFORM_WIN32_NT){if(SysInfo->OsVer.dwMajorVersion==5&&SysInfo->OsVer.dwMinorVersion==1){m_StrMsg.Format("对方系统信息:\r\n\tWindowsXP%s",SysInfo->OsVer.szCSDVersion);}elseif(SysInfo->OsVer.dwMajorVersion==5&&SysInfo->OsVer.dwMinorVersion==0){m_StrMsg.Format("对方系统信息:\r\n\tWindows2K");}}else{m_StrMsg.Format("对方系统信息:\r\n\tOtherSystem\r\n");}m_StrMsg ="\r\n";m_StrMsg ="\tComputerNameis";m_StrMsg =SysInfo->szComputerName;m_StrMsg ="\r\n";m_StrMsg ="\tUserNameis";m_StrMsg =SysInfo->szUserName;}在这里,完成了远程控制代码。如果要实现更多的功能,框架可能无法更好地扩展。本例主要用于演示非阻塞模式Winsock应用程序开发。如果实例中的套接字采用阻塞模式,则必须配合多线程完成,并将接收部分单独放置在一个线程中,否则接收数据的函数recv()整个程序将在等待接收数据的到来时进行“卡死”。