WinSock網(wǎng)絡(luò)編程實用寶典
WinSock網(wǎng)絡(luò)編程實用寶典
1、TCP/IP體系結(jié)構(gòu)
TCP/IP協(xié)議實際上就是在物理網(wǎng)上的一組完整的網(wǎng)絡(luò)協(xié)議。其中TCP是提供傳輸層服務(wù),而IP則是提供網(wǎng)絡(luò)層服務(wù)。TCP/IP包括以下協(xié)議:(結(jié)構(gòu)如圖1.1)
(圖1.1)
IP: 網(wǎng)間協(xié)議(Internet Protocol) 負責(zé)主機間數(shù)據(jù)的路由和網(wǎng)絡(luò)上數(shù)據(jù)的存儲。同時為ICMP,TCP, UDP提供分組發(fā)送服務(wù)。用戶進程通常不需要涉及這一層。
ARP: 地址解析協(xié)議(Address Resolution Protocol)
此協(xié)議將網(wǎng)絡(luò)地址映射到硬件地址。
RARP: 反向地址解析協(xié)議(Reverse Address Resolution Protocol)
此協(xié)議將硬件地址映射到網(wǎng)絡(luò)地址
ICMP: 網(wǎng)間報文控制協(xié)議(Internet Control Message Protocol)
此協(xié)議處理信關(guān)和主機的差錯和傳送控制。
TCP: 傳送控制協(xié)議(Transmission Control Protocol)
這是一種提供給用戶進程的可靠的全雙工字節(jié)流面向連接的協(xié)議。它要為用戶進程提供虛電路服務(wù),并為數(shù)據(jù)可靠傳輸建立檢查。(注:大多數(shù)網(wǎng)絡(luò)用戶程序使用TCP)
UDP: 用戶數(shù)據(jù)報協(xié)議(User Datagram Protocol)
這是提供給用戶進程的無連接協(xié)議,用于傳送數(shù)據(jù)而不執(zhí)行正確性檢查。
FTP: 文件傳輸協(xié)議(File Transfer Protocol)
允許用戶以文件操作的方式(文件的增、刪、改、查、傳送等)與另一主機相互通信。
SMTP: 簡單郵件傳送協(xié)議(Simple Mail Transfer Protocol)
SMTP協(xié)議為系統(tǒng)之間傳送電子郵件。
TELNET:終端協(xié)議(Telnet Terminal Procotol)
允許用戶以虛終端方式訪問遠程主機
HTTP: 超文本傳輸協(xié)議(Hypertext Transfer Procotol)
TFTP: 簡單文件傳輸協(xié)議(Trivial File Transfer Protocol)
2、TCP/IP特點
TCP/IP協(xié)議的核心部分是傳輸層協(xié)議(TCP、UDP),網(wǎng)絡(luò)層協(xié)議(IP)和物理接口層,這三層通常是在操作系統(tǒng)內(nèi)核中實現(xiàn)。因此用戶一般不涉及。編程時,編程界面有兩種形式:一、是由內(nèi)核心直接提供的系統(tǒng)調(diào)用;二、使用以庫函數(shù)方式提供的各種函數(shù)。前者為核內(nèi)實現(xiàn),后者為核外實現(xiàn)。用戶服務(wù)要通過核外的應(yīng)用程序才能實現(xiàn),所以要使用套接字(socket)來實現(xiàn)。
圖1.2是TCP/IP協(xié)議核心與應(yīng)用程序關(guān)系圖。
(圖1.2)
二、專用術(shù)語
1、套接字
套接字是網(wǎng)絡(luò)的基本構(gòu)件。它是可以被命名和尋址的通信端點,使用中的每一個套接字都有其類型和一個與之相連聽進程。套接字存在通信區(qū)域(通信區(qū)域又稱地址簇)中。套接字只與同一區(qū)域中的套接字交換數(shù)據(jù)(跨區(qū)域時,需要執(zhí)行某和轉(zhuǎn)換進程才能實現(xiàn))。WINDOWS 中的套接字只支持一個域——網(wǎng)際域。套接字具有類型。
WINDOWS SOCKET 1.1 版本支持兩種套接字:流套接字(SOCK_STREAM)和數(shù)據(jù)報套接字(SOCK_DGRAM)
2、WINDOWS SOCKETS 實現(xiàn)
一個WINDOWS SOCKETS 實現(xiàn)是指實現(xiàn)了WINDOWS SOCKETS規(guī)范所描述的全部功能的一套軟件。一般通過DLL文件來實現(xiàn)
3、阻塞處理例程
阻塞處理例程(blocking hook,阻塞鉤子)是WINDOWS SOCKETS實現(xiàn)為了支持阻塞套接字函數(shù)調(diào)用而提供的一種機制。
4、多址廣播(multicast,多點傳送或組播)
是一種一對多的傳輸方式,傳輸發(fā)起者通過一次傳輸就將信息傳送到一組接收者,與單點傳送
(unicast)和廣播(Broadcast)相對應(yīng)。
一、客戶機/服務(wù)器模式
在TCP/IP網(wǎng)絡(luò)中兩個進程間的相互作用的主機模式是客戶機/服務(wù)器模式(Client/Server model)。該模式的建立基于以下兩點:1、非對等作用;2、通信完全是異步的??蛻魴C/服務(wù)器模式在操作過程中采取的是主動請示方式:
首先服務(wù)器方要先啟動,并根據(jù)請示提供相應(yīng)服務(wù):(過程如下)
1、打開一通信通道并告知本地主機,它愿意在某一個公認地址上接收客戶請求。
2、等待客戶請求到達該端口。
3、接收到重復(fù)服務(wù)請求,處理該請求并發(fā)送應(yīng)答信號。
4、返回第二步,等待另一客戶請求
5、關(guān)閉服務(wù)器。
客戶方:
1、打開一通信通道,并連接到服務(wù)器所在主機的特定端口。
2、向服務(wù)器發(fā)送服務(wù)請求報文,等待并接收應(yīng)答;繼續(xù)提出請求……
3、請求結(jié)束后關(guān)閉通信通道并終止。
二、基本套接字
為了更好說明套接字編程原理,給出幾個基本的套接字,在以后的篇幅中會給出更詳細的使用說明。
1、創(chuàng)建套接字——socket()
功能:使用前創(chuàng)建一個新的套接字
格式:SOCKET PASCAL FAR socket(int af,int type,int procotol);
參數(shù):af: 通信發(fā)生的區(qū)域
type: 要建立的套接字類型
procotol: 使用的特定協(xié)議
2、指定本地地址——bind()
功能:將套接字地址與所創(chuàng)建的套接字號聯(lián)系起來。
格式:int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int namelen);
參數(shù):s: 是由socket()調(diào)用返回的并且未作連接的套接字描述符(套接字號)。
其它:沒有錯誤,bind()返回0,否則SOCKET_ERROR
地址結(jié)構(gòu)說明:
struct sockaddr_in
{
short sin_family;//AF_INET
u_short sin_port;//16位端口號,網(wǎng)絡(luò)字節(jié)順序
struct in_addr sin_addr;//32位IP地址,網(wǎng)絡(luò)字節(jié)順序
char sin_zero[8];//保留
}
3、建立套接字連接——connect()和accept()
功能:共同完成連接工作
格式:int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int namelen);
SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR * name,int FAR * addrlen);
參數(shù):同上
4、監(jiān)聽連接——listen()
功能:用于面向連接服務(wù)器,表明它愿意接收連接。
格式:int PASCAL FAR listen(SOCKET s, int backlog);
5、數(shù)據(jù)傳輸——send()與recv()
功能:數(shù)據(jù)的發(fā)送與接收
格式:int PASCAL FAR send(SOCKET s,const char FAR * buf,int len,int flags);
int PASCAL FAR recv(SOCKET s,const char FAR * buf,int len,int flags);
參數(shù):buf:指向存有傳輸數(shù)據(jù)的緩沖區(qū)的指針。
6、多路復(fù)用——select()
功能:用來檢測一個或多個套接字狀態(tài)。
格式:int PASCAL FAR select(int nfds,fd_set FAR * readfds,fd_set FAR * writefds,
fd_set FAR * exceptfds,const struct timeval FAR * timeout);
參數(shù):readfds:指向要做讀檢測的指針
writefds:指向要做寫檢測的指針
exceptfds:指向要檢測是否出錯的指針
timeout:最大等待時間
7、關(guān)閉套接字——closesocket()
功能:關(guān)閉套接字s
格式:BOOL PASCAL FAR closesocket(SOCKET s);
三、典型過程圖
2.1 面向連接的套接字的系統(tǒng)調(diào)用時序圖
2.2 無連接協(xié)議的套接字調(diào)用時序圖
2.3 面向連接的應(yīng)用程序流程圖
Windows Socket1.1 程序設(shè)計
一、簡介
Windows Sockets 是從 Berkeley Sockets 擴展而來的,其在繼承 Berkeley Sockets 的基礎(chǔ)上,又進行了新的擴充。這些擴充主要是提供了一些異步函數(shù),并增加了符合WINDOWS消息驅(qū)動特性的網(wǎng)絡(luò)事件異步選擇機制。
Windows Sockets由兩部分組成:開發(fā)組件和運行組件。
開發(fā)組件:Windows Sockets 實現(xiàn)文檔、應(yīng)用程序接口(API)引入庫和一些頭文件。
運行組件:Windows Sockets 應(yīng)用程序接口的動態(tài)鏈接庫(WINSOCK.DLL)。
二、主要擴充說明
1、異步選擇機制:
Windows Sockets 的異步選擇函數(shù)提供了消息機制的網(wǎng)絡(luò)事件選擇,當使用它登記網(wǎng)絡(luò)事件發(fā)生時,應(yīng)用程序相應(yīng)窗口函數(shù)將收到一個消息,消息中指示了發(fā)生的網(wǎng)絡(luò)事件,以及與事件相關(guān)的一些信息。
Windows Sockets 提供了一個異步選擇函數(shù) WSAAsyncSelect(),用它來注冊應(yīng)用程序感興趣的網(wǎng)絡(luò)事件,當這些事件發(fā)生時,應(yīng)用程序相應(yīng)的窗口函數(shù)將收到一個消息。
函數(shù)結(jié)構(gòu)如下:
int PASCAL FAR WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent); |
參數(shù)說明:
hWnd:窗口句柄
wMsg:需要發(fā)送的消息
lEvent:事件(以下為事件的內(nèi)容)
值: | 含義: |
FD_READ | 期望在套接字上收到數(shù)據(jù)(即讀準備好)時接到通知 |
FD_WRITE | 期望在套接字上可發(fā)送數(shù)據(jù)(即寫準備好)時接到通知 |
FD_OOB | 期望在套接字上有帶外數(shù)據(jù)到達時接到通知 |
FD_ACCEPT | 期望在套接字上有外來連接時接到通知 |
FD_CONNECT | 期望在套接字連接建立完成時接到通知 |
FD_CLOSE | 期望在套接字關(guān)閉時接到通知 |
例如:我們要在套接字讀準備好或?qū)憸蕚浜脮r接到通知,語句如下:
rc=WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE); |
如果我們需要注銷對套接字網(wǎng)絡(luò)事件的消息發(fā)送,只要將 lEvent 設(shè)置為0
2、異步請求函數(shù)
在 Berkeley Sockets 中請求服務(wù)是阻塞的,WINDOWS SICKETS 除了支持這一類函數(shù)外,還增加了相應(yīng)的異步請求函數(shù)(WSAAsyncGetXByY();)。
3、阻塞處理方法
Windows Sockets 為了實現(xiàn)當一個應(yīng)用程序的套接字調(diào)用處于阻塞時,能夠放棄CPU讓其它應(yīng)用程序運行,它在調(diào)用處于阻塞時便進入一個叫“HOOK”的例程,此例程負責(zé)接收和分配WINDOWS消息,使得其它應(yīng)用程序仍然能夠接收到自己的消息并取得控制權(quán)。
WINDOWS 是非搶先的多任務(wù)環(huán)境,即若一個程序不主動放棄其控制權(quán),別的程序就不能執(zhí)行。因此在設(shè)計Windows Sockets 程序時,盡管系統(tǒng)支持阻塞操作,但還是反對程序員使用該操作。但由于 SUN 公司下的 Berkeley Sockets 的套接字默認操作是阻塞的,WINDOWS 作為移植的 SOCKETS 也不可避免對這個操作支持。
在Windows Sockets 實現(xiàn)中,對于不能立即完成的阻塞操作做如下處理:DLL初始化→循環(huán)操作。在循環(huán)中,它發(fā)送任何 WINDOWS 消息,并檢查這個 Windows Sockets 調(diào)用是否完成,在必要時,它可以放棄CPU讓其它應(yīng)用程序執(zhí)行(當然使用超線程的CPU就不會有這個麻煩了^_^)。我們可以調(diào)用 WSACancelBlockingCall() 函數(shù)取消此阻塞操作。
在 Windows Sockets 中,有一個默認的阻塞處理例程 BlockingHook() 簡單地獲取并發(fā)送 WINDOWS 消息。如果要對復(fù)雜程序進行處理,Windows Sockets 中還有 WSASetBlockingHook() 提供用戶安裝自己的阻塞處理例程能力;與該函數(shù)相對應(yīng)的則是 SWAUnhookBlockingHook(),它用于刪除先前安裝的任何阻塞處理例程,并重新安裝默認的處理例程。請注意,設(shè)計自己的阻塞處理例程時,除了函數(shù) WSACancelBlockingHook() 之外,它不能使用其它的 Windows Sockets API 函數(shù)。在處理例程中調(diào)用 WSACancelBlockingHook()函數(shù)將取消處于阻塞的操作,它將結(jié)束阻塞循環(huán)。
4、出錯處理
Windows Sockets 為了和以后多線程環(huán)境(WINDOWS/UNIX)兼容,它提供了兩個出錯處理函數(shù)來獲取和設(shè)置當前線程的最近錯誤號。(WSAGetLastEror()和WSASetLastError())
5、啟動與終止
使用函數(shù) WSAStartup() 和 WSACleanup() 啟動和終止套接字。
三、Windows Sockets網(wǎng)絡(luò)程序設(shè)計核心
我們終于可以開始真正的 Windows Sockets 網(wǎng)絡(luò)程序設(shè)計了。不過我們還是先看一看每個 Windows Sockets 網(wǎng)絡(luò)程序都要涉及的內(nèi)容。讓我們一步步慢慢走。
1、啟動與終止
在所有 Windows Sockets 函數(shù)中,只有啟動函數(shù) WSAStartup() 和終止函數(shù) WSACleanup() 是必須使用的。
啟動函數(shù)必須是第一個使用的函數(shù),而且它允許指定 Windows Sockets API 的版本,并獲得 SOCKETS的特定的一些技術(shù)細節(jié)。本結(jié)構(gòu)如下:
int PASCAL FAR WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); |
其中 wVersionRequested 保證 SOCKETS 可正常運行的 DLL 版本,如果不支持,則返回錯誤信息。
我們看一下下面這段代碼,看一下如何進行 WSAStartup() 的調(diào)用
WORD wVersionRequested;// 定義版本信息變量 WSADATA wsaData;//定義數(shù)據(jù)信息變量 int err;//定義錯誤號變量 wVersionRequested = MAKEWORD(1,1);//給版本信息賦值 err = WSAStartup(wVersionRequested, &wsaData);//給錯誤信息賦值 if(err!=0) { return;//告訴用戶找不到合適的版本 } //確認 Windows Sockets DLL 支持 1.1 版本 //DLL 版本可以高于 1.1 //系統(tǒng)返回的版本號始終是最低要求的 1.1,即應(yīng)用程序與DLL 中可支持的最低版本號 if(LOBYTE(wsaData.wVersion)!= 1|| HIBYTE(wsaData.wVersion)!=1) { WSACleanup();//告訴用戶找不到合適的版本 return; } //Windows Sockets DLL 被進程接受,可以進入下一步操作 |
關(guān)閉函數(shù)使用時,任何打開并已連接的 SOCK_STREAM 套接字被復(fù)位,但那些已由 closesocket() 函數(shù)關(guān)閉的但仍有未發(fā)送數(shù)據(jù)的套接字不受影響,未發(fā)送的數(shù)據(jù)仍將被發(fā)送。程序運行時可能會多次調(diào)用 WSAStartuo() 函數(shù),但必須保證每次調(diào)用時的 wVersionRequested 的值是相同的。
2、異步請求服務(wù)
Windows Sockets 除支持 Berkeley Sockets 中同步請求,還增加了了一類異步請求服務(wù)函數(shù) WSAAsyncGerXByY()。該函數(shù)是阻塞請求函數(shù)的異步版本。應(yīng)用程序調(diào)用它時,由 Windows Sockets DLL 初始化這一操作并返回調(diào)用者,此函數(shù)返回一個異步句柄,用來標識這個操作。當結(jié)果存儲在調(diào)用者提供的緩沖區(qū),并且發(fā)送一個消息到應(yīng)用程序相應(yīng)窗口。常用結(jié)構(gòu)如下:
HANDLE taskHnd; char hostname="rs6000"; taskHnd = WSAAsyncBetHostByName(hWnd,wMsg,hostname,buf,buflen); |
需要注意的是,由于 Windows 的內(nèi)存對像可以設(shè)置為可移動和可丟棄,因此在操作內(nèi)存對象是,必須保證 WIindows Sockets DLL 對象是可用的。
3、異步數(shù)據(jù)傳輸
使用 send() 或 sendto() 函數(shù)來發(fā)送數(shù)據(jù),使用 recv() 或recvfrom() 來接收數(shù)據(jù)。Windows Sockets 不鼓勵用戶使用阻塞方式傳輸數(shù)據(jù),因為那樣可能會阻塞整個 Windows 環(huán)境。下面我們看一個異步數(shù)據(jù)傳輸實例:
假設(shè)套接字 s 在連接建立后,已經(jīng)使用了函數(shù) WSAAsyncSelect() 在其上注冊了網(wǎng)絡(luò)事件 FD_READ 和 FD_WRITE,并且 wMsg 值為 UM_SOCK,那么我們可以在 Windows 消息循環(huán)中增加如下的分支語句:
case UM_SOCK: switch(lParam) { case FD_READ: len = recv(wParam,lpBuffer,length,0); break; case FD_WRITE: while(send(wParam,lpBuffer,len,0)!=SOCKET_ERROR) break; } break; |
4、出錯處理
Windows 提供了一個函數(shù)來獲取最近的錯誤碼 WSAGetLastError(),推薦的編寫方式如下:
len = send (s,lpBuffer,len,0); of((len==SOCKET_ERROR)&&(WSAGetLastError()==WSAWOULDBLOCK)){...} |
基于Visual C++的Winsock API研究
為了方便網(wǎng)絡(luò)編程,90年代初,由Microsoft聯(lián)合了其他幾家公司共同制定了一套WINDOWS下的網(wǎng)絡(luò)編程接口,即Windows Sockets規(guī)范,它不是一種網(wǎng)絡(luò)協(xié)議,而是一套開放的、支持多種協(xié)議的Windows下的網(wǎng)絡(luò)編程接口?,F(xiàn)在的Winsock已經(jīng)基本上實現(xiàn)了與協(xié)議無關(guān),你可以使用Winsock來調(diào)用多種協(xié)議的功能,但較常使用的是TCP/IP協(xié)議。Socket實際在計算機中提供了一個通信端口,可以通過這個端口與任何一個具有Socket接口的計算機通信。應(yīng)用程序在網(wǎng)絡(luò)上傳輸,接收的信息都通過這個Socket接口來實現(xiàn)。
微軟為VC定義了Winsock類如CAsyncSocket類和派生于CAsyncSocket 的CSocket類,它們簡單易用,讀者朋友當然可以使用這些類來實現(xiàn)自己的網(wǎng)絡(luò)程序,但是為了更好的了解Winsock API編程技術(shù),我們這里探討怎樣使用底層的API函數(shù)實現(xiàn)簡單的 Winsock 網(wǎng)絡(luò)應(yīng)用程式設(shè)計,分別說明如何在Server端和Client端操作Socket,實現(xiàn)基于TCP/IP的數(shù)據(jù)傳送,最后給出相關(guān)的源代碼。
在VC中進行WINSOCK的API編程開發(fā)的時候,需要在項目中使用下面三個文件,否則會出現(xiàn)編譯錯誤。
1.WINSOCK.H: 這是WINSOCK API的頭文件,需要包含在項目中。
2.WSOCK32.LIB: WINSOCK API連接庫文件。在使用中,一定要把它作為項目的非缺省的連接庫包含到項目文件中去。
3.WINSOCK.DLL: WINSOCK的動態(tài)連接庫,位于WINDOWS的安裝目錄下。
一、服務(wù)器端操作 socket(套接字)
1)在初始化階段調(diào)用WSAStartup()
此函數(shù)在應(yīng)用程序中初始化Windows Sockets DLL ,只有此函數(shù)調(diào)用成功后,應(yīng)用程序才可以再調(diào)用其他Windows Sockets DLL中的API函數(shù)。在程式中調(diào)用該函數(shù)的形式如下:WSAStartup((WORD)((1<<8|1),(LPWSADATA)&WSAData),其中(1<<8|1)表示我們用的是WinSocket1.1版本,WSAata用來存儲系統(tǒng)傳回的關(guān)于WinSocket的資料。
2)建立Socket
初始化WinSock的動態(tài)連接庫后,需要在服務(wù)器端建立一個監(jiān)聽的Socket,為此可以調(diào)用Socket()函數(shù)用來建立這個監(jiān)聽的Socket,并定義此Socket所使用的通信協(xié)議。此函數(shù)調(diào)用成功返回Socket對象,失敗則返回INVALID_SOCKET(調(diào)用WSAGetLastError()可得知原因,所有WinSocket 的函數(shù)都可以使用這個函數(shù)來獲取失敗的原因)。
SOCKET PASCAL FAR socket( int af, int type, int protocol )
參數(shù): af:目前只提供 PF_INET(AF_INET);
type:Socket 的類型 (SOCK_STREAM、SOCK_DGRAM);
protocol:通訊協(xié)定(如果使用者不指定則設(shè)為0);
如果要建立的是遵從TCP/IP協(xié)議的socket,第二個參數(shù)type應(yīng)為SOCK_STREAM,如為UDP(數(shù)據(jù)報)的socket,應(yīng)為SOCK_DGRAM。
3)綁定端口
接下來要為服務(wù)器端定義的這個監(jiān)聽的Socket指定一個地址及端口(Port),這樣客戶端才知道待會要連接哪一個地址的哪個端口,為此我們要調(diào)用bind()函數(shù),該函數(shù)調(diào)用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );
參 數(shù): s:Socket對象名;
name:Socket的地址值,這個地址必須是執(zhí)行這個程式所在機器的IP地址;
namelen:name的長度;
如果使用者不在意地址或端口的值,那么可以設(shè)定地址為INADDR_ANY,及Port為0,Windows Sockets 會自動將其設(shè)定適當之地址及Port (1024 到 5000之間的值)。此后可以調(diào)用getsockname()函數(shù)來獲知其被設(shè)定的值。
4)監(jiān)聽
當服務(wù)器端的Socket對象綁定完成之后,服務(wù)器端必須建立一個監(jiān)聽的隊列來接收客戶端的連接請求。listen()函數(shù)使服務(wù)器端的Socket 進入監(jiān)聽狀態(tài),并設(shè)定可以建立的最大連接數(shù)(目前最大值限制為 5, 最小值為1)。該函數(shù)調(diào)用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR listen( SOCKET s, int backlog ); 參 數(shù): s:需要建立監(jiān)聽的Socket; backlog:最大連接個數(shù); |
服務(wù)器端的Socket調(diào)用完listen()后,如果此時客戶端調(diào)用connect()函數(shù)提出連接申請的話,Server 端必須再調(diào)用accept() 函數(shù),這樣服務(wù)器端和客戶端才算正式完成通信程序的連接動作。為了知道什么時候客戶端提出連接要求,從而服務(wù)器端的Socket在恰當?shù)臅r候調(diào)用accept()函數(shù)完成連接的建立,我們就要使用WSAAsyncSelect()函數(shù),讓系統(tǒng)主動來通知我們有客戶端提出連接請求了。該函數(shù)調(diào)用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent ); 參數(shù): s:Socket 對象; hWnd :接收消息的窗口句柄; wMsg:傳給窗口的消息; lEvent:被注冊的網(wǎng)絡(luò)事件,也即是應(yīng)用程序向窗口發(fā)送消息的網(wǎng)路事件,該值為下列值FD_READ、FD_WRITE、FD_OOB、FD_ACCEPT、FD_CONNECT、FD_CLOSE的組合,各個值的具體含意為FD_READ:希望在套接字S收到數(shù)據(jù)時收到消息;FD_WRITE:希望在套接字S上可以發(fā)送數(shù)據(jù)時收到消息;FD_ACCEPT:希望在套接字S上收到連接請求時收到消息;FD_CONNECT:希望在套接字S上連接成功時收到消息;FD_CLOSE:希望在套接字S上連接關(guān)閉時收到消息;FD_OOB:希望在套接字S上收到帶外數(shù)據(jù)時收到消息。 |
具體應(yīng)用時,wMsg應(yīng)是在應(yīng)用程序中定義的消息名稱,而消息結(jié)構(gòu)中的lParam則為以上各種網(wǎng)絡(luò)事件名稱。所以,可以在窗口處理自定義消息函數(shù)中使用以下結(jié)構(gòu)來響應(yīng)Socket的不同事件:
switch(lParam) {case FD_READ: … break; case FD_WRITE、 … break; … } |
5)服務(wù)器端接受客戶端的連接請求
當Client提出連接請求時,Server 端hwnd視窗會收到Winsock Stack送來我們自定義的一個消息,這時,我們可以分析lParam,然后調(diào)用相關(guān)的函數(shù)來處理此事件。為了使服務(wù)器端接受客戶端的連接請求,就要使用accept() 函數(shù),該函數(shù)新建一Socket與客戶端的Socket相通,原先監(jiān)聽之Socket繼續(xù)進入監(jiān)聽狀態(tài),等待他人的連接要求。該函數(shù)調(diào)用成功返回一個新產(chǎn)生的Socket對象,否則返回INVALID_SOCKET。
SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen ); 參數(shù):s:Socket的識別碼; addr:存放來連接的客戶端的地址; addrlen:addr的長度 |
6)結(jié)束 socket 連接
結(jié)束服務(wù)器和客戶端的通信連接是很簡單的,這一過程可以由服務(wù)器或客戶機的任一端啟動,只要調(diào)用closesocket()就可以了,而要關(guān)閉Server端監(jiān)聽狀態(tài)的socket,同樣也是利用此函數(shù)。另外,與程序啟動時調(diào)用WSAStartup()憨數(shù)相對應(yīng),程式結(jié)束前,需要調(diào)用 WSACleanup() 來通知Winsock Stack釋放Socket所占用的資源。這兩個函數(shù)都是調(diào)用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR closesocket( SOCKET s ); 參 數(shù):s:Socket 的識別碼; int PASCAL FAR WSACleanup( void ); 參 數(shù): 無 |
二、客戶端Socket的操作
1)建立客戶端的Socket
客戶端應(yīng)用程序首先也是調(diào)用WSAStartup() 函數(shù)來與Winsock的動態(tài)連接庫建立關(guān)系,然后同樣調(diào)用socket() 來建立一個TCP或UDP socket(相同協(xié)定的 sockets 才能相通,TCP 對 TCP,UDP 對 UDP)。與服務(wù)器端的socket 不同的是,客戶端的socket 可以調(diào)用 bind() 函數(shù),由自己來指定IP地址及port號碼;但是也可以不調(diào)用 bind(),而由 Winsock來自動設(shè)定IP地址及port號碼。
2)提出連接申請
客戶端的Socket使用connect()函數(shù)來提出與服務(wù)器端的Socket建立連接的申請,函數(shù)調(diào)用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen ); 參 數(shù):s:Socket 的識別碼; name:Socket想要連接的對方地址; namelen:name的長度 |
三、數(shù)據(jù)的傳送
雖然基于TCP/IP連接協(xié)議(流套接字)的服務(wù)是設(shè)計客戶機/服務(wù)器應(yīng)用程序時的主流標準,但有些服務(wù)也是可以通過無連接協(xié)議(數(shù)據(jù)報套接字)提供的。先介紹一下TCP socket 與UDP socket 在傳送數(shù)據(jù)時的特性:Stream (TCP) Socket 提供雙向、可靠、有次序、不重復(fù)的資料傳送。Datagram (UDP) Socket 雖然提供雙向的通信,但沒有可靠、有次序、不重復(fù)的保證,所以UDP傳送數(shù)據(jù)可能會收到無次序、重復(fù)的資料,甚至資料在傳輸過程中出現(xiàn)遺漏。由于UDP Socket 在傳送資料時,并不保證資料能完整地送達對方,所以絕大多數(shù)應(yīng)用程序都是采用TCP處理Socket,以保證資料的正確性。一般情況下TCP Socket 的數(shù)據(jù)發(fā)送和接收是調(diào)用send() 及recv() 這兩個函數(shù)來達成,而 UDP Socket則是用sendto() 及recvfrom() 這兩個函數(shù),這兩個函數(shù)調(diào)用成功發(fā)揮發(fā)送或接收的資料的長度,否則返回SOCKET_ERROR。
int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags ); 參數(shù):s:Socket 的識別碼 buf:存放要傳送的資料的暫存區(qū) len buf:的長度 flags:此函數(shù)被調(diào)用的方式 |
對于Datagram Socket而言,若是 datagram 的大小超過限制,則將不會送出任何資料,并會傳回錯誤值。對Stream Socket 言,Blocking 模式下,若是傳送系統(tǒng)內(nèi)的儲存空間不夠存放這些要傳送的資料,send()將會被block住,直到資料送完為止;如果該Socket被設(shè)定為 Non-Blocking 模式,那么將視目前的output buffer空間有多少,就送出多少資料,并不會被 block 住。flags 的值可設(shè)為 0 或 MSG_DONTROUTE及 MSG_OOB 的組合。
int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags ); 參數(shù):s:Socket 的識別碼 buf:存放接收到的資料的暫存區(qū) len buf:的長度 flags:此函數(shù)被調(diào)用的方式 |
對Stream Socket 言,我們可以接收到目前input buffer內(nèi)有效的資料,但其數(shù)量不超過len的大小。
四、自定義的CMySocket類的實現(xiàn)代碼:
根據(jù)上面的知識,我自定義了一個簡單的CMySocket類,下面是我定義的該類的部分實現(xiàn)代碼:
////////////////////////////////////// CMySocket::CMySocket() : file://類的構(gòu)造函數(shù) { WSADATA wsaD; memset( m_LastError, 0, ERR_MAXLENGTH ); // m_LastError是類內(nèi)字符串變量,初始化用來存放最后錯誤說明的字符串; // 初始化類內(nèi)sockaddr_in結(jié)構(gòu)變量,前者存放客戶端地址,后者對應(yīng)于服務(wù)器端地址; memset( &m_sockaddr, 0, sizeof( m_sockaddr ) ); memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) ); int result = WSAStartup((WORD)((1<<8|1), &wsaD);//初始化WinSocket動態(tài)連接庫; if( result != 0 ) // 初始化失??; { set_LastError( "WSAStartup failed!", WSAGetLastError() ); return; } } ////////////////////////////// CMySocket::~CMySocket() { WSACleanup(); }//類的析構(gòu)函數(shù); //////////////////////////////////////////////////// int CMySocket::Create( void ) {// m_hSocket是類內(nèi)Socket對象,創(chuàng)建一個基于TCP/IP的Socket變量,并將值賦給該變量; if ( (m_hSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )) == INVALID_SOCKET ) { set_LastError( "socket() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } /////////////////////////////////////////////// int CMySocket::Close( void )//關(guān)閉Socket對象; { if ( closesocket( m_hSocket ) == SOCKET_ERROR ) { set_LastError( "closesocket() failed", WSAGetLastError() ); return ERR_WSAERROR; } file://重置sockaddr_in 結(jié)構(gòu)變量; memset( &m_sockaddr, 0, sizeof( sockaddr_in ) ); memset( &m_rsockaddr, 0, sizeof( sockaddr_in ) ); return ERR_SUCCESS; } ///////////////////////////////////////// int CMySocket::Connect( char* strRemote, unsigned int iPort )//定義連接函數(shù); { if( strlen( strRemote ) == 0 || iPort == 0 ) return ERR_BADPARAM; hostent *hostEnt = NULL; long lIPAddress = 0; hostEnt = gethostbyname( strRemote );//根據(jù)計算機名得到該計算機的相關(guān)內(nèi)容; if( hostEnt != NULL ) { lIPAddress = ((in_addr*)hostEnt->h_addr)->s_addr; m_sockaddr.sin_addr.s_addr = lIPAddress; } else { m_sockaddr.sin_addr.s_addr = inet_addr( strRemote ); } m_sockaddr.sin_family = AF_INET; m_sockaddr.sin_port = htons( iPort ); if( connect( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR ) { set_LastError( "connect() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } /////////////////////////////////////////////////////// int CMySocket::Bind( char* strIP, unsigned int iPort )//綁定函數(shù); { if( strlen( strIP ) == 0 || iPort == 0 ) return ERR_BADPARAM; memset( &m_sockaddr,0, sizeof( m_sockaddr ) ); m_sockaddr.sin_family = AF_INET; m_sockaddr.sin_addr.s_addr = inet_addr( strIP ); m_sockaddr.sin_port = htons( iPort ); if ( bind( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR ) { set_LastError( "bind() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } ////////////////////////////////////////// int CMySocket::Accept( SOCKET s )//建立連接函數(shù),S為監(jiān)聽Socket對象名; { int Len = sizeof( m_rsockaddr ); memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) ); if( ( m_hSocket = accept( s, (SOCKADDR*)&m_rsockaddr, &Len ) ) == INVALID_SOCKET ) { set_LastError( "accept() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } ///////////////////////////////////////////////////// int CMySocket::asyncSelect( HWND hWnd, unsigned int wMsg, long lEvent ) file://事件選擇函數(shù); { if( !IsWindow( hWnd ) || wMsg == 0 || lEvent == 0 ) return ERR_BADPARAM; if( WSAAsyncSelect( m_hSocket, hWnd, wMsg, lEvent ) == SOCKET_ERROR ) { set_LastError( "WSAAsyncSelect() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } //////////////////////////////////////////////////// int CMySocket::Listen( int iQueuedConnections )//監(jiān)聽函數(shù); { if( iQueuedConnections == 0 ) return ERR_BADPARAM; if( listen( m_hSocket, iQueuedConnections ) == SOCKET_ERROR ) { set_LastError( "listen() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } //////////////////////////////////////////////////// int CMySocket::Send( char* strData, int iLen )//數(shù)據(jù)發(fā)送函數(shù); { if( strData == NULL || iLen == 0 ) return ERR_BADPARAM; if( send( m_hSocket, strData, iLen, 0 ) == SOCKET_ERROR ) { set_LastError( "send() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } ///////////////////////////////////////////////////// int CMySocket::Receive( char* strData, int iLen )//數(shù)據(jù)接收函數(shù); { if( strData == NULL ) return ERR_BADPARAM; int len = 0; int ret = 0; ret = recv( m_hSocket, strData, iLen, 0 ); if ( ret == SOCKET_ERROR ) { set_LastError( "recv() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ret; } void CMySocket::set_LastError( char* newError, int errNum ) file://WinSock API操作錯誤字符串設(shè)置函數(shù); { memset( m_LastError, 0, ERR_MAXLENGTH ); memcpy( m_LastError, newError, strlen( newError ) ); m_LastError[strlen(newError)+1] = '\0'; } |
有了上述類的定義,就可以在網(wǎng)絡(luò)程序的服務(wù)器和客戶端分別定義CMySocket對象,建立連接,傳送數(shù)據(jù)了。例如,為了在服務(wù)器和客戶端發(fā)送數(shù)據(jù),需要在服務(wù)器端定義兩個CMySocket對象ServerSocket1和ServerSocket2,分別用于監(jiān)聽和連接,客戶端定義一個CMySocket對象ClientSocket,用于發(fā)送或接收數(shù)據(jù),如果建立的連接數(shù)大于一,可以在服務(wù)器端再定義CMySocket對象,但要注意連接數(shù)不要大于五。
由于Socket API函數(shù)還有許多,如獲取遠端服務(wù)器、本地客戶機的IP地址、主機名等等,讀者可以再此基礎(chǔ)上對CMySocket補充完善,實現(xiàn)更多的功能。
TCP/IP Winsock編程要點
利用Winsock編程由同步和異步方式,同步方式邏輯清晰,編程專注于應(yīng)用,在搶先式的多任務(wù)操作系統(tǒng)中(WinNt、Win2K)采用多線程方式效率基本達到異步方式的水平,應(yīng)此以下為同步方式編程要點。
1、快速通信
Winsock的Nagle算法將降低小數(shù)據(jù)報的發(fā)送速度,而系統(tǒng)默認是使用Nagle算法,使用
int setsockopt( SOCKET s, int level, int optname, const char FAR *optval, int optlen );函數(shù)關(guān)閉它 |
例子:
SOCKET sConnect; sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); int bNodelay = 1; int err; err = setsockopt( sConnect, IPPROTO_TCP, TCP_NODELAY, (char *)&bNodelay, sizoeof(bNodelay));//不采用延時算法 if (err != NO_ERROR) TRACE ("setsockopt failed for some reason\n");; |
2、SOCKET的SegMentSize和收發(fā)緩沖
TCPSegMentSize是發(fā)送接受時單個數(shù)據(jù)報的最大長度,系統(tǒng)默認為1460,收發(fā)緩沖大小為8192。
在SOCK_STREAM方式下,如果單次發(fā)送數(shù)據(jù)超過1460,系統(tǒng)將分成多個數(shù)據(jù)報傳送,在對方接受到的將是一個數(shù)據(jù)流,應(yīng)用程序需要增加斷幀的判斷。當然可以采用修改注冊表的方式改變1460的大小,但MicrcoSoft認為1460是最佳效率的參數(shù),不建議修改。
在工控系統(tǒng)中,建議關(guān)閉Nagle算法,每次發(fā)送數(shù)據(jù)小于1460個字節(jié)(推薦1400),這樣每次發(fā)送的是一個完整的數(shù)據(jù)報,減少對方對數(shù)據(jù)流的斷幀處理。
3、同步方式中減少斷網(wǎng)時connect函數(shù)的阻塞時間
同步方式中的斷網(wǎng)時connect的阻塞時間為20秒左右,可采用gethostbyaddr事先判斷到服務(wù)主機的路徑是否是通的,或者先ping一下對方主機的IP地址。
A、采用gethostbyaddr阻塞時間不管成功與否為4秒左右。
例子:
LONG lPort=3024; struct sockaddr_in ServerHostAddr;//服務(wù)主機地址 ServerHostAddr.sin_family=AF_INET; ServerHostAddr.sin_port=::htons(u_short(lPort)); ServerHostAddr.sin_addr.s_addr=::inet_addr("192.168.1.3"); HOSTENT* pResult=gethostbyaddr((const char *) & (ServerHostAddr.sin_addr.s_addr),4,AF_INET); if(NULL==pResult) { int nErrorCode=WSAGetLastError(); TRACE("gethostbyaddr errorcode=%d",nErrorCode); } else { TRACE("gethostbyaddr %s\n",pResult->h_name);; } |
B、采用PING方式時間約2秒左右
暫略
4、同步方式中解決recv,send阻塞問題
采用select函數(shù)解決,在收發(fā)前先檢查讀寫可用狀態(tài)。
A、讀
例子:
TIMEVAL tv01 = {0, 1};//1ms鐘延遲,實際為0-10毫秒 int nSelectRet; int nErrorCode; FD_SET fdr = {1, sConnect}; nSelectRet=::select(0, &fdr, NULL, NULL, &tv01);//檢查可讀狀態(tài) if(SOCKET_ERROR==nSelectRet) { nErrorCode=WSAGetLastError(); TRACE("select read status errorcode=%d",nErrorCode); ::closesocket(sConnect); goto 重新連接(客戶方),或服務(wù)線程退出(服務(wù)方); } if(nSelectRet==0)//超時發(fā)生,無可讀數(shù)據(jù) { 繼續(xù)查讀狀態(tài)或向?qū)Ψ街鲃影l(fā)送 } else { 讀數(shù)據(jù) } |
B、寫
TIMEVAL tv01 = {0, 1};//1ms鐘延遲,實際為9-10毫秒 int nSelectRet; int nErrorCode; FD_SET fdw = {1, sConnect}; nSelectRet=::select(0, NULL, NULL,&fdw, &tv01);//檢查可寫狀態(tài) if(SOCKET_ERROR==nSelectRet) { nErrorCode=WSAGetLastError(); TRACE("select write status errorcode=%d",nErrorCode); ::closesocket(sConnect); //goto 重新連接(客戶方),或服務(wù)線程退出(服務(wù)方); } if(nSelectRet==0)//超時發(fā)生,緩沖滿或網(wǎng)絡(luò)忙 { //繼續(xù)查寫狀態(tài)或查讀狀態(tài) } else { //發(fā)送 } |
5、改變TCP收發(fā)緩沖區(qū)大小
系統(tǒng)默認為8192,利用如下方式可改變。
SOCKET sConnect; sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); int nrcvbuf=1024*20; int err=setsockopt( sConnect, SOL_SOCKET, SO_SNDBUF,//寫緩沖,讀緩沖為SO_RCVBUF (char *)&nrcvbuf, sizeof(nrcvbuf)); if (err != NO_ERROR) { TRACE("setsockopt Error!\n"); } 在設(shè)置緩沖時,檢查是否真正設(shè)置成功用 int getsockopt( SOCKET s, int level, int optname, char FAR *optval, int FAR *optlen ); |
6、服務(wù)方同一端口多IP地址的bind和listen
在可靠性要求高的應(yīng)用中,要求使用雙網(wǎng)和多網(wǎng)絡(luò)通道,再服務(wù)方很容易實現(xiàn),用如下方式可建立客戶對本機所有IP地址在端口3024下的請求服務(wù)。
SOCKET hServerSocket_DS=INVALID_SOCKET; struct sockaddr_in HostAddr_DS;//服務(wù)器主機地址 LONG lPort=3024; HostAddr_DS.sin_family=AF_INET; HostAddr_DS.sin_port=::htons(u_short(lPort)); HostAddr_DS.sin_addr.s_addr=htonl(INADDR_ANY); hServerSocket_DS=::socket( AF_INET, SOCK_STREAM,IPPROTO_TCP); if(hServerSocket_DS==INVALID_SOCKET) { AfxMessageBox("建立數(shù)據(jù)服務(wù)器SOCKET 失敗!"); return FALSE; } if(SOCKET_ERROR==::bind(hServerSocket_DS,(struct sockaddr *)(&(HostAddr_DS)),sizeof(SOCKADDR))) { int nErrorCode=WSAGetLastError (); TRACE("bind error=%d\n",nErrorCode); AfxMessageBox("Socket Bind 錯誤!"); return FALSE; } if(SOCKET_ERROR==::listen(hServerSocket_DS,10))//10個客戶 { AfxMessageBox("Socket listen 錯誤!"); return FALSE; } AfxBeginThread(ServerThreadProc,NULL,THREAD_PRIORITY_NORMAL); |
在客戶方要復(fù)雜一些,連接斷后,重聯(lián)不成功則應(yīng)換下一個IP地址連接。也可采用同時連接好后備用的方式。
7、用TCP/IP Winsock實現(xiàn)變種Client/Server
傳統(tǒng)的Client/Server為客戶問、服務(wù)答,收發(fā)是成對出現(xiàn)的。而變種的Client/Server是指在連接時有客戶和服務(wù)之分,建立好通信連接后,不再有嚴格的客戶和服務(wù)之分,任何方都可主動發(fā)送,需要或不需要回答看應(yīng)用而言,這種方式在工控行業(yè)很有用,比如RTDB作為I/O Server的客戶,但I/O Server也可主動向RTDB發(fā)送開關(guān)狀態(tài)變位、隨即事件等信息。在很大程度上減少了網(wǎng)絡(luò)通信負荷、提高了效率。
采用1-6的TCP/IP編程要點,在Client和Server方均已接收優(yōu)先,適當控制時序就能實現(xiàn)。
Windows Sockets API實現(xiàn)網(wǎng)絡(luò)異步通訊
摘要:本文對如何使用面向連接的流式套接字實現(xiàn)對網(wǎng)卡的編程以及如何實現(xiàn)異步網(wǎng)絡(luò)通訊等問題進行了討論與闡述。
一、 引言
在80年代初,美國加利福尼亞大學(xué)伯克利分校的研究人員為TCP/IP網(wǎng)絡(luò)通信開發(fā)了一個專門用于網(wǎng)絡(luò)通訊開發(fā)的API。這個API就是Socket接口(套接字)--當今在TCP/IP網(wǎng)絡(luò)最為通用的一種API,也是在互聯(lián)網(wǎng)上進行應(yīng)用開發(fā)最為通用的一種API。在微軟聯(lián)合其它幾家公司共同制定了一套Windows下的網(wǎng)絡(luò)編程接口Windows Sockets規(guī)范后,由于在其規(guī)范中引入了一些異步函數(shù),增加了對網(wǎng)絡(luò)事件異步選擇機制,因此更加符合Windows的消息驅(qū)動特性,使網(wǎng)絡(luò)開發(fā)人員可以更加方便的進行高性能網(wǎng)絡(luò)通訊程序的設(shè)計。本文接下來就針對Windows Sockets API進行面向連接的流式套接字編程以及對異步網(wǎng)絡(luò)通訊的編程實現(xiàn)等問題展開討論。
二、 面向連接的流式套接字編程模型的設(shè)計
本文在方案選擇上采用了在網(wǎng)絡(luò)編程中最常用的一種模型--客戶機/服務(wù)器模型。這種客戶/服務(wù)器模型是一種非對稱式編程模式。該模式的基本思想是把集中在一起的應(yīng)用劃分成為功能不同的兩個部分,分別在不同的計算機上運行,通過它們之間的分工合作來實現(xiàn)一個完整的功能。對于這種模式而言其中一部分需要作為服務(wù)器,用來響應(yīng)并為客戶提供固定的服務(wù);另一部分則作為客戶機程序用來向服務(wù)器提出請求或要求某種服務(wù)。
本文選取了基于TCP/IP的客戶機/服務(wù)器模型和面向連接的流式套接字。其通信原理為:服務(wù)器端和客戶端都必須建立通信套接字,而且服務(wù)器端應(yīng)先進入監(jiān)聽狀態(tài),然后客戶端套接字發(fā)出連接請求,服務(wù)器端收到請求后,建立另一個套接字進行通信,原來負責(zé)監(jiān)聽的套接字仍進行監(jiān)聽,如果有其它客戶發(fā)來連接請求,則再建立一個套接字。默認狀態(tài)下最多可同時接收5個客戶的連接請求,并與之建立通信關(guān)系。因此本程序的設(shè)計流程應(yīng)當由服務(wù)器首先啟動,然后在某一時刻啟動客戶機并使其與服務(wù)器建立連接。服務(wù)器與客戶機開始都必須調(diào)用Windows Sockets API函數(shù)socket()建立一個套接字sockets,然后服務(wù)器方調(diào)用bind()將套接字與一個本地網(wǎng)絡(luò)地址捆扎在一起,再調(diào)用listen()使套接字處于一種被動的準備接收狀態(tài),同時規(guī)定它的請求隊列長度。在此之后服務(wù)器就可以通過調(diào)用accept()來接收客戶機的連接。
相對于服務(wù)器,客戶端的工作就顯得比較簡單了,當客戶端打開套接字之后,便可通過調(diào)用connect()和服務(wù)器建立連接。連接建立之后,客戶和服務(wù)器之間就可以通過連接發(fā)送和接收資料。最后資料傳送結(jié)束,雙方調(diào)用closesocket()關(guān)閉套接字來結(jié)束這次通訊。整個通訊過程的具體流程框圖可大致用下面的流程圖來表示:
![]() 面向連接的流式套接字編程流程示意圖 |
三、 軟件設(shè)計要點以及異步通訊的實現(xiàn)
根據(jù)前面設(shè)計的程序流程,可將程序劃分為兩部分:服務(wù)器端和客戶端。而且整個實現(xiàn)過程可以大致用以下幾個非常關(guān)鍵的Windows Sockets API函數(shù)將其慣穿下來:
服務(wù)器方:
socket()->bind()->listen->accept()->recv()/send()->closesocket() |
客戶機方:
socket()->connect()->send()/recv()->closesocket() |
有鑒于以上幾個函數(shù)在整個網(wǎng)絡(luò)編程中的重要性,有必要結(jié)合程序?qū)嵗龑ζ渥鲚^深入的剖析。服務(wù)器端應(yīng)用程序在使用套接字之前,首先必須擁有一個Socket,系統(tǒng)調(diào)用socket()函數(shù)向應(yīng)用程序提供創(chuàng)建套接字的手段。該套接字實際上是在計算機中提供了一個通信埠,可以通過這個埠與任何一個具有套接字接口的計算機通信。應(yīng)用程序在網(wǎng)絡(luò)上傳輸、接收的信息都通過這個套接字接口來實現(xiàn)的。在應(yīng)用開發(fā)中如同使用文件句柄一樣,可以對套接字句柄進行讀寫操作:
sock=socket(AF_INET,SOCK_STREAM,0); |
函數(shù)的第一個參數(shù)用于指定地址族,在Windows下僅支持AF_INET(TCP/IP地址);第二個參數(shù)用于描述套接字的類型,對于流式套接字提供有SOCK_STREAM;最后一個參數(shù)指定套接字使用的協(xié)議,一般為0。該函數(shù)的返回值保存了新套接字的句柄,在程序退出前可以用 closesocket(sock);函數(shù)來將其釋放。服務(wù)器方一旦獲取了一個新的套接字后應(yīng)通過bind()將該套接字與本機上的一個端口相關(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ù)的第二個參數(shù)是一個指向包含有本機IP地址和端口信息的sockaddr_in結(jié)構(gòu)類型的指針,其成員描述了本地端口號和本地主機地址,經(jīng)過bind()將服務(wù)器進程在網(wǎng)絡(luò)上標識出來。需要注意的是由于1024以內(nèi)的埠號都是保留的埠號因此如無特別需要一般不能將sockin.sin_port的埠號設(shè)置為1024以內(nèi)的值。然后調(diào)用listen()函數(shù)開始偵聽,再通過accept()調(diào)用等待接收連接以完成連接的建立:
//連接請求隊列長度為1,即只允許有一個請求,若有多個請求, //則出現(xiàn)錯誤,給出錯誤代碼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()放到一個線程中去是因為在執(zhí)行到該函數(shù)時如沒有客戶連接服務(wù)器的請求到來,服務(wù)器就會停在accept語句上等待連接請求的到來,這勢必會引起程序的阻塞,雖然也可以通過設(shè)置套接字為非阻塞方式使在沒有客戶等待時可以使accept()函數(shù)調(diào)用立即返回,但這種輪詢套接字的方式會使CPU處于忙等待方式,從而降低程序的運行效率大大浪費系統(tǒng)資源。考慮到這種情況,將套接字設(shè)置為阻塞工作方式,并為其單獨開辟一個子線程,將其阻塞控制在子線程范圍內(nèi)而不會造成整個應(yīng)用程序的阻塞。對于網(wǎng)絡(luò)事件的響應(yīng)顯然要采取異步選擇機制,只有采取這種方式才可以在由網(wǎng)絡(luò)對方所引起的不可預(yù)知的網(wǎng)絡(luò)事件發(fā)生時能馬上在進程中做出及時的響應(yīng)處理,而在沒有網(wǎng)絡(luò)事件到達時則可以處理其他事件,這種效率是很高的,而且完全符合Windows所標榜的消息觸發(fā)原則。前面那段代碼中的WSAAsyncSelect()函數(shù)便是實現(xiàn)網(wǎng)絡(luò)事件異步選擇的核心函數(shù)。
通過第四個參數(shù)注冊應(yīng)用程序感興取的網(wǎng)絡(luò)事件,在這里通過FD_READ|FD_CLOSE指定了網(wǎng)絡(luò)讀和網(wǎng)絡(luò)斷開兩種事件,當這種事件發(fā)生時變會發(fā)出由第三個參數(shù)指定的自定義消息WM_SOCKET_MSG,接收該消息的窗口通過第二個參數(shù)指定其句柄。在消息處理函數(shù)中可以通過對消息參數(shù)低字節(jié)進行判斷而區(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ā)生。此時有字符到達,需要進行接收處理 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ā)生。此時客戶機關(guān)閉或退出。 ……//進行相應(yīng)的處理 break; default: break; } } |
在這里需要實現(xiàn)對自定義消息WM_SOCKET_MSG的響應(yīng),需要在頭文件和實現(xiàn)文件中分別添加其消息映射關(guān)系:
頭文件:
//{{AFX_MSG(CNetServerView) //}}AFX_MSG void OnSocket(WPARAM wParam,LPARAM lParam); DECLARE_MESSAGE_MAP() |
實現(xiàn)文件:
BEGIN_MESSAGE_MAP(CNetServerView, CView) //{{AFX_MSG_MAP(CNetServerView) //}}AFX_MSG_MAP ON_MESSAGE(WM_SOCKET_MSG,OnSocket) END_MESSAGE_MAP() |
在進行異步選擇使用WSAAsyncSelect()函數(shù)時,有以下幾點需要引起特別的注意:
1. 連續(xù)使用兩次WSAAsyncSelect()函數(shù)時,只有第二次設(shè)置的事件有效,如:
WSAAsyncSelect(s,hwnd,wMsg1,FD_READ); WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE); |
這樣只有當FD_CLOSE事件發(fā)生時才會發(fā)送wMsg2消息。
2.可以在設(shè)置過異步選擇后通過再次調(diào)用WSAAsyncSelect(s,hwnd,0,0);的形式取消在套接字上所設(shè)置的異步事件。
3.Windows Sockets DLL在一個網(wǎng)絡(luò)事件發(fā)生后,通常只會給相應(yīng)的應(yīng)用程序發(fā)送一個消息,而不能發(fā)送多個消息。但通過使用一些函數(shù)隱式地允許重發(fā)此事件的消息,這樣就可能再次接收到相應(yīng)的消息。
4.在調(diào)用過closesocket()函數(shù)關(guān)閉套接字之后不會再發(fā)生FD_CLOSE事件。
以上基本完成了服務(wù)器方的程序設(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()等三個函數(shù)可供選擇,其中前兩種分別用于緩沖發(fā)送和集中發(fā)送,而send()則為可控緩沖發(fā)送,并且還可以指定傳輸控制標志為MSG_OOB進行帶外數(shù)據(jù)的發(fā)送或是為MSG_DONTROUTE尋徑控制選項。在信宿地址的網(wǎng)絡(luò)號部分指定數(shù)據(jù)發(fā)送需要經(jīng)過的網(wǎng)絡(luò)接口,使其可以不經(jīng)過本地尋徑機制直接發(fā)送出去。這也是其同write()函數(shù)的真正區(qū)別所在。由于接收數(shù)據(jù)系統(tǒng)調(diào)用和發(fā)送數(shù)據(jù)系統(tǒng)調(diào)用是一一對應(yīng)的,因此對于數(shù)據(jù)的接收,在此不再贅述,相應(yīng)的三個接收函數(shù)分別為:read()、readv()和recv()。由于后者功能上的全面,本文在實現(xiàn)上選擇了send()-recv()函數(shù)對,在具體編程中應(yīng)當視具體情況的不同靈活選擇適當?shù)陌l(fā)送-接收函數(shù)對。
小結(jié):TCP/IP協(xié)議是目前各網(wǎng)絡(luò)操作系統(tǒng)主要的通訊協(xié)議,也是 Internet的通訊協(xié)議,本文通過Windows Sockets API實現(xiàn)了對基于TCP/IP協(xié)議的面向連接的流式套接字網(wǎng)絡(luò)通訊程序的設(shè)計,并通過異步通訊和多線程等手段提高了程序的運行效率,避免了阻塞的發(fā)生。
用VC++6.0的Sockets API實現(xiàn)一個聊天室程序
1.VC++網(wǎng)絡(luò)編程及Windows Sockets API簡介
VC++對網(wǎng)絡(luò)編程的支持有socket支持,WinInet支持,MAPI和ISAPI支持等。其中,Windows Sockets API是TCP/IP網(wǎng)絡(luò)環(huán)境里,也是Internet上進行開發(fā)最為通用的API。最早美國加州大學(xué)Berkeley分校在UNIX下為TCP/IP協(xié)議開發(fā)了一個API,這個API就是著名的Berkeley Socket接口(套接字)。在桌面操作系統(tǒng)進入Windows時代后,仍然繼承了Socket方法。在TCP/IP網(wǎng)絡(luò)通信環(huán)境下,Socket數(shù)據(jù)傳輸是一種特殊的I/O,它也相當于一種文件描述符,具有一個類似于打開文件的函數(shù)調(diào)用-socket()??梢赃@樣理解篠ocket實際上是一個通信端點,通過它,用戶的Socket程序可以通過網(wǎng)絡(luò)和其他的Socket應(yīng)用程序通信。Socket存在于一個"通信域"(為描述一般的線程如何通過Socket進行通信而引入的一種抽象概念)里,并且與另一個域的Socket交換數(shù)據(jù)。Socket有三類。第一種是SOCK_STREAM(流式),提供面向連接的可靠的通信服務(wù),比如telnet,http。第二種是SOCK_DGRAM(數(shù)據(jù)報),提供無連接不可靠的通信,比如UDP。第三種是SOCK_RAW(原始),主要用于協(xié)議的開發(fā)和測試,支持通信底層操作,比如對IP和ICMP的直接訪問。
2.Windows Socket機制分析
2.1一些基本的Socket系統(tǒng)調(diào)用
主要的系統(tǒng)調(diào)用包括:socket()-創(chuàng)建Socket;bind()-將創(chuàng)建的Socket與本地端口綁定;connect()與accept()-建立Socket連接;listen()-服務(wù)器監(jiān)聽是否有連接請求;send()-數(shù)據(jù)的可控緩沖發(fā)送;recv()-可控緩沖接收;closesocket()-關(guān)閉Socket。
2.2Windows Socket的啟動與終止
啟動函數(shù)WSAStartup()建立與Windows Sockets DLL的連接,終止函數(shù)WSAClearup()終止使用該DLL,這兩個函數(shù)必須成對使用。
2.3異步選擇機制
Windows是一個非搶占式的操作系統(tǒng),而不采取UNIX的阻塞機制。當一個通信事件產(chǎn)生時,操作系統(tǒng)要根據(jù)設(shè)置選擇是否對該事件加以處理,WSAAsyncSelect()函數(shù)就是用來選擇系統(tǒng)所要處理的相應(yīng)事件。當Socket收到設(shè)定的網(wǎng)絡(luò)事件中的一個時,會給程序窗口一個消息,這個消息里會指定產(chǎn)生網(wǎng)絡(luò)事件的Socket,發(fā)生的事件類型和錯誤碼。
2.4異步數(shù)據(jù)傳輸機制
WSAAsyncSelect()設(shè)定了Socket上的須響應(yīng)通信事件后,每發(fā)生一個這樣的事件就會產(chǎn)生一個WM_SOCKET消息傳給窗口。而在窗口的回調(diào)函數(shù)中就應(yīng)該添加相應(yīng)的數(shù)據(jù)傳輸處理代碼。
3.聊天室程序的設(shè)計說明
3.1實現(xiàn)思想
在Internet上的聊天室程序一般都是以服務(wù)器提供服務(wù)端連接響應(yīng),使用者通過客戶端程序登錄到服務(wù)器,就可以與登錄在同一服務(wù)器上的用戶交談,這是一個面向連接的通信過程。因此,程序要在TCP/IP環(huán)境下,實現(xiàn)服務(wù)器端和客戶端兩部分程序。
3.2服務(wù)器端工作流程
服務(wù)器端通過socket()系統(tǒng)調(diào)用創(chuàng)建一個Socket數(shù)組后(即設(shè)定了接受連接客戶的最大數(shù)目),與指定的本地端口綁定bind(),就可以在端口進行偵聽listen()。如果有客戶端連接請求,則在數(shù)組中選擇一個空Socket,將客戶端地址賦給這個Socket。然后登錄成功的客戶就可以在服務(wù)器上聊天了。
3.3客戶端工作流程
客戶端程序相對簡單,只需要建立一個Socket與服務(wù)器端連接,成功后通過這個Socket來發(fā)送和接收數(shù)據(jù)就可以了。
4.核心代碼分析
限于篇幅,這里僅給出與網(wǎng)絡(luò)編程相關(guān)的核心代碼,其他的諸如聊天文字的服務(wù)器和客戶端顯示讀者可以自行添加。
4.1服務(wù)器端代碼
開啟服務(wù)器功能:
void OnServerOpen() //開啟服務(wù)器功能 { WSADATA wsaData; int iErrorCode; char chInfo[64]; if (WSAStartup(WINSOCK_VERSION, &wsaData)) //調(diào)用Windows Sockets DLL { MessageBeep(MB_ICONSTOP); MessageBox("Winsock無法初始化!", AfxGetAppName(), MB_OK|MB_ICONSTOP); WSACleanup(); return; } else WSACleanup(); if (gethostname(chInfo, sizeof(chInfo))) { ReportWinsockErr("\n無法獲取主機!\n "); return; } CString csWinsockID = "\n==>>服務(wù)器功能開啟在端口:No. "; csWinsockID += itoa(m_pDoc->m_nServerPort, chInfo, 10); csWinsockID += "\n"; PrintString(csWinsockID); //在程序視圖顯示提示信息的函數(shù),讀者可自行創(chuàng)建 m_pDoc->m_hServerSocket=socket(PF_INET, SOCK_STREAM, DEFAULT_PROTOCOL); //創(chuàng)建服務(wù)器端Socket,類型為SOCK_STREAM,面向連接的通信 if (m_pDoc->m_hServerSocket == INVALID_SOCKET) { ReportWinsockErr("無法創(chuàng)建服務(wù)器socket!"); return;} m_pDoc->m_sockServerAddr.sin_family = AF_INET; m_pDoc->m_sockServerAddr.sin_addr.s_addr = INADDR_ANY; m_pDoc->m_sockServerAddr.sin_port = htons(m_pDoc->m_nServerPort); if (bind(m_pDoc->m_hServerSocket, (LPSOCKADDR)&m_pDoc->m_sockServerAddr, sizeof(m_pDoc->m_sockServerAddr)) == SOCKET_ERROR) //與選定的端口綁定 {ReportWinsockErr("無法綁定服務(wù)器socket!"); return;} iErrorCode=WSAAsyncSelect(m_pDoc->m_hServerSocket,m_hWnd, WM_SERVER_ACCEPT, FD_ACCEPT); //設(shè)定服務(wù)器相應(yīng)的網(wǎng)絡(luò)事件為FD_ACCEPT,即連接請求, // 產(chǎn)生相應(yīng)傳遞給窗口的消息為WM_SERVER_ACCEPT if (iErrorCode == SOCKET_ERROR) { ReportWinsockErr("WSAAsyncSelect設(shè)定失敗!"); return;} if (listen(m_pDoc->m_hServerSocket, QUEUE_SIZE) == SOCKET_ERROR) //開始監(jiān)聽客戶連接請求 {ReportWinsockErr("服務(wù)器socket監(jiān)聽失敗!"); m_pParentMenu->EnableMenuItem(ID_SERVER_OPEN, MF_ENABLED); return;} m_bServerIsOpen = TRUE; //監(jiān)視服務(wù)器是否打開的變量 return; } |
響應(yīng)客戶發(fā)送聊天文字到服務(wù)器:ON_MESSAGE(WM_CLIENT_READ, OnClientRead)
LRESULT OnClientRead(WPARAM wParam, LPARAM lParam) { int iRead; int iBufferLength; int iEnd; int iRemainSpace; char chInBuffer[1024]; int i; for(i=0;(i {} if(i==MAXClient) return 0L; iBufferLength = iRemainSpace = sizeof(chInBuffer); iEnd = 0; iRemainSpace -= iEnd; iBytesRead = recv(m_aClientSocket[i], (LPSTR)(chInBuffer+iEnd), iSpaceRemaining, NO_FLAGS); //用可控緩沖接收函數(shù)recv()來接收字符 iEnd+=iRead; if (iBytesRead == SOCKET_ERROR) ReportWinsockErr("recv出錯!"); chInBuffer[iEnd] = '\0'; if (lstrlen(chInBuffer) != 0) {PrintString(chInBuffer); //服務(wù)器端文字顯示 OnServerBroadcast(chInBuffer); //自己編寫的函數(shù),向所有連接的客戶廣播這個客戶的聊天文字 } return(0L); } |
對于客戶斷開連接,會產(chǎn)生一個FD_CLOSE消息,只須相應(yīng)地用closesocket()關(guān)閉相應(yīng)的Socket即可,這個處理比較簡單。
4.2客戶端代碼
連接到服務(wù)器:
void OnSocketConnect() { WSADATA wsaData; DWORD dwIPAddr; SOCKADDR_IN sockAddr; if(WSAStartup(WINSOCK_VERSION,&wsaData)) //調(diào)用Windows Sockets DLL {MessageBox("Winsock無法初始化!",NULL,MB_OK); return; } m_hSocket=socket(PF_INET,SOCK_STREAM,0); //創(chuàng)建面向連接的socket sockAddr.sin_family=AF_INET; //使用TCP/IP協(xié)議 sockAddr.sin_port=m_iPort; //客戶端指定的IP地址 sockAddr.sin_addr.S_un.S_addr=dwIPAddr; int nConnect=connect(m_hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)); //請求連接 if(nConnect) ReportWinsockErr("連接失敗!"); else MessageBox("連接成功!",NULL,MB_OK); int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,FD_READ); //指定響應(yīng)的事件,為服務(wù)器發(fā)送來字符 if(iErrorCode==SOCKET_ERROR) MessageBox("WSAAsyncSelect設(shè)定失敗!"); } |
接收服務(wù)器端發(fā)送的字符也使用可控緩沖接收函數(shù)recv(),客戶端聊天的字符發(fā)送使用數(shù)據(jù)可控緩沖發(fā)送函數(shù)send(),這兩個過程比較簡單,在此就不加贅述了。
5.小結(jié)
通過聊天室程序的編寫,可以基本了解Windows Sockets API編程的基本過程和精要之處。本程序在VC++6.0下編譯通過,在使用windows 98/NT的局域網(wǎng)里運行良好。
用VC++制作一個簡單的局域網(wǎng)消息發(fā)送工程
本工程類似于oicq的消息發(fā)送機制,不過他只能夠發(fā)送簡單的字符串。雖然簡單,但他也是一個很好的VC網(wǎng)絡(luò)學(xué)習(xí)例子。
本例通過VC帶的SOCKET類,重載了他的一個接受類mysock類,此類可以吧接收到的信息顯示在客戶區(qū)理。以下是實現(xiàn)過程:
建立一個MFC 單文檔工程,工程名為oicq,在第四步選取WINDOWS SOCKetS支持,其它取默認設(shè)置即可。為了簡單,這里直接把about對話框作些改變,作為發(fā)送信息界面。
這里通過失去對話框來得到發(fā)送的字符串、獲得焦點時把字符串發(fā)送出去。創(chuàng)建oicq類的窗口,獲得VIEW類指針,進而可以把接收到的信息顯示出來。
extern CString bb; void CAboutDlg::OnKillFocus(CWnd* pNewWnd) { // TODO: Add your message handler code here CDialog::OnKillFocus(pNewWnd); bb=m_edit; } 對于OICQVIEW類 char aa[100]; CString mm; CDC* pdc; class mysock:public CSocket //派生mysock類,此類既有接受功能 {public:void OnReceive(int nErrorCode) //可以隨時接收信息 { CSocket::Receive((void*)aa,100,0); mm=aa; CString ll=" ";//在顯示消息之前,消除前面發(fā)送的消息 pdc->TextOut(50,50,ll); pdc->TextOut(50,50,mm); } }; mysock sock1; CString bb; BOOL COicqView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { CView::OnSetFocus(pOldWnd); // TODO: Add your message handler code here and/or call default bb="besting:"+bb; //確定發(fā)送者身份為besting sock1.SendTo(bb,100,1060,"192.168.0.255",0); //獲得焦點以廣播形式發(fā)送信息,端口號為1060 return CView::OnSetCursor(pWnd, nHitTest, message); } int COicqView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) == -1) return -1; sock1.Create(1060,SOCK_DGRAM,NULL);//以數(shù)據(jù)報形式發(fā)送消息 static CClientDC wdc(this); //獲得當前視類的指針 pdc=&wdc; // TODO: Add your specialized creation code here return 0; } |
運行一下,打開ABOUT對話框,輸入發(fā)送信息,enter鍵就可以發(fā)送信息了,是不是有點像qq?。?BR>
用Winsock實現(xiàn)語音全雙工通信使用
摘要:在Windows 95環(huán)境下,基于TCP/IP協(xié)議,用Winsock完成了話音的端到端傳輸。采用雙套接字技術(shù),闡述了主要函數(shù)的使用要點,以及基于異步選擇機制的應(yīng)用方法。同時,給出了相應(yīng)的實例程序。
一、引言
Windows 95作為微機的操作系統(tǒng),已經(jīng)完全融入了網(wǎng)絡(luò)與通信功能,不僅可以建立純Windows 95環(huán)境下的“對等網(wǎng)絡(luò)”,而且支持多種協(xié)議,如TCP/IP、IPX/SPX、NETBUI等。在TCP/IP協(xié)議組中,TPC是一種面向連接的協(xié)義,為用戶提供可靠的、全雙工的字節(jié)流服務(wù),具有確認、流控制、多路復(fù)用和同步等功能,適于數(shù)據(jù)傳輸。UDP協(xié)議則是無連接的,每個分組都攜帶完整的目的地址,各分組在系統(tǒng)中獨立傳送。它不能保證分組的先后順序,不進行分組出錯的恢復(fù)與重傳,因此不保證傳輸?shù)目煽啃裕?,它提供高傳輸效率的?shù)據(jù)報服務(wù),適于實時的語音、圖像傳輸、廣播消息等網(wǎng)絡(luò)傳輸。
Winsock接口為進程間通信提供了一種新的手段,它不但能用于同一機器中的進程之間通信,而且支持網(wǎng)絡(luò)通信功能。隨著Windows 95的推出。Winsock已經(jīng)被正式集成到了Windows系統(tǒng)中,同時包括了16位和32位的編程接口。而Winsock的開發(fā)工具也可以在Borland C++4.0、Visual C++2.0這些C編譯器中找到,主要由一個名為winsock.h的頭文件和動態(tài)連接庫winsock.dll或wsodk32.dll組成,這兩種動態(tài)連接庫分別用于Win16和Win32的應(yīng)用程序。
本文針對話音的全雙工傳輸要求,采用UDP協(xié)議實現(xiàn)了實時網(wǎng)絡(luò)通信。使用VisualC++2.0編譯環(huán)境,其動態(tài)連接庫名為wsock32.dll。
二、主要函數(shù)的使用要點
通過建立雙套接字,可以很方便地實現(xiàn)全雙工網(wǎng)絡(luò)通信。
1.套接字建立函數(shù):
SOCKET socket(int family,int type,int protocol) |
對于UDP協(xié)議,寫為:
SOCKRET s; s=socket(AF_INET,SOCK_DGRAM,0); 或s=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP) |
為了建立兩個套接字,必須實現(xiàn)地址的重復(fù)綁定,即,當一個套接字已經(jīng)綁定到某本地地址后,為了讓另一個套接字重復(fù)使用該地址,必須為調(diào)用bind()函數(shù)綁定第二個套接字之前,通過函數(shù)setsockopt()為該套接字設(shè)置SO_REUSEADDR套接字選項。通過函數(shù)getsockopt()可獲得套接字選項設(shè)置狀態(tài)。需要注意的是,兩個套接字所對應(yīng)的端口號不能相同。此外,還涉及到套接字緩沖區(qū)的設(shè)置問題,按規(guī)定,每個區(qū)的設(shè)置范圍是:不小于512個字節(jié),大大于8k字節(jié),根據(jù)需要,文中選用了4k字節(jié)。
2.套接字綁定函數(shù)
int bind(SOCKET s,struct sockaddr_in*name,int namelen) |
s是剛才創(chuàng)建好的套接字,name指向描述通訊對象的結(jié)構(gòu)體的指針,namelen是該結(jié)構(gòu)體的長度。該結(jié)構(gòu)體中的分量包括:IP地址(對應(yīng)name.sin_addr.s_addr)、端口號(name.sin_port)、地址類型(name.sin_family,一般都賦成AF_INET,表示是internet地址)。
(1)IP地址的填寫方法:在全雙工通信中,要把用戶名對應(yīng)的點分表示法地址轉(zhuǎn)換成32位長整數(shù)格式的IP地址,使用inet_addr()函數(shù)。
(2)端口號是用于表示同一臺計算機不同的進程(應(yīng)用程序),其分配方法有兩種:1)進程可以讓系統(tǒng)為套接字自動分配一端口號,只要在調(diào)用bind前將端口號指定為0即可。由系統(tǒng)自動分配的端口號位于1024~5000之間,而1~1023之間的任一TCP或UDP端口都是保留的,系統(tǒng)不允許任一進程使用保留端口,除非其有效用戶ID是零(超級用戶)。
2)進程可為套接字指定一特定端口。這對于需要給套接字分配一眾所端口的服務(wù)器是很有用的。指定范圍為1024和65536之間??扇我庵付?。
在本程序中,對兩個套接字的端口號規(guī)定為2000和2001,前者對應(yīng)發(fā)送套接字,后者對應(yīng)接收套接字。
端口號要從一個16位無符號數(shù)(u_short類型數(shù))從主機字節(jié)順序轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)順序,使用htons()函數(shù)。
根據(jù)以上兩個函數(shù),可以給出雙套接字建立與綁定的程序片斷。
//設(shè)置有關(guān)的全局變量 SOCKET sr,ss; HPSTR sockBufferS,sockBufferR; HANDLE hSendData,hReceiveData; DWROD dwDataSize=1024*4; struct sockaddr_in therel.there2; #DEFINE LOCAL_HOST_ADDR 200.200.200.201 #DEFINE REMOTE_HOST-ADDR 200.200.200.202 #DEFINE LOCAL_HOST_PORT 2000 #DEFINE LOCAL_HOST_PORT 2001 //套接字建立函數(shù) BOOL make_skt(HWND hwnd) { struct sockaddr_in here,here1; ss=socket(AF_INET,SOCK_DGRAM,0); sr=socket(AF_INET,SOCK_DGRAM,0); if((ss==INVALID_SOCKET)||(sr==INVALID_SOCKET)) { MessageBox(hwnd,“套接字建立失敗!”,“”,MB_OK); return(FALSE); } here.sin_family=AF_INET; here.sin_addr.s_addr=inet_addr(LOCAL_HOST_ADDR); here.sin_port=htons(LICAL_HOST_PORT); //another socket herel.sin_family=AF_INET; herel.sin_addr.s_addr(LOCAL_HOST_ADDR); herel.sin_port=htons(LOCAL_HOST_PORT1); SocketBuffer();//套接字緩沖區(qū)的鎖定設(shè)置 setsockopt(ss,SOL_SOCKET,SO_SNDBUF,(char FAR*)sockBufferS,dwDataSize); if(bind(ss,(LPSOCKADDR)&here,sizeof(here))) { MessageBox(hwnd,“發(fā)送套接字綁定失敗!”,“”,MB_OK); return(FALSE); } setsockopt(sr SQL_SOCKET,SO_RCVBUF|SO_REUSEADDR,(char FAR*) sockBufferR,dwDataSize); if(bind(sr,(LPSOCKADDR)&here1,sizeof(here1))) { MessageBox(hwnd,“接收套接字綁定失敗!”,“”,MB_OK); return(FALSE); } return(TRUE); } //套接字緩沖區(qū)設(shè)置 void sockBuffer(void) { hSendData=GlobalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize); if(!hSendData) { MessageBox(hwnd,“發(fā)送套接字緩沖區(qū)定位失敗!”,NULL, MB_OK|MB_ICONEXCLAMATION); return; } if((sockBufferS=GlobalLock(hSendData)==NULL) { MessageBox(hwnd,“發(fā)送套接字緩沖區(qū)鎖定失敗!”,NULL, MB_OK|MB_ICONEXCLAMATION); GlobalFree(hRecordData[0]; return; } hReceiveData=globalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize); if(!hReceiveData) { MessageBox(hwnd,"“接收套接字緩沖區(qū)定位敗!”,NULL MB_OK|MB_ICONEXCLAMATION); return; } if((sockBufferT=Globallock(hReceiveData))=NULL) MessageBox(hwnd,"發(fā)送套接字緩沖區(qū)鎖定失敗!”,NULL, MB_OK|MB_ICONEXCLAMATION); GlobalFree(hRecordData[0]); return; } { |
3.數(shù)據(jù)發(fā)送與接收函數(shù);
int sendto(SOCKET s.char*buf,int len,int flags,struct sockaddr_in to,int tolen); int recvfrom(SOCKET s.char*buf,int len,int flags,struct sockaddr_in fron,int*fromlen) |
其中,參數(shù)flags一般取0。
recvfrom()函數(shù)實際上是讀取sendto()函數(shù)發(fā)過來的一個數(shù)據(jù)包,當讀到的數(shù)據(jù)字節(jié)少于規(guī)定接收的數(shù)目時,就把數(shù)據(jù)全部接收,并返回實際接收到的字節(jié)數(shù);當讀到的數(shù)據(jù)多于規(guī)定值時,在數(shù)據(jù)報文方式下,多余的數(shù)據(jù)將被丟棄。而在流方式下,剩余的數(shù)據(jù)由下recvfrom()讀出。為了發(fā)送和接收數(shù)據(jù),必須建立數(shù)據(jù)發(fā)送緩沖區(qū)和數(shù)據(jù)接收緩沖區(qū)。規(guī)定:IP層的一個數(shù)據(jù)報最大不超過64K(含數(shù)據(jù)報頭)。當緩沖區(qū)設(shè)置得過多、過大時,常因內(nèi)存不夠而導(dǎo)致套接字建立失敗。在減小緩沖區(qū)后,該錯誤消失。經(jīng)過實驗,文中選用了4K字節(jié)。
此外,還應(yīng)注意這兩個函數(shù)中最后參數(shù)的寫法,給sendto()的最后參數(shù)是一個整數(shù)值,而recvfrom()的則是指向一整數(shù)值的指針。
4.套接字關(guān)閉函數(shù):closesocket(SOCKET s)
通訊結(jié)束時,應(yīng)關(guān)閉指定的套接字,以釋與之相關(guān)的資源。
在關(guān)閉套接字時,應(yīng)先對鎖定的各種緩沖區(qū)加以釋放。其程序片斷為:
void CloseSocket(void) { GlobalUnlock(hSendData); GlobalFree(hSenddata); GlobalUnlock(hReceiveData); GlobalFree(hReceiveDava); if(WSAAysncSelect(ss,hwnd,0,0)=SOCKET_ERROR) { MessageBos(hwnd,“發(fā)送套接字關(guān)閉失敗!”,“”,MB_OK); return; } if(WSAAysncSelect(sr,hwnd,0,0)==SOCKET_ERROR) { MessageBox(hwnd,“接收套接字關(guān)閉失敗!”,“”,MB_OK); return; } WSACleanup(); closesockent(ss); closesockent(sr); return; } |
三、Winsock的編程特點與異步選擇機制
1 阻塞及其處理方式
在網(wǎng)絡(luò)通訊中,由于網(wǎng)絡(luò)擁擠或一次發(fā)送的數(shù)據(jù)量過大等原因,經(jīng)常會發(fā)生交換的數(shù)據(jù)在短時間內(nèi)不能傳送完,收發(fā)數(shù)據(jù)的函數(shù)因此不能返回,這種現(xiàn)象叫做阻塞。Winsock對有可能阻塞的函數(shù)提供了兩種處理方式:阻塞和非阻塞方式。在阻塞方式下,收發(fā)數(shù)據(jù)的函數(shù)在被調(diào)用后一直要到傳送完畢或者出錯才能返回。在阻塞期間,被阻的函數(shù)不會斷調(diào)用系統(tǒng)函數(shù)GetMessage()來保持消息循環(huán)的正常進行。對于非阻塞方式,函數(shù)被調(diào)用后立即返回,當傳送完成后由Winsock給程序發(fā)一個事先約定好的消息。
在編程時,應(yīng)盡量使用非阻塞方式。因為在阻塞方式下,用戶可能會長時間的等待過程中試圖關(guān)閉程序,因為消息循環(huán)還在起作用,所以程序的窗口可能被關(guān)閉,這樣當函數(shù)從Winsock的動態(tài)連接庫中返回時,主程序已經(jīng)從內(nèi)存中刪除,這顯然是極其危險的。
2 異步選擇函數(shù)WSAAsyncSelect()的使用
Winsock通過WSAAsyncSelect()自動地設(shè)置套接字處于非阻塞方式。使用WindowsSockets實現(xiàn)Windows網(wǎng)絡(luò)程序設(shè)計的關(guān)鍵就是它提供了對網(wǎng)絡(luò)事件基于消息的異步存取,用于注冊應(yīng)用程序感興趣的網(wǎng)絡(luò)事件。它請求Windows Sockets DLL在檢測到套接字上發(fā)生的網(wǎng)絡(luò)事件時,向窗口發(fā)送一個消息。對UDP協(xié)議,這些網(wǎng)絡(luò)事件主要為:
FD_READ 期望在套接字收到數(shù)據(jù)(即讀準備好)時接收通知;
FD_WRITE 期望在套接字可發(fā)送數(shù)(即寫準備好)時接收通知;
FD_CLOSE 期望在套接字關(guān)閉時接電通知
消息變量wParam指示發(fā)生網(wǎng)絡(luò)事件的套接字,變量1Param的低字節(jié)描述發(fā)生的網(wǎng)絡(luò)事件,高字包含錯誤碼。如在窗口函數(shù)的消息循環(huán)中均加一個分支:
int ok=sizeof(SOCKADDR); case wMsg; switch(1Param) { case FD_READ: //套接字上讀數(shù)據(jù) if(recvfrom(sr.lpPlayData[j],dwDataSize,0,(struct sockaddr FAR*)&there1, (int FAR*)&ok)==SOCKET_ERROR0 { MessageBox)hwnd,“數(shù)據(jù)接收失敗!”,“”,MB_OK); return(FALSE); } case FD_WRITE: //套接字上寫數(shù)據(jù) } break; |
在程序的編制中,應(yīng)根據(jù)需要靈活地將WSAAsyncSelect()函靈敏放在相應(yīng)的消息循環(huán)之中,其它說明可參見文獻[1]。此外,應(yīng)該指出的是,以上程序片斷中的消息框主要是為程序調(diào)試方便而設(shè)置的,而在正式產(chǎn)品中不再出現(xiàn)。同時,按照程序容錯誤設(shè)計,應(yīng)建立一個專門的容錯處理函數(shù)。程序中可能出現(xiàn)的各種錯誤都將由該函數(shù)進行處理,依據(jù)錯誤的危害程度不同,建立幾種不同的處理措施。這樣,才能保證雙方通話的順利和可靠。
四、結(jié)論
本文是多媒體網(wǎng)絡(luò)傳輸項目的重要內(nèi)容之一,目前,結(jié)合硬件全雙工語音卡等設(shè)備,已經(jīng)成功地實現(xiàn)了話音的全雙工的通信。有關(guān)整個多媒體傳輸系統(tǒng)設(shè)計的內(nèi)容,將有另文敘述。
VC編程輕松獲取局域網(wǎng)連接通知
摘要:本文從解決實際需要出發(fā),通過采用Windows Socket API等網(wǎng)絡(luò)編程技術(shù)實現(xiàn)了在局域網(wǎng)共享一條電話線的情況下,當服務(wù)器撥號上網(wǎng)時能及時通知各客戶端通過代理服務(wù)器進行上網(wǎng)。本文還特別給出了基于Microsoft Visual C++ 6.0的部分關(guān)鍵實現(xiàn)代碼。
一、 問題提出的背景
筆者所使用的局域網(wǎng)擁有一個服務(wù)器及若干分布于各辦公室的客戶機,通過網(wǎng)卡相連。服務(wù)器不提供專線上網(wǎng),但可以撥號上網(wǎng),而各客戶機可以通過裝在服務(wù)器端的代理服務(wù)器共用一條電話線上網(wǎng),但前提必須是服務(wù)器已經(jīng)撥號連接??紤]到經(jīng)濟原因,服務(wù)器不可能長時間連在網(wǎng)上,因此經(jīng)常出現(xiàn)由于分布于各辦公室的客戶機不能知道服務(wù)器是否處于連線狀態(tài)而造成的想上網(wǎng)時服務(wù)器沒有撥號,或是服務(wù)器已經(jīng)撥號而客戶機卻并不知曉的情況,這無疑會在工作中帶來極大的不便。而筆者作為一名程序設(shè)計人員,有必要利用自己的專業(yè)優(yōu)勢來解決實際工作中所遇到的一些問題。通過對實際情況的分析,可以歸納為一點:當服務(wù)器在進行撥號連接時能及時通知在網(wǎng)絡(luò)上的各個客戶機,而各客戶機在收到服務(wù)器發(fā)來的消息后可以根據(jù)自己的情況來決定是否上網(wǎng)。這樣就可以在同一時間內(nèi)同時為較多的客戶機提供上網(wǎng)服務(wù),此舉不僅提高了利用效率也大大節(jié)省了上網(wǎng)話費。
二、 程序主要設(shè)計思路及實現(xiàn)
由于本網(wǎng)絡(luò)是通過網(wǎng)卡連接的局域網(wǎng),因此可以首選Windows Socket API進行套接字編程。整個系統(tǒng)分為兩部分:服務(wù)端和客戶端。服務(wù)端運行于服務(wù)器上負責(zé)監(jiān)視服務(wù)器是否在進行撥號連接,一旦發(fā)現(xiàn)馬上通過網(wǎng)絡(luò)發(fā)送消息通知客戶端;而客戶端軟件則只需完成同服務(wù)端軟件的連接并能接收到從服務(wù)端發(fā)送來的通知消息即可。服務(wù)器端要完成比客戶端更為繁重的任務(wù)。下面對這幾部分的實現(xiàn)分別加以描述:
(一)監(jiān)視撥號連接事件的發(fā)生
在采用撥號上網(wǎng)時,首先需要通過撥號連接通過電話線連接到ISP上,然后才能享受到ISP所提供的各種互聯(lián)網(wǎng)服務(wù)。而要捕獲撥號連接發(fā)生的事件不能依賴于消息通知,因為此時發(fā)出的消息同一個對話框出現(xiàn)在屏幕上時所產(chǎn)生的消息是一樣的。唯一同其他對話框區(qū)別的是其標題是固定的"撥號連接",因此在無其他特殊情況下(如其他程序的標題也是"撥號連接"時)可以認定當桌面上的所有程序窗口出現(xiàn)以"撥號連接" 為標題的窗口時,即可認定此時正在進行撥號連接。因此可以通過搜尋并判斷窗口標題的辦法對撥號連接進行監(jiān)視,具體可以用CWnd類的FindWindows()函數(shù)來實現(xiàn):
CWnd *pWnd=CWnd::FindWindow(NULL,"撥號連接"); |
第一個參數(shù)為NULL,指定對當前所有窗口都進行搜索。第二個參數(shù)就是待搜尋的窗口標題,一旦找到將返回該窗口的窗口句柄。因此可以在窗口句柄不為空的情況下去通知客戶端服務(wù)器現(xiàn)在正在撥號。由于一般的撥號連接都需要一段時間的連接應(yīng)答后才能登錄到ISP上,因此從提高程序運行效率角度出發(fā)可以通過定時器的使用來每間隔一段時間(如500毫秒)去搜尋一次,以確保能監(jiān)視到每一次的撥號連接而又不致過分加重CPU的負擔(dān)。
(二)服務(wù)器端網(wǎng)絡(luò)通訊功能的實現(xiàn)
在此采用的是可靠的有連接的流式套接字,并且采用了多線程和異步通知機制能有效避免一些函數(shù)如accept()等的阻塞會引起整個程序的阻塞。由于套接字編程方面的書籍資料非常豐富,對其進行網(wǎng)絡(luò)編程做了很詳細的描述,故本文在此只針對一些關(guān)鍵部分做簡要說明,有關(guān)套接字網(wǎng)絡(luò)編程的詳細內(nèi)容請參閱相關(guān)資料。采用流式套接字的服務(wù)器端的主要設(shè)計流程可以歸結(jié)為以下幾步:
1. 創(chuàng)建套接字
sock=socket(AF_INET,SOCK_STREAM,0); |
該函數(shù)的第一個參數(shù)用于指定地址族,在Windows下僅支持AF_INET(TCP/IP地址);第二個參數(shù)用于描述套接字的類型,對于流式套接字提供有SOCK_STREAM;最后一個參數(shù)指定套接字使用的協(xié)議,一般為0。該函數(shù)的返回值保存了新套接字的句柄,在程序退出前可以用closesocket()函數(shù)來將其釋放。
2. 綁定套接字
服務(wù)器方一旦獲取了一個新的套接字后應(yīng)通過bind()將該套接字與本機上的一個端口相關(guān)聯(lián)。此時需要預(yù)先對一個指向包含有本機IP地址和端口信息的sockaddr_in結(jié)構(gòu)填充一些必要的信息,如本地端口號和本地主機地址等。然后就可經(jīng)過bind()將服務(wù)器進程在網(wǎng)絡(luò)上標識出來。需要注意的是由于1024以內(nèi)的埠號都是保留的端口號因此如無特別需要一般不能將sockin.sin_port的端口號設(shè)置為1024以內(nèi)的值:
…… sockin.sin_family=AF_INET; sockin.sin_addr.s_addr=0; sockin.sin_port=htons(USERPORT); bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); …… |
3. 偵聽套接字
listen(sock,1); |
4. 等待客戶機的連接
這里需要通過accept()調(diào)用等待接收客戶端的連接以完成連接的建立,由于該函數(shù)在沒有客戶端進行申請連接之前會處于阻塞狀態(tài),因此如果采取通常的單線程模式會導(dǎo)致整個程序一直處于阻塞狀態(tài)而不能響應(yīng)其他的外界消息,因此為該部分代碼單獨開辟一個線程,這樣阻塞將被限制在該線程內(nèi)而不會影響到程序整體。
AfxBeginThread(Server,NULL);//創(chuàng)建一個新的線程 …… UINT Server(LPVOID lpVoid)//線程的處理函數(shù) { //獲取當前視類的指針,以確保訪問的是當前的實例對象。 CNetServerView* pView=((CNetServerView*)( (CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetActiveView()); while(pView->nNumConns<1)//當前的連接者個數(shù) { int nLen=sizeof(SOCKADDR); pView->newskt= accept(pView->sock, (LPSOCKADDR)& pView->sockin,(LPINT)& nLen); WSAAsyncSelect(pView->newskt, pView->m_hWnd,WM_SOCKET_MSG,FD_CLOSE); pView->nNumConns++; } return 1; } |
這里在accept ()后使用了WSAAsyncSelect()異步選擇函數(shù)。對于網(wǎng)絡(luò)事件的響應(yīng)最好采取異步選擇機制,只有采取這種方式才可以在由網(wǎng)絡(luò)對方所引起的不可預(yù)知的網(wǎng)絡(luò)事件發(fā)生時能馬上在進程中做出及時的響應(yīng)處理,而在沒有網(wǎng)絡(luò)事件到達時則可以處理其他事件,這種效率是很高的,而且完全符合Windows所標榜的消息觸發(fā)原則。WSAAsyncSelect()函數(shù)便是實現(xiàn)網(wǎng)絡(luò)事件異步選擇的核心函數(shù)。通過第四個參數(shù)FD_CLOSE注冊了應(yīng)用程序感興取的網(wǎng)絡(luò)事件是網(wǎng)絡(luò)斷開,當客戶方端開連接時該事件會被檢測到,同時會發(fā)出由第三個參數(shù)指定的自定義消息WM_SOCKET_MSG。
5. 發(fā)送/接收
當客戶機同服務(wù)器建立好連接后就可以通過send()/recv()函數(shù)進行發(fā)送和接收數(shù)據(jù)了,對于本程序只需在監(jiān)測到有撥號連接事件發(fā)生時向客戶機發(fā)送通知消息即可:
char buffer[1]={'a'}; send(newskt,buffer,1,0);//向客戶機發(fā)送字符a,表示現(xiàn)在服務(wù)器正在撥號。 |
6. 關(guān)閉套接字
在全部通訊完成之后,在退出程序之前需要調(diào)用closesocket();函數(shù)把創(chuàng)建的套接字關(guān)閉。
(三)客戶機端的程序設(shè)計
客戶機的編程要相對簡單許多,全部通訊過程只需以下四步:
1. 創(chuàng)建套接字
2. 建立連接
3. 發(fā)送/接收
4. 關(guān)閉套接字
具體實現(xiàn)過程同服務(wù)器編程基本類似,只是由于需要接收數(shù)據(jù),因此待監(jiān)測的網(wǎng)絡(luò)事件為FD_CLOSE和FD_READ,在消息響應(yīng)函數(shù)中可以通過對消息參數(shù)的低位字節(jié)進行判斷而區(qū)分出具體發(fā)生是何種網(wǎng)絡(luò)事件,并對其做出響應(yīng)的反應(yīng)。下面結(jié)合部分主要實現(xiàn)代碼對實現(xiàn)過程進行解釋:
…… m_ServIP=SERVERIP; //指定服務(wù)器的IP地址 m_Port=htons(USERPORT); //指定服務(wù)器的端口號 if((IPaddr=inet_addr(m_ServIP))==INADDR_NONE) //轉(zhuǎn)換成網(wǎng)絡(luò)地址 return FALSE; else { sock=socket(AF_INET,SOCK_STREAM,0); //創(chuàng)建套接字 sockin.sin_family=AF_INET; //填充結(jié)構(gòu) sockin.sin_addr.S_un.S_addr=IPaddr; sockin.sin_port=m_Port; connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); //建立連接 //設(shè)定異步選擇事件 WSAAsyncSelect(sock,m_hWnd,WM_SOCKET_MSG,FD_CLOSE|FD_READ); //在這里可以通過震鈴、彈出對話框等方式通知客戶已經(jīng)連上服務(wù)器 } …… //網(wǎng)絡(luò)事件的消息處理函數(shù) int message=lParam & 0x0000FFFF;//取消息參數(shù)的低位 switch(message) //判斷發(fā)生的是何種網(wǎng)絡(luò)事件 { case FD_READ: //讀事件 AfxBeginThread(Read,NULL); break; case FD_CLOSE: //服務(wù)器關(guān)閉事件 …… break; } |
在讀事件的消息處理過程中,單獨為讀處理過程開辟了一個線程,在該線程中接收從服務(wù)器發(fā)送過來的信息,并通過震鈴、彈出對話框等方式通知客戶端現(xiàn)在服務(wù)器正在撥號:
…… int a=recv(pView->sock,cDataBuffer,1,0); //接收從服務(wù)器發(fā)送來的消息 if(a>0) AfxMessageBox("撥號連接已啟動!"); //通知用戶 …… |
三、必要的完善
前面只是介紹了程序設(shè)計的整體框架和設(shè)計思路,僅僅是一個雛形,有許多重要的細節(jié)沒有完善,不能用于實際使用。下面就對一些完全必要的細節(jié)做適當?shù)耐晟疲?BR>
(一) 界面的隱藏
由于本程序系自動檢測、自動通知,完全不需要人工干預(yù),因此可以將其視為后臺運行的服務(wù)程序,因此程序主界面現(xiàn)在已無存在的必要,可以在應(yīng)用程序類的初始化實例函數(shù)InitInstance()中將ShowWindow();的參數(shù)SW_SHOW改成SW_HIDE即可。當需要有對話框彈出通知用戶時僅對話框出現(xiàn),主界面仍隱藏,因此是完全可行的。
(二) 自啟動的實現(xiàn)
由于服務(wù)端軟件需要時刻監(jiān)視有無進行撥號連接,所以必須具缸云舳奶匭浴6突Ф巳砑捎誚郵障⒑屯ㄖ突Ф伎梢宰遠瓿?,因此染J芫弒缸云舳匭栽蚩梢醞耆牙胗沒У母稍ざ〉媒細叩淖遠潭?。设置自启动的虆Q裕梢源右韻錄父鐾揪都右鑰悸牽?BR>
1. 在"啟動"菜單上添加指向程序的快捷方式。
2. 在Autoexec.bat中添加啟動程序的命令行。
3. 在Win.ini中的[windows]節(jié)的run項目后添加程序路徑。
4. 修改注冊表,添加鍵值的具體路徑為:
"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run"
并將添加的鍵值修改為程序的存放路徑即可。以上幾種方法既可以手工添加,也可以通過編程使之自動完成。
?。ㄈ?自動續(xù)聯(lián)
對于服務(wù)/客戶模式的網(wǎng)絡(luò)通訊程序普遍要求服務(wù)端要先于客戶端運行,而本系統(tǒng)的客戶、服務(wù)端均為自啟動,不能保證服務(wù)器先于客戶機啟動,而且本系統(tǒng)要求只要客戶機和服務(wù)器連接在網(wǎng)絡(luò)上就要不間斷保持連接,因此需要使客戶和服務(wù)端都要具備自動續(xù)聯(lián)的功能。
對于服務(wù)器端,當客戶端斷開時,需要關(guān)閉當前的套接字,并重新啟動一個新的套接字以等待客戶機的再次連接。這可以放在FD_CLOSE事件對應(yīng)的消息WM_SOCKET_MSG的消息響應(yīng)函數(shù)中來完成。而對于客戶端,如果先于服務(wù)器而啟動,則connect()函數(shù)將返回失敗,因此可以在程序啟動時用SetTimer()設(shè)置一個定時器,每隔一段時間(10秒)就試圖連接服務(wù)器一次,當connect()函數(shù)返回成功即服務(wù)器已啟動并與之連接上之后可以用KillTimer()函數(shù)將定時器關(guān)閉。另外當服務(wù)器關(guān)閉時需要再次開啟定時器,以確保當服務(wù)器再次運行時能與之建立連接,可以通過響應(yīng)FD_CLOSE事件來捕獲該事件的發(fā)生。
小結(jié):本文通過Windows Sockets API實現(xiàn)了基于TCP/IP協(xié)議的面向連接的流式套接字的網(wǎng)絡(luò)通訊程序的設(shè)計,通過網(wǎng)絡(luò)通訊程序的支持可以把服務(wù)器捕獲到的撥號連接發(fā)生的事件及時通知給客戶端,最后通過對一些必要的細節(jié)的完善很好解決了在局域網(wǎng)上能及時得到服務(wù)器撥號連接的消息通知。本文所述程序在Windows 98 SE下,由Microsoft Visual C++ 6.0編譯通過;使用的代理服務(wù)器軟件為WinGate 4.3.0;上網(wǎng)方式為撥號上網(wǎng)。
VC++編程實現(xiàn)網(wǎng)絡(luò)嗅探器
引言
從事網(wǎng)絡(luò)安全的技術(shù)人員和相當一部分準黑客(指那些使用現(xiàn)成的黑客軟件進行攻擊而不是根據(jù)需要去自己編寫代碼的人)都一定不會對網(wǎng)絡(luò)嗅探器(sniffer)感到陌生,網(wǎng)絡(luò)嗅探器無論是在網(wǎng)絡(luò)安全還是在黑客攻擊方面均扮演了很重要的角色。通過使用網(wǎng)絡(luò)嗅探器可以把網(wǎng)卡設(shè)置于混雜模式,并可實現(xiàn)對網(wǎng)絡(luò)上傳輸?shù)臄?shù)據(jù)包的捕獲與分析。此分析結(jié)果可供網(wǎng)絡(luò)安全分析之用,但如為黑客所利用也可以為其發(fā)動進一步的攻擊提供有價值的信息??梢?,嗅探器實際是一把雙刃劍。 雖然網(wǎng)絡(luò)嗅探器技術(shù)被黑客利用后會對網(wǎng)絡(luò)安全構(gòu)成一定的威脅,但嗅探器本身的危害并不是很大,主要是用來為其他黑客軟件提供網(wǎng)絡(luò)情報,真正的攻擊主要是由其他黑軟來完成的。而在網(wǎng)絡(luò)安全方面,網(wǎng)絡(luò)嗅探手段可以有效地探測在網(wǎng)絡(luò)上傳輸?shù)臄?shù)據(jù)包信息,通過對這些信息的分析利用是有助于網(wǎng)絡(luò)安全維護的。權(quán)衡利弊,有必要對網(wǎng)絡(luò)嗅探器的實現(xiàn)原理進行介紹。
嗅探器設(shè)計原理
嗅探器作為一種網(wǎng)絡(luò)通訊程序,也是通過對網(wǎng)卡的編程來實現(xiàn)網(wǎng)絡(luò)通訊的,對網(wǎng)卡的編程也是使用通常的套接字(socket)方式來進行。但是,通常的套接字程序只能響應(yīng)與自己硬件地址相匹配的或是以廣播形式發(fā)出的數(shù)據(jù)幀,對于其他形式的數(shù)據(jù)幀比如已到達網(wǎng)絡(luò)接口但卻不是發(fā)給此地址的數(shù)據(jù)幀,網(wǎng)絡(luò)接口在驗證投遞地址并非自身地址之后將不引起響應(yīng),也就是說應(yīng)用程序無法收取到達的數(shù)據(jù)包。而網(wǎng)絡(luò)嗅探器的目的恰恰在于從網(wǎng)卡接收所有經(jīng)過它的數(shù)據(jù)包,這些數(shù)據(jù)包即可以是發(fā)給它的也可以是發(fā)往別處的。顯然,要達到此目的就不能再讓網(wǎng)卡按通常的正常模式工作,而必須將其設(shè)置為混雜模式。
具體到編程實現(xiàn)上,這種對網(wǎng)卡混雜模式的設(shè)置是通過原始套接字(raw socket)來實現(xiàn)的,這也有別于通常經(jīng)常使用的數(shù)據(jù)流套接字和數(shù)據(jù)報套接字。在創(chuàng)建了原始套接字后,需要通過setsockopt()函數(shù)來設(shè)置IP頭操作選項,然后再通過bind()函數(shù)將原始套接字綁定到本地網(wǎng)卡。為了讓原始套接字能接受所有的數(shù)據(jù),還需要通過ioctlsocket()來進行設(shè)置,而且還可以指定是否親自處理IP頭。至此,實際就可以開始對網(wǎng)絡(luò)數(shù)據(jù)包進行嗅探了,對數(shù)據(jù)包的獲取仍象流式套接字或數(shù)據(jù)報套接字那樣通過recv()函數(shù)來完成。但是與其他兩種套接字不同的是,原始套接字此時捕獲到的數(shù)據(jù)包并不僅僅是單純的數(shù)據(jù)信息,而是包含有 IP頭、 TCP頭等信息頭的最原始的數(shù)據(jù)信息,這些信息保留了它在網(wǎng)絡(luò)傳輸時的原貌。通過對這些在低層傳輸?shù)脑夹畔⒌姆治隹梢缘玫接嘘P(guān)網(wǎng)絡(luò)的一些信息。由于這些數(shù)據(jù)經(jīng)過了網(wǎng)絡(luò)層和傳輸層的打包,因此需要根據(jù)其附加的幀頭對數(shù)據(jù)包進行分析。下面先給出結(jié)構(gòu).數(shù)據(jù)包的總體結(jié)構(gòu):
數(shù)據(jù)包 | ||
IP頭 | TCP頭(或其他信息頭) | 數(shù)據(jù) |
數(shù)據(jù)在從應(yīng)用層到達傳輸層時,將添加TCP數(shù)據(jù)段頭,或是UDP數(shù)據(jù)段頭。其中UDP數(shù)據(jù)段頭比較簡單,由一個8字節(jié)的頭和數(shù)據(jù)部分組成,具體格式如下:
16位 | 16位 |
源端口 | 目的端口 |
UDP長度 | UDP校驗和 |
而TCP數(shù)據(jù)頭則比較復(fù)雜,以20個固定字節(jié)開始,在固定頭后面還可以有一些長度不固定的可選項,下面給出TCP數(shù)據(jù)段頭的格式組成:
16位 | 16位 | |||||||
源端口 | 目的端口 | |||||||
順序號 | ||||||||
確認號 | ||||||||
TCP頭長 | (保留)7位 | URG | ACK | PSH | RST | SYN | FIN | 窗口大小 |
校驗和 | 緊急指針 | |||||||
可選項(0或更多的32位字) | ||||||||
數(shù)據(jù)(可選項) |
對于此TCP數(shù)據(jù)段頭的分析在編程實現(xiàn)中可通過數(shù)據(jù)結(jié)構(gòu)_TCP來定義:
typedef struct _TCP{ WORD SrcPort; // 源端口 WORD DstPort; // 目的端口 DWORD SeqNum; // 順序號 DWORD AckNum; // 確認號 BYTE DataOff; // TCP頭長 BYTE Flags; // 標志(URG、ACK等) WORD Window; // 窗口大小 WORD Chksum; // 校驗和 WORD UrgPtr; // 緊急指針 } TCP; typedef TCP *LPTCP; typedef TCP UNALIGNED * ULPTCP; |
在網(wǎng)絡(luò)層,還要給TCP數(shù)據(jù)包添加一個IP數(shù)據(jù)段頭以組成IP數(shù)據(jù)報。IP數(shù)據(jù)頭以大端點機次序傳送,從左到右,版本字段的高位字節(jié)先傳輸(SPARC是大端點機;Pentium是小端點機)。如果是小端點機,就要在發(fā)送和接收時先行轉(zhuǎn)換然后才能進行傳輸。IP數(shù)據(jù)段頭格式如下:
16位 | 16位 | |||
版本 | IHL | 服務(wù)類型 | 總長 | |
標識 | 標志 | 分段偏移 | ||
生命期 | 協(xié)議 | 頭校驗和 | ||
源地址 | ||||
目的地址 | ||||
選項(0或更多) |
同樣,在實際編程中也需要通過一個數(shù)據(jù)結(jié)構(gòu)來表示此IP數(shù)據(jù)段頭,下面給出此數(shù)據(jù)結(jié)構(gòu)的定義:
typedef struct _IP{ union{ BYTE Version; // 版本 BYTE HdrLen; // IHL }; BYTE ServiceType; // 服務(wù)類型 WORD TotalLen; // 總長 WORD ID; // 標識 union{ WORD Flags; // 標志 WORD FragOff; // 分段偏移 }; BYTE TimeToLive; // 生命期 BYTE Protocol; // 協(xié)議 WORD HdrChksum; // 頭校驗和 DWORD SrcAddr; // 源地址 DWORD DstAddr; // 目的地址 BYTE Options; // 選項 } IP; typedef IP * LPIP; typedef IP UNALIGNED * ULPIP; |
在明確了以上幾個數(shù)據(jù)段頭的組成結(jié)構(gòu)后,就可以對捕獲到的數(shù)據(jù)包進行分析了。
嗅探器的具體實現(xiàn)
根據(jù)前面的設(shè)計思路,不難寫出網(wǎng)絡(luò)嗅探器的實現(xiàn)代碼,下面就給出一個簡單的示例,該示例可以捕獲到所有經(jīng)過本地網(wǎng)卡的數(shù)據(jù)包,并可從中分析出協(xié)議、IP源地址、IP目標地址、TCP源端口號、TCP目標端口號以及數(shù)據(jù)包長度等信息。由于前面已經(jīng)將程序的設(shè)計流程講述的比較清楚了,因此這里就不在贅述了,下面就結(jié)合注釋對程序的具體是實現(xiàn)進行講解,同時為程序流程的清晰起見,去掉了錯誤檢查等保護性代碼。主要代碼實現(xiàn)清單為:
// 檢查 Winsock 版本號,WSAData為WSADATA結(jié)構(gòu)對象 WSAStartup(MAKEWORD(2, 2), &WSAData); // 創(chuàng)建原始套接字 sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)); // 設(shè)置IP頭操作選項,其中flag 設(shè)置為ture,親自對IP頭進行處理 setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag)); // 獲取本機名 gethostname((char*)LocalName, sizeof(LocalName)-1); // 獲取本地 IP 地址 pHost = gethostbyname((char*)LocalName)); // 填充SOCKADDR_IN結(jié)構(gòu) addr_in.sin_addr = *(in_addr *)pHost->h_addr_list[0]; //IP addr_in.sin_family = AF_INET; addr_in.sin_port = htons(57274); // 把原始套接字sock 綁定到本地網(wǎng)卡地址上 bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in)); // dwValue為輸入輸出參數(shù),為1時執(zhí)行,0時取消 DWORD dwValue = 1; // 設(shè)置 SOCK_RAW 為SIO_RCVALL,以便接收所有的IP包。其中SIO_RCVALL // 的定義為: #define SIO_RCVALL _WSAIOW(IOC_VENDOR,1) ioctlsocket(sock, SIO_RCVALL, &dwValue); |
前面的工作基本上都是對原始套接字進行設(shè)置,在將原始套接字設(shè)置完畢,使其能按預(yù)期目的工作時,就可以通過recv()函數(shù)從網(wǎng)卡接收數(shù)據(jù)了,接收到的原始數(shù)據(jù)包存放在緩存RecvBuf[]中,緩沖區(qū)長度BUFFER_SIZE定義為65535。然后就可以根據(jù)前面對IP數(shù)據(jù)段頭、TCP數(shù)據(jù)段頭的結(jié)構(gòu)描述而對捕獲的數(shù)據(jù)包進行分析:
while (true) { // 接收原始數(shù)據(jù)包信息 int ret = recv(sock, RecvBuf, BUFFER_SIZE, 0); if (ret > 0) { // 對數(shù)據(jù)包進行分析,并輸出分析結(jié)果 ip = *(IP*)RecvBuf; tcp = *(TCP*)(RecvBuf + ip.HdrLen); TRACE("協(xié)議: %s\r\n",GetProtocolTxt(ip.Protocol)); TRACE("IP源地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.SrcAddr)); TRACE("IP目標地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.DstAddr)); TRACE("TCP源端口號: %d\r\n",tcp.SrcPort); TRACE("TCP目標端口號:%d\r\n",tcp.DstPort); TRACE("數(shù)據(jù)包長度: %d\r\n\r\n\r\n",ntohs(ip.TotalLen)); } } |
其中,在進行協(xié)議分析時,使用了GetProtocolTxt()函數(shù),該函數(shù)負責(zé)將IP包中的協(xié)議(數(shù)字標識的)轉(zhuǎn)化為文字輸出,該函數(shù)實現(xiàn)如下:
#define PROTOCOL_STRING_ICMP_TXT "ICMP" #define PROTOCOL_STRING_TCP_TXT "TCP" #define PROTOCOL_STRING_UDP_TXT "UDP" #define PROTOCOL_STRING_SPX_TXT "SPX" #define PROTOCOL_STRING_NCP_TXT "NCP" #define PROTOCOL_STRING_UNKNOW_TXT "UNKNOW" …… CString CSnifferDlg::GetProtocolTxt(int Protocol) { switch (Protocol){ case IPPROTO_ICMP : //1 /* control message protocol */ return PROTOCOL_STRING_ICMP_TXT; case IPPROTO_TCP : //6 /* tcp */ return PROTOCOL_STRING_TCP_TXT; case IPPROTO_UDP : //17 /* user datagram protocol */ return PROTOCOL_STRING_UDP_TXT; default: return PROTOCOL_STRING_UNKNOW_TXT; } |
最后,為了使程序能成功編譯,需要包含頭文件winsock2.h和ws2tcpip.h。在本示例中將分析結(jié)果用TRACE()宏進行輸出,在調(diào)試狀態(tài)下運行,得到的一個分析結(jié)果如下:
協(xié)議: UDP
IP源地址: 172.168.1.5
IP目標地址: 172.168.1.255
TCP源端口號: 16707
TCP目標端口號:19522
數(shù)據(jù)包長度: 78
……
協(xié)議: TCP
IP源地址: 172.168.1.17
IP目標地址: 172.168.1.1
TCP源端口號: 19714
TCP目標端口號:10
數(shù)據(jù)包長度: 200
……
從分析結(jié)果可以看出,此程序完全具備了嗅探器的數(shù)據(jù)捕獲以及對數(shù)據(jù)包的分析等基本功能。
小結(jié)
本文介紹的以原始套接字方式對網(wǎng)絡(luò)數(shù)據(jù)進行捕獲的方法實現(xiàn)起來比較簡單,尤其是不需要編寫VxD虛擬設(shè)備驅(qū)動程序就可以實現(xiàn)抓包,使得其編寫過程變的非常簡便,但由于捕獲到的數(shù)據(jù)包頭不包含有幀信息,因此不能接收到與 IP 同屬網(wǎng)絡(luò)層的其它數(shù)據(jù)包, 如 ARP數(shù)據(jù)包、RARP數(shù)據(jù)包等。在前面給出的示例程序中考慮到安全因素,沒有對數(shù)據(jù)包做進一步的分析,而是僅僅給出了對一般信息的分析方法。通過本文的介紹,可對原始套接字的使用方法以及TCP/IP協(xié)議結(jié)構(gòu)原理等知識有一個基本的認識。本文所述代碼在Windows 2000下由Microsoft Visual C++ 6.0編譯調(diào)試通過。