手把手教你玩轉(zhuǎn)網(wǎng)絡(luò)編程模型之完成例程(Completion Routine)篇
----- By PiggyXP(小豬)
前 言
記得寫這個(gè)系列的上一篇文章的時(shí)候已經(jīng)是四年前了,準(zhǔn)確的說(shuō)是四年半以前了,翻開我塵封已久的blog,感覺到上面已經(jīng)落了厚厚的一層塵土,突然又來(lái)了感覺,于是我翻箱倒柜的找出以前的資料,上傳到了我的空間里,而且,順便又為在網(wǎng)絡(luò)編程苦海中苦苦尋覓的朋友帶來(lái)一份禮物,這次為大家?guī)?lái)的是重疊IO模型里面的“完成例程”的實(shí)現(xiàn)方式及示例代碼。
本文凝聚著筆者心血,如要轉(zhuǎn)載,請(qǐng)指明原作者及出處,謝謝!不過(guò)代碼寫得不好,歡迎改進(jìn),而且沒有版權(quán),請(qǐng)隨便散播、使用。^_^
OK, Let’s go ! Have fun!!
本文配套的示例源碼下載地址(在我的下載空間里)
http://download.csdn.net/user/PiggyXP
(VC++ 2008編寫的多客戶端MFC代碼,配有非常非常詳盡的注釋,功能只是簡(jiǎn)單的顯示一下各個(gè)客戶端發(fā)來(lái)的字符,作為教學(xué)代碼,為了使得代碼結(jié)構(gòu)清晰明了,簡(jiǎn)化了很多地方,用于產(chǎn)品開發(fā)的話還需要做很多改進(jìn),有錯(cuò)誤或者不足的地方,非常歡迎大家不吝指出。)
代碼界面示意圖:

本文假設(shè)你已經(jīng)對(duì)重疊I/O的機(jī)制已有了解,否則請(qǐng)先參考本系列的前一篇《手把手教你玩轉(zhuǎn)重疊IO模型》
目錄:
1. 完成例程的優(yōu)點(diǎn)
2. 完成例程的基本原理
3. 關(guān)于完成例程的函數(shù)介紹
4. 完成例程的實(shí)現(xiàn)步驟
5. 實(shí)際應(yīng)用中應(yīng)該進(jìn)一步完善的地方
一.
完成例程的優(yōu)點(diǎn)
1. 首先需要指明的是,這里的“完成例程”(Completion Routine)并非是大家所常聽到的 “完成端口”(Completion Port),而是另外一種管理重疊I/O請(qǐng)求的方式,而至于什么是重疊I/O,簡(jiǎn)單來(lái)講就是Windows系統(tǒng)內(nèi)部管理I/O的一種方式,核心就是調(diào)用的ReadFile和WriteFile函數(shù),在制定設(shè)備上執(zhí)行I/O操作,不光是可用于網(wǎng)絡(luò)通信,也可以用于其他需要的地方。
在Windows系統(tǒng)中,管理重疊I/O可以有三種方式:
(1) 上一篇中提到的基于事件通知的重疊I/O模型
(2) 本篇中將要講述的基于“完成例程”的重疊I/O模型
(3) 下一篇中將要講到的“完成端口”模型
雖然都是基于重疊I/O,但是因?yàn)榍皟煞N模型都是需要自己來(lái)管理任務(wù)的分派 ,所以性能上沒有區(qū)別,而完成端口是創(chuàng)建完成端口對(duì)象使操作系統(tǒng)親自來(lái)管理任務(wù)的分派,所以完成端口肯定是能獲得最好的性能。
2. 如果你想要使用重疊I/O機(jī)制帶來(lái)的高性能模型,又懊惱于基于事件通知的重疊模型要收到64個(gè)等待事件的限制,還有點(diǎn)畏懼完成端口稍顯復(fù)雜的初始化過(guò)程,那么“完成例程”無(wú)疑是你最好的選擇!^_^ 因?yàn)橥瓿衫虜[脫了事件通知的限制,可以連入任意數(shù)量客戶端而不用另開線程,也就是說(shuō)只用很簡(jiǎn)單的一些代碼就可以利用Windows內(nèi)部的I/O機(jī)制來(lái)獲得網(wǎng)絡(luò)服務(wù)器的高性能,是不是心動(dòng)了呢?那就一起往下看。。。。。。。。。。
3. 而且個(gè)人感覺“完成例程”的方式比重疊I/O更好理解,因?yàn)榫秃臀覀儌鹘y(tǒng)的“回調(diào)函數(shù)”是一樣的,也更容易使用一些,推薦!
二.
完成例程的基本原理
概括一點(diǎn)說(shuō),上一篇拙作中提到的那個(gè)基于事件通知的重疊I/O模型,在你投遞了一個(gè)請(qǐng)求以后(比如WSARecv),系統(tǒng)在完成以后是用事件來(lái)通知你的,而在完成例程中,系統(tǒng)在網(wǎng)絡(luò)操作完成以后會(huì)自動(dòng)調(diào)用你提供的回調(diào)函數(shù),區(qū)別僅此而已,是不是很簡(jiǎn)單呢?
首先這里統(tǒng)一幾個(gè)名詞,包括“重疊操作”、“重疊請(qǐng)求”、“投遞請(qǐng)求”等等,這是為了配合這的重疊I/O才這么講的,說(shuō)的直白一些,也就是你在代碼中發(fā)出的WSARecv()、WSASend()等等網(wǎng)絡(luò)函數(shù)調(diào)用。
上篇文章中偷懶沒畫圖,這次還是畫個(gè)流程圖來(lái)說(shuō)明吧,采用完成例程的服務(wù)器端,通信流程簡(jiǎn)單的來(lái)講是這樣的:

