kavin

网络安全编程:非阻塞模式开发

kavin 安全防护 2022-12-03 360浏览 0

网络安全编程:非阻塞模式开发

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. 远程控制软件框架设计

远程控制分为控制端和被控制端,控制端通常为客户端,而被控制端通常为服务器端。对于客户端来说,它需要3种通知码,即FD_CONNECT、FD_CLOSE和FD_READ。对于服务器端来说,它需要3种通知码,即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可以看出,服务器对于客户端的反馈数据协议格式有3部分内容,第1部分bType用于区分是文本数据和特定数据结构的数据,第2部分bClass用于区分不同的特定数据结构,第3部分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()函数中,第1个参数是新创建的用于监听的套接字m_ListenSock,第2个参数使用MFC的成员函数GetSafeHwnd()来得到当前窗体的句柄,第3个参数UM_SERVER是一个自定义的类型,最后一个参数FD_ACCEPT是该套接字要接收的通知码。函数中的第3个参数是一个自定义的消息。在服务器端,该消息的定义如下:

#defineUM_SERVER(WM_USER+200)

当有客户端与服务器端连接时,系统会发送UM_SERVER消息到与监听套接字关联的句柄指定的窗口。当窗口收到该消息后,需要对该消息进行处理。该处理函数也需要手动进行添加,添加有3处地方。

第1处是在类定义中添加,代码如下:

//生成的消息映射函数
//{{AFX_MSG(CServerDlg)
virtualBOOLOnInitDialog();
afx_msgvoidOnSysCommand(UINTnID,LPARAMlParam);
afx_msgvoidOnPaint();
afx_msgHCURSOROnQueryDragIcon();
afx_msgVOIDOnSock(WPARAMwParam,LPARAMlParam);
afx_msgvoidOnClose();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()

在这里添加afx_msg VOID OnSock(WPARAM wParam, LPARAM lParam);

第2处在类实现中添加对应的函数实现代码,如下:

VOIDCServerDlg::OnSock(WPARAMwParam,LPARAMlParam)
{
}

第3处是要添加消息映射,代码如下:

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_MAP
END_MESSAGE_MAP()

在这里添加ON_MESSAGE(UM_SERVER, OnSock)。

通过以上3步,在程序中就可以接收并响应对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_ACCEPT
caseFD_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_READ
caseFD_READ:
{
charszBuf[MAXBYTE]={0};
recv(m_ClientSock,szBuf,MAXBYTE,0);
DispatchMsg(szBuf);
m_StrMsg="对方发来命令:";
m_StrMsg+=szBuf;
break;
}
//处理FD_CLOSE
caseFD_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_LEN20
typedefstruct_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_ACCEPT
caseFD_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_READ
caseFD_READ:
{
DATA_MSGDataMsg;
recv(m_Socket,(char*)&DataMsg,sizeof(DataMsg),0);
DispatchMsg((char*)&DataMsg);
break;
}
//处理FD_CLOSE
caseFD_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()在等待接收数据的到来时会将整个程序“卡死”。

继续浏览有关 安全 的文章
发表评论