第一章 序言程序員
我寫這個專題的目的,一方面是爲了經過對網絡編程再一次系統的總結,提升本身的網絡編程水平,特別是Windows下的網絡編程水平。同時,我也但願,能爲衆多初學網絡編程的人提供一點幫助,由於我開始學習網絡編程的時候,能找到的資料就不多。固然,花錢能夠買到翻譯版本的書:)編程
首先向你們推薦一本很好的參考書,NetworkProgramming for Microsoft Windows 2nd,windows
初學網絡編程的時候我還不知道有這樣一本好書,只是上各大論壇把能找到的網絡編程方面的文章和代碼下載下來,而後本身研究。後來看到別人推薦這一本書,下載了一個,看了感受很是好,裏面的內容寫得很規範,條理也很清楚,英文好的朋友能夠直接閱讀,否則就只好去弄一本翻譯好的來研究了。、安全
我試着從Windows編程的基礎開始,一直到探索創建高性能的網絡應用程序。我說過,我並非以高手的身份寫這本書,而是以和你們一塊兒學習的心態學習網絡編程,寫書只是讓本身的思路更清晰,之後還能夠翻閱。因此,我不保證書中全部的內容都是絕對正確和標準的,有不妥的地方,還但願高手批評指正。服務器
這本書是徹底免費的,讀者能夠任意使用書中的代碼。可是若是須要轉載,請註明原做者和出處。若是有商業運做的需求,請直接和我聯繫。網絡
第二章 Windows網絡編程基礎數據結構
這本書主要探索Windows網絡編程,開發平臺是Windows 2000 和VisualC++.NET,從一個合格的C++程序員到網絡編程高手,仍是須要花很多功夫,至少我認爲寫一個聊天程序很簡單,而要寫一個能同時響應成千上萬用戶的高性能網絡程序,的確不容易。這篇文章所介紹的方法也並非能直接應用於每個具體的應用程序,只能做爲學習的參考資料。併發
開發高性能網絡遊戲恐怕是促使不少程序員研究網絡編程的緣由(包括我),如今的大型網絡遊戲對同時在線人數的要求比較高,真正的項目每每採起多個服務器(組)負荷分擔的方式工做,我將首先把注意力放到單個服務器的狀況。app
你們都知道,咱們用得最多的協議是UDP和TCP,UDP是不可靠傳輸服務,TCP是可靠傳輸服務。UDP就像點對點的數據傳輸同樣,發送者把數據打包,包上有收信者的地址和其餘必要信息,至於收信者能不能收到,UDP協議並不保證。而TCP協議就像(實際他們是一個層次的網絡協議)是創建在UDP的基礎上,加入了校驗和重傳等複雜的機制來保證數據可靠的傳達到收信者。關於網絡協議的具體內容,讀者能夠參考專門介紹網絡協議的書籍,或者查看RFC中的有關內容。本書直接探討編程實現網絡程序的問題。異步
2.1 Window Socket介紹
WindowsSocket是從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了。我認爲這確實是一個好的想法,可是真正作項目的時候,不一樣的狀況對程序有不一樣的要求,若是不深刻學習網絡編程的各方面知識,是不可能寫出符合要求的程序,在學習網絡編程之前,我建議讀者先學習一下網絡協議。
2.2 第一個網絡程序
因爲服務器/客戶端模式的網絡應用比較多,並且服務器端的設計是重點和難點。因此我想首先探討服務器的設計方法,在完成服務器的設計後再探討其餘模式的網絡程序。
設計一個基本的網絡服務器有如下幾個步驟:
一、初始化Windows Socket
二、建立一個監聽的Socket
三、設置服務器地址信息,並將監聽端口綁定到這個地址上
四、開始監聽
五、接受客戶端鏈接
六、和客戶端通訊
七、結束服務並清理Windows Socket和相關數據,或者返回第4步
咱們能夠看出設計一個最簡單的服務器並不須要太多的代碼,它徹底能夠作一個小型的聊天程序,或進行數據的傳輸。可是這只是咱們的開始,咱們的最終目的是創建一個有大規模響應能力的網絡服務器。若是讀者對操做系統部分的線程使用還有疑問,我建議你如今就開始複習,由於咱們常用線程來提升程序性能,其實線程就是讓CPU不停的工做,而不是總在等待I/O,或者是一個CPI,累死了仍是一個CPU。千萬不要覺得線程越多的服務器,它的性能就越好,線程的切換也是須要消耗時間的,對於I/O等待少的程序,線程越多性能反而越低。
下面是簡單的服務器和客戶端源代碼。(阻塞模式下的,供初學者理解)
#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();
}
# 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();
}
2.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 733MHz ,1 GB內存。
具體的結果分析你們能夠看看原書中做者的敘述,我關心的是哪一種模式是我須要的。首先是服務器,勿庸置疑,確定是完成端口模式。那麼客戶端呢,固然也能夠採用完成端口,可是不一樣模式是在不一樣的操做系統下支持的,看下圖:
完成端口在Windows 98下是不支持的,雖然咱們能夠假定全部的用戶都已經裝上了Windows2000和Windows XP,。可是,若是是商業程序,這種想法在現階段不該該有,咱們不能讓用戶爲了使用咱們的客戶端而去升級他的操做系統。Overlapped I/O能夠在Windows 98下實現,性能也不錯,可是實現和理解起來快遇上完成端口了。並且,最關鍵的一點,客戶端程序不是用來進行大規模網絡響應的,客戶端的主要工做應該是進行諸如圖形運算等非網絡方面的任務。原書做者,包括我強烈推薦你們使用WSAAsyncSelect模式實現客戶端,由於它實現起來比較直接和容易,並且他徹底能夠知足客戶端編程的需求。
下面是一段源代碼,雖然咱們是用它來寫客戶端,我仍是把它的服務端代碼放上來,一方面是有興趣的朋友能夠用他作測試和了解如何用它實現服務器;另外一方面是客戶端的代碼能夠很容易的從它修改而成,不一樣的地方只要參考一下2.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;
}
2.4 小節
目前爲止,我很是簡要的介紹了Windows網絡編程的一些東西,附上了一些源代碼。能夠說,讀者特別是初學者,看了後不必定就能立刻寫出程序來,而那些代碼也不是能夠直接應用於實際的項目。別急,萬里長征纔開始第一步呢,不少書裏都是按照基礎到應用的順序來寫的,可是我喜歡更直接一點,更實用一些的方式。並且,我寫的這個專題,畢竟不是商業化的,時間上不能投入過多,只是做爲給初學者的一個小小幫助。更多的仍是但願讀者本身刻苦研究,有問題的時候能夠到個人論壇上給我留言,之後有機會我也會公佈一些實際的代碼。但願結交更多熱愛編程和中國遊戲事業的朋友。下一章裏我將主要講解完成端口編程,這也是我寫這篇文章的初衷,但願對你們能有所幫助。
第三章 完成端口模式下的高性能網絡服務器
3.1開始
完成端口聽起來好像很神祕和複雜,其實並無想象的那麼難。這方面的文章在論壇上能找到的我差很少都看過,寫得好點的就是CSDN.NET上看到的一組系列文章,不過我認爲它只是簡單的翻譯了一下Network Programming for Microsoft Windows 2nd 中的相關內容,附上的代碼好像不是原書中的,多是另外一本外文書裏的。我看了之後,以爲還不如看原版的更容易理解。因此在個人開始部分,我主要帶領初學者理解一下完成端口的有關內容,是我開發的經驗,其餘的請參考原書的相關內容。
採用完成端口的好處是,操做系統的內部重疊機制能夠保證大量的網絡請求都被服務器處理,而不是像WSAAsyncSelect 和WSAEventSelect的那樣對併發的網絡請求有限制,這一點從上一章的測試表格中能夠清楚的看出。
完成端口就像一種消息通知的機制,咱們建立一個線程來不斷讀取完成端口狀態,接收到相應的完成通知後,就進行相應的處理。其實感受就像WSAAsyncSelect同樣,不過仍是有一些的不一樣。好比咱們想接收消息,WSAAsyncSelect會在消息到來的時候直接通知Windows消息循環,而後就能夠調用WSARecv來接收消息了;而完成端口則首先調用一個WSARecv表示程序須要接收消息(這時可能尚未任何消息到來),可是隻有當消息來的時候WSARecv纔算完成,用戶就能夠處理消息了,而後再調用一個WSARecv表示等待下一個消息,如此不停循環,我想這就是完成端口的最大特色吧。
Per-handleData 和 Per-I/OOperation Data 是兩個比較重要的概念,Per-handle Data用來把客戶端數據和對應的完成通知關聯起來,這樣每次咱們處理完成通知的時候,就能知道它是哪一個客戶端的消息,而且能夠根據客戶端的信息做出相應的反應,我想也能夠理解爲Per-Client handle Data吧。Per-I/O Operation Data則不一樣,它記錄了每次I/O通知的信息,好比接收消息時咱們就能夠從中讀出消息的內容,也就是和I/O操做有關的信息都記錄在裏面了。當你親手實現完成端口的時候就能夠理解他們的不一樣和用途了。
CreateIoCompletionPort函數中有個參數NumberOfConcurrentThreads,完成端口編程裏有個概念Worker Threads。這裏比較容易引發混亂,NumberOfConcurrentThreads須要設置多少,又須要建立多少個Worker Threads纔算合適?NumberOfConcurrentThreads的數目和CPU數量同樣最好,由於少了就無法利用多CPU的優點,而多了則會由於線程切換形成性能降低。WorkerThreads的數量是否是也要同樣多呢,固然不是,它的數量取決於應用程序的須要。舉例來講,咱們在Worker Threads裏進行消息處理,若是這個過程當中有可能會形成線程阻塞,那若是咱們只有一個Worker Thread,咱們就不能很快響應其餘客戶端的請求了,而只有當這個阻塞操做完成了後才能繼續處理下一個完成消息。可是若是咱們還有其餘的Worker Thread,咱們就能繼續處理其餘客戶端的請求,因此到底須要多少的Worker Thread,須要根據應用程序來定,而不是能夠事先估算出來的。若是工做者線程裏沒有阻塞操做,對於某些狀況來講,一個工做者線程就能夠知足須要了。
其餘問題,Network Programming forMicrosoft Windows 2nd中,做者還提出瞭如何安全的退出應用程序等等實現中的細節問題,這裏我就不一一講述了,請讀者參考原書的相關內容,若是仍有疑問,能夠聯繫我。
3.2實現
下面是通常的實現步驟
1. 得到計算機信息,獲得CPU的數量。建立一個完成端口,第四個參數置0,指定NumberOfConcurrentThreads爲CPU個數。
2. Determine how many processorsexist on the system.
3. Create worker threads toservice completed I/O requests on the completion port using processorinformation in step 2. In the case of this simple example, we create one workerthread per processor because we do not expect our threads to ever get in asuspended condition in which there would not be enough threads to execute foreach processor. When the CreateThreadfunction is called, you must supply a worker routine that the thread executesupon creation. We will discuss the worker thread's responsibilities later inthis section.
4. Prepare a listening socket tolisten for connections on port 5150.
5. Accept inbound connectionsusing the accept function.
6. Create a data structure torepresent per-handle data and save the accepted socket handle in the structure.
7. Associate the new socket handlereturned from accept with the completion portby calling CreateIoCompletionPort. Pass theper-handle data structure to CreateIoCompletionPortvia the completion key parameter.
8. Start processing I/O on theaccepted connection. Essentially, you want to post one or more asynchronous WSARecv or WSASendrequests on the new socket using the overlapped I/O mechanism. When these I/Orequests complete, a worker thread services the I/O requests and continuesprocessing future I/O requests, as we will see later in the worker routinespecified in step 3.
9. Repeat steps 5–8 until serverterminates.
那麼學習完成端口編程從哪裏開始比較好,對於初學者而言,直接進入編程並非一個好主意,我建議初學者首先學習用異步Socket模式,即WSAEventSelect模式構建一個簡單的聊天服務器。當把Windows網絡編程的概念有一個清晰的認識以後,再深刻研究完成端口編程。
接着就是深刻研究具體的編程實現了,從NetworkProgramming for Microsoft Windows 2nd中摘錄的這段經典代碼能夠說是很是合適的,這裏我只簡單解釋一下其中比較關鍵的地方,還有不明白的能夠參看原書,或者聯繫我。
主程序段:
1. HANDLE CompletionPort;
2. WSADATA wsd;
3. SYSTEM_INFO SystemInfo;
4. SOCKADDR_IN InternetAddr;
5. SOCKET Listen;
6. int i;
7.
8. typedef struct _PER_HANDLE_DATA
9. {
10. SOCKET Socket;
11. SOCKADDR_STORAGE ClientAddr;
12. // 在這裏還能夠加入其餘和客戶端關聯的數據
13. } PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
14.
15. // 初始化Windows Socket 2.2
16. StartWinsock(MAKEWORD(2,2), &wsd);
17.
18. // Step 1:
19. // 建立完成端口
20.
21. CompletionPort = CreateIoCompletionPort(
22. INVALID_HANDLE_VALUE, NULL, 0, 0);
23.
24. // Step 2:
25. // 檢測系統信息
26.
27. GetSystemInfo(&SystemInfo);
28.
29. // Step 3: 建立工做者線程,數量和CPU的數量同樣多
30. // Create worker threads based on the number of
31. // processors available on the system. For this
32. // simple case, we create one worker thread for each
33. // processor.
34.
35. for(i = 0; i < SystemInfo.dwNumberOfProcessors; i++)
36. {
37. HANDLE ThreadHandle;
38.
39. // Create a server worker thread, and pass the
40. // completion port to the thread. NOTE: the
41. // ServerWorkerThread procedure is not defined
42. // in this listing.
43.
44. ThreadHandle = CreateThread(NULL, 0,
45. ServerWorkerThread, CompletionPort,
46. 0, NULL;
47.
48. // Close the thread handle
49. CloseHandle(ThreadHandle);
50. }
51.
52. // Step 4:
53. // 建立監聽Socket
54.
55. Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
56. WSA_FLAG_OVERLAPPED);
57.
58. InternetAddr.sin_family = AF_INET;
59. InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
60. InternetAddr.sin_port = htons(5150);
61. bind(Listen, (PSOCKADDR) &InternetAddr,
62. sizeof(InternetAddr));
63.
64. // 開始監聽
65.
66. listen(Listen, 5);
67.
68. while(TRUE)
69. {
70. PER_HANDLE_DATA *PerHandleData=NULL;
71. SOCKADDR_IN saRemote;
72. SOCKET Accept;
73. int RemoteLen;
74. // Step 5: 等待客戶端鏈接,而後將客戶端Socket加入完成端口
75. // Accept connections and assign to the completion
76. // port
77.
78. RemoteLen = sizeof(saRemote);
79. Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote,
80. &RemoteLen);
81.
82. // Step 6: 初始化客戶端數據
83. // Create per-handle data information structure to
84. // associate with the socket
85. PerHandleData = (LPPER_HANDLE_DATA)
86. GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
87.
88. printf("Socket number %d connected\n", Accept);
89. PerHandleData->Socket = Accept;
90. memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);
91.
92. // Step 7:
93. // Associate the accepted socket with the
94. // completion port
95.
96. CreateIoCompletionPort((HANDLE) Accept,
97. CompletionPort, (DWORD) PerHandleData, 0);
98.
99. // Step 8: 發出對客戶端的I/O請求,等待完成消息
100. // Start processing I/O on the accepted socket.
101. // Post one or more WSASend() or WSARecv() calls
102. // on the socket using overlapped I/O.
103. WSARecv(...);
104. }
105.
106.
工做者線程
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort = (HANDLE) CompletionPortID;
DWORD BytesTransferred;
LPOVERLAPPED Overlapped;
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_DATA PerIoData;
DWORD SendBytes, RecvBytes;
DWORD Flags;
while(TRUE)
{
// 等待完成端口消息,未收到消息德時候則阻塞線程
ret = GetQueuedCompletionStatus(CompletionPort,
&BytesTransferred,(LPDWORD)&PerHandleData,
(LPOVERLAPPED *) &PerIoData, INFINITE);
// First check to see if an error has occurred
// on the socket; if so, close the
// socket and clean up the per-handle data
// and per-I/O operation data associated with
// the socket
if (BytesTransferred == 0 &&
(PerIoData->OperationType == RECV_POSTED ││
PerIoData->OperationType == SEND_POSTED))
{
// A zero BytesTransferred indicates that the
// socket has been closed by the peer, so
// you should close the socket. Note:
// Per-handle data was used to reference the
// socket associated with the I/O operation.
closesocket(PerHandleData->Socket);
GlobalFree(PerHandleData);
GlobalFree(PerIoData);
continue;
}
// Service the completed I/O request. You can
// determine which I/O request has just
// completed by looking at the OperationType
// field contained in the per-I/O operation data.
if (PerIoData->OperationType == RECV_POSTED)
{
// Do something with the received data
// in PerIoData->Buffer
}
// Post another WSASend or WSARecv operation.
// As an example, we will post another WSARecv()
// I/O operation.
Flags = 0;
// Set up the per-I/O operation data for the next
// overlapped call
ZeroMemory(&(PerIoData->Overlapped),
sizeof(OVERLAPPED));
PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;
PerIoData->OperationType = RECV_POSTED;
WSARecv(PerHandleData->Socket,
&(PerIoData->DataBuf), 1, &RecvBytes,
&Flags, &(PerIoData->Overlapped), NULL);
}
}
3.3 小節
講這麼點就完了?你必定認爲我介紹的東西並無超過原書中的內容,實事上完成端口編程的精髓就是上面的代碼和原書中的有關敘述。若是我再把他們完整的重複一遍,那又有什麼意思呢?根據個人經驗,設計網絡服務器的真正難點,不在於完成端口技術,因此我想利用小節把本身編程中的一些經驗告訴你們。
首先是服務器的管理,一個服務器首先要分析它的設計目標是應對不少的鏈接仍是很大的數據傳送量。這樣在設計工做者線程時就能夠最大限度的提升性能。管理客戶端方面,咱們能夠將客戶端的數據捆綁到Perhand-Data數據結構上,若是還有須要,能夠建一個表來記錄客戶端的宏觀狀況。
在Ares引擎中,我將文件傳送和大容量數據傳送功能也封裝進了服務器和客戶端。我建議服務器和客戶端都應該封裝這些功能,儘管咱們並非作FTP服務器,可是當客戶端須要和服務器交換文件和大塊數據時,你會發現這樣作,靈活性和性能都能作得比用單純的FTP協議來更好,因此在你的服務器和客戶端能夠傳送數據包之後,把他們都作進去吧。
爲了服務器不被黑客攻擊,或被BUG弄崩潰,咱們還須要認真設計服務器的認證機制,以及密切注意程序中的溢出,必定要在每個使用緩衝區的地方加上檢查代碼。能夠說並無現成的辦法來解決這個問題,否則就沒有人研究網絡安全了,因此咱們要作的是儘可能減小錯誤,即便出現錯誤也不會形成太大損失,在發現錯誤的時候可以很快糾正同類錯誤。
還有就是對客戶端狀況的檢測,好比客戶端的正常和非正常斷開鏈接。若是不注意這一點,就會形成服務器資源持續消耗而最終崩潰,由於咱們的服務器不可能老是重啓,而是要持續的運行,越久越好。還有好比客戶端斷開鏈接後又嘗試鏈接,可是在服務器看來這個客戶「仍然在線「,這個時候咱們不能單純的拒絕客戶端的鏈接,也不能單純的接收。
講了幾點服務器設計中的問題,他們只是衆多問題中的一小部分,限於時間緣由,在這個版本的文章中就說這麼多。你必定會發現,其實網絡編程最困難和有成就的地方,並非服務器用了什麼模式等等,而是真正深刻設計的時候碰到的衆多問題。正是那些沒有標準答案的問題,值得咱們去研究和解決。
第四章 做者的話
寫這篇文章的目的,一方面是簡要的談談遊戲編程中的網絡部分。另外一方面是結交衆多開發的朋友。畢竟咱們作東西不可能不和他人交流,也不可能只作非商業化的項目。我開發的Ares引擎就是同時爲了這兩個目的,到我寫這篇文章的時候,引擎的版本仍然是3.2,並非我不想繼續開發,也不是沒有新的改變了。偏偏相反,我有不少新的想法,急切想把他們加入新的版本中,只是如今手上還有短時間的項目沒有完成。
有但願交流的朋友,但願合做開發的朋友,有項目委託的朋友。。。聯繫我。