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