標籤:
對於許多初學者來說,網路通訊程式的開發,普遍的一個現象就是覺得難以入手。許多概念,諸如:同步(Sync)/非同步(Async),阻塞(Block)/非阻塞(Unblock)等,初學者往往迷惑不清,只知其所以而不知起所以然。
非同步方式指的是發送方不等接收方響應,便接著發下個資料包的通訊方式;而同步指發送方發出資料後,等收到接收方發回的響應,才發下一個資料包的通訊方式。
阻塞通訊端是指執行此通訊端的網路調用時,直到成功才返回,否則一直阻塞在此網路調用上,比如調用recv()函數讀取網路緩衝區中的資料,如果沒有資料到達,將一直掛在recv()這個函數調用上,直到讀到一些資料,此函數調用才返回;而非阻塞通訊端是指執行此通訊端的網路調用時,不管是否執行成功,都立即返回。比如調用recv()函數讀取網路緩衝區中資料,不管是否讀到資料都立即返回,而不會一直掛在此函數調用上。在實際Windows網路通訊軟體開發中,非同步非阻塞通訊端是用的最多的。平常所說的C/S(用戶端/伺服器)結構的軟體就是非同步非阻塞模式的。
對於這些概念,初學者的理解也許只能似是而非,我將用一個最簡單的例子說明非同步非阻塞Socket的基本原理和工作機制。目的是讓初學者不僅對Socket非同步非阻塞的概念有個非常透徹的理解,而且也給他們提供一個用Socket開發網路通訊應用程式的快速入門方法。作業系統是Windows 98(或NT4.0),開發工具是Visual C++6.0。
MFC提供了一個非同步類CAsyncSocket,它封裝了非同步、非阻塞Socket的準系統,用它做常用的網路通訊軟體很方便。但它屏蔽了Socket的非同步、非阻塞等概念,開發人員無需瞭解非同步、非阻塞Socket的原理和工作機制。因此,建議初學者學習編網路通訊程式時,暫且不要用MFC提供的類,而先用Winsock2 API,這樣有助於對非同步、非阻塞Socket編程機制的理解。
為了簡單起見,伺服器端和用戶端的應用程式均是基於MFC的標準對話方塊,網路通訊部分基於Winsock2 API實現。
先做伺服器端應用程式。
用MFC嚮導做一個基於對話方塊的應用程式SocketSever,注意第三步中不要選上Windwos Sockets選項。在做好工程後,建立一個SeverSock,將它設定為非同步非阻塞模式,並為它註冊各種網路非同步事件,然後與自訂的網路非同步事件聯絡上,最後還要將它設定為監聽模式。在自訂的網路非同步事件的回呼函數中,你可以得到各種網路非同步事件,根據它們的類型,做不同的處理。下面將詳細介紹如何編寫相關代碼。
在SocketSeverDlg.h檔案的類定義之前增加如下定義:
#define NETWORK_EVENT WM_USER+166 file://定義網路事件
SOCKET ServerSock; file://伺服器端Socket
在類定義中增加如下定義:
class CSocketSeverDlg : CDialog
{
public:
SOCKET ClientSock[CLNT_MAX_NUM]; file://儲存與用戶端通訊的Socket的數組
/*各種網路非同步事件的處理函數*/
void OnClose(SOCKET CurSock); file://對端Socket斷開
void OnSend(SOCKET CurSock); file://發送網路資料包
void OnReceive(SOCKET CurSock); file://網路資料包到達
void OnAccept(SOCKET CurSock); file://用戶端串連請求
BOOL InitNetwork(); file://初始化網路函數
void OnNetEvent(WPARAM wParam, LPARAM lParam); file://非同步事件回呼函數
…
};
在SocketSeverDlg.cpp檔案中增加訊息映射,其中OnNetEvent是非同步事件回呼函數名:
ON_MESSAGE(NETWORK_EVENT,OnNetEvent)
定義初始化網路函數,在SocketSeverDlg.cpp檔案的OnInitDialog()中調此函數即可。
BOOL CSocketSeverDlg::InitNetwork()
{
WSADATA wsaData;
//初始化TCP協議
BOOL ret = WSAStartup(MAKEWORD(2,2), &wsaData);
if(ret != 0)
{
MessageBox(‘初始化網路通訊協定失敗!‘);
return FALSE;
}
//建立伺服器端通訊端
ServerSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(ServerSock == INVALID_SOCKET)
{
MessageBox(‘建立通訊端失敗!‘);
closesocket(ServerSock);
WSACleanup();
return FALSE;
}
//綁定到本地一個連接埠上
sockaddr_in localaddr;
localaddr.sin_family = AF_INET;
localaddr.sin_port = htons(8888); //連接埠號碼不要與其他應用程式衝突
localaddr.sin_addr.s_addr = 0;
if(bind(ServerSock ,(struct sockaddr*)&localaddr,sizeof(sockaddr))
= = SOCKET_ERROR)
{
MessageBox(‘綁定地址失敗!‘);
closesocket(ServerSock);
WSACleanup();
return FALSE;
}
//將SeverSock設定為非同步非阻塞模式,並為它註冊各種網路非同步事件,其中m_hWnd
//為應用程式的主對話方塊或主視窗的控制代碼
if(WSAAsyncSelect(ServerSock, m_hWnd, NETWORK_EVENT, FD_ACCEPT | FD_CLOSE | FD_READ | FD_WRITE) == SOCKET_ERROR)
{
MessageBox(‘註冊網路非同步事件失敗!‘);
WSACleanup();
return FALSE;
}
listen(ServerSock, 5); file://設定偵聽模式
return TRUE;
}
下面定義網路非同步事件的回呼函數
void CSocketSeverDlg::OnNetEvent(WPARAM wParam, LPARAM lParam)
{
//調用Winsock API函數,得到網路事件類型
int iEvent = WSAGETSELECTEVENT(lParam);
//調用Winsock API函數,得到發生此事件的用戶端通訊端
SOCKET CurSock= (SOCKET)wParam;
switch(iEvent)
{
case FD_ACCEPT: //用戶端串連請求事件
OnAccept(CurSock);
break;
case FD_CLOSE: //用戶端斷開事件:
OnClose(CurSock);
break;
case FD_READ: //網路資料包到達事件
OnReceive(CurSock);
break;
case FD_WRITE: //發送網路資料事件
OnSend(CurSock);
break;
default: break;
}
}
以下是發生在相應Socket上的各種網路非同步事件的處理函數,其中OnAccept傳進來的參數是伺服器端建立的通訊端,OnClose()、OnReceive()和OnSend()傳進來的參數均是伺服器端在接受用戶端串連時新建立的用與此用戶端通訊的Socket。
void CSocketSeverDlg::OnAccept(SOCKET CurSock)
{
//接受串連請求,並儲存與發起串連請求的用戶端進行通訊Socket
//為新的socket註冊非同步事件,注意沒有Accept事件
}
void CSocketSeverDlg::OnClose(SOCET CurSock)
{
//結束與相應的用戶端的通訊,釋放相應資源
}
void CSocketSeverDlg::OnSend(SOCET CurSock)
{
//在給用戶端發資料時做相關預先處理
}
void CSocketSeverDlg::OnReceive(SOCET CurSock)
{
//讀出網路緩衝區中的資料包
}
用同樣的方法建立一個用戶端應用程式。初始化網路部分,不需要將通訊端設定為監聽模式。註冊非同步事件時,沒有FD_ACCEPT,但增加了FD_CONNECT事件,因此沒有OnAccept()函數,但增加了OnConnect()函數。向伺服器發出串連請求時,使用connect()函數,串連成功後,會響應到OnConnect()函數中。下面是OnConnect()函數的定義,傳進來的參數是用戶端Socket和伺服器端發回來的串連是否成功的標誌。
void CSocketClntDlg::OnConnect(SOCKET CurSock, int error)
{
if(0 = = error)
{
if(CurSock = = ClntSock)
MessageBox(‘串連伺服器成功!‘);
}
}
定義OnReceive()函數,處理網路資料到達事件;
定義OnSend()函數,處理髮送網路資料事件;
定義OnClose()函數,處理伺服器的關閉事件。
以上就是用基於Windows訊息機制的非同步I/O模型做伺服器和用戶端應用程式的基本方法。另外還可以用事件模型、重疊模型或完成連接埠模型,讀者可以參考有關書籍。
在實現了上面的例子後,你將對Winsock編網路通訊程式的機制有了一定的瞭解。接下來你可以進行更精彩的編程, 不僅可以在網上傳輸普通資料,而且還以傳輸語音、視頻資料,你還可以自己做一個網路資源共用的伺服器軟體,和你的同學在實驗室的區域網路裡可以共同分享你的成果。
同步伺服器通訊端掛起應用程式的執行,直到通訊端上接收到串連請求。同步伺服器通訊端不適用於在操作中大量使用網路的應用程式,但它們可能適用於簡單的網路應用程式。使用 Bind 和 Listen 方法設定 Socket 以在終結點上偵聽之後,Socket 就可以隨時使用 Accept 方法接受傳入的串連請求了。應用程式被掛起,直到調用 Accept 方法時接收到串連請求。
接收到串連請求時,Accept 返回一個與串連用戶端關聯的新 Socket 執行個體。下面的樣本讀取用戶端資料,在控制台上顯示該資料,然後將該資料回顯到用戶端。Socket 不指定任何訊息協議,因此字串“<EOF>”標記訊息資料的結尾。它假定一個名為 listener 的 Socket 已初始化,並綁定到一個終結點。
Console.WriteLine("Waiting for a connection...");
Socket handler = listener.Accept();
String data = null;
while (true) {
bytes = new byte[1024];
int bytesRec = handler.Receive(bytes);
data += Encoding.ASCII.GetString(bytes,0,bytesRec);
if (data.IndexOf("<EOF>") > -1) {
break;
}
}
Console.WriteLine( "Text received : {0}", data);
byte[] msg = Encoding.ASCII.GetBytes(data);
handler.Send(msg);
handler.Shutdown(SocketShutdown.Both);
handler.Close();
基於MFC的socket編程(非同步非阻塞通訊)