Winsock是Windows下的網(wǎng)絡編程接口,它是由Unix下的BSD Socket發(fā)展而來,是一個與網(wǎng)絡協(xié)議無關的編程接口。
Windows下網(wǎng)絡編程的規(guī)范-Windows Sockets是Windows下得到廣泛應用的、開放的、支持多種協(xié)議的網(wǎng)絡編程接口。從1991年的1.0版到1995年的2.0.8版,經(jīng)過不斷完善并在Intel、Microsoft、Sun、SGI、Informix、Novell等公司的全力支持下,已成為Windows網(wǎng)絡編程的事實上的標準。Windows Sockets規(guī)范以U.C. Berkeley大學BSD UNIX中流行的Socket接口為范例定義了一套Micosoft Windows下網(wǎng)絡編程接口。它不僅包含了人們所熟悉的Berkeley Socket風格的庫函數(shù);也包含了一組針對Windows的擴展庫函數(shù),以使程序員能充分地利用Windows消息驅(qū)動機制進行編程。Windows Sockets規(guī)范本意在于提供給應用程序開發(fā)者一套簡單的API,并讓各家網(wǎng)絡軟件供應商共同遵守。此外,在一個特定版本W(wǎng)indows的基礎上,Windows Sockets也定義了一個二進制接口(ABI),以此來保證應用Windows Sockets API的應用程序能夠在任何網(wǎng)絡軟件供應商的符合Windows Sockets協(xié)議的實現(xiàn)上工作。因此這份規(guī)范定義了應用程序開發(fā)者能夠使用,并且網(wǎng)絡軟件供應商能夠?qū)崿F(xiàn)的一套庫函數(shù)調(diào)用和相關語義。遵守這套Windows Sockets規(guī)范的網(wǎng)絡軟件,我們稱之為Windows Sockets兼容的,而Windows Sockets兼容實現(xiàn)的提供者,我們稱之為Windows Sockets提供者。一個網(wǎng)絡軟件供應商必須百分之百地實現(xiàn)Windows Sockets規(guī)范才能做到現(xiàn)Windows Sockets兼容。任何能夠與Windows Sockets兼容實現(xiàn)協(xié)同工作的應用程序就被認為是具有Windows Sockets接口。我們稱這種應用程序為Windows Sockets應用程序。Windows Sockets規(guī)范定義并記錄了如何使用API與Internet協(xié)議族(IPS,通常我們指的是TCP/IP)連接,尤其要指出的是所有的Windows Sockets實現(xiàn)都支持流套接口和數(shù)據(jù)報套接口.應用程序調(diào)用Windows Sockets的API實現(xiàn)相互之間的通訊。Windows Sockets又利用下層的網(wǎng)絡通訊協(xié)議功能和操作系統(tǒng)調(diào)用實現(xiàn)實際的通訊工作。它們之間的關系如圖
通信的基礎是套接口(Socket),一個套接口是通訊的一端。在這一端上你可以找到與其對應的一個名字。一個正在被使用的套接口都有它的類型和與其相關的進程。套接口存在于通訊域中。通訊域是為了處理一般的線程通過套接口通訊而引進的一種抽象概念。套接口通常和同一個域中的套接口交換數(shù)據(jù)(數(shù)據(jù)交換也可能穿越域的界限,但這時一定要執(zhí)行某種解釋程序)。Windows Sockets規(guī)范支持單一的通訊域,即Internet域。各種進程使用這個域互相之間用Internet協(xié)議族來進行通訊(Windows Sockets 1.1以上的版本支持其他的域,例如Windows Sockets 2)。套接口可以根據(jù)通訊性質(zhì)分類;這種性質(zhì)對于用戶是可見的。應用程序一般僅在同一類的套接口間通訊。不過只要底層的通訊協(xié)議允許,不同類型的套接口間也照樣可以通訊。用戶目前可以使用兩種套接口,即流套接口和數(shù)據(jù)報套接口。流套接口提供了雙向的,有序的,無重復并且無記錄邊界的數(shù)據(jù)流服務。數(shù)據(jù)報套接口支持雙向的數(shù)據(jù)流,但并不保證是可靠,有序,無重復的。也就是說,一個從數(shù)據(jù)報套接口接收信息的進程有可能發(fā)現(xiàn)信息重復了,或者和發(fā)出時的順序不同。數(shù)據(jù)報套接口的一個重要特點是它保留了記錄邊界。對于這一特點,數(shù)據(jù)報套接口采用了與現(xiàn)在許多包交換網(wǎng)絡(例如以太網(wǎng))非常類似的模型。
一個在建立分布式應用時最常用的范例便是客戶機/服務器模型。在這種方案中客戶應用程序向服務器程序請求服務。這種方式隱含了在建立客戶機/服務器間通訊時的非對稱性。客戶機/服務器模型工作時要求有一套為客戶機和服務器所共識的慣例來保證服務能夠被提供(或被接受)。這一套慣例包含了一套協(xié)議。它必須在通訊的兩頭都被實現(xiàn)。根據(jù)不同的實際情況,協(xié)議可能是對稱的或是非對稱的。在對稱的協(xié)議中,每一方都有可能扮演主從角色;在非對稱協(xié)議中,一方被不可改變地認為是主機,而另一方則是從機。一個對稱協(xié)議的例子是Internet中用于終端仿真的TELNET。而非對稱協(xié)議的例子是Internet中的FTP。無論具體的協(xié)議是對稱的或是非對稱的,當服務被提供時必然存在"客戶進程"和"服務進程"。一個服務程序通常在一個眾所周知的地址監(jiān)聽對服務的請求,也就是說,服務進程一直處于休眠狀態(tài),直到一個客戶對這個服務的地址提出了連接請求。在這個時刻,服務程序被"驚醒"并且為客戶提供服務-對客戶的請求作出適當?shù)姆磻_@一請求/相應的過程可以簡單的用圖表示。雖然基于連接的服務是設計客戶機/服務器應用程序時的標準,但有些服務也是可以通過數(shù)據(jù)報套接口提供的。
數(shù)據(jù)報套接口可以用來向許多系統(tǒng)支持的網(wǎng)絡發(fā)送廣播數(shù)據(jù)包。要實現(xiàn)這種功能,網(wǎng)絡本身必須支持廣播功能,因為系統(tǒng)軟件并不提供對廣播功能的任何模擬。廣播信息將會給網(wǎng)絡造成極重的負擔,因為它們要求網(wǎng)絡上的每臺主機都為它們服務,所以發(fā)送廣播數(shù)據(jù)包的能力被限制于那些用顯式標記了允許廣播的套接口中。廣播通常是為了如下兩個原因而使用的:1. 一個應用程序希望在本地網(wǎng)絡中找到一個資源,而應用程序?qū)υ撡Y源的地址又沒有任何先驗的知識。2. 一些重要的功能,例如路由要求把它們的信息發(fā)送給所有可以找到的鄰機。被廣播信息的目的地址取決于這一信息將在何種網(wǎng)絡上廣播。Internet域中支持一個速記地址用于廣播-INADDR_BROADCAST。由于使用廣播以前必須捆綁一個數(shù)據(jù)報套接口,所以所有收到的廣播消息都帶有發(fā)送者的地址和端口。
Intel處理器的字節(jié)順序是和DEC VAX處理器的字節(jié)順序一致的。因此它與68000型處理器以及Internet的順序是不同的,所以用戶在使用時要特別小心以保證正確的順序。任何從Windows Sockets函數(shù)對IP地址和端口號的引用和傳送給Windows Sockets函數(shù)的IP地址和端口號均是按照網(wǎng)絡順序組織的,這也包括了sockaddr_in結(jié)構這一數(shù)據(jù)類型中的IP地址域和端口域(但不包括sin_family域)�?紤]到一個應用程序通常用與"時間"服務對應的端口來和服務器連接,而服務器提供某種機制來通知用戶使用另一端口。因此getservbyname()函數(shù)返回的端口號已經(jīng)是網(wǎng)絡順序了,可以直接用來組成一個地址,而不需要進行轉(zhuǎn)換。然而如果用戶輸入一個數(shù),而且指定使用這一端口號,應用程序則必須在使用它建立地址以前,把它從主機順序轉(zhuǎn)換成網(wǎng)絡順序(使用htons()函數(shù))。相應地,如果應用程序希望顯示包含于某一地址中的端口號(例如從getpeername()函數(shù)中返回的),這一端口號就必須在被顯示前從網(wǎng)絡順序轉(zhuǎn)換到主機順序(使用ntohs()函數(shù))。由于Intel處理器和Internet的字節(jié)順序是不同的,上述的轉(zhuǎn)換是無法避免的,應用程序的編寫者應該使用作為Windows Sockets API一部分的標準的轉(zhuǎn)換函數(shù),而不要使用自己的轉(zhuǎn)換函數(shù)代碼。因為將來的Windows Sockets實現(xiàn)有可能在主機字節(jié)順序與網(wǎng)絡字節(jié)順序相同的機器上運行。因此只有使用標準的轉(zhuǎn)換函數(shù)的應用程序是可移植的。
在MFC中MS為套接口提供了相應的類CAsyncSocket和CSocket,CAsyncSocket提供基于異步通信的套接口封裝功能,CSocket則是由CAsyncSocket派生,提供更加高層次的功能,例如可以將套接口上發(fā)送和接收的數(shù)據(jù)和一個文件對象(CSocketFile)關聯(lián)起來,通過讀寫文件來達到發(fā)送和接收數(shù)據(jù)的目的,此外CSocket提供的通信為同步通信,數(shù)據(jù)未接收到或是未發(fā)送完之前調(diào)用不會返回。此外通過MFC類開發(fā)者可以不考慮網(wǎng)絡字節(jié)順序和忽略掉更多的通信細節(jié)。
在一次網(wǎng)絡通信/連接中有以下幾個參數(shù)需要被設置:本地IP地址 - 本地端口號 - 對方端口號 - 對方IP地址。左邊兩部分稱為一個半關聯(lián),當與右邊兩部分建立連接后就稱為一個全關聯(lián)。在這個全關聯(lián)的套接口上可以雙向的交換數(shù)據(jù)。如果是使用無連接的通信則只需要建立一個半關聯(lián),在發(fā)送和接收時指明另一半的參數(shù)就可以了,所以可以說無連接的通信是將數(shù)據(jù)發(fā)送到另一臺主機的指定端口。此外不論是有連接還是無連接的通信都不需要雙方的端口號相同。
在創(chuàng)建CAsyncSocket對象時通過調(diào)用
BOOL CAsyncSocket::Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long lEvent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE, LPCTSTR lpszSocketAddress = NULL )通過指明lEvent所包含的標記來確定需要異步處理的事件,對于指明的相關事件的相關函數(shù)調(diào)用都不需要等待完成后才返回,函數(shù)會馬上返回然后在完成任務后發(fā)送事件通知,并利用重載以下成員函數(shù)來處理各種網(wǎng)絡事件: 標記 事件 需要重載的函數(shù)
FD_READ 有數(shù)據(jù)到達時發(fā)生 void OnReceive( int nErrorCode );
FD_WRITE 有數(shù)據(jù)發(fā)送時產(chǎn)生 void OnSend( int nErrorCode );
FD_OOB 收到外帶數(shù)據(jù)時發(fā)生 void OnOutOfBandData( int nErrorCode );
FD_ACCEPT 作為服務端等待連接成功時發(fā)生 void OnAccept( int nErrorCode );
FD_CONNECT 作為客戶端連接成功時發(fā)生 void OnConnect( int nErrorCode );
FD_CLOSE 套接口關閉時發(fā)生 void OnClose( int nErrorCode );
我們看到重載的函數(shù)中都有一個參數(shù)nErrorCode,為零則表示正常完成,非零則表示錯誤。通過int CAsyncSocket::GetLastError()可以得到錯誤值。
下面我們看看套接口類所提供的一些功能,通過這些功能我們可以方便的建立網(wǎng)絡連接和發(fā)送數(shù)據(jù)。
BOOL CAsyncSocket::Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long lEvent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE, LPCTSTR lpszSocketAddress = NULL );用于創(chuàng)建一個本地套接口,其中nSocketPort為使用的端口號,為零則表示由系統(tǒng)自動選擇,通常在客戶端都使用這個選擇。nSocketType為使用的協(xié)議族,SOCK_STREAM表明使用有連接的服務,SOCK_DGRAM表明使用無連接的數(shù)據(jù)報服務。lpszSocketAddress為本地的IP地址,可以使用點分法表示如10.1.1.3。
BOOL CAsyncSocket::Bind( UINT nSocketPort, LPCTSTR lpszSocketAddress = NULL )作為等待連接方時產(chǎn)生一個網(wǎng)絡半關聯(lián),或者是使用UDP協(xié)議時產(chǎn)生一個網(wǎng)絡半關聯(lián)。
BOOL CAsyncSocket::Listen( int nConnectionBacklog = 5 )作為等待連接方時指明同時可以接受的連接數(shù),請注意不是總共可以接受的連接數(shù)。
BOOL CAsyncSocket::Accept( CAsyncSocket& rConnectedSocket, SOCKADDR* lpSockAddr = NULL, int* lpSockAddrLen = NULL )作為等待連接方將等待連接建立,當連接建立后一個新的套接口將被創(chuàng)建,該套接口將會被用于通信。
BOOL CAsyncSocket::Connect( LPCTSTR lpszHostAddress, UINT nHostPort );作為連接方發(fā)起與等待連接方的連接,需要指明對方的IP地址和端口號。
void CAsyncSocket::Close( );關閉套接口。
int CAsyncSocket::Send( const void* lpBuf, int nBufLen, int nFlags = 0 )
int CAsyncSocket::Receive( void* lpBuf, int nBufLen, int nFlags = 0 );在建立連接后發(fā)送和接收數(shù)據(jù),nFlags為標記位,雙方需要指明相同的標記。
int CAsyncSocket::SendTo( const void* lpBuf, int nBufLen, UINT nHostPort, LPCTSTR lpszHostAddress = NULL, int nFlags = 0 )
int CAsyncSocket::ReceiveFrom( void* lpBuf, int nBufLen, CString& rSocketAddress, UINT& rSocketPort, int nFlags = 0 );對于無連接通信發(fā)送和接收數(shù)據(jù),需要指明對方的IP地址和端口號,nFlags為標記位,雙方需要指明相同的標記。
我們可以看到大多數(shù)的函數(shù)都返回一個布爾值表明是否成功。如果發(fā)生錯誤可以通過int CAsyncSocket::GetLastError()得到錯誤值。
由于CSocket由CAsyncSocket派生所以擁有CAsyncSocket的所有功能,此外你可以通過BOOL CSocket::Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, LPCTSTR lpszSocketAddress = NULL )來創(chuàng)建套接口,這樣創(chuàng)建的套接口沒有辦法異步處理事件,所有的調(diào)用都必需完成后才會返回。
在上面的介紹中我們看到MFC提供的套接口類屏蔽了大多數(shù)的細節(jié),我們只需要做很少的工作就可以開發(fā)出利用網(wǎng)絡進行通信的軟件。