摘要:本文對如何使用面向連接的流式套接字實(shí)現(xiàn)對網(wǎng)卡的編程以及如何實(shí)現(xiàn)異步網(wǎng)絡(luò)通訊等問題進(jìn)行了討論與闡述。
一、 引言 在80年代初,美國加利福尼亞大學(xué)伯克利分校的研究人員為TCP/IP網(wǎng)絡(luò)通信開發(fā)了一個(gè)專門用于網(wǎng)絡(luò)通訊開發(fā)的API。這個(gè)API就是Socket接口(套接字)--當(dāng)今在TCP/IP網(wǎng)絡(luò)最為通用的一種API,也是在互聯(lián)網(wǎng)上進(jìn)行應(yīng)用開發(fā)最為通用的一種API。在微軟聯(lián)合其它幾家公司共同制定了一套Windows下的網(wǎng)絡(luò)編程接口Windows Sockets規(guī)范后,由于在其規(guī)范中引入了一些異步函數(shù),增加了對網(wǎng)絡(luò)事件異步選擇機(jī)制,因此更加符合Windows的消息驅(qū)動特性,使網(wǎng)絡(luò)開發(fā)人員可以更加方便的進(jìn)行高性能網(wǎng)絡(luò)通訊程序的設(shè)計(jì)。本文接下來就針對Windows Sockets API進(jìn)行面向連接的流式套接字編程以及對異步網(wǎng)絡(luò)通訊的編程實(shí)現(xiàn)等問題展開討論。
二、 面向連接的流式套接字編程模型的設(shè)計(jì) 本文在方案選擇上采用了在網(wǎng)絡(luò)編程中最常用的一種模型--客戶機(jī)/服務(wù)器模型。這種客戶/服務(wù)器模型是一種非對稱式編程模式。該模式的基本思想是把集中在一起的應(yīng)用劃分成為功能不同的兩個(gè)部分,分別在不同的計(jì)算機(jī)上運(yùn)行,通過它們之間的分工合作來實(shí)現(xiàn)一個(gè)完整的功能。對于這種模式而言其中一部分需要作為服務(wù)器,用來響應(yīng)并為客戶提供固定的服務(wù);另一部分則作為客戶機(jī)程序用來向服務(wù)器提出請求或要求某種服務(wù)。
本文選取了基于TCP/IP的客戶機(jī)/服務(wù)器模型和面向連接的流式套接字。其通信原理為:服務(wù)器端和客戶端都必須建立通信套接字,而且服務(wù)器端應(yīng)先進(jìn)入監(jiān)聽狀態(tài),然后客戶端套接字發(fā)出連接請求,服務(wù)器端收到請求后,建立另一個(gè)套接字進(jìn)行通信,原來負(fù)責(zé)監(jiān)聽的套接字仍進(jìn)行監(jiān)聽,如果有其它客戶發(fā)來連接請求,則再建立一個(gè)套接字。默認(rèn)狀態(tài)下最多可同時(shí)接收5個(gè)客戶的連接請求,并與之建立通信關(guān)系。因此本程序的設(shè)計(jì)流程應(yīng)當(dāng)由服務(wù)器首先啟動,然后在某一時(shí)刻啟動客戶機(jī)并使其與服務(wù)器建立連接。服務(wù)器與客戶機(jī)開始都必須調(diào)用Windows Sockets API函數(shù)socket()建立一個(gè)套接字sockets,然后服務(wù)器方調(diào)用bind()將套接字與一個(gè)本地網(wǎng)絡(luò)地址捆扎在一起,再調(diào)用listen()使套接字處于一種被動的準(zhǔn)備接收狀態(tài),同時(shí)規(guī)定它的請求隊(duì)列長度。在此之后服務(wù)器就可以通過調(diào)用accept()來接收客戶機(jī)的連接。
相對于服務(wù)器,客戶端的工作就顯得比較簡單了,當(dāng)客戶端打開套接字之后,便可通過調(diào)用connect()和服務(wù)器建立連接。連接建立之后,客戶和服務(wù)器之間就可以通過連接發(fā)送和接收資料。最后資料傳送結(jié)束,雙方調(diào)用closesocket()關(guān)閉套接字來結(jié)束這次通訊。整個(gè)通訊過程的具體流程框圖可大致用下面的流程圖來表示:
 面向連接的流式套接字編程流程示意圖 |
