codefans

          導(dǎo)航

          <2005年8月>
          31123456
          78910111213
          14151617181920
          21222324252627
          28293031123
          45678910

          統(tǒng)計(jì)

          常用鏈接

          留言簿(2)

          隨筆分類(lèi)

          隨筆檔案

          文章分類(lèi)

          文章檔案

          程序設(shè)計(jì)鏈接

          搜索

          最新評(píng)論

          閱讀排行榜

          評(píng)論排行榜

          WinSock網(wǎng)絡(luò)編程實(shí)用寶典

          WinSock網(wǎng)絡(luò)編程實(shí)用寶典

          一、TCP/IP 體系結(jié)構(gòu)與特點(diǎn)

            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   //MAXClient是服務(wù)器可響應(yīng)連接的最大數(shù)目
            {}
           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)  編輯  收藏

          評(píng)論

          # re: WinSock網(wǎng)絡(luò)編程實(shí)用寶典 2005-08-20 01:49 春雷的博客

          我轉(zhuǎn)載的(http://blog.yesky.com/233/xioxu/124233.shtml)  回復(fù)  更多評(píng)論   


          只有注冊(cè)用戶(hù)登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 双城市| 揭阳市| 大荔县| 抚顺市| 高安市| 巴林左旗| 泸水县| 安溪县| 南丹县| 民和| 延庆县| 图们市| 民县| 永修县| 增城市| 云安县| 瑞昌市| 巴林左旗| 张家川| 陆丰市| 颍上县| 碌曲县| 荔波县| 淮南市| 九台市| 隆德县| 兖州市| 永嘉县| 鲁甸县| 大名县| 巩义市| 留坝县| 磴口县| 永兴县| 宝鸡市| 翁牛特旗| 静安区| 资兴市| 拉孜县| 姚安县| 青铜峡市|