MFC--網路程式設計之CAsyncSocket詳解

星語征途發表於2018-07-20

原文連結地址:https://blog.csdn.net/u012372584/article/details/76146844

CAsyncSocket類是從Object類派生而來。CAsyncSocket物件稱為非同步套接字物件

使用CAsyncSocket進行網路程式設計,可以充分利用Windows作業系統提供的訊息驅動機制,通過應用程式框架來傳遞訊息,方便地處理各種網路事件。另一方面,作為MFC微軟基礎類庫中的一員,CAsyncSocket可以和MFC的其他類融為一體,大大擴充套件了網路程式設計的空間,方便了程式設計。

 

1. 使用CAsyncSocket的一般步驟

網路應用程式一般採用客戶端/伺服器模式,他們使用的CAsyncSocket程式設計有所不同,下面以表格的形式方式看一下伺服器和客戶端之間的不同

序號 服務端 客戶端
1

構造一個套接字

CAsyncSocket sockServer

構造一個套接字

CAsyncSocket sockClient

2

建立SOCKET控制程式碼,繫結到指定的埠

sockServer.Create(nPort);

建立SOCKET控制程式碼,使用預設引數

sockClient.Create();

3

啟動監聽,時刻準備接收連線請求

sockServer.Listen();

 
4  

請求連結伺服器

sockClient.Connect(strAddress,nPort)

5

構造一個新的空套接字

CAsyncSocket sockRecv;

接收連線

sockServer.Accept(sockRecv);

 
6

接收資料

sockRecv.Receive(pBuffer,nLen);

傳送連線

sockClient.Send(pBuffer,nLen);

7

傳送資料

sockRecv.Send(pBuffer,nLen);

接收資料

sockClient.Receive(pBuffer,nLen);

8

關閉套接字物件

sockRecv.Close();

關閉套接字物件

sockClient.Close(

ps:客戶端與服務端都要首先構造一個CAsyncSocket物件,然後使用該物件的Create成員函式來建立底層的SOCKET控制程式碼。伺服器端要繫結到特定的埠

對於伺服器端的套接字物件,應使用CAsyncSocket::Listen函式進行監聽狀態,一旦收到來自客戶端的連結請求,就呼叫CAsyncSocket::Accept來接收。對於客戶端的套接字物件,應當使用CAsyncSocket::Connect來連線到一個伺服器端的套接字物件。建立連結之後,雙方就可以按照應用層協議交換資料了。

這裡需要注意,Accept是將一個新的空CAsyncSocket物件作為它的引數,在呼叫Accept之前必須構造這個物件。與客戶端套接字的連線是通過它建立的,如果這個套接字物件退出,連線也就關閉。對於這個新的套接字物件,不需要呼叫Create來建立它的底層套接字

 

呼叫CAsyncSocket物件的其他成員函式,如Send和Receive執行與其他套接字物件的通訊,這些成員函式與Windows Sockets API函式在形式和用法上基本是一致的。

 

關閉並銷燬CAsyncSocket物件。如果在堆疊上建立了套接字物件,當包含此物件的函式退出時,會呼叫該類的解構函式,銷燬該物件。在銷燬該物件之前,解構函式會呼叫該物件的Close成員函式。如果在堆上使用new建立了套接字物件,可先呼叫Close成員函式關閉它,在使用delete來刪除釋放該物件

 

2.在使用CAsyncSocket進行網路通訊時,我們還需要處理以下幾個問題:

 

① 堵塞處理,CAsyncSocket物件專用於非同步操作,不支援堵塞工作模式,如果應用程式需要支援堵塞操作,必須自己解決

② 位元組順序的轉換。在不同的結構型別的計算機之間進行資料傳輸時,可能會有計算機之間位元組儲存順序不一致的情況。使用者程式需要自己對不用的位元組順序進行轉換

③ 字串轉換。同樣,不同結構型別的計算機的字串儲存順序也可能不同,需要自行轉換,如Unicode和ANSI字串之間的轉換

3. 分步驟詳解

建立CAsyncSocket物件

建立非同步套接字物件一般是分為兩個步驟,首先要構造CAsyncSocket物件,其次建立該物件底層的SOCKET控制程式碼

 

(1)建立空的CAsyncSocket物件

通過呼叫CAsyncSocket建構函式,建立一個新的空CAsyncSocket套接字物件,建構函式還帶引數。套接字物件建立之後必須呼叫他的成員函式來建立底層的套接字資料結構,並繫結他的地址

方法一:

CAsyncSocket Sock
Sock.Create(...)


方法二:

CAsyncSocket *pSock = new CAsyncSocket;
pSock->Create(...);
 
delete pSock;
pSock =NULL;


ps:之前見過很多朋友釋放指標的時候總是delete就完事了,往往不知正在給自己的程式帶來前所未有的災難,而此時的指標我們稱之為“野指標”,注意:野指標不是NULL指標,而是不可用記憶體的指標,即垃圾指標;野指標是很危險的,因為我們無法通過if去判斷指標是正常指標還是野指標,所以,我們在書寫程式碼時一定要養成良好的程式設計習慣!!!避免野指標的方法我們可以通過以下幾點:

① 宣告指標一定要初始化,如果不初始化為NULL,那麼此時一定要指向一塊合法的記憶體

② 當呼叫delete或者free去釋放指標後,一定要將指標重新指向NULL,可以參照SkinUI的SafeDelete

③ 指標操作超出了變數的作用範圍,比如如下程式碼,

class A
{
   void Fun(){}
};
class B
{
   A *m_A;
   B(){m_A=NULL;}
    
   void Fun()
   {
      A a
      m_A =&a;
      //注意變數A的生命週期,當該方法執行完畢後,A會被釋放,此時m_A就變成了無效的野指標
   }
   void Fun1()
   {
      m_A->Fun(); //m_A為野指標,到這裡也就出現了錯誤
   }
};

通過上述方法可以大大的降低程式碼出現野指標的風險。

 

(2)建立CAsyncSocket套接字的底層套接字控制程式碼

通過CAsyncSocket::Create建立該物件的底層套接字控制程式碼,決定套接字物件的具體特性。

函式原型如下:

BOOL Create(
     UINT nSocketPort = 0,
     int nSocketType = SOCK_STREAM,
     long lEvent = FD_READ|FD_WRITE|FD_OOB|FD_ACCEPT|FD_CONNECT|FD_CLOSE,
     LPCTSTR lpszSocketAddress = NULL
    );

引數:

nSocketPort,指定了一個分配給套接字的傳輸層埠號,預設值為0,表示讓系統為這個套接字分配一個自由的埠號。但是對於伺服器應用程式而言,一般都需要事先分配一個公認的埠號,所以切記,伺服器應用程式呼叫此函式時,必須分配一個埠號

nSocketType,套接字的型別,當指定為SOCK_STREAM時表示生成流式套接字,若使用SOCK_DGRAM表示生成資料包套接字

lEvent,指定為CAsyncSocket物件生成通知訊息的套接字事件,預設對所有的套接字事件都生成通知訊息

lpszSocketAddress,指定套接字的網路地址,對Internet通訊域來說,就是主機的域名或者ip地址,比如www.gymsaga.com或123.123.123.123。如果使用預設值,表示使用預設的本機ip地址

4 關於CAsyncSocket可以接受並處理的訊息事件

在CAsyncSocket::Create中,引數lEvent指定了為CAsyncSocket物件生成通知訊息的套接字事件,最能體現CAsyncSocket對Windows訊息驅動機制的支援

先認識一下這六種相關事件和通知訊息

關於lEvent引數的符號常量,我們可以在WinSock中找到

/*
 * Define flags to be used with the WSAAsyncSelect() call.
 */
#define FD_READ         0x01
#define FD_WRITE        0x02
#define FD_OOB          0x04
#define FD_ACCEPT       0x08
#define FD_CONNECT      0x10
#define FD_CLOSE        0x20

它們代表了MFC套接字物件可以接收並處理的6種網路事件,當事件發生時,套接字物件會收到相應的通知訊息,並自動執行套接字物件響應的事件處理函式

1:FD_READ :   通知有資料可讀。當一個套接字物件的資料輸入緩衝區收到其他套接字物件傳送來的資料時,發生此事件,並通過該套接字物件 ,告訴它可以呼叫Receive成員來接收資料

2:FD_WRITE:   通知可以寫資料,當一個套接字物件的資料輸出緩衝區中的資料已經傳送出去,輸出緩衝區已騰空時,發生此事件,並通過該套接字物件,告訴它可以呼叫Send函式向外傳送資料

3:FD_ACCEPT:  通知監聽套接字有連線請求可以接收。當客戶端的連結請求到達伺服器時,進一步說,是當客戶端的連線請求已經進入伺服器監聽套接字的接收緩衝區佇列時,發生此事件,並通過監聽套接字物件,告訴它可以呼叫Accept成員來接收待決的連結請求。這個事件僅對流式套接字有效,並且發生在伺服器端

4:FD_CONNECT: 通知請求連結的套接字,連結的要求已經被處理。當客戶端的連線請求已被處理時,發生此事件。存在兩種情況:一種是伺服器端已接收了連結請求,雙方的連線已經建立,通知客戶端套接字,可以使用連結來傳輸資料了;另一種情況是連結請求被拒絕,通知客戶機套接字,它所請求的連線失敗。這個事件僅對流式套接字有效,並且發生在客戶端

5:FD_CLOSE:   通知套接字已關閉。當連結的套接字關閉時發生

6:FD_OOB:     通知將帶外資料到達。當對方的流失套接字傳送帶外資料時,發生此事件,並通知接收套接字,正在傳送的套接字有帶外資料要求傳送,帶外資料是有沒對連結的流失套接字相關的在邏輯上獨立的通道,帶外資料通道典型的是用來傳送緊急資料。MFC支援帶外有資料,使用CAsyncSocket類的高階使用者可能需要使用帶外資料通道,但不鼓勵使用CSocket類的使用者使用它,更容易的方法是建立第二個套接字來傳送這樣的資料

 

MFC框架對這六種事件的處理

當上述的網路事件發生時,MFC框架做何處理呢?MFC框架按照Windows系統的訊息驅動把訊息傳送給相應的套接字物件,並呼叫作為該物件函式的事件處理函式,事件與處理函式一一對映。

 

在afxSock.h中我們可以找到CAsyncSocket類對這六種對應事件的處理函式

// Overridable callbacks
protected:
    virtual void OnReceive(int nErrorCode);
    virtual void OnSend(int nErrorCode);
    virtual void OnOutOfBandData(int nErrorCode);
    virtual void OnAccept(int nErrorCode);
    virtual void OnConnect(int nErrorCode);
    virtual void OnClose(int nErrorCode);

其中引數nErrorCode的值,是在函式被呼叫時,由MFC框架提供的,表明套接字最新的狀況,如果是0,說明成功,如果為非零值,說明套接字物件有某種錯誤

當某個網路事件發生時,MFC框架會自動呼叫套接字物件對應的事件處理函式。這就相當於給套接字物件一個通知,告訴它某個重要的事件已經發生,所以也稱為套接字類的通知函式或者回撥函式

 

5.過載套接字物件的回撥函式

在程式設計中,一般我們不會直接去使用CAsyncSocket或者CSocket,而是從他們派生出自己的套接字類來。然後在派生類中對這些虛擬函式進行過載處理,加入應用程式對於網路事件處理的特定程式碼

如果是從CAsyncSocket類派生了自己的套接字類,就必須過載該應用程式所感興趣的那些網路事件所對應的通知函式。如果從CSocket類派生一個類,是否過載所感興趣的通知函式則由自己決定。也可以使用CSocket類本身的回撥函式,但預設情況下,CSocket本身的回撥函式什麼也不做,只是一個空函式。

MFC框架自動呼叫通知函式,使得使用者可以在套接字被通知的時候來優化套接字的行為。例如,使用者可以從自己的OnReceive通知函式中呼叫套接字物件的成員函式Receive,就是說,在被通知的時候,已經有資料可讀了,才呼叫Receive來讀取它。這個方法不是必須的,但它是一個有效的方案。此外,也可以使用自己的通知函式跟蹤程式,列印TRACE訊息等

對於CSocket物件,還有如下一些不同之處

在一個諸如接收或者傳送資料的操作期間,一個CSocket物件成為同步的,在同步狀態期間,在當前套接字等待它想要的通知時,任何的為其他套接字的通知被排成佇列,一旦該套接字完成了它的同步操作,並再次成為非同步的,其他的套接字才可以開始接收排列的通知

 

重要的一點是:在CSocket中,從來不呼叫OnConncet通知函式,對於連線,簡單的呼叫Conncet函式,僅當連線完成時,無論成功還是失敗,該函式都返回,連線通知如何被處理是一個MFC內部的實現細節。

 

相關文章