Winsocket入门教程三:以Windows消息机制驱动的客户端程序

来源:互联网 发布:网络分配器的作用 编辑:程序博客网 时间:2024/05/18 23:15
  前面两讲为大家介绍了编写传统socket程序的两种方法,今天将为大家介绍一种使用Windows消息机制编写socket客户端程序的方法。使用Windows消息机制编写socket程序主要有以下的好处:一是我们可以将大部分的recv操作以及close操作放到消息处理函数里面,以利于代码的维护;二是当有数据可读的时候,本地程序会接到相应的消息,我们可以在这时候读取数据。大家可以想像一下,在传统的socket程序中,如果一个远程程序在你没有向它发送请求的时候给你传送数据的话,如果本地程序没有进行相应的检测(一种方法是通过计时器进行检测),是不能及时根据它发送给你的数据进行相应的操作的。而如果使用消息机制的话,就能很好的解决这个问题;三是可以轻松的检测出远程程序主动关闭和意外退出的情况。如果使用传统方式的话,我们必须定时进行相应的检测才能知道该情况的发生。

     为了使用消息机制驱动的网络程序,我们必须使用WSAAsyncSelect函数注册Windows消息以及我们感兴趣的网络事件。WSAAsyncSelect函数的函数原型如下所示:

      int WSAAsyncSelect(SOCKET s, HWNDhWnd, unsigned int wMsg, long lEvent)

      s
[in] 需要获取相应网络事件的套接字。
      hWnd
          [in] 需要获取相应网络事件的窗口。
      wMsg
          [in] 当网络事件发生时候将会收到的消息。
      lEvent
          [in] 可以使用位域组合的网络事件。
     Return Values
          成功返回0,失败返回SOCKET_ERROR。在返回SOCKET_ERROR的时候,你可以使用WSAGetLastError来获取相应的错误代码。
     
     做为一个客户端程序,我们主要对以下Winsocket中定义的网络事件感兴趣。
     FD_CONNECT
          当连接完成的时候希望收到我们注册的网络消息。
     FD_READ
          当有数据可读时希望收到我们注册的网络消息。
     FD_WRITE
           当可以向对方写数据时希望收到我们注册的网络消息。
     FD_CLOSE
           当套接字关闭的时候希望收到我们注册的网络消息。
     
     假设我们已经在应用程序中创建了套接字s,获取了窗口句柄hwnd以及定义了WM_SOCKET消息,我们可以按照如下的方式调用WSAAsyncSelect函数,以使我们可以在有数据可读和套接字关闭的时候收到WM_SOCKET消息。
    
view plain
  1. int iRet = WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);  
 
     但我们需要注意的是,如果对同一个套接字多次调用WSAAsyncSelect函数,函数以前注册的消息以及网络事件都会失效。例如我们还是想完成上面代码的功能,但按以下方式调用函数。
      
view plain
  1. int iRet = WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_READ);  
  2. iRet = WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_CLOSE);  
      这样的话,我们的socket程序就只会在FD_CLOSE事件发生的时候才能收到WM_SOCKET消息。我们还可以使用如下的代码来注销掉一个套接字上的消息和网络事件,表明该套接字不想再收到任何的网络消息.
      
view plain
  1. int iRet = WSAAsyncSelect(s, hwnd, 0, 0);  
      但要注意的是,进行以上函数调用后虽然不会再收到WM_SOCKET消息,但是放到消息队列中的消息并不会被丢弃,所以我们必须对这些消息进行处理。
      另外一个需要注意的地方是,WSAAsyncSelect函数的调用会默认的将套接字设置为非阻塞状态,如果你想将其设置为阻塞状态,请使用ioctlsocket函数。
      一个典型的WM_SOCKET消息处理函数以如下的方式定下:
      LRESULT OnSocket(WPARAM wParam, LPARAM lParam)
      wParam
           表示收到消息的套接字。
      lParam
          表示收到消息所带的事件。其中低16位表示收到消息的消息码,高16位表示收到消息的错误码。在这里请不要使用WSAGetLastError来获取该错误码,因为该错误码和WSAGetLastError获取的错误码值是不一样的。我们一般可以使用WSAGETSELECTEVENT和WSAGETSELECTERROR这两个宏来获取相应的事件和错误代码。具体代码如下:
      
