一、引言
上一文中我们介绍了使用WSAEventSelect实现异步通知IO的方法,本文我们主要讨论下使用WSAAsyncSelect处理socket的方法。本文的主要目标,是创建一个带界面的回声服务端,接收并返回客户端传过来的字符串,并在界面上显示该字符串。为此,我们将采用MFC的编程环境,建立如下的对话框程序:
二、WSAAsyncSelect函数
WSAAsyncSelect也是windows下一种异步的select,它可以注册IO事件,当发生注册的IO事件时,它会发送一个我们自定义的消息给我们的窗口,而我们在窗口的消息处理函数中就可以处理这些消息了。它的原型如下:
int WSAAsyncSelect(
__in SOCKET s,
__in HWND hWnd,
__in unsigned int wMsg,
__in long lEvent
);
s —— 用于监视的socket。
hWnd —— 用来接收socket消息的窗口。
wMsg —— 是我们自定义的消息,一般情况下使用“WM_USER+ 数字”的形式进行定义。
lEvent —— 进行监视的IO事件,包含如下几种:
FD_READ:套接字可读通知。
FD_WRITE:可写通知。
FD_ACCEPT:服务器接收连接的通知。
FD_CONNECT:有客户连接通知。
FD_OOB:外带数据到达通知。
FD_CLOSE:套接字关闭通知。
FD_QOS:服务质量发生变化通知。
FD_GROUP_QOS:组服务质量发生变化通知。
FD_ROUTING_INTERFACE_CHANGE:与路由器接口发生变化的通知。
FD_ADDRESS_LIST_CHANGE:本地地址列表发生变化的通知。
返回值 —— 正常情况下返回0,出现错误时返回错误码。
我们知道Windows系统中消息都会携带两个参数wParam和lParam,我们在进行窗口的消息处理时往往会利用这两个参数。WSAAsyncSelect发送的socket消息也不例外,它的wParam参数携带的是发生IO事件的socket。而lParam低字节表明已发生的事件。高字节包含错误代码。我们可以使用WSAGETSElECTERROR宏来读取lParam的高字节获取错误码,使用WSAGETSELECTEVENT宏读取lParam的低字节来获取发生的事件类型。
三、编程步骤
3.1 自定义消息
我们先自定义一个socket消息以用WSAAsyncSelect发送:
#define WM_SOCKET WM_USER+1
3.2 注册服务端的socket
我们先经过一般步骤创建服务端的socket,并然后用WSAAsyncSelect进行注册:
WSAStartup(MAKEWORD(2,2),&m_wsaData);
m_servSock=socket(AF_INET,SOCK_STREAM,0);
memset(&m_servAddr,0,sizeof(m_servAddr));
m_servAddr.sin_family=AF_INET;
m_servAddr.sin_addr.s_addr=htonl(INADDR_ANY);
m_servAddr.sin_port=htons(atoi("8888"));
bind(m_servSock,(SOCKADDR*)&m_servAddr,sizeof(m_servAddr));
listen(m_servSock,5);
WSAAsyncSelect(m_servSock,this->m_hWnd,WM_SOCKET,FD_ACCEPT|FD_READ);
最后一句代码中,我们使用WSAAsyncSelect注册了服务端的socketm_servSock,WSAAsyncSelect的第二个参数填写了我们窗口的m_hWnd值,第三个参数是我们自定义的消息,而第四个参数填写的是注册的IO事件,分别表示接受链接和有数据可读。注册完成之后,一旦有新的客户端连接,系统就会发送一个WM_SOCKET消息给我们的窗口。
3.3 处理socket消息
为了能够处理socket消息,我们先引入窗口的消息处理函数,从类向导中添加虚函数OnWndMsg,在OnWndMsg中我们进行socket消息的处理:
BOOL CWSAAsyncSelectServDlg::OnWndMsg(UINT message,WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
switch(message)
{
case WM_SOCKET:
{
SOCKETsock = (SOCKET) wParam;
if(m_servSock == sock)
{
//获?取¨?
m_clntAddrSz=sizeof(m_clntAddr);
m_clntSock=accept(m_servSock,(SOCKADDR*)&m_clntAddr,&m_clntAddrSz);
WSAAsyncSelect(m_clntSock,this->m_hWnd,WM_SOCKET,FD_READ);
//将?m_clntSock和¨ªm_clntAddr存ä?到Ì?映®3射¦?中D
m_sockMap.SetAt(m_clntSock,m_clntAddr);
PrintClientConnectMsg();
}
else
{
m_strLen= recv(sock,buf,BUF_SIZE - 1,0);
send(sock,buf,m_strLen,0);
buf[m_strLen]=0;
PrintClientSendMsg(sock);
}
}
break;
default:
break;
}
return CDialogEx::OnWndMsg(message, wParam, lParam,pResult);
}
从上述函数的WM_SOCKETswitch分支可以看到,我们通过wParam获取到了发生IO事件的socket,如果有需要还可以通过lParam获取到错误码或者事件类型。获取到socket之后,先判断socket是不是服务端的m_servSock。如果是,表明有新的客户端链接,接收连接并打印客户端信息;如果不是则是由客户端传来了数据,调用recv接收数据并send给客户端,最后打印数据。
下面是用来打印客户端信息和客户端数据的两个函数:
void CWSAAsyncSelectServDlg::PrintClientConnectMsg(void)
{
CString str;
CTimelocTime=CTime::GetCurrentTime(); //获?取¨?当Ì¡À前¡ã时º¡À间?
CStringstrTime=locTime.Format("%Y-%m-%d%H:%M:%S");//将?当Ì¡À前¡ã时º¡À间?转Áa换?成¨¦CString类¤¨¤型¨ª
str.Format("%s Client %s:%d Connectted",strTime,inet_ntoa(m_clntAddr.sin_addr),m_clntAddr.sin_port);
this->m_list.AddString(str);
}
void CWSAAsyncSelectServDlg::PrintClientSendMsg(SOCKET sock)
{
CString str;
m_clntAddr=m_sockMap[sock];
CTimelocTime=CTime::GetCurrentTime();
CStringstrTime=locTime.Format("%Y-%m-%d%H:%M:%S");//将?当Ì¡À前¡ã时º¡À间?转Áa换?成¨¦CString类¤¨¤型¨ª
str.Format("%s Client %s:%d say:%s",strTime,inet_ntoa(m_clntAddr.sin_addr),m_clntAddr.sin_port,buf);
this->m_list.AddString(str);
}
运行程序,效果如下,服务端不单返回了客户端传过来的信息,还对信息进行了打印。
客户端1:
客户端2:
服务端:
Github位置:
https://github.com/HymanLiuTS/NetDevelopment
克隆本项目:
git clone git@github.com:HymanLiuTS/NetDevelopment.git
获取本文源代码:
git checkout NL53