根據前面設計的程序流程,可將程序劃分爲兩部分:服務器端和客戶端。而且整個實現過程可以大致用以下幾個非常關鍵的Windows Sockets API函數將其慣穿下來:
服務器方:
socket()->bind()->listen->accept()->recv()/send()->closesocket() |
客戶機方:
socket()->connect()->send()/recv()->closesocket() |
有鑑於以上幾個函數在整個網絡編程中的重要性,有必要結合程序實例對其做較深入的剖析。服務器端應用程序在使用套接字之前,首先必須擁有一個Socket,系統調用socket()函數嚮應用程序提供創建套接字的手段。該套接字實際上是在計算機中提供了一個通信埠,可以通過這個埠與任何一個具有套接字接口的計算機通信。應用程序在網絡上傳輸、接收的信息都通過這個套接字接口來實現的。在應用開發中如同使用文件句柄一樣,可以對套接字句柄進行讀寫操作:
sock=socket(AF_INET,SOCK_STREAM,0); |
函數的第一個參數用於指定地址族,在Windows下僅支持AF_INET(TCP/IP地址);第二個參數用於描述套接字的類型,對於流式套接字提供有SOCK_STREAM;最後一個參數指定套接字使用的協議,一般爲0。該函數的返回值保存了新套接字的句柄,在程序退出前可以用 closesocket(sock);函數來將其釋放。服務器方一旦獲取了一個新的套接字後應通過bind()將該套接字與本機上的一個端口相關聯:
sockin.sin_family=AF_INET; sockin.sin_addr.s_addr=0; sockin.sin_port=htons(USERPORT); bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin))); |
該函數的第二個參數是一個指向包含有本機IP地址和端口信息的sockaddr_in結構類型的指針,其成員描述了本地端口號和本地主機地址,經過bind()將服務器進程在網絡上標識出來。需要注意的是由於1024以內的埠號都是保留的埠號因此如無特別需要一般不能將sockin.sin_port的埠號設置爲1024以內的值。然後調用listen()函數開始偵聽,再通過accept()調用等待接收連接以完成連接的建立:
//連接請求隊列長度爲1,即只允許有一個請求,若有多個請求, //則出現錯誤,給出錯誤代碼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; } |
這裏之所以把accept()放到一個線程中去是因爲在執行到該函數時如沒有客戶連接服務器的請求到來,服務器就會停在accept語句上等待連接請求的到來,這勢必會引起程序的阻塞,雖然也可以通過設置套接字爲非阻塞方式使在沒有客戶等待時可以使accept()函數調用立即返回,但這種輪詢套接字的方式會使CPU處於忙等待方式,從而降低程序的運行效率大大浪費系統資源。考慮到這種情況,將套接字設置爲阻塞工作方式,併爲其單獨開闢一個子線程,將其阻塞控制在子線程範圍內而不會造成整個應用程序的阻塞。對於網絡事件的響應顯然要採取異步選擇機制,只有採取這種方式纔可以在由網絡對方所引起的不可預知的網絡事件發生時能馬上在進程中做出及時的響應處理,而在沒有網絡事件到達時則可以處理其他事件,這種效率是很高的,而且完全符合Windows所標榜的消息觸發原則。前面那段代碼中的WSAAsyncSelect()函數便是實現網絡事件異步選擇的核心函數。
通過第四個參數註冊應用程序感興取的網絡事件,在這裏通過FD_READ|FD_CLOSE指定了網絡讀和網絡斷開兩種事件,當這種事件發生時變會發出由第三個參數指定的自定義消息WM_SOCKET_MSG,接收該消息的窗口通過第二個參數指定其句柄。在消息處理函數中可以通過對消息參數低字節進行判斷而區別出發生的是何種網絡事件:
void CNetServerView::OnSocket(WPARAM wParam,LPARAM lParam) { int iReadLen=0; int message=lParam & 0x0000FFFF; switch(message) { case FD_READ://讀事件發生。此時有字符到達,需要進行接收處理 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://網絡斷開事件發生。此時客戶機關閉或退出。 ……//進行相應的處理 break; default: break; } } |
在這裏需要實現對自定義消息WM_SOCKET_MSG的響應,需要在頭文件和實現文件中分別添加其消息映射關係:
頭文件:
//{{AFX_MSG(CNetServerView) //}}AFX_MSG void OnSocket(WPARAM wParam,LPARAM lParam); DECLARE_MESSAGE_MAP() |
實現文件:
BEGIN_MESSAGE_MAP(CNetServerView, CView) //{{AFX_MSG_MAP(CNetServerView) //}}AFX_MSG_MAP ON_MESSAGE(WM_SOCKET_MSG,OnSocket) END_MESSAGE_MAP() |
在進行異步選擇使用WSAAsyncSelect()函數時,有以下幾點需要引起特別的注意:
1. 連續使用兩次WSAAsyncSelect()函數時,只有第二次設置的事件有效,如:
WSAAsyncSelect(s,hwnd,wMsg1,FD_READ); WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE); |
這樣只有當FD_CLOSE事件發生時纔會發送wMsg2消息。
2.可以在設置過異步選擇後通過再次調用WSAAsyncSelect(s,hwnd,0,0);的形式取消在套接字上所設置的異步事件。
3.Windows Sockets DLL在一個網絡事件發生後,通常只會給相應的應用程序發送一個消息,而不能發送多個消息。但通過使用一些函數隱式地允許重發此事件的消息,這樣就可能再次接收到相應的消息。
4.在調用過closesocket()函數關閉套接字之後不會再發生FD_CLOSE事件。
以上基本完成了服務器方的程序設計,下面對於客戶端的實現則要簡單多了,在用socket()創建完套接字之後只需通過調用connect()完成同服務器的連接即可,剩下的工作同服務器完全一樣:用send()/recv()發送/接收收據,用closesocket()關閉套接字:
sockin.sin_family=AF_INET; //地址族 sockin.sin_addr.S_un.S_addr=IPaddr; //指定服務器的IP地址 sockin.sin_port=m_Port; //指定連接的端口號 int nConnect=connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); |
本文采取的是可靠的面向連接的流式套接字。在數據發送上有write()、writev()和send()等三個函數可供選擇,其中前兩種分別用於緩衝發送和集中發送,而send()則爲可控緩衝發送,並且還可以指定傳輸控制標誌爲MSG_OOB進行帶外數據的發送或是爲MSG_DONTROUTE尋徑控制選項。在信宿地址的網絡號部分指定數據發送需要經過的網絡接口,使其可以不經過本地尋徑機制直接發送出去。這也是其同write()函數的真正區別所在。由於接收數據系統調用和發送數據系統調用是一一對應的,因此對於數據的接收,在此不再贅述,相應的三個接收函數分別爲:read()、readv()和recv()。由於後者功能上的全面,本文在實現上選擇了send()-recv()函數對,在具體編程中應當視具體情況的不同靈活選擇適當的發送-接收函數對。
小結:TCP/IP協議是目前各網絡操作系統主要的通訊協議,也是 Internet的通訊協議,本文通過Windows Sockets API實現了對基於TCP/IP協議的面向連接的流式套接字網絡通訊程序的設計,並通過異步通訊和多線程等手段提高了程序的運行效率,避免了阻塞的發生。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=465009