view plain
  1. WORD wEvent = WSAGETSELECTEVENT(lParam);  
  2. WORD wError = WSAGETSELECTERROR(lParam);  
 
      
      在这里我们需要注意两点:一是网络消息和相关的消息处理函数你可以取任何的名字,并不一定非要是WM_SOCKET和OnSocket;二就是虽然我们可以一次注册多个事件,但是一个消息一次只会携带所收到的一个事件。
      一个典型的OnSocket函数的处理方法如下所示:
    
view plain
  1. LRESULT OnSocket(WPARAM wParam, LPARAM lParam)  
  2. {  
  3.     SOCKET s = wParam;  
  4.     WORD uEvent = WSAGETSELECTEVENT(lParam);  
  5.     WORD uError = WSAGETSELECTERROR(lParam);  
  6.     switch(uEvent)  
  7.     {  
  8.     case FD_CONNECT:  
  9.          // do something  
  10.          break;  
  11.     case FD_READ:  
  12.          // do something  
  13.          break;  
  14.     case FD_WRITE:  
  15.          // do something  
  16.          break;  
  17.     case FD_CLOSE:  
  18.          // do something  
  19.          break;  
  20.     default:  
  21.          break;  
  22.     }  
  23.     return 0;  
  24. }  
 
     
     下面再让我们来看看什么样的操作会触发相应的网络事件,并让系统向应用程序投递WM_SOCKET消息。
     FD_CONNECT:
          当调用connect函数,并且在该套接字上成功的建立了连接。在调用WSAAsyncSelect然后再调用connect函数的时候,connect函数的返回值始终是SOCKET_ERROR,这时候我们可以用WSAGetLastError检测错误码,如果错误码为WSAEWOULDBLOCK,则说明建立连接成功,程序会收到WM_SOCKET消息。
     FD_WRITE:
          在调用connect函数以后,如果成功建立连接,并且已经注册了要接收FD_WRITE事件,则在接收到FD_CONNECT事件后,会收到携带该事件的消息。此时表示可以用send或sendto发送数据。
          在调用send或sendto时收到WSAEWOULDBEBLOCK错误码。
     FD_READ:
          当有数据可读,并且程序没有收到携带FD_READ事件的WM_SOCKET消息。
          当调用recv或recvfrom后,仍然有数据可读。请注意在处理携带FD_READ的WM_SOCKET消息的时候不要使用循环的方式一次性的将所有的数据读完,因为这样程序会收到多个携带FD_READ的消息。如果你要使用循环读取数据的话,请首先注销掉FD_READ事件。
     FD_CLOSE: 仅仅在面向连接的程序中会收到携带该事件的消息
          当远程程序正常关闭的时候,请注意这时候您必须收完所有的数据,才会收到相应的消息。
          当远程程序异常关闭的时候。
          当本地程序使用shutdown关闭的时候,请注意您仍然需要收完所有的数据,才会收到相应的消息。
          
          下面让我们来看一个使用消息机制驱动的简单客户端程序的关键代码,该程序使用的框架为MFC创建的对话框程序。
          首先我们需要在OnInitDialog()中加入下面的代码:
          
view plain
  1. m_sktSession = socket(AF_INET,  SOCK_STREAM, 0);  
  2.     int iRet = WSAAsyncSelect(m_sktSession, GetSafeHwnd(), WM_SOCKET, FD_CONNECT);  
  3.     ASSERT(SOCKET_ERROR != iRet);  
  4.     // 初始化连接套接字地址信息  
  5.     const char szIP[] = "127.0.0.1";  
  6.     const unsigned short uPort = 10001;  
  7.     sockaddr_in adrServ;                        // 表示网络地址  
  8.     ZeroMemory(&adrServ, sizeof(sockaddr_in));        
  9.     adrServ.sin_family      = AF_INET;          // 初始化地址格式,只能为AF_INET  
  10.     adrServ.sin_port        = htons(uPort);     // 初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序  
  11.     adrServ.sin_addr.s_addr = inet_addr(szIP);  // 初始化IP, 由于网络字节顺序和主机字节顺序相反,所以必须使用inet_addr将主机字节顺序转换成网络字节顺序  
  12.     iRet = connect(m_sktSession, (sockaddr*)&adrServ, sizeof(adrServ));  
  13.     ASSERT(WSAEWOULDBLOCK == WSAGetLastError()); // iRet总是返回SOCKET_ERROR, 所以通过判断WSAEWOULDBLOCK来确定连接是否成功  
 

          OnSocket函数中的代码如下所示:
          
