posts - 0, comments - 1, trackbacks - 0, articles - 5
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

      [声明:本文章来源于网络,原作者不详!]

       利用Socket编程是一类典型的网络通信程序,特别是在实时性要求比较高的项目中,Winsock编程方法是非常实用的。
 
       下面介绍在VC 6.0环境下开发Winsock程序的方法。

       这里并没有直接应用MFC 提供的CSocket类,这是因为考虑到对于类而言,其成员函数调用必然是完全阻塞方式的,因此只能用于人工线程中。
基于这种思想,可以在CObject类基础上派生一个套接字类,其使用方式为阻塞方式,
 
       虽然增加了使用的条件,但可以保证其正常工作,而不会出现不加控制地使用CSocket对象带来的冲突现象。

       下面首先将具体介绍有关的套接字类的定义,新创建的套接字功能主要通过调用CSocket的相关操作实现。

1. 套接字类CBlockingSocket

       首先需要定义此套接字类,在类中设置了一个属性变量:SOCKET m_hSocket;  
 
       m_hSocket 表示套接字的句柄。
      另外还构造了一组方法,其功能与CSocket类是对应的,下面以创建、监听、连接建立和消息的接收和发送为例,介绍其实现方法,。

       创建

       创建套接字即要求创建相应的连接,缺省类型为面向连接的流,具体实现为:

       void CBlockingSocket::Create(int nType)
       {
             ASSERT(m_hSocket 
== NULL);
             
if((m_hSocket = socket(AF_INET, nType, 0)) == INVALID_SOCKET) 
             {
                    
throw new CBlockingSocketException("创建套接字");   
             }
       }

 

       监听

       Listen函数完成监听连接的任务,在实现时要求最多有10个连接请求排队,这在一般的应用中是完全足够的。

     void CBlockingSocket::Listen()
     {
          ASSERT(m_hSocket 
!= NULL);
         
if(listen(m_hSocket, 10== SOCKET_ERROR) 
          {
                
throw new CBlockingSocketException("Listen");  
          }
     }

 

       建立连接

       连接的实际建立可以由Connect实现,同样地,缺省的建立方式为面向连接的流。

     void CBlockingSocket::Create(int nType )
    {
          ASSERT(m_hSocket 
== NULL);
         
if((m_hSocket = socket(AF_INET, nType, 0)) == INVALID_SOCKET) 
         {
                
throw new CBlockingSocketException("创建套接字");   
         }
    }

 

       发送消息

       Send函数的作用是将数据块按一个消息发送,参数pch即为发送的消息,nSize为消息长度,nSecs可以限制操作时间。如果客户方取消读操作,则返回值将小于指定消息长度。

     int CBlockingSocket::Send(const char* pch, const int nSize, const int nSecs)
    {
           ASSERT(m_hSocket 
!= NULL);
           FD_SET fd 
= {1, m_hSocket};
            TIMEVAL tv 
= {nSecs, 0};

            
if(select(0, NULL, &fd, NULL, &tv) == 0
            {
                        
throw new CBlockingSocketException("发送超时");
            }

            
if((int nBytesSent = send(m_hSocket, pch, nSize, 0)) == SOCKET_ERROR) 
            {
                        
throw new CBlockingSocketException("发送");
            }

            
return nBytesSent;
    }

 

         此外,如果数据块比较大,可以将数据块分成多个消息发送,此工作由函数Write完成。具体实现时将通过循环调用Send函数来实现部分消息发送,通过对局部量nBytesThisTime 和nBytesSent的维护,保证整个数据块的正常发送。

 int CBlockingSocket::Write(const char* pch, const int nSize, const int nSecs)
 {
        
int nBytesSent = 0,nBytesThisTime;
        
const char* pch1 = pch;
        
do 
        {
             nBytesThisTime 
= Send(pch1, nSize - nBytesSent, nSecs);
             nBytesSent 
+= nBytesThisTime;
             pch1 
+= nBytesThisTime; 
        } 
while(nBytesSent < nSize);

        
return nBytesSent;
 }

 

       接收消息

       Receive函数的作用是与发送消息对应的,可以将接收到的消息重组为数据块。

         int CBlockingSocket::Receive(char* pch, const int nSize, const int nSecs)
         {
                ASSERT(m_hSocket 
!= NULL);
                  FD_SET fd 
= {1, m_hSocket};
                  TIMEVAL tv 
= {nSecs, 0};

                  
if(select(0&fd, NULL, NULL, &tv) == 0
                  {
                          
throw new CBlockingSocketException("接收超时"); 
                  }
  
                 
if((int nBytesReceived = recv(m_hSocket, pch, nSize, 0)) == SOCKET_ERROR) 
                  {
                           
throw new CBlockingSocketException("接收");
                  }

                 
return nBytesReceived;
         }

 

 2.地址类CSockAddr

       在应用中还实现了一个Winsock地址类,它继承了sockaddr_in结构的主要属性:
      
 属性 类型  含义
Sin_family short  协议类别
Sin_port  u_short 端口
Sin_addr  in_addr 地址

      CSockAddr类有多种构造函数,如协议类别默认为AF_INET,端口号和地址均初始化为0,比较常用的构造函数实现如下,其中IP地址串参数已经是网络地址顺序形式。

      CSockAddr(const char* pchIP, const USHORT ushPort = 0
      {
              sin_family 
= AF_INET;
              sin_port = htons(ushPort);
              sin_addr.s_addr 
= inet_addr(pchIP); 
       }

       CSockAddr还包括一组成员函数,这些成员函数主要是对属性的存取。如按点分十进制方式返回地址、获得端口、获得地址等等。

3 套接字异常类CBlockingSocketException

        CBlockingSocketException用于处理套接字阻塞异常,可由CException类派生实现。
       类中定义了两个属性变量:
              m_nError               表示错误代码,
              m_strMessage     表示错误信息。
  
       在截获到异常时,需要利用这两个属性设置提示信息。异常消息的获得由GetErrorMessage函数完成,实现为:

 

       BOOL CBlockingSocketException::GetErrorMessage(LPTSTR lpstrError, UINT nMaxError,PUINT pnHelpContext)
       {    
              
char text[200];

                
if(m_nError == 0
                       wsprintf(text, 
"%s 错误", (const char*) m_strMessage);  
                
else
                       wsprintf(text, 
"%s 错误 #%d", (const char*) m_strMessage, m_nError);    

                strncpy(lpstrError, text, nMaxError 
- 1);

                
return TRUE;
      }

 

4. 服务器应用程序

       为了利用套接字实现服务器应用程序,需要在应用程序类的初始化实例函数中增加以下代码,进行套接字的初始化工作:

 

    if (!AfxSocketInit())   
    {
         AfxMessageBox(IDP_SOCKETS_INIT_FAILED);

        
return FALSE;   
    }

 

       我们的目的是当客户发出请求后,在服务器应用程序端显示相应的信息,同时向客户返回确认信息。

       首先创建一个全局的套接字对象:CBlockingSocket g_sListen;
        另外需要重新实现视类的初始化函数,先建立服务器的地址,设置通令端口为5858,这并不是必要的,只要保证客户方的通信端口与服务器的端口一致就可以了。
 
       然后创建g_sListen,并将其绑定到服务器上,调用套接字的监听操作,若监听到客户请求,将创建一个新的线程,并在此线程中处理客户请求。

      void CTestsockView::OnInitialUpdate() 
      {
              CEditView::OnInitialUpdate();
              
try 
              {
                      CSockAddr saServer;
    
                      saServer 
= CSockAddr(INADDR_ANY, (USHORT) 5858);
    
                      g_sListen.Create();
                      g_sListen.Bind(saServer);
                      g_sListen.Listen();
// start listening
                      g_bListening = TRUE;
                     g_nConnection 
= 0;

                      AfxBeginThread(ServerThreadProc,GetSafeHwnd(), HREAD_PRIORITY_NORMAL);  
              }

              
catch(CBlockingSocketException* e) 
              {
                      g_sListen.Cleanup();
                      LogBlockingSocketException(GetSafeHwnd(), 
"VIEW:", e);
                      e
->Delete();
              }
       }


       下面,再来看线程函数ServerThreadProc是如何处理客户请求的:

       UINT ServerThreadProc(LPVOID pParam)
       {
              CSockAddr saClient;
              CBlockingSocket sConnect;

              
try 
              {
                            
if(!g_sListen.Accept(sConnect, saClient)) 
                            {
                                        g_bListening 
= FALSE;
                                         
return 0;       
                            }

                            g_nConnection
++;

                            AfxBeginThread(ServerThreadProc, pParam, THREAD_PRIORITY_NORMAL);
                            DoRequest(pParam, sConnect, saClient);
                            sConnect.Close();   
              }
// 析构函数不能关闭它

              
catch(CBlockingSocketException* pe) 
              {
                            LogBlockingSocketException(pParam, 
"服务器:", pe);
                            pe
->Delete();   
              }

              
return 0;
       } 

 

           如果g_sListen调用Accept失败,说明视或应用程序关闭了连接的套接字,此时将调整当前状态,在具体处理客户请求之前,为了不影响继续接收其它客户的请求,可以再创建新的线程,
 即构造多线程,每个线程处理自已的具体事务。

           具体处理工作由DoRequest函数完成,由于析构函数不能关闭临时建立的套接字,所以在处理完之后,需要主动关闭。

           DoRequest的工作包括在服务器应用视中显示相应的提示语句,这是通过向其发送消息完成的,
          另外还将显示客户发送的信息,信息由套接字的Receive()函数获得,同样通过向窗口发送消息完成,这里的消息发送窗口由参数pParam确定。
          最后,调用套接字的.Send()函数向客户端发送一条简短的确认信息。

      void DoRequest(LPVOID pParam, CBlockingSocket& sockCon, LPCSOCKADDR psa)
      {
              
char inbuff[500],text1[200];
              wsprintf(text1, 
"建立连接\r\n");

              ::SendMessage((HWND) pParam, EM_SETSEL, (WPARAM
50065535);
              ::SendMessage((HWND) pParam, EM_REPLACESEL, (WPARAM
0, (LPARAM) text1);

              
try
              {
                            
int len=sockCon.Receive(inbuff,500,20);

                            inbuff[len]
='\0';

                            ::SendMessage((HWND) pParam, EM_SETSEL, (WPARAM
50065535);
                            ::SendMessage((HWND)pParam,EM_REPLACESEL,(WPARAM)
0,(LPARAM)inbuff);

                            sockCon.Send(
"ok!\r\nBye!",9,5);

              }
catch(CBlockingSocketException* pe) 
              {
                            LogBlockingSocketException(pParam, 
"服务器:", pe);
                            pe
->Delete();   
               }
 }


5. 客户端应用程序

  在客户端应用程序中同样使用了前面介绍的套接字类CBlockingSocket,Winsock地址类CSockAddr和套接字异常类CBlockingSocketException。

  这里将主要介绍客户如何建立与服务器的连接,如何向服务器发送消息,以及如何接收和处理服务器返回的消息。

  为了实现与服务器的连接,需要在菜单上增加一个连接项,资源标识为ID_BEGIN_LINK,消息映射设置为ON_COMMAND(ID_BEGIN_LINK, OnBeginLink),
 下面再来分析消息处理函数OnBeginLink的实现:

 

       void CTestclientsockView::OnBeginLink() 
       {
              CBlockingSocket sClient;
             
char inbuff[200];

             
try 
             {
                          CSockAddr saClient(
"146.127.35.70",5858);
    
                          sClient.Create();
                          sClient.Connect(saClient);
                          sClient.Send(
"hello ,你好!\n",5,5);

    
                      int len=sClient.Receive(inbuff,200,20);

                          inbuff[len]
='\0';

                          SendMessage(EM_SETSEL, (WPARAM) 
50065535);
                          SendMessage(EM_REPLACESEL, (WPARAM) 
0, (LPARAM) inbuff);

                          sClient.Close();
              }

             
catch(CBlockingSocketException* e) 
             {
                          sClient.Cleanup();
                          LogBlockingSocketException(GetSafeHwnd(), 
"VIEW:", e);
                          e
->Delete();   
             }
       }


           首先需要建立与服务器的连接,这里创建了一个地址对象saClient,在创建时提供了两个参数,其中“146.127.35.70”为服务器的IP地址,另外将端口号设置为5858
 即与服务器应用程序保持一致。接下来创建一个客户端的套接字对象,并通过调用函数Connect建立与服务器的连接。
 
           连接建立后,可以向服务器发送信息了,这里用套接字的函数Send()发送了一条简短的欢迎信息,同时调用Receive()函数接收服务器返回的信息。
 
           最后向视发送显示消息,将服务器所返回的确认信息显示在主窗口中。

           这里还需要说明的是,如果在连接或通信过程中出现异常,无论是客户应用程序还是服务器应用程序都将做出相应的响应。
          具体的处理工作由函数LogBlockingSocketException实现,这里的参数pParam 为拥有目的窗口的 HWND ,这是由另一个线程提供的。

      void LogBlockingSocketException(LPVOID pParam, char* pch, CBlockingSocketException* pe)
      {    
                CString strGmt 
= CTime::GetCurrentTime().FormatGmt("%m/%d/%y %H:%M:%S GMT");

 
               char text1[200], text2[50];

                 pe
->GetErrorMessage(text2, 49);

                 wsprintf(text1, 
"WINSOCK 错误--%s %s -- %s\r\n", pch, text2, (const char*) strGmt);

                 ::SendMessage((HWND) pParam, EM_SETSEL, (WPARAM) 
6553465535);
                 ::SendMessage((HWND) pParam, EM_REPLACESEL, (WPARAM) 
0, (LPARAM) text1);
     }

 

          通过调用套接字异常的GetErrorMessage函数可以得到具体的错误信息,同样通过向窗口发送消息的方法,可以将错误信息显示在主窗口中。

Feedback

# re: [转载]VC++下实现Socket编程的方法  回复  更多评论   

2012-06-25 10:11 by 11
xiexie

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理