從圖中可以看到,服務(wù)器端存在一個(gè)明顯的異步過(guò)程,也就是說(shuō)我們把客戶端連入的SOCKET與一個(gè)重疊結(jié)構(gòu)綁定之后,便可以將通訊過(guò)程全權(quán)交給系統(tǒng)內(nèi)部自己去幫我們調(diào)度處理了,我們?cè)谥骶€程中就可以去做其他的事情,邊等候系統(tǒng)完成的通知就OK,這也就是完成例程高性能的原因所在。
如果還沒有看明白,我們打個(gè)通俗易懂的比方,完成例程的處理過(guò)程,也就像我們告訴系統(tǒng),說(shuō)“我想要在網(wǎng)絡(luò)上接收網(wǎng)絡(luò)數(shù)據(jù),你去幫我辦一下”(投遞WSARecv操作),“不過(guò)我并不知道網(wǎng)絡(luò)數(shù)據(jù)合適到達(dá),總之在接收到網(wǎng)絡(luò)數(shù)據(jù)之后,你直接就調(diào)用我給你的這個(gè)函數(shù)(比如_CompletionProess),把他們保存到內(nèi)存中或是顯示到界面中等等,全權(quán)交給你處理了”,于是乎,系統(tǒng)在接收到網(wǎng)絡(luò)數(shù)據(jù)之后,一方面系統(tǒng)會(huì)給我們一個(gè)通知,另外同時(shí)系統(tǒng)也會(huì)自動(dòng)調(diào)用我們事先準(zhǔn)備好的回調(diào)函數(shù),就不需要我們自己操心了。
看到這里,各位應(yīng)該已經(jīng)對(duì)完成例程的體系結(jié)構(gòu)有了比價(jià)清晰的了解了吧,下面各位喝點(diǎn)咖啡轉(zhuǎn)轉(zhuǎn)脖子休息休息,然后就進(jìn)入到下面的具體實(shí)現(xiàn)部分了。
一.
完成例程的函數(shù)介紹
這個(gè)部分將要介紹在完成例程模型中會(huì)使用到的關(guān)鍵函數(shù),內(nèi)容比較枯燥,大家要做好心理準(zhǔn)備。不過(guò)在實(shí)際應(yīng)用以前,很多東西肯定也不會(huì)理解得太深刻,可以先泛泛的了解一下,以后再回頭復(fù)習(xí)這里的知識(shí)就可以了。
厄。。。。。。仔細(xì)審查了一下代碼,發(fā)現(xiàn)其實(shí)這里也沒有什么新函數(shù)好介紹了,大部分都是使用重疊模型那一章里介紹的一樣的函數(shù),需要查看的朋友請(qǐng)看這里《手把手教你玩轉(zhuǎn)重疊IO模型》
,這里就不再重復(fù)了:
這里只補(bǔ)充一個(gè)知識(shí)點(diǎn),就是咱們完成例程方式和前面的事件通知方式最大的不同之處就在于,我們需要提供一個(gè)回調(diào)函數(shù)供系統(tǒng)收到網(wǎng)絡(luò)數(shù)據(jù)后自動(dòng)調(diào)用,回調(diào)函數(shù)的參數(shù)定義應(yīng)該遵照如下的函數(shù)原型:
1. 完成例程回調(diào)函數(shù)原型及傳遞方式
函數(shù)應(yīng)該是這樣定義的,函數(shù)名字隨便起,但是參數(shù)類型不能錯(cuò)
view plaincopy to
clipboardprint?
- Void CALLBACK _CompletionRoutineFunc(
- DWORD dwError,
- DWORD cbTransferred,
- LPWSAOVERLAPPED lpOverlapped,
- DWORD dwFlags
還有一點(diǎn)需要重點(diǎn)提一下的是,因?yàn)槲覀冃枰o系統(tǒng)提供一個(gè)如上面定義的那樣的回調(diào)函數(shù),以便系統(tǒng)在完成了網(wǎng)絡(luò)操作后自動(dòng)調(diào)用,這里就需要提一下究竟是如何把這個(gè)函數(shù)與系統(tǒng)內(nèi)部綁定的呢?如下所示,在WSARecv函數(shù)中是這樣綁定的
view plaincopy to
clipboardprint?
1.
int WSARecv(
2.
SOCKET s,
3.
LPWSABUF lpBuffers,
4.
5.
DWORD dwBufferCount,
6.
LPDWORD lpNumberOfBytesRecvd,
7.
8.
LPDWORD lpFlags,
9.
LPWSAOVERLAPPED lpOverlapped,
10.
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
11.
12.
);
其他參數(shù)我們可以先不用先細(xì)看,只看最后一個(gè),看到了嗎?直接在WSARecv的最后一個(gè)參數(shù)上,傳遞一下我們回調(diào)函數(shù)的指針就行了,這里注意一下,咱們這次多次提到的這個(gè)“完成例程”,其實(shí)就是指的咱們提供的這個(gè)回調(diào)函數(shù)。
view plaincopy to
clipboardprint?
1.
舉個(gè)例子:(變量的定義順序和上面的說(shuō)明的順序是對(duì)應(yīng)的,下同)
2.
SOCKET s;
3.
WSABUF DataBuf;
4.
5.
#define DATA_BUFSIZE 4096
6.
char buffer[DATA_BUFSIZE];
7.
ZeroMemory(buffer, DATA_BUFSIZE);
8.
DataBuf.len = DATA_BUFSIZE;
9.
DataBuf.buf = buffer;
10. DWORD dwBufferCount = 1, dwRecvBytes = 0, Flags = 0;
11.
12. WSAOVERLAPPED AcceptOverlapped ;
13.
14. ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
15.
16.
17.
18. WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes,
19. &Flags, &AcceptOverlapped, _CompletionRoutine);
其他的函數(shù)我這里就不一一介紹了,因?yàn)槲覀儺吘惯€有MSDN這么個(gè)好幫手,而且在講后面的完成例程和完成端口的時(shí)候我還會(huì)講到一些 ^_^
四.
完成例程的實(shí)現(xiàn)步驟
基礎(chǔ)知識(shí)方面需要知道的就是這么多,下面我們配合代碼,來(lái)一步步的講解如何親手實(shí)現(xiàn)一個(gè)完成例程模型(前面幾步的步驟和基于事件通知的重疊I/O方法是一樣的)。
【第一步】創(chuàng)建一個(gè)套接字,開始在指定的端口上監(jiān)聽連接請(qǐng)求
和其他的SOCKET初始化全無(wú)二致,直接照搬即可,在此也不多費(fèi)唇舌了,需要注意的是為了一目了然,我去掉了錯(cuò)誤處理,平常可不要這樣啊,盡管這里出錯(cuò)的幾率比較小。
view plaincopy to
clipboardprint?
1.
WSADATA wsaData;
2.
WSAStartup(MAKEWORD(2,2),&wsaData);
3.
4.
ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
5.
6.
SOCKADDR_IN ServerAddr;
7.
ServerAddr.sin_family=AF_INET;
8.
ServerAddr.sin_addr.S_un.S_addr =htonl(INADDR_ANY);
9.
ServerAddr.sin_port=htons(11111);
10.
11.
12. bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
13.
14. listen(ListenSocket, 5);
【第二步】接受一個(gè)入站的連接請(qǐng)求
一個(gè)accept就完了,都是一樣一樣一樣一樣的啊~~~~~~~~~~
至于AcceptEx的使用,在完成端口中我會(huì)講到,這里就先不一次灌輸這么多了,不消化啊^_^
view plaincopy to
clipboardprint?
1.
AcceptSocket = accept (ListenSocket, NULL,NULL) ;
當(dāng)然,這里是我偷懶,如果想要獲得連入客戶端的信息(記得論壇上也常有人問(wèn)到),accept的后兩個(gè)參數(shù)就不要用NULL,而是這樣
view plaincopy to
clipboardprint?
1.
SOCKADDR_IN ClientAddr;
2.
int addr_length=sizeof(ClientAddr);
3.
AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);
4.
5.
LPCTSTR lpIP = inet_ntoa(ClientAddr.sin_addr);
6.
UINT nPort = ClientAddr.sin_port;
【第三步】準(zhǔn)備好我們的重疊結(jié)構(gòu)
有新的套接字連入以后,新建立一個(gè)WSAOVERLAPPED重疊結(jié)構(gòu)(當(dāng)然也可以提前建立好),準(zhǔn)備綁定到我們的重疊操作上去。這里也可以看到和上一篇中的明顯區(qū)別,就是不用再為WSAOVERLAPPED結(jié)構(gòu)綁定一個(gè)hEvent了。
view plaincopy to
clipboardprint?
-
- WSAOVERLAPPED AcceptOverlapped;
- ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
【第四步】開始在套接字上投遞WSARecv請(qǐng)求,需要將第三步準(zhǔn)備的WSAOVERLAPPED結(jié)構(gòu)和我們定義的完成例程函數(shù)為參數(shù)
各個(gè)變量都已經(jīng)初始化OK以后,我們就可以開始進(jìn)行具體的Socket通信函數(shù)調(diào)用了,然后讓系統(tǒng)內(nèi)部的重疊結(jié)構(gòu)來(lái)替我們管理I/O請(qǐng)求,我們只用等待網(wǎng)絡(luò)通信完成后調(diào)用咱們的回調(diào)函數(shù)就OK了。
這個(gè)步驟的重點(diǎn)就是 綁定一個(gè)Overlapped變量和一個(gè)完成例程函數(shù)
view plaincopy to
clipboardprint?
1.
2.
3.
if(WSARecv(
4.
AcceptSocket,
5.
&DataBuf,
6.
1,
7.
&dwRecvBytes,
8.
&Flags,
9.
&AcceptOverlapped,
10.
_CompletionRoutine) == SOCKET_ERROR)
11.
{
12.
if(WSAGetLastError() != WSA_IO_PENDING)
13.
{
14.
ReleaseSocket(nSockIndex);
15.
continue;
16.
}
17.
}
18.
}
【第五步】 調(diào)用WSAWaitForMultipleEvents函數(shù)或者SleepEx函數(shù)等待重疊操作返回的結(jié)果
我們?cè)谇懊嫣岬竭^(guò),投遞完WSARecv操作,并綁定了Overlapped結(jié)構(gòu)和完成例程函數(shù)之后,我們基本就是完事大吉了,等了系統(tǒng)自己去完成網(wǎng)絡(luò)通信,并在接收到數(shù)據(jù)的時(shí)候,會(huì)自動(dòng)調(diào)用我們的完成例程函數(shù)。
而我們?cè)谥骶€程中需要做的事情只有:做別的事情,并且等待系統(tǒng)完成了完成例程調(diào)用后的返回結(jié)果。
就是說(shuō)在WSARecv調(diào)用發(fā)起完畢之后,我們不得不在后面再緊跟上一些等待完成結(jié)果的代碼。有兩種辦法可以實(shí)現(xiàn):
1) 和上一篇重疊I/O中講到的一樣,我們可以使用WSAWaitForMultipleEvent來(lái)等待重疊操作的事件通知, 方法如下:
view plaincopy to
clipboardprint?
1.
2.
3.
4.
WSAEVENT EventArray[1];
5.
EventArray[0] = WSACreateEvent();
6.
7.
8.
DWORD dwIndex = WSAWaitForMultipleEvents(1,EventArray,FALSE,WSA_INFINITE,TRUE);
這里參數(shù)的含義我就不細(xì)說(shuō)了,MSDN上一看就明白,調(diào)用這個(gè)函數(shù)以后,線程就會(huì)置于一個(gè)警覺的等待狀態(tài),注意
fAlertable 參數(shù)一定要設(shè)置為 TRUE。
2) 可以直接使用SleepEx函數(shù)來(lái)完成等待,效果都是一樣的。
SleepEx函數(shù)調(diào)用起來(lái)就簡(jiǎn)單得多,它的函數(shù)原型定義是這樣的
view plaincopy to
clipboardprint?
- DWORD SleepEx(
- DWORD dwMilliseconds,
- BOOL bAlertable
- );
調(diào)用這個(gè)函數(shù)的時(shí)候,同樣注意用一個(gè)DWORD類型變量來(lái)保存它的返回值,后面會(huì)派上用場(chǎng)。
【第六步】通過(guò)等待函數(shù)的返回值取得重疊操作的完成結(jié)果
這是我們最關(guān)心的事情,費(fèi)了那么大勁投遞的這個(gè)重疊操作究竟是個(gè)什么結(jié)果呢?就是通過(guò)上一步中我們調(diào)用的等待函數(shù)的DWORD類型的返回值,正常情況下,在操作完成之后,應(yīng)該是返回WAIT_IO_COMPLETION,如果返回的是 WAIT_TIMEOUT,則表示等待設(shè)置的超時(shí)時(shí)間到了,但是重疊操作依舊沒有完成,應(yīng)該通過(guò)循環(huán)再繼續(xù)等待。如果是其他返回值,那就壞事了,說(shuō)明網(wǎng)絡(luò)通信出現(xiàn)了其他異常,程序就可以報(bào)錯(cuò)退出了……
判斷返回值的代碼大致如下:
view plaincopy to
clipboardprint?
-
-
- if(dwIndex == WAIT_IO_COMPLETION)
- {
- TRACE("重疊操作完成...\n");
- }
- else if( dwIndex==WAIT_TIMEOUT )
- {
- TRACE(“超時(shí)了,繼續(xù)調(diào)用等待函數(shù)”);
- }
- else
- {
- TRACE(“廢了…”);
- }
操作完成了之后,就說(shuō)明我們上一個(gè)操作已經(jīng)成功了,成功了之后做什么?當(dāng)然是繼續(xù)投遞下一個(gè)重疊操作了啊…..繼續(xù)上面的循環(huán)。
【第七步】繼續(xù)回到第四步,在套接字上繼續(xù)投遞WSARecv請(qǐng)求,重復(fù)步驟4-7
大家可以參考我的代碼,在這里就先不寫了,因?yàn)楦魑欢家欢ū任?/span>smart,領(lǐng)悟了關(guān)鍵所在以后,稍作思考就可以靈活變通了。
【第八步】“享受”接收到的數(shù)據(jù)
朋友們看到這里一定會(huì)問(wèn),我忙活了這么久,那客戶端傳來(lái)的數(shù)據(jù)在哪里接收啊?怎么一點(diǎn)都沒有提到呢……
這個(gè)問(wèn)題問(wèn)得好,我們寫了這么多代碼圖個(gè)什么呢?
其實(shí)想要讀取客戶端的數(shù)據(jù)很簡(jiǎn)單,因?yàn)槲覀冊(cè)?/span>WSARecv調(diào)用的時(shí)候,是傳遞了一個(gè)WSABUF的變量的,用于保存網(wǎng)絡(luò)數(shù)據(jù),而在我們寫的完成例程回調(diào)函數(shù)里面,就可以取到客戶端傳送來(lái)的網(wǎng)絡(luò)數(shù)據(jù)了。
因?yàn)橄到y(tǒng)在調(diào)用我們完成例程函數(shù)的時(shí)候,其實(shí)網(wǎng)絡(luò)操作已經(jīng)完成了,WSABUF里面已經(jīng)有我們需要的數(shù)據(jù)了,只是通過(guò)完成例程來(lái)進(jìn)行后期的處理。具體可以參考示例代碼。 而DataBuf.buf就是一個(gè)char*字符串指針,聽?wèi){你的處理吧,我就不多說(shuō)了。
一.
實(shí)際應(yīng)用中應(yīng)該完善的地方
其 實(shí)我一直都很想把我以前做的工程中的代碼貼出來(lái)給大家分享,但是代碼實(shí)在是太繁雜了,僅僅把網(wǎng)絡(luò)通信的部分剝離出來(lái),不經(jīng)過(guò)測(cè)試的話,肯定還會(huì)有其他的很 多問(wèn)題,反而誤導(dǎo)了初學(xué)者,不過(guò)我的計(jì)劃是在寫下一個(gè)“完成端口”部分的時(shí)候,直接把項(xiàng)目中的一部分代碼拿出來(lái)試試看吧……
總之網(wǎng)絡(luò)服務(wù)器端程序,在實(shí)際應(yīng)用的時(shí)候,關(guān)鍵的幾點(diǎn)就是:
1) 要考慮到客戶端很多、通信量很大的時(shí)候,如何去處理,如何盡可能的減小開銷,提高效率;
2) 多個(gè)線程之間共用一些變量的時(shí)候,一定要注意到同步問(wèn)題;
3) 作為一個(gè)網(wǎng)絡(luò)程序,出現(xiàn)異常是家常便飯,一定要把代碼寫得盡可能的健壯,要盡量全面的考慮處理各種各樣的錯(cuò)誤;
4) 盡量不要出現(xiàn)各種字符緩沖區(qū)的問(wèn)題,寫安全的代碼,防止被黑客利用……(這點(diǎn)似乎扯遠(yuǎn)了,但是確實(shí)是一個(gè)很現(xiàn)實(shí)的問(wèn)題)。
其他的問(wèn)題,還希望各位這方面的網(wǎng)絡(luò)專家使勁批評(píng)指正,因?yàn)榇a是很多年前的了,一定存在著很多的問(wèn)題。
--- Finished at DLUT
--- 2009-02-16