view plain
  1. LRESULT CClientWinSelectDlg::OnSocket(WPARAM wParam, LPARAM lParam)  
  2. {  
  3.     SOCKET s = wParam;  
  4.     unsigned short uEvent = WSAGETSELECTEVENT(lParam);  
  5.     unsigned short uError = WSAGETSELECTERROR(lParam);  
  6.     int iRet = SOCKET_ERROR;  
  7.     const size_t uBufLen = 128;  
  8.     char szBuf[uBufLen] = {0};  
  9.     switch(uEvent)  
  10.     {  
  11.     case FD_CONNECT:  
  12.         ASSERT(0 == uError);  
  13.         iRet = WSAAsyncSelect(s, GetSafeHwnd(), WM_SOCKET, FD_WRITE | FD_CLOSE); // 在连接成功后,会收到携带FD_WRITE事件的WM_SOCKET消息  
  14.         ASSERT(0 == iRet);  
  15.         UpdateData();  
  16.         m_strRecv = "Connection OK. Now you can send message to server/r/n";  
  17.         UpdateData(FALSE);  
  18.         break;  
  19.     case FD_READ:  
  20.         ASSERT(0 == uError);  
  21.         iRet = recv(s, szBuf, uBufLen - 1, 0);  
  22.         ASSERT(SOCKET_ERROR != iRet);  
  23.         UpdateData();  
  24.         m_strRecv.Format("%s%s/r/n", m_strRecv, szBuf);  
  25.         UpdateData(FALSE);  
  26.           
  27.         break;  
  28.     case FD_WRITE: // FD_WRITE事件不同于FD_READ, 我们只能通过FD_WRITE事件判断是否可以向对方发送数据, 而不是在事件处理中向对方发数据  
  29.         ASSERT(0 == uError);  
  30.         iRet = WSAAsyncSelect(s, GetSafeHwnd(), WM_SOCKET, FD_READ | FD_CLOSE);  
  31.         ASSERT(0 == iRet);  
  32.         break;  
  33.     case FD_CLOSE:  
  34.         while (0 < recv(s, szBuf, uBufLen - 1, 0));  
  35.         iRet = WSAAsyncSelect(s, GetSafeHwnd(), 0, 0);  
  36.         ASSERT(SOCKET_ERROR != iRet);  
  37.         closesocket(s);  
  38.         m_sktSession = INVALID_SOCKET;  
  39.         UpdateData();  
  40.         m_strRecv.Format("%sconnection has clsoed/r/n", m_strRecv);  
  41.         UpdateData(FALSE);  
  42.         break;  
  43.     default:  
  44.         break;  
  45.     }  
  46.     return 0;  
  47. }  
 

          发送数据的代码如下所示:
          
view plain
  1. void CClientWinSelectDlg::OnClickedButtonSend()  
  2. {  
  3.     // TODO: 在此添加控件通知处理程序代码  
  4.     int iRet = WSAAsyncSelect(m_sktSession, GetSafeHwnd(), WM_SOCKET, FD_WRITE | FD_CLOSE);  
  5.     ASSERT(0 == iRet);  
  6.     UpdateData();  
  7.     iRet = send(m_sktSession, (LPCSTR)m_strSend, m_strSend.GetLength(), 0);  
  8.     m_strSend.Empty();  
  9.     UpdateData(FALSE);  
  10.     ASSERT(SOCKET_ERROR != iRet);  
  11. }  
 

          关闭操作如下所示:
          
view plain
  1. CClientWinSelectDlg::~CClientWinSelectDlg()  
  2. {  
  3.     int iRet = SOCKET_ERROR;  
  4.     if (INVALID_SOCKET != m_sktSession)  
  5.     {  
  6.         iRet = shutdown(m_sktSession, SD_SEND);  
  7.         ASSERT(SOCKET_ERROR != iRet);  
  8.     }  
  9. }  
 

         程序的运行结果如下所示:

        

          程序源代码地址:http://e.ys168.com/?shining100 密码为123456
                    

          以上就是使用消息机制创建简单客户端程序的全部过程以及内容了。Winsocket入门教程这个系列也暂时告一段落了,本人也是因为工作才学的菜鸟呀,希望在有时间深入的学习以后,能为大家带来更多精彩的内容。