三、 軟件設(shè)計(jì)要點(diǎn)以及異步通訊的實(shí)現(xiàn)
根據(jù)前面設(shè)計(jì)的程序流程,可將程序劃分為兩部分:服務(wù)器端和客戶端。而且整個(gè)實(shí)現(xiàn)過程可以大致用以下幾個(gè)非常關(guān)鍵的Windows Sockets API函數(shù)將其慣穿下來:
服務(wù)器方:
| socket()->bind()->listen->accept()->recv()/send()->closesocket() |
客戶機(jī)方:
| socket()->connect()->send()/recv()->closesocket() |
有鑒于以上幾個(gè)函數(shù)在整個(gè)網(wǎng)絡(luò)編程中的重要性,有必要結(jié)合程序?qū)嵗龑ζ渥鲚^深入的剖析。服務(wù)器端應(yīng)用程序在使用套接字之前,首先必須擁有一個(gè)Socket,系統(tǒng)調(diào)用socket()函數(shù)向應(yīng)用程序提供創(chuàng)建套接字的手段。該套接字實(shí)際上是在計(jì)算機(jī)中提供了一個(gè)通信埠,可以通過這個(gè)埠與任何一個(gè)具有套接字接口的計(jì)算機(jī)通信。應(yīng)用程序在網(wǎng)絡(luò)上傳輸、接收的信息都通過這個(gè)套接字接口來實(shí)現(xiàn)的。在應(yīng)用開發(fā)中如同使用文件句柄一樣,可以對套接字句柄進(jìn)行讀寫操作:
| sock=socket(AF_INET,SOCK_STREAM,0); |
函數(shù)的第一個(gè)參數(shù)用于指定地址族,在Windows下僅支持AF_INET(TCP/IP地址);第二個(gè)參數(shù)用于描述套接字的類型,對于流式套接字提供有SOCK_STREAM;最后一個(gè)參數(shù)指定套接字使用的協(xié)議,一般為0。該函數(shù)的返回值保存了新套接字的句柄,在程序退出前可以用 closesocket(sock);函數(shù)來將其釋放。服務(wù)器方一旦獲取了一個(gè)新的套接字后應(yīng)通過bind()將該套接字與本機(jī)上的一個(gè)端口相關(guān)聯(lián):
sockin.sin_family=AF_INET; sockin.sin_addr.s_addr=0; sockin.sin_port=htons(USERPORT); bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin))); |
該函數(shù)的第二個(gè)參數(shù)是一個(gè)指向包含有本機(jī)IP地址和端口信息的sockaddr_in結(jié)構(gòu)類型的指針,其成員描述了本地端口號和本地主機(jī)地址,經(jīng)過bind()將服務(wù)器進(jìn)程在網(wǎng)絡(luò)上標(biāo)識出來。需要注意的是由于1024以內(nèi)的埠號都是保留的埠號因此如無特別需要一般不能將sockin.sin_port的埠號設(shè)置為1024以內(nèi)的值。然后調(diào)用listen()函數(shù)開始偵聽,再通過accept()調(diào)用等待接收連接以完成連接的建立:
//連接請求隊(duì)列長度為1,即只允許有一個(gè)請求,若有多個(gè)請求, //則出現(xiàn)錯(cuò)誤,給出錯(cuò)誤代碼WSAECONNREFUSED。 listen(sock,1); //開啟線程避免主程序的阻塞 AfxBeginThread(Server,NULL); …… UINT Server(LPVOID lpVoid) { …… int nLen=sizeof(SOCKADDR); pView->newskt=accept(pView->sock,(LPSOCKADDR)& pView->sockin,(LPINT)& nLen); …… WSAAsyncSelect(pView->newskt,pView->m_hWnd,WM_SOCKET_MSG,FD_READ|FD_CLOSE); return 1; }
|
這里之所以把a(bǔ)ccept()放到一個(gè)線程中去是因?yàn)樵趫?zhí)行到該函數(shù)時(shí)如沒有客戶連接服務(wù)器的請求到來,服務(wù)器就會停在accept語句上等待連接請求的到來,這勢必會引起程序的阻塞,雖然也可以通過設(shè)置套接字為非阻塞方式使在沒有客戶等待時(shí)可以使accept()函數(shù)調(diào)用立即返回,但這種輪詢套接字的方式會使CPU處于忙等待方式,從而降低程序的運(yùn)行效率大大浪費(fèi)系統(tǒng)資源??紤]到這種情況,將套接字設(shè)置為阻塞工作方式,并為其單獨(dú)開辟一個(gè)子線程,將其阻塞控制在子線程范圍內(nèi)而不會造成整個(gè)應(yīng)用程序的阻塞。對于網(wǎng)絡(luò)事件的響應(yīng)顯然要采取異步選擇機(jī)制,只有采取這種方式才可以在由網(wǎng)絡(luò)對方所引起的不可預(yù)知的網(wǎng)絡(luò)事件發(fā)生時(shí)能馬上在進(jìn)程中做出及時(shí)的響應(yīng)處理,而在沒有網(wǎng)絡(luò)事件到達(dá)時(shí)則可以處理其他事件,這種效率是很高的,而且完全符合Windows所標(biāo)榜的消息觸發(fā)原則。前面那段代碼中的WSAAsyncSelect()函數(shù)便是實(shí)現(xiàn)網(wǎng)絡(luò)事件異步選擇的核心函數(shù)。
通過第四個(gè)參數(shù)注冊應(yīng)用程序感興取的網(wǎng)絡(luò)事件,在這里通過FD_READ|FD_CLOSE指定了網(wǎng)絡(luò)讀和網(wǎng)絡(luò)斷開兩種事件,當(dāng)這種事件發(fā)生時(shí)變會發(fā)出由第三個(gè)參數(shù)指定的自定義消息WM_SOCKET_MSG,接收該消息的窗口通過第二個(gè)參數(shù)指定其句柄。在消息處理函數(shù)中可以通過對消息參數(shù)低字節(jié)進(jìn)行判斷而區(qū)別出發(fā)生的是何種網(wǎng)絡(luò)事件:
void CNetServerView::OnSocket(WPARAM wParam,LPARAM lParam) { int iReadLen=0; int message=lParam & 0x0000FFFF; switch(message) { case FD_READ://讀事件發(fā)生。此時(shí)有字符到達(dá),需要進(jìn)行接收處理 char cDataBuffer[MTU*10]; //通過套接字接收信息 iReadLen = recv(newskt,cDataBuffer,MTU*10,0); //將信息保存到文件 if(!file.Open("ServerFile.txt",CFile::modeReadWrite)) file.Open("E:ServerFile.txt",CFile::modeCreate|CFile::modeReadWrite); file.SeekToEnd(); file.Write(cDataBuffer,iReadLen); file.Close(); break; case FD_CLOSE://網(wǎng)絡(luò)斷開事件發(fā)生。此時(shí)客戶機(jī)關(guān)閉或退出。 ……//進(jìn)行相應(yīng)的處理 break; default: break; } } |
在這里需要實(shí)現(xiàn)對自定義消息WM_SOCKET_MSG的響應(yīng),需要在頭文件和實(shí)現(xiàn)文件中分別添加其消息映射關(guān)系:
頭文件:
//{{AFX_MSG(CNetServerView) //}}AFX_MSG void OnSocket(WPARAM wParam,LPARAM lParam); DECLARE_MESSAGE_MAP() |
實(shí)現(xiàn)文件:
BEGIN_MESSAGE_MAP(CNetServerView, CView) //{{AFX_MSG_MAP(CNetServerView) //}}AFX_MSG_MAP ON_MESSAGE(WM_SOCKET_MSG,OnSocket) END_MESSAGE_MAP()
|
在進(jìn)行異步選擇使用WSAAsyncSelect()函數(shù)時(shí),有以下幾點(diǎn)需要引起特別的注意:
1. 連續(xù)使用兩次WSAAsyncSelect()函數(shù)時(shí),只有第二次設(shè)置的事件有效,如:
WSAAsyncSelect(s,hwnd,wMsg1,FD_READ); WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE); |
這樣只有當(dāng)FD_CLOSE事件發(fā)生時(shí)才會發(fā)送wMsg2消息。
2.可以在設(shè)置過異步選擇后通過再次調(diào)用WSAAsyncSelect(s,hwnd,0,0);的形式取消在套接字上所設(shè)置的異步事件。
3.Windows Sockets DLL在一個(gè)網(wǎng)絡(luò)事件發(fā)生后,通常只會給相應(yīng)的應(yīng)用程序發(fā)送一個(gè)消息,而不能發(fā)送多個(gè)消息。但通過使用一些函數(shù)隱式地允許重發(fā)此事件的消息,這樣就可能再次接收到相應(yīng)的消息。
4.在調(diào)用過closesocket()函數(shù)關(guān)閉套接字之后不會再發(fā)生FD_CLOSE事件。
以上基本完成了服務(wù)器方的程序設(shè)計(jì),下面對于客戶端的實(shí)現(xiàn)則要簡單多了,在用socket()創(chuàng)建完套接字之后只需通過調(diào)用connect()完成同服務(wù)器的連接即可,剩下的工作同服務(wù)器完全一樣:用send()/recv()發(fā)送/接收收據(jù),用closesocket()關(guān)閉套接字:
sockin.sin_family=AF_INET; //地址族 sockin.sin_addr.S_un.S_addr=IPaddr; //指定服務(wù)器的IP地址 sockin.sin_port=m_Port; //指定連接的端口號 int nConnect=connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin));
|
本文采取的是可靠的面向連接的流式套接字。在數(shù)據(jù)發(fā)送上有write()、writev()和send()等三個(gè)函數(shù)可供選擇,其中前兩種分別用于緩沖發(fā)送和集中發(fā)送,而send()則為可控緩沖發(fā)送,并且還可以指定傳輸控制標(biāo)志為MSG_OOB進(jìn)行帶外數(shù)據(jù)的發(fā)送或是為MSG_DONTROUTE尋徑控制選項(xiàng)。在信宿地址的網(wǎng)絡(luò)號部分指定數(shù)據(jù)發(fā)送需要經(jīng)過的網(wǎng)絡(luò)接口,使其可以不經(jīng)過本地尋徑機(jī)制直接發(fā)送出去。這也是其同write()函數(shù)的真正區(qū)別所在。由于接收數(shù)據(jù)系統(tǒng)調(diào)用和發(fā)送數(shù)據(jù)系統(tǒng)調(diào)用是一一對應(yīng)的,因此對于數(shù)據(jù)的接收,在此不再贅述,相應(yīng)的三個(gè)接收函數(shù)分別為:read()、readv()和recv()。由于后者功能上的全面,本文在實(shí)現(xiàn)上選擇了send()-recv()函數(shù)對,在具體編程中應(yīng)當(dāng)視具體情況的不同靈活選擇適當(dāng)?shù)陌l(fā)送-接收函數(shù)對。
小結(jié):TCP/IP協(xié)議是目前各網(wǎng)絡(luò)操作系統(tǒng)主要的通訊協(xié)議,也是 Internet的通訊協(xié)議,本文通過Windows Sockets API實(shí)現(xiàn)了對基于TCP/IP協(xié)議的面向連接的流式套接字網(wǎng)絡(luò)通訊程序的設(shè)計(jì),并通過異步通訊和多線程等手段提高了程序的運(yùn)行效率,避免了阻塞的發(fā)生。