從一個合格的C++程式員到網路編程高手,還是需要花不少功夫,寫一個聊天程式很簡單,而要寫一個能同時響應成千上萬使用者的高效能網路程式,的確不容易。這篇文章所介紹的方法也並不是能直接應用於每一個具體的應用程式,只能作為學習的參考資料。
開發高效能網路遊戲恐怕是促使很多程式員研究網路編程的原因,現在的大型網路遊戲對同時線上人數的要求比較高,真正的項目往往採取多個伺服器(組)負荷分擔的方式工作,首先把注意力放到單個伺服器的情況。
大家都知道,用得最多的協議是UDP和TCP,UDP是不可靠傳輸服務,TCP是可靠傳輸服務。UDP就像點對點的資料轉送一樣,寄件者把資料打包,包上有收信者的地址和其他必要資訊,至於收信者能不能收到,UDP協議並不保證。而TCP協議就像(實際他們是一個層次的網路通訊協定)是建立在UDP的基礎上,加入了校正和重傳等複雜的機制來保證資料可靠的傳達到收信者。關於網路通訊協定的具體內容,讀者可以參考專門介紹網路通訊協定的書籍,或者查看RFC中的有關內容。本書直接探討編程實現網路程式的問題。
1.1 Window Socket介紹
Windows Socket是從UNIX Socket繼承發展而來,最新的版本是2.2。進行Windows網路編程,你需要在你的程式中包含WINSOCK2.H或MSWSOCK.H,同時你需要添加引入庫WS2_32. LIB或WSOCK32.LIB。準備好後,你就可以著手建立你的第一個網路程式了。
Socket編程有阻塞和非阻塞兩種,在作業系統I/O實現時又有幾種模型,包括Select,WSAAsyncSelect,WSAEventSelect ,IO重疊模型,完成連接埠等。要學習基本的網路編程概念,可以選擇從阻塞模式開始,而要開發真正實用的程式,就要進行非阻塞模式的編程(很難想象一個大型伺服器採用阻塞模式進行網路通訊)。在選擇I/O模型時,我建議初學者可以從WSAAsyncSelect模型開始,因為它比較簡單,而且有一定的實用性。但是,幾乎所有人都認識到,要開發同時響應成千上萬使用者的網路程式,完成連接埠模型是最好的選擇。
既然完成連接埠模型是最好的選擇,那為什麼我們不直接寫出一個使用完成連接埠的程式,然後大家稍加修改就OK了。這確實是一個好的想法,但是真正做項目的時候,不同的情況對程式有不同的要求,如果不深入學習網路編程的各方面知識,是不可能寫出符合要求的程式,在學習網路編程以前,我建議讀者先學習一下網路通訊協定。
1.2 第一個網路程式
由於伺服器/用戶端模式的網路應用比較多,而且伺服器端的設計是重點和痛點。所以我想首先探討伺服器的設計方法,在完成伺服器的設計後再探討其他模式的網路程式。
設計一個基本的網路伺服器有以下幾個步驟:
1、初始化Windows Socket
2、建立一個監聽的Socket
3、設定伺服器位址資訊,並將監聽連接埠綁定到這個地址上
4、開始監聽
5、接受用戶端串連
6、和用戶端通訊
7、結束服務並清理Windows Socket和相關資料,或者返回第4步
我們可以看出設計一個最簡單的伺服器並不需要太多的代碼,它完全可以做一個小型的聊天程式,或進行資料的傳輸。但是這隻是我們的開始,我們的最終目的是建立一個有大規模響應能力的網路伺服器。如果讀者對作業系統部分的線程使用還有疑問,我建議你現在就開始複習,因為我們經常使用線程來提高程式效能,其實線程就是讓CPU不停的工作,而不是總在等待I/O,或者是一個CPI,累死了還是一個CPU。千萬不要以為線程越多的伺服器,它的效能就越好,線程的切換也是需要消耗時間的,對於I/O等待少的程式,線程越多效能反而越低。
下面是簡單的伺服器和用戶端原始碼。(阻塞模式下的,供初學者理解)
TCPServer
#include <winsock2.h>
void main(void)
{
WSADATA wsaData;
SOCKET ListeningSocket;
SOCKET NewConnection;
SOCKADDR_IN ServerAddr;
SOCKADDR_IN ClientAddr;
int Port = 5150;
// 初始化Windows Socket 2.2
WSAStartup(MAKEWORD(2,2), &wsaData);
// 建立一個新的Socket來響應用戶端的串連請求
ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 填寫伺服器位址資訊
// 連接埠為5150
// IP地址為INADDR_ANY,注意使用htonl將IP地址轉換為網路格式
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(Port);
ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 綁定監聽連接埠
bind(ListeningSocket, (SOCKADDR *)&ServerAddr, sizeof(ServerAddr));
// 開始監聽,指定最大同時串連數為5
listen(ListeningSocket, 5);
// 接受新的串連
NewConnection = accept(ListeningSocket, (SOCKADDR *) &ClientAddr,&ClientAddrLen));
// 新的串連建立後,就可以互相通訊了,在這個簡單的例子中,我們直接關閉串連,
// 並關閉監聽Socket,然後退出應用程式
//
closesocket(NewConnection);
closesocket(ListeningSocket);
// 釋放Windows Socket DLL的相關資源
WSACleanup();
}
TCPClient
# include <winsock2.h>
void main(void)
{
WSADATA wsaData;
SOCKET s;
SOCKADDR_IN ServerAddr;
int Port = 5150;
//初始化Windows Socket 2.2
WSAStartup(MAKEWORD(2,2), &wsaData);
// 建立一個新的Socket來串連伺服器
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 填寫用戶端地址資訊
// 連接埠為5150
// 伺服器IP地址為"136.149.3.29",注意使用inet_addr將IP地址轉換為網路格式
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(Port);
ServerAddr.sin_addr.s_addr = inet_addr("136.149.3.29");
// 向伺服器發出串連請求
connect(s, (SOCKADDR *) &ServerAddr, sizeof(ServerAddr));
// 新的串連建立後,就可以互相通訊了,在這個簡單的例子中,我們直接關閉串連,
// 並關閉監聽Socket,然後退出應用程式
closesocket(s);
// 釋放Windows Socket DLL的相關資源
WSACleanup();
}
1.3 WSAAsyncSelect模式
前面說過,Windows網路編程模式有好幾種,他們各有特點,實現起來複雜程度各不相同,適用範圍也不一樣。是Network Programming for Microsoft Windows 2nd 一書中對不同模式的一個效能測試結果。伺服器採用Pentium 4 1.7 GHz Xeon的CPU,768M記憶體;用戶端有3台PC,配置分別是Pentium 2 233MHz ,128 MB 記憶體,Pentium 2 350 MHz ,128 MB記憶體,Itanium 733 MHz ,1 GB記憶體。
具體的結果分析大家可以看看原書中作者的敘述,我關心的是哪種模式是我需要的。首先是伺服器,勿庸置疑,肯定是完成連接埠模式。那麼用戶端呢,當然也可以採用完成連接埠,但是不同模式是在不同的作業系統下支援的.
完成連接埠在Windows 98下是不支援的,雖然我們可以假定所有的使用者都已經裝上了Windows 2000和Windows XP,。但是,如果是商業程式,這種想法在現階段不應該有,我們不能讓使用者為了使用我們的用戶端而去升級他的作業系統。Overlapped I/O可以在Windows 98下實現,效能也不錯,但是實現和理解起來快趕上完成連接埠了。而且,最關鍵的一點,用戶端程式不是用來進行大規模網路響應的,用戶端的主要工作應該是進行諸形運算等非網路方面的任務。原書作者,包括我強烈推薦大家使用WSAAsyncSelect模式實現用戶端,因為它實現起來比較直接和容易,而且他完全可以滿足用戶端編程的需求。
下面是一段原始碼,雖然我們是用它來寫用戶端,我還是把它的服務端代碼放上來,一方面是有興趣的朋友可以用他做測試和瞭解如何用它實現伺服器;另一方面是用戶端的代碼可以很容易的從它修改而成,不同的地方只要參考一下1.1節裡的代碼就知道了。
#define WM_SOCKET WM_USER + 1
#include <winsock2.h>
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance, LPSTR lpCmdLine,
int nCmdShow)
{
WSADATA wsd;
SOCKET Listen;
SOCKADDR_IN InternetAddr;
HWND Window;
// 建立主視窗
Window = CreateWindow();
// 初始化Windows Socket 2.2
WSAStartup(MAKEWORD(2,2), &wsd);
// 建立監聽Socket
Listen = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 設定伺服器位址
InternetAddr.sin_family = AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sin_port = htons(5150);
// 綁定Socket
bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr));
// 設定Windows訊息,這樣當有Socket事件發生時,視窗就能收到對應的訊息通知
// 伺服器一般設定 FD_ACCEPT │ FD_READ | FD_CLOSE
// 用戶端一般設定 FD_CONNECT │ FD_READ | FD_CLOSE
WSAAsyncSelect(Listen, Window, WM_SOCKET, FD_ACCEPT │ FD_READ | FD_CLOSE);
// 開始監聽
listen(Listen, 5);
// Translate and dispatch window messages
// until the application terminates
while (1) {
// ...
}
}
BOOL CALLBACK ServerWinProc(HWND hDlg,UINT wMsg,
WPARAM wParam, LPARAM lParam)
{
SOCKET Accept;
switch(wMsg)
{
case WM_PAINT:
// Process window paint messages
break;
case WM_SOCKET:
// Determine whether an error occurred on the
// socket by using the WSAGETSELECTERROR() macro
if (WSAGETSELECTERROR(lParam))
{
// Display the error and close the socket
closesocket( (SOCKET) wParam);
break;
}
// Determine what event occurred on the
// socket
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
// Accept an incoming connection
Accept = accept(wParam, NULL, NULL);
// Prepare accepted socket for read,
// write, and close notification
WSAAsyncSelect(Accept, hDlg, WM_SOCKET,
FD_READ │ FD_WRITE │ FD_CLOSE);
break;
case FD_READ:
// Receive data from the socket in
// wParam
break;
case FD_WRITE:
// The socket in wParam is ready
// for sending data
break;
case FD_CLOSE:
// The connection is now closed
closesocket( (SOCKET)wParam);
break;
}
break;
}
return TRUE;
}
1.4 小節
目前為止,我非常簡要的介紹了Windows網路編程的一些東西,附上了一些原始碼。可以說,讀者特別是初學者,看了後不一定就能馬上寫出程式來,而那些代碼也不是可以直接應用於實際的項目。別急,萬裡長徵才開始第一步呢,很多書裡都是按照基礎到應用的順序來寫的,但是我喜歡更直接一點,更實用一些的方式。而且,我寫的這個專題,畢竟不是商業化的,時間上不能投入過多,只是作為給初學者的一個小小協助。更多的還是希望讀者自己刻苦研究,有問題的時候可以到我的論壇上給我留言,以後有機會我也會公布一些實際的代碼。希望結交更多熱愛編程和中國遊戲事業的朋友。下一章裡我將主要講解完成連接埠編程,這也是我寫這篇文章的初衷,希望對大家能有所協助。