程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> Windows Sockets API實現網絡異步通訊

Windows Sockets API實現網絡異步通訊

編輯:關於C語言

[文章信息] 作者:信息產業部電子第二十二研究所青島分所郎銳時間:2002-09-10出處:yesky責任編輯:方舟 [文章導讀] 本文對如何使用面向連接的流式套接字實現對網卡的編程以及如何實現異步網絡通訊等問題進行了討論與闡述

摘要: 本文對如何使用面向連接的流式套接字實現對網卡的編程以及如何實現異步網絡通訊等問題進行了討論與闡述。

一、 引言

在80年代初,美國加利福尼亞大學伯克利分校的研究人員為TCP/IP網絡通信開發了一個專門用於網絡通訊開發的API。這個API就是Socket接口(套接字)--當今在TCP/IP網絡最為通用的一種API,也是在互聯網上進行應用開發最為通用的一種API。在微軟聯合其它幾家公司共同制定了一套Windows下的網絡編程接口Windows Sockets規范後,由於在其規范中引入了一些異步函數,增加了對網絡事件異步選擇機制,因此更加符合Windows的消息驅動特性,使網絡開發人員可以更加方便的進行高性能網絡通訊程序的設計。本文接下來就針對Windows Sockets API進行面向連接的流式套接字編程以及對異步網絡通訊的編程實現等問題展開討論。

二、 面向連接的流式套接字編程模型的設計

本文在方案選擇上采用了在網絡編程中最常用的一種模型--客戶機/服務器模型。這種客戶/服務器模型是一種非對稱式編程模式。該模式的基本思想是把集中在一起的應用劃分成為功能不同的兩個部分,分別在不同的計算機上運行,通過它們之間的分工合作來實現一個完整的功能。對於這種模式而言其中一部分需要作為服務器,用來響應並為客戶提供固定的服務;另一部分則作為客戶機程序用來向服務器提出請求或要求某種服務。

本文選取了基於TCP/IP的客戶機/服務器模型和面向連接的流式套接字。其通信原理為:服務器端和客戶端都必須建立通信套接字,而且服務器端應先進入監聽狀態,然後客戶端套接字發出連接請求,服務器端收到請求後,建立另一個套接字進行通信,原來負責監聽的套接字仍進行監聽,如果有其它客戶發來連接請求,則再建立一個套接字。默認狀態下最多可同時接收5個客戶的連接請求,並與之建立通信關系。因此本程序的設計流程應當由服務器首先啟動,然後在某一時刻啟動客戶機並使其與服務器建立連接。服務器與客戶機開始都必須調用Windows Sockets API函數socket()建立一個套接字sockets,然後服務器方調用bind()將套接字與一個本地網絡地址捆扎在一起,再調用listen()使套接字處於一種被動的准備接收狀態,同時規定它的請求隊列長度。在此之後服務器就可以通過調用accept()來接收客戶機的連接。

相對於服務器,客戶端的工作就顯得比較簡單了,當客戶端打開套接字之後,便可通過調用connect()和服務器建立連接。連接建立之後,客戶和服務器之間就可以通過連接發送和接收資料。最後資料傳送結束,雙方調用closesocket()關閉套接字來結束這次通訊。整個通訊過程的具體流程框圖可大致用下面的流程圖來表示:

面向連接的流式套接字編程流程示意圖
三、 軟件設計要點以及異步通訊的實現

  根據前面設計的程序流程,可將程序劃分為兩部分:服務器端和客戶端。而且整個實現過程可以大致用以下幾個非常關鍵的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.可以在設置過異步選擇後通過

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved