天天看點

MFC 網絡程式設計

1.        IP位址

2.        用戶端/伺服器模式

3.        套接字

4.        Windows套接字

5.        Windows套接字程式設計機理

6.        WinSock API

7.        MFC中的套接字

8.        MFC中的套接字2

9.        CAsyncSocket與CSocket的比較

一、IP位址

IP位址用于表示網絡上的各個不同主機的節點,就像家庭住址一樣,郵差通過家庭住址以決定将該信件投往何處。IP位址是一個32位的二進制數。

IP位址包含兩部分:網絡号和主機号,又稱為字首位址和字尾位址。

IP協定将IP位址劃分為5中基本類型:A、B、C、D、E

網絡類别 最大網絡數 第一個網絡号 最後一個網絡号 最高位二進制數
A類 126 1 126 0000 0000
B類 16383 128.1 191.255 1000 0000
C類 2097151 192.0.1 233.255.255 1100 0000

    網絡中要傳輸的資料包括TCP、UDP、ICMP以及IGMP資料都是以IP資料報格式傳輸的,他們都是先封裝為IP資料報,再封裝為以太網幀,最後通過資料線路傳輸。

二、用戶端/伺服器模式

1、                                           二者通信的模式如下:

伺服器程式特點:

l  一般啟動後就一直處于運作狀态,以等待客戶機程序的請求;

l  使用的端口往往是熟知端口,便于客戶機程序連接配接請求;

l  一般擁有較多的系統資源,以便及時響應各個客戶機程序的請求;

l  可以并行處理多個客戶機程序的請求,但數目是有一定的限制;

l  在通信時一般處于被動的一方,不需要知道客戶機的IP位址和端口資訊。

客戶機程式特點

l  在需要伺服器程序的服務時将向伺服器程序請求服務,并建立通信連接配接,得到滿足并完成處理後就終止通信連接配接;

l  使用向系統申請的臨時端口與伺服器程序進行通信,通信完成後将釋放該端口;

l  擁有相對較少的系統資源;

l  在通信時屬于主動的一方,需要事先知道伺服器的IP位址和端口資訊。

2、   用戶端/伺服器程式設計

l  伺服器的并發

伺服器的并發可以通過三種方式實作:多線程、消息驅動、循環處理。

(1)、多線程

     處理方式是當有一個客戶機連接配接請求到來時,伺服器建立一個新線程與這個客戶機進行互動,通過為每一個連接配接請求建立一個線程,伺服器可以同時給多個客戶機提供服務,同時處理多個請求。

(2)、消息驅動

消息驅動之需要一個線程,在消息驅動模式下,當請求到來時,系統發出指定的消息,消息出發伺服器進行處理。需要重寫消息回調函數。

(3)、循環處理

伺服器通過主動輪詢檢視是否有客戶機到達的請求,在循環處理過程中,程式一般不能在I/O請求上阻塞,應采用異步I/O調用。當沒有資料傳輸時功能調用及時傳回,以便同時進行其他處理,實際執行的就是對用戶端的排隊處理。

說明:循環處理是指一個時刻隻能處理一個請求的一種伺服器的實作,并發伺服器是指一個時刻可以處理多個請求的一種伺服器。

l 伺服器的設計算法

面向連接配接的多線程:當有一個新的客戶請求市建立一個新線程。

無連接配接的多線程:每收到一個來自用戶端的資料報後就建立一個從線程,在這個線程裡。處理完成請求後就退出。

三、套接字socke

套接字的概念最初是由BSD Unix作業系統所實作的,是網絡通信的基本構架以及一種網絡程式設計接口,還可以稱作:插座、插口。可以形象地将套接字了解為應用程式與網絡協定之間的插口,也就是程式設計接口。是應用層與傳輸層之間的接口。

從實作的角度來講,非常複雜。套接字是一個複雜的軟體機構,包含了一定的資料結構,包含許多選項,由作業系統核心管理。

從使用的角度來講,非常簡單。對于套接字的操作形成了一種網絡應用程式的程式設計接口(API)。套接字是網絡通信的基石。

套接字是對網絡中不同主機上應用程序之間進行雙向通信的端點的抽象,一個套接字就是網絡上程序通信的一端,提供了應用層程序利用網絡協定棧交換資料的機制。

套接字的概念與檔案句柄類似,一個套接字就是一個通信辨別,由一個短整數表示,實際上就是一個句柄,代表網絡協定中的一組資料,該資料包含了通信雙方的IP位址和目前連接配接的端口資訊。在網絡中要全局地辨別一個參與通信的程序,需要采用三元組:協定、主機IP位址、端口号。要描述兩個應用程序之間的端到端的通信關聯則需要一個五元組:協定、信源機IP位址、信源應用程序端口、信宿機IP位址、信宿應用程序端口。

套接字連接配接的理論步驟:

1)       在伺服器端聲明一個用于監聽用戶端的套接字對象,同時對某個端口設定為監聽狀态;

2)       在用戶端聲明套接字對象,并通過IP和端口向伺服器請求連接配接;

3)       伺服器端監聽到請求指令後,建立一個套接字對象,用于與用戶端綁定,傳送和接收資料,伺服器用于監聽的套接字繼續監聽下一個用戶端的請求。

從套接字所處的地位來講,套接字上聯應用程序,下聯網絡協定棧,是應用程式通過網絡協定棧進行通信的接口,是應用程式與網絡協定棧進行互動的接口。

根據傳輸協定的不同,套接字的類型有三類:

l  流式套接字(SOCK_STREAM)

用于提供面向連接配接、可靠的資料傳輸服務。該服務将保證資料能夠實作無差錯、無重複發送,并按順序接收。流套接字之是以能夠實作可靠的資料服務,原因在于其使用了傳輸控制協定——TCP。這類套接字中,傳輸資料之前必須在兩個應用程序之間建立一條通信連接配接,這就確定了參與通信的兩個應用程序都是活動并且響應的。當連接配接建立之後,應用程序隻要通過套接字向TCP層發送資料流,而另一個應用程序便可以接收到相應的資料流,它們不需要知道傳輸層是如何對資料流進行處理。特别需要注意的是通信連接配接必須顯式建立。該套接字類型适合傳輸大量的資料,但不支援廣播和多點傳播方式。

l  資料報式套接字(SOCK_DGRAM)

提供了一種無連接配接的服務,通信雙方不需要建立任何顯式連接配接,資料可以發送到指定的套接字,并且可以從指定的套接字接收資料。該服務并不能保證資料傳輸的可靠性,資料有可能在傳輸過程中丢失或出現資料重複,且無法保證順序地接收到資料。資料報套接字使用UDP進行資料的傳輸。由于資料包套接字不能保證資料傳輸的可靠性,對于有可能出現的資料丢失情況,需要在程式中做相應的處理。與資料報套接字相比,使用流式套接字是一個更為可靠的方法,但對于某些應用,建立一個顯式連接配接所導緻的系統開銷是令人難以接收的,并且資料報套接字支援廣播和多點傳播方式。

l  原始套接字

與标準套接字(标準套接字指的是前面介紹的流套接字和資料報套接字)的差別在于:原始套接字可以讀寫核心沒有處理的IP資料包,而流套接字隻能讀取TCP的資料,資料報套接字隻能讀取UDP的資料。使用原始套接字的主要目的是為了避開TCP/IP處理機制,被傳送的資料包可以被直接傳送給需要它的應用程式。是以,其主要是在編寫自定義底層協定的應用程式時使用,例如各種不同的TCP/IP實用程式(如ping和arp)都使用原始套接字實作,也可以用來實作資料包捕捉分析等。

從應用程式設計角度來看,套接字就是TCP/IP網絡程式設計接口的集合,它是應用程式與TCP/IP協定族通信的中間軟體抽象層,其中包含了許多函數或例程,程式員可以用它們來開發網絡應用程式。

網絡位元組順序:

不同體系的CPU在記憶體中的資料存儲往往存在差異。如Intel的x86系列處理器将低序位元組存儲在起始位址,而一些RISC架構的處理器,如IBM的370主機使用的PowerPC或Motorola公司生産的CPU,都将高序位元組存儲在起始位置。這兩種不同的存儲方式被稱為低位優先(little-endian)和高位優先(big-endian)。

對于網絡上的位元組表示法有一個标準——網絡位元組順序,它與高位優先相同。以便于不同體系結構的計算機間的通信。Intel 采用的位元組順序稱為"小頭方式",即低位元組在前,高位元組在後 的方式,而标準的網絡順序是"大頭方式",即高位元組在前,低位元組在後的方式。

一般情況下,使用者不需要處理在網絡上發送和接收資料的位元組順序的轉換,但是在下列情況下,需要使用者手動轉換位元組順序。

使用者傳輸的資訊需要網絡解釋,這與發送到其他機器的資料不一樣。如使用者傳輸端口和位址時,必須由網絡了解。當與之通信的伺服器應用程式不是MFC應用程式時,如果通信的兩台機器使用的位元組順序不同,則需要調用位元組轉換。

而下列情況下,不需要使用者手動調用位元組轉換。兩台機器使用相同的位元組順序,并且兩端約定不進行位元組交換。與之通信的伺服器是MFC應用程式。使用者有與之通信的伺服器的源代碼,是以,可以顯式地說明是否轉換位元組順序。可以将伺服器轉換成MFC程式。

在後面會介紹到MFC的 CAsyncSocket類,如果使用此類,使用者必須自己管理需要的位元組順序轉換。

Windows Socket 标準化"大頭方式"位元組順序模型,并提供與"小頭方式"位元組順序的轉換函數。而 CSocket 使用的 CArchive 類使用"小頭方式"位元組 順序,但是 CArchive 類處理了位元組順序轉換的細節。通過在應用程式中使用标準的位元組順序,或使用 WindowsSockets 位元組順序轉換函數,使用者可以編寫靈活的代碼。

如果使用MFC Sockets 程式設計,即用戶端和伺服器端都使用MFC,則不需要關心位元組順序的細節。如果編寫與非MFC應用程式進行通信的應用程式,如FTP伺服器,則使用者在将資料傳入存檔對象前,需要自己管理位元組順序轉換。Windows Sockets 提供了 4 個轉換函數,ntohs()、ntohl()、htons()和 htonl()後面将會介紹到。

四、Windows套接字

Microsoft将Unix套接字中的大部分函數移植到Windows作業系統,形成了Windows套接字。Windows套接字針對Windows作業系統的消息驅動機制,對原有的Unix套接字進行了擴充,定義了一部分新的函數。是 Windows 平台下定義的可以相容二進制資料傳輸的網絡程式設計接口,是基于伯克利加利福尼亞大學的 BSD UNIX Sockets 的實作,目前的版本是 2.0。此規範包括 BSD 格式的 Sockets 函數和 Windows 擴充函數。使用 Windows Sockets 的應用程式可以與任何相容 Windows Sockets API 的網絡程式進行資料通信。

Windows的網絡通信建立在TCP/IP協定的基礎上,TCP/IP協定族包含一系列構成網際網路基礎結構的網絡協定。

Windows 套接字是開放的網絡程式設計接口,完成網絡環境中的資料傳輸功能。

Windows Sockets規範

目前,市面上很多網絡軟體支援 Windows Sockets,包括傳輸控制協定/Internet 協定(TCP/IP)、 Xerox 網絡系統(XNS)、DECNet 協定、Novell 公司的 Internet 包交換和順序包交換協定(IPX/SPX) 等。雖然現在的 Windows Sockets 規範定義了提取 TCP/IP 的 Sockets,但是,任何網絡協定可以通過提供自己實作的 Windows Sockets 的 DLL 版本支援 Windows Sockets。終端仿真器和電子郵件系統都是使用 Windows Sockets 典型執行個體。因為 Windows Sockets 是抽象于底層網絡的,是以,開發人員不 需要了解有關網絡的知識,就可以編寫運作在任何支援 Sockets 的網絡上的應用程式。

五、Windows套接字程式設計機理

    使用 Windows Socket 程式設計時,需要了解幾種程式設計方式,了解這幾種程式設計方式的機理,進而能夠根 據實際情況編寫适合系統需求的程式。主要包括以下幾個方面:阻塞操作、非阻塞操作、異步方式、資料收發。

Windows Socket 中最簡單的方式就是阻塞操作,這也是 Windows 套接字的預設方式。在此種方式下,所有的 I/O 操作都會阻塞,直到操作完全執行完畢。是以,任何線程在同一時間隻能執行一個讀寫操作。如果線程正在執行接收操作,而又沒有資料到達,則線程會阻塞直到有資料到達。雖然此種方式操作最簡單,但是并不是最有效的方式。

與阻塞操作相反,非阻塞讀寫操作在執行操作後立即傳回,并傳回錯誤代碼為WSAEWOULDBLOCK 表示操作還沒有完全執行完。在此種機制下,需要處理當操作執行完成後的代碼,在Windows Socket 中使用網絡事件通知的方式實作。使用者可以使用 WSPSelect()函數注冊感興趣的事件,則當接收到相應的網絡事件,系統會為程式發送事件通知,程式可以再根據自己的需要進行資料處理。

重疊讀寫操作,就是同時執行多個讀寫操作。在 Windows Socket中使用帶有WSA_FLAG_OVERLAPPED選項的WSPSocket()函數建立支援重疊讀寫操作的套接字。用戶端使用WSPRecv()函數或 WSPRecvFrom()函數提供接收資料的緩沖區。如果同時提供一個或多個緩沖區,則資料被放置到其中任何一個使用者緩沖區 中。資料發送端則使用 WSPSend()函數或 WSPSendTo()函數 提供發送資料的緩沖區。重疊讀寫操作都會立即傳回,傳回 0 表示讀寫操作立即完 成,并且使用事件對象 或回調函數通知程式是否已經成功發送或接收,傳回值WSA_IO_PENDING 表示讀寫操作成功,但是還沒有執行完畢。

Windows Socket 中使用 WSPSend()函數和 WSPSendTo()函數完成套接字資料發送功能。使用 WSPRecv()函數和 WSPRecvFrom()函數完成資料接收功能。并且這些函數可以實作自動增加資料包標頭和自動減去資料包標頭的功能,簡化資料解析的過程。

六、WinSockAPI

WinSock套接字是一個基于套接字模型的API,提供了許多套接字函數,他們并不代表協定的某一層次,其實質就是一組程式設計接口。使用者可以利用這些函數進行編寫網絡程式。

套接字程式設計相關資料結構(套接字尋址):

結構體 說明
sockaddr 用于儲存套接字的位址資訊
ockaddr_in 與sockaddr類似

1)       sockaddr結構體

        struct sockaddr{unsigned short sa_family; char sa_data[14]};

    參數說明:sa_family :用于指定位址族,如果是TCP/IP通信,該值取PF_INET和AF_NET;

sa_data :用于儲存套接字的IP位址和端口号資訊。

2)       sockaddr_in資料結構

        struct sockaddr_in {short int sin_family; unsigned short int sin_port;

                       struct in_addr sin_addr;unsigned char sin_zero[8];};

    參數說明:sin_family:用于指定位址族,必須是AF_INET。

sin_port:套接字通信的端口号;

sin_addr:通信的4位元組IP位址,也是一個結構體。

sin_zero[8]:用以填充0,保持與structsockaddr同樣大小。

由于sockaddr資料結構與sockaddr_in資料結構的大小是相同的,指向sockaddr_in的指針可以通過強制轉換,轉換成指向sockaddr結構的指針。以下舉例說明定義一個資料結構以及怎樣定制一個資料結構的方法:

sockaddr_in addr;

       addr.sin_family = AF_INET;

       addr.sin_port = htons(整型的端口号);

       addr.sin_addr.S_un.S_addr = INADDR_ANY;//代指本機,一般推薦

最後一行可以使用另一種方法:

    addr.sin_addr.S_un.S_addr=inet_addr("點分十進制字元串");

3)IPv4套接字位址結構以“sockaddr_in”命名。

以下是關于WinSock API常用的函數:

函數名 解釋 适用場合
socket 用于建立一個套接字 TCP、UDP
bind 實作套接字與主機本地IP位址和端口号的綁定 TCP、UDP
listen 用于将建立的套接字設定為監聽模式 TCP
accept 用于接受用戶端使用connect函數發出的連接配接請求,傳回一個新的套接字 TCP
connect 用于發出一個連接配接請求 TCP
recv 用于從連接配接的套接字中取資料 TCP
send 用于向已建立連接配接的套接字發送資料 TCP
closesocket 關閉套接字連接配接 TCP、UDP
shutdown 關閉套接字讀寫通道,停止套接字接收/傳送的功能。
recvfrom 在無連接配接的套接字上接收資料。 UDP
sendto 在無連接配接套接字上發送資料。 UDP

1)       socket函數用于建立套接字并指定套接字的服務類型。

       SOCKET socket(int af, int type,  intprotocol);

參數說明:af :表示一個位址家族,IPv4是AF_INET,IPv6是AF_INET6。

          type :辨別套接字類型,流式套接字還是資料報套接字或者原始套接字。

          Protocol :表示一個特殊的協定,通常為0,表示采用TCP/IP協定。

傳回值:建立成功傳回建立套接字描述,否則傳回SOCKET_ERROR錯誤,可以使用函數WSAGetLastError擷取相應的錯誤代碼。

af的其他常量:

type的可選值:

2)       bind函數用于綁定本地IP位址和端口号到建立立的套接字。

       int bind ( SOCKET s, const struct sockaddr FAR *name, int namelen );

參數說明:s :是一個套接字。

          name :是一個sockaddr_in結構的指針,包含了要綁定的位址和端口号。

          namelen :确定name緩沖區的長度。

傳回值:執行成功傳回0,否則傳回SOCKET_ERROR。

示例代碼:int port = 9090;

SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in addr;

addr.sin_family = AF_INET;

addr.sin_port = htons(port);

addr.sin_addr.S_un.S_addr = inet_addr(IPString);

bind(s, (sockaddr*)&addr, sizeof(addr));

3)       listen函數(不會阻塞程序)

       int listen ( SOCKET s, int backlog );

參數說明:s :套接字

          Backlog :等待連接配接的最大隊列長度,比如:設定為3,有4個客戶請求時,最後一個會得到錯誤資訊。SOMAXCONN表示最大的連接配接數。

傳回值:成功傳回0,失敗傳回SOCKET_ERROR。

4)       accept函數(會阻塞程序)

       SOCKET accept ( SCOKET s, struct sockaddr FAR *addr, int FAR *addrlen );

參數說明:s :是一個處于監聽狀态的套接字。

          addr :是一個sockaddr_in的指針,包含一組客戶的端口号、IP位址等資訊。

          addrlen :用于接收參數addr的長度。可用sizeof()擷取。

傳回值:成功傳回建立套接字的描述句柄,否則傳回INVALID_SOCKET。

5)       connect函數(會阻塞程序)

        int connect ( SOCKET s, const struct sockaddr FAR *name, int namelen );

參數說明:s :辨別一個套接字。

          name :連接配接主機的IP位址和端口号。

          namelen :name緩沖區的長度。

傳回值:成功傳回0,否則傳回SOCKET_ERROR。

6)       recv函數

        int recv ( SOCKET s, char FAR *buf,int len, int flags );

參數說明:s :連接配接好的套接字。

          buf :接收資料的緩沖區。

          len :buf的長度。

         flags :函數的調用方式。一般情況為0,MSG_PEEK 檢視資料,輸入隊列不删除,MSG_OOB 處理帶外資料。

    傳回值:成功傳回接收到的位元組數,否則傳回SOCKET_ERROR。

7)       send函數

       int send ( SOCKET s, const char FAR *buf, int len,int flags );

參數說明:s :已建立連接配接的套接字。

          buf :發送資料的緩沖區。

          len :buf緩沖區的長度。為0是不發送任何資料。

          flag :表示函數的調用方式。通常為0,MSG_DONTROUTE不為資料選擇路由MSG_OOB發送帶外資料警隊TCP有效。

傳回值:成功傳回發送的位元組數,失敗傳回SOCKET_ERROR。

有關這個函數的兩種形式加以說明,兩種形式指的是使用字元數組和字元串的形式:

字元數組:charbuf[1024];

             send(m_link, buf, strlen(buf), 0);

字元串  :CString str;

             send(m_link, str.GetBuffer(0),str.GetLength(), 0);

8)       closesocket函數

        int closesocket (SOCKET s );

    參數說明:s :要關閉的套接字,如果s設定為SO_DONTLINGER,等所有資料發完後才關閉套接字連接配接。

    傳回值:成功傳回0,失敗傳回SOCKET_ERROR。

9)       shutdown函數禁止在套接字上發送和接收資料,但是不會關閉套接字。

一般在關閉套接字之前調用。

        int shutdown ( SOCKET s, int how );

參數說明:s :要關閉的套接字連接配接。

          how :标志位。SD_RECEIVE 禁止調用接收函數

                        SD_SEND 禁止調用發送資料函數

                        SD_BOTH 接收和發送函數均禁止。

    傳回值:成功傳回0,失敗傳回SOCKET_ERROR。

10)    recvfrom函數

         int recvfrom ( SOCKET s,char FAR *buf,int len, int flags,struct socketaddr FAR *from, int FAR *fromlen );

    參數說明:s :已建立連接配接的套接字。

              buf :接收資料緩沖區。

              len :緩沖區長度。

             flags :接收方式指針。

              from:可選指針,指向裝有源位址的緩沖區。

             fromlen :可選指針,指向from緩沖區的長度。

    傳回值:成功傳回接收到的位元組數,否則傳回SOCKET_ERROR。

    說明:其中的最後兩個參數是用來存放對方的SOCKET資訊的資料結構。這個資料結構可以用在sendto函數的後兩個參數中。

11)    sendto函數

         intsendto ( SOCKET s, const char FAR *buf, int len,int flags, const structsockaddr FAR *to, int tolen );

    參數說明:s :建立連接配接成功的套接字

              buf :發送資料的緩沖區。

              len :緩沖區buf的長達。

             flags :函數調用方式。

              to :可選指針,指向目的套接字的位址。

             tolen :to所指位址的長度。

    傳回值:成功傳回發送的位元組數,失敗傳回SOCKET_ERROR。

    關于WinSock API的擴充函數:

htonl函數 :将4位元組主機位元組順序的數轉換為網絡位元組順序。

格式 :u_long htonl (u_long hostlong);

htons函數 :将2位元組主機位元組順序的資料轉換為網絡位元組順序。

格式 :u_short htons (u_short hostshort);

ntohl函數 :4位元組網絡位元組順序的數轉換為主機位元組順序。

格式 :u_long ntohl (u_long netlong);

ntohs函數 :将2位元組網絡位元組順序的資料轉換為主機位元組順序。

格式 :u_short ntohs(u_short netshort);

inet_addr函數 :将點分十進制字元串表示的IP位址轉換為網絡位元組順序的IP位址。

格式 :unsigned long inet_addr (const char* cp);

inet_ntoa函數 :将網絡位元組順序表示的IP位址轉換為點分十進制數表示的IP位址。

格式:char* FAR inet_ntoa(struct in_addrin);

WinSock2的擴充函數

Windows 擴充函數 功    能
WSAAccept() accept()函數的擴充版本,允許條件接收和 Socket 分組
WSAAsyncGetHostByAddr() 根據位址異步擷取主機,基于消息實作
WSAAsyncGetHostByName() 根據名稱異步擷取主機,基于消息實作
WSAAsyncGetProtoByName() 根據名稱異步擷取協定資訊,基于消息實作
WSAAsyncGetProtoByNumber() 根據協定号異步擷取協定資訊,基于消息實作
WSAAsyncGetServByName() 根據伺服器名稱和端口号,異步擷取伺服器資訊,其是基于消息實作的
WSAAsyncGetServByPort() 根據端口号和協定,異步擷取伺服器資訊,其是基于消息實作的
WSAAsyncSelect() 實作異步版本的 select()函數
WSACancelAsyncRequest() 取消異步擷取系列的函數,即取消WSAAsyncGetXByY()函數
WSACleanup() 退出底層的 Windows Socket DLL的引用
WSACloseEvent() 銷毀事件對象
WSAConnect() Connect()函數的擴充版本,允許交換連接配接資料和QOS标準
WSACreateEvent() 建立事件對象
WSADuplicateSocket() 複制 Socket
WSAEnumNetworkEvents() 枚舉網絡事件
WSAEnumProtocols() 枚舉目前系統中每個有效的協定信
WSAEventSelect() 連接配接網絡事件和事件對象
WSAGetLastError() 擷取最近的 Windows Socket 錯誤資訊
WSAGetOverlappedResult() 傳回重疊操作的完成狀态
WSAGetQOSByName() 根據服務名擷取 QOS 參數
WSAHtonl() Htonl()函數的擴充版本,将32位整數從主機位元組順序轉換成網絡位元組順序
WSAHtons() Htons()函數的擴充版本,将16位整數從主機位元組順序轉換成網絡位元組順序
WSAIoctl() ioctl 函數的重疊執行版本
WSAJoinLeaf() 增加一個結點到會話中
WSANtohl() ntohl()函數的擴充版本,将32位整數從網絡位元組順序轉換成主機位元組順序
WSANtohs() ntohs()函數的擴充版本,将16位整數從網絡位元組順序轉換成主機位元組順序
WSAProviderConfigChange() 接收安裝服務或解除安裝服務的通知消息
WSARecv() Recv()函數的擴充版本
WSARecvFrom() recvfrom()函數的擴充版本
WSAResetEvent() 重置事件對象
WSASend() send()函數的擴充版本
WSASendTo() sendto()函數的擴充版本
WSASetEvent() 設定事件對象
WSASetLastError() 設定最近的錯誤資訊
WSASocket() socket()函數的擴充版本。使用WSAPROTOCOL_INFO結構作為輸入參數并建立重疊socket
WSAStartup() 初始化 Windows Sockets DLL
WSAWaitForMultipleEvents() 在多個事件對象上阻塞

上面這些擴充函數是對Windows Socket 規範提供的Socket 函數的封裝,支援消息和函數處理。如在WSAAsyncGetServByName()函數中,可以指定接收消息的對話框句柄和消息,當異步函數執行完畢後,會發送消息給對話框,使用者可以在對話框中捕獲相應的消息進行處理。這與Windows的消息程式設計模式是一緻的。是以,Windows Socket 擴充函數的封裝友善了Socket 程式的開發。使用者可以盡量使用擴充函數開發 Socket 程式。

部分函數說明:

WSAStartup函數:初始化ws2_32.lib動态庫,确定windows Socket使用的版本。

          格式 :int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

    傳回值:成功傳回0

     下面的代碼用于确定其使用版本:

         WSADATA  wsd;

         If(WSAStartup(MAKEWORD(2,0), &wsd) != 0)//定義的版本号并初始化

         {

               Return;

}

WSACleanup函數 :終止使用WinSock,釋放為應用程式配置設定的相關資源。當時用完Windows Socket時,應該調用本函數釋放配置設定給應用程式或動态庫的資源。

格式 :int WSACleanup (void);

WSAAsyncSelect函數 :用于将網絡中發生的事件關聯到視窗的某個消息中。

               格式 :int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, longlEvent);

    示例代碼:綁定到視窗之後隻需将消息函數PreTranslateMessage即可。

綁定函數:

    WSAAsyncSelect(m_client,m_hWnd, RECEIVEMEAASGE, FD_READ);

 client :SOCKET連接配接

 m_hWnd :視窗句柄,不可用GetSafeHwnd來擷取。

 RECEIVEMEAASGE :消息編号,是一個符号常量。

 FD_READ :可處理的事件

 視窗消息:

BOOLCTCPApiProConCliDlg::PreTranslateMessage(MSG* pMsg)

{

       //TODO: Add your specialized code here and/or call the base class

       if(pMsg->message== RECEIVEMEAASGE)

       {

              HandleReceive();//自定義消息處理函數

              returnTRUE;

       }

       else

       {

              returnCDialog::PreTranslateMessage(pMsg);

       }

}

擷取主機資訊函數

Windows Socket 規範中定義了一組專門用于處理域名、通信服務和通信協定等網絡資訊的資料庫函數。使用這些函數可以擷取網絡能力的檢測,使用getXbyY的函數形式,含義是通過Y值擷取X值。主 要包括如下9個函數。

函數名 描述
gethostbyaddr 傳回對應于位址的主機資訊
gethostbyname 傳回對應于給定主機名的包含主機名字和位址資訊的hostent結構指針。
gethostname 傳回本地計算機的标準主機名
getpeername 傳回和套接字連接配接的對方位址
getsockname 傳回和套接字連接配接的本機位址名
getprotobyname 傳回與協定名相對的協定資訊
getprotobynumbe 傳回與協定号相對應的協定資訊
getservbyname 傳回對應于給定服務名和協定名的相關服務資訊。
getservbyport 傳回對應于給定端口号和協定名的相關服務資訊。
getsockopt 取得套接字選項
setsockopt 設定套接字選項

(1)gethostbyaddr()函數:根據網絡位址擷取主機資訊。其函數原型為:

       struct HOSTENT FAR *gethostbyaddr (

const char FAR * addr,    //網絡位元組順序的位址指針

int len,                //addr參數的長度

int type,                //指定位址的類型

);

傳回值 :函數傳回一個HOSTEN 結構的指針,其中包含傳入的網絡位址對應的名稱和位址,所有資料都是以NULL結束。如果傳回值為 NULL,表示函數調用失敗。

(2)gethostbyname()函數:從主機資料庫中根據主機名稱擷取主機資訊。其函數原型為:

struct hostent FAR * gethostbyname (

const char FAR * name );// 以 NULL 結束的主機名稱

傳回值:函數傳回一個指向hostent結構的指針,結構中的name參數中包含查詢到的結果值。如果傳回值為 NULL表示函數調用失敗。

補充:struct hostent

     struct hostent

     {

        char    *h_name;               

        char    **h_aliases;

        int     h_addrtype;

        int     h_length;

        char    **h_addr_list;

        #define h_addrh_addr_list[0]

    };

參數說明:

h_name:表示的是主機的規範名。例如www.google.com的規範名其實是www.l.google.com。

    h_aliases:表示的是主機的别名www.google.com就是google他自己的别名。有的時候,有的主機可能有好幾個别名,這些,其實都是為了易于使用者記憶而為自己的網站多取的名字。

    h_addrtype:表示的是主機ip位址的類型,到底是ipv4,還是pv6

    h_length:表示的是主機ip位址的長度

    h_addr_lisst:表示的是主機的ip位址,注意,這個是以網絡位元組序存儲的。千萬不要直接用printf帶%s參數來打這個東西,會有問題的。是以到真正需要列印出這個IP的話,需要調用inet_ntop()。

(3)gethostname()函數:傳回本地主機的機器名稱。其函數原型為:

int gethostname(

char FAR * name,

int namelen

);

參數說明:name :存放傳回的主機名稱的緩沖區的指針

          namelen :存放緩沖區的長度 此函數傳回本地主機的機器名稱到 name 參數指定的緩沖區中,傳回的主機名是以 NULL 結束的字元串。傳回的主機名的形式,可能是簡單的主機名也可能是完整的域稱。

傳回值:如果函數調用成功,則傳回0,否則傳回SOCKET_ERROR和相應的錯誤代碼,使用 WSAGet LastError()函數可以擷取錯誤代碼的值。

示例:(2)、(3)的應用

charhostname[256]; //存放的是主機名

      struct hostent *host;

      if(gethostname(hostname, 256) == 0)

      {

             host = gethostbyname(hostname);

             for(int i = 0;host != NULL&& host->h_addr_list[i] != NULL; i ++)

             {

                    LPCTSTR hostip =inet_ntoa(*(struct in_addr*)host->h_addr_list[i]);//主機IP

                m_edit_ip = hostip;//直接是一個字元串類型

             }

             UpdateData(FALSE);

      }

(6)getprotobyname()函數:根據協定名稱擷取協定資訊。其函數原型為:

struct PROTOENT FAR * getprotobyname (

const char FAR * name );// 以 NULL 結束的協定名的指針

傳回值:函數傳回name參數指定的包含協定名和協定号的 PROTOENT 結構指針。如果傳回值為NULL,表示函數調用失敗。

(7)getprotobynumber()函數:根據協定号擷取協定資訊。其函數原型為:

struct PROTOENT FAR * getprotobynumber (

int number );//要查詢的協定的主機位元組順序的協定号

傳回值:函數傳回包含協定名和協定号的PROTOENT結構指針。如果傳回值為 NULL,表示函數調用失敗。

(8)getservbyname()函數:根據伺服器名和協定擷取伺服器資訊。其函數原型為:

struct servent FAR * getservbyname (

const char FAR * name, // 指向伺服器 名稱的以 NULL 結束的字元串的指針

const char FAR * proto);       //指向協定名 的以 NULL 結束的字元串的指針

傳回值:函數傳回包含服務名稱和服務号的SERVENT結構的指針。如果傳回值為 NULL,表示函數調用失敗。

(9)getservbyport()函數:根據端口号和協定擷取服務資訊。其函數原型為:

struct servent FAR * getservbyport (

int port,   // 指定網絡位元組順序的服務的端口

const char FAR* proto);      //協定名指針

傳回值:以函數傳回包含服務名稱和服務号的SERVENT結構的指針。如果傳回值為 NULL,表示函數調用失敗。

上面這些函數是用于擷取有關網絡方面的通信、協定、域名等方面的資訊的資料庫函數。使用者可以使用這些函數查詢到socket程式所使用的網絡資源資訊。在socket程式中需要使用這些函數配合socket()函數完成socket通信功能。

函數總結:Socket接口包括三類函數:

第一類是WinSock API包含的Berkeley socket函數。這類函數分兩部分。第一部分是用于網絡I/O的函數,如

accept、closesocket、connect、recv、recvfrom、select、send、sendto

另一部分是不涉及網絡I/O、在本地端完成的函數,如

bind、getpeername、getsockname、getsocketopt、htonl、htons、inet_addr、inet_nton、ioctlsocket、listen、ntohl、ntohs、setsocketopt、shutdow、socket等

第二類是檢索有關域名、通信服務和協定等Internet資訊的資料庫函數,如

gethostbyaddr、gethostbyname、gethostname、getprotolbyname、getprotolbynumber、getserverbyname、getservbyport。

第三類是Berkekley socket例程的Windows專用的擴充函數,如gethostbyname對應的WSAAsynGetHostByName(其他資料庫函數除了gethostname都有異步版本),select對應的WSAAsynSelect,判斷是否阻塞的函數WSAIsBlocking,得到上一次Windsock API錯誤資訊的WSAGetLastError,等等。

使用winSock API程式設計的準備工作如下(TCP/UDP都需要):

1)       在頭檔案StdAfx.h中引入頭檔案并導入網絡庫檔案。

       方式一:#include<stdio.h>

#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

       方式二:#include<stdio.h>

#include <winsock.h>

#pragma comment(lib, "wsock32.lib")

        說明:可以使用連結庫函數:工程->設定->Link->輸入ws2_32.lib,OK!替換最後一句。

2)       在CXXXApp的初始化函數中進行初始化套接字操作:

               WSADATA wsd;

                  if (WSAStartup(MAKEWORD(2, 2),&wsd) != 0)

                  {

                          WSACleanup();

                          exit(1);

                  }

基于TCP的程式設計流程:

基于UDP的程式設計流程如下:

七、MFC中的套接字

随着計算機網絡化的深入,計算機網絡程式設計在程式設計的過程中變得日益重要。由于C++語言對底層操作的優越性,許多文章都曾經介紹過用VC++進行Socket程式設計的方法。但由于都是直接利用動态連接配接庫wsock32.dll進行操作,實作比較繁瑣。其實,VC++的MFC類庫中提供了CAsyncSocket這樣一個套接字類,用他來實作Socket程式設計,是非常友善的。

在MFC中累的繼承關系如下所示:CObject–> CAsyncSocket –> CSocket

MFC 提供兩個類支援使用 Windows Sockets API 進行網絡程式設計。類 CAsyncSocket 一對一地封裝 了 Windows Sockets API。類 CSocket 提供了從 CArchive 對象中序列化資料的 Sockets 功能。

下面對兩個累的介紹分别叙述如下:

1、   CAsyncSocket類的相關知識

為了簡化套接字程式設計,MFC使用CAsyncSocket類較為底層封裝了Windows Sockets API。使用此類可以直接使用 Sockets API 編寫靈活的程式,同時又可以友善地處理網絡事件。除了使用C++将Sockets包裝成面向對象的形式外,類還将與Sockets相關的Windows消息轉換成事件通知。CAsyncSocket 類的部分函數與Windows Socket API的函數是一對一的,但是簡化了事件通知的開發過程。

原本WinSock API是同步模式,如果網絡出了問題或者其它原因他會一直阻塞,原因就是太過簡單的同步模式,它是讓像send()這樣的調用一直處于線程挂起狀态,英特網上的通信可不像在PC裡CPU跟外設的通信一樣簡單,比如我們打開一個網頁,不可能一下就打開,阻塞是常有的事,很多時候我們都在等,等待你的PC連到外面集線器的那根線上什麼時候有信号過來,事實是這樣,需要等待,但是我們不能讓線程傻傻地等待,采用多線程的方法,讓另外一個線程等待,設定逾時計時器等等方法,讓原來的線程可以幹别的事,比如寫一行字‘正在連接配接...’,這就是異步方式,CAsyncSocket就是個比較好的異步方式封裝。

CAsyncSock類是一個異步非阻塞Socket封裝類,該類封裝了一些基本的WinSockAPI函數,提供了與較底層的Windows套接字的對話接口,其對象既可以工作在“阻塞模式”也可以工作在“非阻塞模式”,一般主要用其功能的是在非阻塞模式下工作。以下是CAsyncSocket的模式切換方式:

DWORD dw;

dw=0; //切換為同步模式

IOCtl(FIONBIO,&dw);

dw=1; //切換回異步模式

IOCtl(FIONBIO,&dw);

CAsyncSocket::Create()有一個參數指明了你想要處理哪些Socket事件,你關心的事件被指定以後,這個Socket預設就被用作了異步方式。那麼CAsyncSocket内部到底是如何将事件交給你的呢?

CAsyncSocket的Create()函數,除了建立了一個SOCKET以外,還建立了個CSocketWnd視窗對象,并使用 WSAAsyncSelect()将這個SOCKET與該視窗對象關聯,以讓該視窗對象處理來自Socket的事件(消息),然而CSocketWnd收到Socket事件之後,隻是簡單地回調CAsyncSocket::OnReceive(),CAsyncSocket::OnSend(), CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虛函數。是以CAsyncSocket的派生類,隻需要在這些虛函數裡添加發送和接收的代碼。以下是這個消息的實作機制:

switch (WSAGETSELECTEVENT(lParam))

{

     case FD_READ:

       pSocket->OnReceive(nErrorCode);

        break;

     case FD_WRITE:

       pSocket->OnSend(nErrorCode);

        break;

     case FD_OOB:

       pSocket->OnOutOfBandData(nErrorCode);

        break;

     case FD_ACCEPT:

       pSocket->OnAccept(nErrorCode);

        break;

     case FD_CONNECT:

       pSocket->OnConnect(nErrorCode);

        break;

     case FD_CLOSE:

        pSocket->OnClose(nErrorCode);

        break;

     }

}

CAsyncSocket異步機制如下:

    當你獲得了一個異步連接配接後,實際上你掃除了發送動作與接收動作之間的依賴性。是以你随時可以發包,也随時可能收到包。發送、接收函數都是異步非阻塞的,頃刻就能傳回,是以收發交錯進行着,你可以一直工作,保持很高的效率。但是,正因為發送、接收函數都是異步非阻塞的,是以僅調用它們并不能保障發送或接收的完成。例如發送函數Send,調用它可能有4種結果:

1、錯誤,Send()==SOCKET_ERROR,GetLastError()!=WSAEWOULDBLOCK,這種情況可能由各種網絡問題導,你需要馬上決定是放棄本次操作,還是啟用某種對策

2、忙,Send()==SOCKET_ERROR,GetLastError()==WSAEWOULDBLOCK,導緻這種情況的原因是,你的發送緩沖區已被填滿或對方的接受緩沖區已被填滿。這種情況你實際上不用馬上理睬。因為CAsyncSocket會記得你的Send WSAEWOULDBLOCK了,待發送的資料會寫入CAsyncSocket内部的發送緩沖區,并會在不忙的時候自動調用OnSend,發送内部緩沖區裡的資料。

3、部分完成,0<Send(pBuf,nLen)<nLen,導緻這種情況的原因是,你的發送緩沖區或對方的接收緩沖區中剩餘的空位不足以容納你這次需要發送的全部資料。處理這種情況的通常做法是繼續發送尚未發送的資料直到全部完成或WSAEWOULDBLOCK。這種情況很容易讓人産生疑惑,既然緩沖區空位不足,那麼本次發送就已經填滿了緩沖區,幹嘛還要繼續發送呢,就像WSAEWOULDBLOCK了一樣直接交給OnSend去處理剩

餘資料的發送不是更合理嗎?然而很遺憾,CAsyncSocket不會記得你隻完成了部分發送任務進而在合适的時候觸發OnSend,因為你并沒有WSAEWOULDBLOCK。你可能認為既然已經填滿緩沖區,繼續發送必然會WSAEWOULDBLOCK,其實不然,假如WSAEWOULDBLOCK是由于對方讀取接收緩沖區不及時引起的,繼續發送确很可能會WSAEWOULDBLOCK,但假如WSAEWOULDBLOCK是由于發送緩沖區被填滿,就不一定了,因為你的網卡處理發送緩沖區中資料的速度不見得比你往發送緩沖區拷貝資料的速度更慢,這要取決與你競争CPU、記憶體、帶寬資源的其他應用程式的具體情況。假如這時候CPU負載較大而網卡負載較低,則雖然剛剛發送緩沖區是滿的,你繼續發送也不會WSAEWOULDBLOCK。

4、完成,Send(pBuf,nLen)==nLen

與OnSend協助Send完成工作一樣,OnRecieve、OnConnect、OnAccept也會分别協助Recieve、Connect、Accept完成工作。這一切都通過消息機制完成。 使用CAsyncSocket時,Send流程和Recieve流程是不同的,不了解這一點就不可能順利使用CAsyncSocket。MSDN對CAsyncSocket的解釋很容易讓你了解為:隻有OnSend被觸發時你Send才有意義,你才應該Send,同樣隻有OnRecieve被觸發時你才應該Recieve。很不幸,你錯了:你會發現,連接配接建立的同時,OnSend就第一次被觸發了,嗯,這很好,但你現在還不想Send,你讓OnSend

傳回,幹點其他的事情,等待下一次OnSend試試看?實際上,你再也等不到OnSend被觸發了。因為,除了第一次以外,OnSend的任何一次觸發,都源于你調用了Send,但碰到了WSAEWOULDBLOCK!是以,使用CAsyncSocket時,針對發送的流程邏輯應該是:你需兩個成員變量,一個發送任務表,一個記錄發送進度。你可以在任何你需要的時候,主動調用Send來發送資料,同時更新任務表和發送進度。而OnSend,則是你的負責擦屁股工作的助手,它被觸發時要幹的事情就是根據任務表和發送進度調用Send繼續發。若又沒能将任務表全部發送完成,更新發送進度,退出,等待下一次OnSend;若任務表已全部發送完畢,則清空任務表及發送進度。

使用CAsyncSocket的接收流程邏輯是不同的:你永遠不需要主動調用Recieve,你隻應該在OnRecieve中等待。由于你不可能知道将要抵達的資料類型及次序,是以你需要定義一個已收資料表作為成員變量來存儲已收到但尚未處理的資料。每次OnRecieve被觸發,你隻需要被動調用一次Recieve來接受固定長度的資料,并添加到你的已收資料表後。然後你需要掃描已收資料表,若其中已包含一條或數條完整的可解析的業務資料包,截取出來,調用業務處理視窗的處理函數來處理或作為消息參數發送給業務處理視窗。而已收資料表中剩下的資料,将等待下次OnRecieve中被再次組合、掃描并處理。

l  m_hSocket

該資料變量包含由CAsyncSocket類封裝的SOCKET類型的套接字句柄。有一個有用的用處就是判斷套接字對象是佛偶Create過,如下代碼:

       if(m_handle.m_hSocket != INVALID_SOCKET)

       {

              m_handle.Close();

       }

意思是當套接字建立之後就不可再建立了,套接字建立了就不是INVALID_SOCKET,

在長連接配接應用中,連接配接可能因為各種原因中斷,是以你需要自動重連。你需要根據CAsyncSocket的成員變量m_hSocket來判斷目前連接配接狀态:

if(m_hSocket==INVALID_SOCKET)。當然,很奇怪的是,即使連接配接已經中斷,OnClose也已經被觸發,你還是需要在OnClose中主動調用Close,否則m_hSocket并不會被自動指派為INVALID_SOCKET。

l  相關函數簡介

    CAsyncSocket類定義了一組非常有用的成員函數,這些函數的功能和格式與WinSockAPI函數類似。

(1)      CAsyncSocket

這是CAsyncSocket的構造函數,構造出一個空的套接字對象,但沒有指定端口和IP,構造完後還必須調用CAsyncSocket類的Create函數建立SOCKET句柄。函數原型:

            CAsyncSocket()

(2)Create

調用它的Create成員函數,來建立底層的套接字資料結構,并綁定它的位址,決定套接字對象的具體特性。函數原型:

        BOOL  Create(

UINT nSocketPor,//套接字綁定端口

Int nSocketType,   //用于選擇套接字類性

Long  Ievent,     //注冊程式感興趣的網絡事件

            LPCTSTR lpszSocketAddress = NULL, //指定與套接字綁定的主機位址。

        );

 傳回值:成功傳回非0,失敗傳回0。

說明:實作代碼

BOOLCAsyncSocket::Create(UINT nSocketPort, int nSocketType,long lEvent, LPCTSTRlpszSocketAddress)

{

if (Socket(nSocketType, lEvent))

{

if (Bind(nSocketPort,lpszSocketAddress))

return TRUE;

int nResult = GetLastError();

Close();

WSASetLastError(nResult);

}

return FALSE;

}

可見在一般的TCP/IP協定中是不需要再重載bind函數的。

(3)Accept

用于連接配接請求,隻能用于基于面向連接配接的類型的套接字。函數原型如下:

       virtual BOOL Accept(

CAsyncSocket& rConnectedSocket,//傳回可用的新套接字

SOCKADDR*  lpSockAddr = NULL,//傳回發送連接配接請求的套接字位址

int*  lpSockAddrLen = NULL// lpSockAddr的實際長度。

          );

    傳回值:成功傳回非0,失敗傳回0。

    說明:服務方如何Accept(),以建立連接配接的問題。簡單的做法就是在監聽的Socket收到OnAccept()時,用一個新的CAsyncSocket對象去建立連接配接,例如:

 void CMySocket::OnAccept( intErrCode )

 {

       CMySocket* pSocket =new CMySocket;

       Accept( *pSocket );

 }

    于是,上面的pSocket和客戶方建立了連接配接,以後的通信就是這個pSocket對象去和客戶方進行,而監聽的Socket仍然繼續在監聽,一旦又有一個客戶方要連接配接服務方,則上面的OnAccept()又會被調用一次。當然pSocket是和客戶方通信的服務方,它不會觸發OnAccept()事件,因為它不是監聽Socket。

(4)AsyncSelect

為套接字請求事件通知,調用後将套接字置于非阻塞狀态。函數原型:

      BOOL AsyncSelect(long lEvent);//注冊程式感興趣網絡事件

傳回值:成功傳回非0,失敗傳回0。

(5)bind

該成員函數為套接字綁定一個本地位址,該函數有兩種形式:

BOOL Bind(

UINT nSocketPort,//綁定的端口号

LPCTSTRT lpszSocketAddress = NULL//制定與套接字綁定的主機位址)

BOOL Bind(

const SOCKADDR* lpSockAddr, //SOCKADDR*類型的指針

int nSockAddrLen//lpSockAddr的位元組長度)

傳回值:成功傳回非0,失敗傳回0。

注意:MFC程式裡建立支援IPX/SPX協定的套接字的時候才需要重載此函數。

(6)Connect

在伺服器端套接字對象已經進入監聽狀态之後,客戶應用程式可以調用CAsyncSocket類的Connect()成員函數,向伺服器發出一個連接配接請求,請求連接配接到伺服器端套接字對象。函數原型:

BOOL Connect(

LPCTSTR  lpszHostAddress, //要求連接配接的網絡位址,可以使域名也可以是字元串

UINT  nHostPort );//要求連接配接的端口号

        BOOL Connect(

const  SOCKADDR*  lpSockAddr,//包含要求連結的套接字位址

int  nSockAddrLen );// lpSockAddr的長度

傳回值:成功傳回非0,失敗傳回0。

說明:如果調用成功或者發生了WSAEWOULDBLOCK錯誤,當調用結束傳回時,都會發生FD_CONNECT事件,MFC架構會自動調用用戶端套接字的OnConnect()事件處理函數,并将錯誤代碼作為參數傳送給它。客戶方在使用CAsyncSocket::Connect()時,往往傳回一個WSAEWOULDBLOCK的錯誤,實際上這不應該算作一個錯誤,它是Socket提醒我們,由于你使用了非阻塞Socket方式,是以(連接配接)操作需要時間,不能瞬間建立。我們可以在Connect()調用之後等待 CAsyncSocket::OnConnect()事件被觸發,一般不要對connect函數的傳回做判斷,過一會兒它連接配接完成後,自然就能跳到FD_CONNECT事件那裡了。

對于傳回值錯誤的說法:異步方式connect是不會立刻成功的,WSAEWOULDBLOCK(10035)告訴你目前這個操作還沒有成功,你還需等待。

是以一般真要捕捉傳回值,那麼就使用如下代碼:

if(!m_Socket.Connect(strServerIP,TCP_PORT))

{     nErr = m_Socket.GetLastError();

       if(nErr!=WSAEWOULDBLOCK)

       {    

DisConnect();

               MessageBox(“連接配接伺服器錯誤”);

       }

}

(7)GetLastError

該函數用于傳回這個線程中最後調用套接字函數的錯誤代碼。函數原型:

       Static int GetLastError()

說明:可以使用傳回的值進行判斷發生何種錯誤。

(8)Listen

監聽套接字對象開始監聽來自用戶端的連接配接請求,函數原型如下:

       BOOL Listen( int  nConnectionBacklog =5);//連接配接請求數

傳回值:成功傳回非0,失敗傳回0。

說明: 當Listen函數确認并接納了一個來自用戶端的連接配接請求後,會觸發FD_ACCEPT事件,監聽套接字會收到通知,表示監聽套接子已經接納了一個客戶的連接配接請求,MFC架構會自動調用監聽套接字的OnAccept事件處理函數,程式設計者一般應重載此函數,在其中調用監聽套接字對象的Accept函數,來接收用戶端的連接配接請求。

執行流程是:首先監聽(listen) 再觸發FD_ACCEPT事件,直接去由OnAccept函數來處理,在其中又由Accept函數處理。

(9)Send

在已建立連接配接的套接字上發送資料,函數原型:

       virtual int  Send(

const  void*  lpBuf, //發送資料緩沖區

int  nBufLen,  //緩沖區長度

int  nFlags = 0);//發送方式标志,一般為0。

傳回值:成功傳回實際發送位元組數,失敗傳回SOCKET_ERROR。

說明:對于一個CAsyncSocket套接字對象,當它的發送緩沖區騰空時,會激發FD_WRITE事件,套接字會得到通知,MFC架構會自動調用這個套接字對象的OnSend事件處理函數。一般程式設計者會重載這個函數,在其中調用Send成員函數來發送資料。

(10)Receive

從已連接配接的套接字上接收資料,函數原型:

       Virtual int  Receive(

Void*  lpBuf,

Int  nBufLen,

Int  nFlags = 0);

傳回值:成功傳回接收到的位元組數,失敗傳回SOCKET_ERROR,如果套接字關閉則傳回0。

說明:對于一個CAsyncSocket套接字對象,當有資料到達它的接收隊列時,會激發FD_READ事件,套接字會得到已經有資料到達的通知,MFC架構會自動調用這個套接字對象的OnReceive事件處理函數。一般程式設計者會重載這個函數,在其中調用Receive成員函數來接收資料。在應用程式将資料取走之前,套接字接收的資料将一直保留在套接字的緩沖區中。

說明:send和receive發送的可以使結構體類型,也可以是CSstring或者Char類型。

一下就以各種類型的資料進行說明:

CString類型:

CString str = _T("你好!");

m_link.Send(str.GetBuffer(0), str.GetLength(), 0);

Charl類型:

char str[] = "你好!";

m_link.Send(str, strlen(str), 0);

結構體類型:

typedef struct mess

{

       char str[100];

       int flag;

       BOOL all;

}MESS;

MESS info;

info.all = FALSE;

info.flag = 1;

info.str = "你好!";

m_link.Send(&info, sizeof(MESS), 0);

說明:在結構體類型中不可有CString類型的資料,因為CString相當于一個指針,也不可有指針類型的資料。隻發了位址,但是位址所指的内容并沒有發送。

(11)SendTo

用于向指定的目标位址發送資料,函數原型:

      int SendTo(

const  void*  lpBuf,//發送資料緩沖區

int  nBufLen,//緩沖區長度

UINT  nHostPort,//目的套接字端口号

LPCTSTR lpszHostAddress = NULL,//目的套接字主機名

int nFlags = 0 );//發送辨別

int  SendTo(

     const void*  lpBuf,

int  nBufLen,

const SOCKADDR*  lpSockAddr,

int  nSockAddrLen,

int  nFlags = 0 );

傳回值:成功傳回實際發送位元組數,失敗傳回SOCKET_ERROR。

(12)ReceiveFrom

從套接字上接收資料并得到發送方位址,函數原型:

int ReceiveFrom (

   void* lpBuf, //接收緩沖區

   int nBufLen, //所接收資料的長度

   CString&rSocketAddress, //遠端套接字位址

   UINT& rSocketPort, //遠端套接字端口号

   int nFlags = 0 //指明資料的接收方式

);

int ReceiveFrom (

              void* lpBuf, //接收緩沖區

              int nBufLen, //所接收資料的長度

              SOCKADDR* lpSockAddr, //遠端套接字結構位址

              int* lpSockAddrLen, //遠端套接字結構位址長度

              int nFlags = 0 //指明資料的接收方式

);

傳回值:成功傳回接收到的位元組數,失敗傳回SOCKET_ERROR,如果套接字關閉則傳回0。

(13)Shutdown

禁止接收或者發送資料,可以選擇關閉套接字的方式。将套接字置為不能發送資料,或不能接收資料,或二者均不能的狀态。

函數原型:

     BOOL ShutDown( int  nHow);

nHow的可選值:receives = 0、sends = 1、both = 2

傳回值:成功傳回非0,失敗傳回0。

(14)close

關閉套接字,釋放資源,函數原型:

        virtual  void  Close( );

(15)SetSocketOpt

設定底層套接字對象的屬性,要擷取套接字的設定資訊,可調用GetSocketOpt()。

(16)IOCtl

控制套接字的工作模式,選擇合适的參數,可以将套接字設定在阻塞模式(Blocking  mode)下工作。

(17)GetPeerName

用于獲得套接字連接配接的IP位址。函數原型:

     BOOL GetPeerName(CString &rPeerAddress, UINT & rPeerPort)

l  CAsyncSocket類可以接受并處理的消息事件

六種套接字相關的事件與通知消息,參數Ievent可以選用的六個符号常量是在winsock.h檔案中定義的。

#define FD_READ         0x01        通知有資料可讀。

#define FD_WRITE        0x02        通知可以寫資料。

#define FD_OOB          0x04        通知将有帶外資料到達。

#define FD_ACCEPT       0x08        通知監聽套接字有連接配接請求可以接受。

#define FD_CONNECT     0x10        通知請求連接配接的套接字,連接配接的要求已被處理。

#define FD_CLOSE        0x20        通知套接字已關閉。

他們代表MFC套接字對象可以接受并處理的六種網絡事件,當事件發生時,套接字對象會收到相應的通知消息,并自動執行套接字對象響應的事件處理函數。

l  CAsyncSocket 類的通知事件

當上述的網絡事件發生時,MFC架構作何處理呢?按照Windows的消息驅動機制,MFC架構應當把消息發送給相應的套接字對象,并調用作為該對象成員函數的事件處理函數。事件與處理函數是一一映射的。在afxSock.h檔案中的CAsyncSocket類的聲明中,定義了與這六個網絡事件對應的事件處理函數:

virtual void OnReceive(int nErrorCode);          對應  FD_READ事件

virtual void OnSend(int nErrorCode);            對應  FD_WRITE事件

virtual void OnAccept(int nErrorCode);          對應  FD_ACCEPT事件

virtual void OnConnect(int nErrorCode);         對應  FD_CONNECT事件

virtual void OnClose(int nErrorCode);           對應  FD_CLOSE事件    

virtual void OnOutOfBandData(int nErrorCode);   對應  FD_OOB事件

參數說明:

    nErrorCode 套接字上最近的錯誤代碼。此成員函數可用的錯誤代碼有:

· 0 函數成功地執行并傳回。 

· WSAENETDOWN Windows Sockets檢測到了網絡故障。

當某個網絡事件發生時,MFC架構會自動調用套接字對象的對應的事件處理函數。這就相當給了套接字對象一個通知,告訴它某個重要的事件已經發生。是以也稱之為套接字類的通知函數(notification functions)或回調函數(callback functions)。如果你從CAsyncSocket類派生了自己的套接字類,你必須重載你的應用程式所感興趣的那些網絡事件所對應的通知函數。MFC架構自動調用通知函數,使得你可以在套接字被通知的時候來優化套接字的行為。

在程式設計的時候,這些事件将來都在自己派生CAsyncSocket的類中去重寫其虛函數。關于其重寫的方式有很多種方法,但是有一種最簡便的方法就是把所有的操作都寫在視窗類的實作檔案中,這樣在派生類中來調用相應的響應函數。其實這些事件對應的響應函數中時調用CAsyncSocket的相應的函數,比如:OnReceive(int nErrorCode) 調用Receive等。記得這些函數需要操作的必須在派生類中去重寫,如果你想使用非阻塞方式的話。

調用方式一 :

使用主視窗指針法,假如現在有一個對話框 CXXXDlg、有一個派生CAsyncSocket的類CMySocket。要實作對OnReceive(int nErrorCode)虛函數的重寫,方式如下:

首先:在CMySocket的頭檔案中加入以下兩行代碼:

           Class CXXXDlg;

           CXXXDlg *m_pDlg;

      在CMySocket的實作檔案中引入CXXXDlg的頭檔案。

第二步:在CXXXDlg中的頭檔案中引入CMySocket的頭檔案,并且定義一個CMySocket的普通變量m_mySock,并在其初始化中将本視窗指針傳給CMySocket的m_pDlg:

       m_mySock.m_pDlg = this;

第三步:在CXXXDlg中添加接收函數,其形式為:

void CXXXDlg::receiveMessage(CMySocket *mySock)

{

    //用mySock來接收消息

}

第四步:在CMySocket中添加虛函數OnReceive(int nErrorCode),并執行以下操作

       If(nErrorCCode== 0)

{

    m_pDlg-> receiveMessage(this);

}

當有消息到達的時候會自動調用CMySocket::OnReceive函數,CMySocket::OnReceive又去調用CXXXDlg::receiveMessage,在其中又去執行CAsyncSocket::Receive來接收消息。

說明:獲得主視窗指針的另一種方法:

       CDlgTcpServerApp *pApp =(CDlgTcpServerApp *)AfxGetApp();

       CDlgTcpServerDlg *pMainWnd =(CDlgTcpServerDlg *)pApp->m_pMainWnd;

pMainWnd為主視窗指針。

調用方式二:

采用自定義消息的方式,将套接字線程收到的事件當做消息發給主視窗,在主視窗中處理事件,主要方法如下:

第一步:在派生套接字類中添加構造函數以及HWND 句柄

       頭檔案中:

HWND m_mainWnd;

       實作檔案中:

Void CListenSocket::CAsyncSocket(HWND hWnd)

       {

             m_mainHwnd = hWnd;

       }

第二步:在主視窗中添加CListenSocket的頭檔案以及定義一個CListenSocket變量

        引入頭檔案:

        #include “ListenSocket.h”

        定義一個變量:

        CListenSocket m_listen(m_hWnd);

第三步:在CListenSocket中的OnXXX(nErrorCode)函數中進行發送消息,PostMessage。

第四步:在CXXXDlg中定義消息即可,并寫消息處理函數。

l  CAsyncSocket的程式設計流程:

序号 伺服器(Server) 客戶機(Client)
1

//構造一個套接字

CAsyncSocket sockSrvr;

//構造一個套接字

CAsyncSocket sockClient;

2

//建立SOCKET句柄,綁定到指定的端口

sockSrvr.Create(nPort);

//建立SOCKET句柄,使用預設參數

sockClient.Create();

3

//啟動監聽,時刻準備接受連接配接請求

sockSrvr.Listen();

4

//請求連接配接到伺服器

sockClient.Connect(strAddr.nport);

5

//構造一個新的空的套接字來接受連接配接

CAsyncSocket sockRecv;

sockSrvr.Accept(sockRecv);

6

//接收資料

sockRecv.Receive(pBuf, nLen);

//發送資料

sockClient.Send(pBuf, nLen);

7

//發送資料

sockRecv.Send(pBuf, nLen);

//接收資料

sockClient.Receive(pBuf, nLen);

8

//關閉套接字對象

sockRecv.Close();

//關閉套接字對象

sockClient.Close();

說明:

1、建立CasyncSocket類對象

   CAsyncSocket類對象稱為異步套接字對象。建立異步套接字對象一般分為兩個步驟,首先通過調用CAsyncSocket類的構造函數,建立一個新的空CAsyncSocket類套接字對象,構造函數不帶參數。然後必須調用它的Create成員函數,來建立底層的套接字資料結構,并綁定它的位址,決定套接字對象的具體特性。

可以使指針對象也可以是普通對象。

2、 執行步驟:

因為CAsyncSocket類較好的封裝了WinSocket API,是以使用CAsyncSocket類的步驟與直接使用WinSocket API的步驟類似,步驟如下:

1)       構造 CAsyncSocket 對象并使用對象建立底層的 SOCKET 句柄。代碼如下:

CAsyncSocket* pSocket = new CAsyncSocket;   // 建立CAsyncSocket對象

pSocket->Create(6650, SOCK_DGRAM ); // 建立無連接配接套接字

2)       如是客戶方程式,用CAsyncSocket∷Connect()成員函數連接配接到服務方;如是服務方程式,用CAsyncSocket∷Listen()成員函數開始監聽,一旦收到連接配接請求,則調用CAsyncSocket∷Accept()成員函數開始接收。注意:CAsyncSocket∷Accept()成員函數要用一個新的并且是空的CAsyncSocket對象作為它的參數,這裡所說的"空的"指的是這個新對象還沒有調用Create()成員函數。

3)       調用其他的CAsyncSocket類的Receive()、ReceiveFrom()、Send()和SendTo()等成員函數進行資料通信。

4)       銷毀 CAsyncSocket 對象。如果套接字對象在棧上,則析構函數會在套接字變量超出作用範圍後,自動執行;如果使用new操作符建立套接字,則需要使用delete操作符銷毀對象。析構函數調用對象的Close()函數關閉套接字連接配接。

在建立 CAsyncSocket 對象時,對象封裝了 Windows 的 SOCKET 句柄并提供了在此句柄上的操作。 當使用者使用 CAsyncSocket 對象時,必須要處理阻塞操作、接收和發送機制使用的位元組順序以及 Unicode 字元集和多位元組字元集的轉換等問題。

圖示:

流程圖:基于TCP

基于UDP

l  MFC中程式設計準備工作

在進行MFC 程式設計的時候,應該和WinSockAPI一樣,有一些初始化工作,在建立工程的過程中,如果選擇了Windows Sockets選項則會自動加入頭檔案和初始化代碼,否則必須手動添加如下代碼:

1)       在StdAfx.h頭檔案中加入以下代碼

#include<afxsock.h>

2)       在CTCPMFCApp::InitInstance()中加入套接字初始化代碼

    if (!AfxSocketInit())

       {

              AfxMessageBox(IDP_SOCKETS_INIT_FAILED);

              return FALSE;

       }

    在你使用CAsyncSocket之前,必須調用AfxSocketInit初始化WinSock環境,而AfxSocketInit會建立一個隐藏的CSocketWnd對象,由于這個對象由Cwnd派生,是以它能夠接收Windows消息。是以它能夠成為高層CAsyncSocket對象與WinSock底層之間的橋梁。例如某CAsyncSocket在Send時WSAEWOULDBLOCK了,它就會發送一條消息給CSocketWnd作為報告,CSocketWnd會維護一個報告登記表,當它收到底層WinSock發出的空閑消息時,就會檢索報告登記表,然後直接調用報告者的OnSend函數。是以前文所說的CAsyncSocket會自動調用OnXxx,實際上是不對的,真正的調用者是CSocketWnd——它是一個CWnd對象,運作在獨立的線程中。

八、MFC中的套接字2

l  CSocket類

在MFC中的另一個套接字是CSocket,CSocket是CAsyncSocket類的派生類,除了繼承了父類中一些常用、易懂的Windows Sockets API函數外,還對CAsyncSocket底層中較難控制的一些函數進行了處理,提供了對比CAsyncSocket類抽象化級别更高套接字的支援。CSocket是用MFC序列化協定的一種版本,通過MFC中的CArchive對象将資料傳遞給套接字對象,或者從套接字對象接收資料,使得套接字上的資料輸入輸出就如同使用MFC的文檔一樣簡捷易用,即CArchive管理着原來必須由使用者自己使用原始API或CAsyncSocket類來管理通信的許多工作,進而友善了使用者的使用。

通過MFC的CArchive對象提供Sockets的存檔功能,使用過程比CAsyncSocket模型要簡單得多。CSocket類從CAsyncSocket類繼承了很多封裝了Windows Sockets API的成員函數。是以,使用CSocket類一般不需要深入了解Socket程式設計。更友善的是,CSocket提供了CArchive的同步操作,實作了Socket通信的文檔序列化。可以使用MFC序列化協定發送資料和接收資料。網絡傳輸層會将資料分割成大小合适的資料包,CSocket類可以處理包 裝和解包工作。

CSocket實作了阻塞操作,其對象在調用Connect、Send、Accept、Close、Receive等成員函數時,這些函數在完成任務之後才會傳回。是以,Connect和Send不會導緻OnConnect和OnSend事件響應函數的調用。如果重載了OnReceive、OnAccept、OnClose事件響應函數,則在網絡事件到達之後将導緻對應的事件響應函數被調用。在這些事件響應函數中應該調用Receive、Accept、Close來完成相應操作。

CSocket在CAsyncSocket的基礎上,修改了Send、Recieve等成員函數,幫你内置了一個用以輪詢收發緩沖區的循環,變成了同步短連接配接模式。短連接配接應用簡單明了,CSocket經常不用派生就可以直接使用,但也有些問題:

1、用作監聽的時候

曾經看到有人自己建立線程,線上程中建立CSocket對象進行Listen、Accept,若Accept成功則再起一個線程繼續Listen、Accept可以說他完全不了解CSocket,實際上CSocket的監聽機制已經内置了多線程機制,你隻需要從CSocket派生,然後重載OnAccept:

//CListenSocket頭檔案

class CListenSocket : public CSocket

{

public:

    CListenSocket(HWNDhWnd=NULL);

    HWND m_hWnd; //事件處理視窗

    virtual void OnAccept(intnErrorCode);

};

//CListenSocket實作檔案

#include "ListenSocket.h"

CListenSocket::CListenSocket(HWND hWnd){m_hWnd=hWnd;}

void CListenSocket::OnAccept(int nErrorCode)

{

   SendMessage(m_hWnd,WM_SOCKET_MSG,SOCKET_CLNT_ACCEPT,0);

    CSocket::OnAccept(nErrorCode);

}

//主線程

m_pListenSocket=new  ListenSocket(m_hWnd);

m_pListenSocket->Create(...);

m_pListenSocket->Listen();

LRESULT CXxxDlg::OnSocketMsg(WPARAM wParam, LPARAM lParam)

{

    UINT type=(UINT)wParam;

    switch(type)

    {

    case SOCKET_CLNT_ACCEPT:

        {

            CSocket*pSocket=new CSocket;

           if(!m_pListenSocket->Accept(*pSocket))

            {

                deletepSocket;

                break;

            }

        }

    }

}

2、用于多線程的時候

常看到人說CSocket在子線程中不能用,其實不然。實際情況是:直接使用CSocket動态建立的對象,将其指針作為參數傳遞給子線程,則子線程中進行收發等各種操作都

沒問題。但如果是使用CSocket派生類建立的對象,就要看你重載了哪些方法,假如你僅重載了OnClose,則子線程中你也可以正常收發,但不能Close!因為CSocket是用内部循環做到同步的,并不依賴各OnXxx,它不需要與CSocketWnd互動。但當你派生并重載OnXxx後,它為了提供消息機制就必須與CSocketWnd互動。當你調用AfxSocketInit時,你的主線程會獲得一個通路CSocketWnd的句柄,對CSocketWnd的通路是MFC自動幫你完成的,是被隐藏的。而你自己建立的子線程并不自動具備通路CSocketWnd的機制,是以子線程中需要通路CSocketWnd的操作都會失敗。常看到的解決辦法是給子線程傳遞SOCKET句柄而不是CSocket對象指針,然後在子線程中建立CSocket臨時對象并Attach傳入的句柄,用完後再Dettach并delete臨時對象。但是。一般使用發送消息的方法:使用自定義消息,不能在子線程中close,那麼,可以給主線程發送一條消息,讓主線程的消息處理函數來完成Close,也很友善。

CSocket一般配合多線程使用,隻要你想收發資料,你就可以建立一個CSocket對象,并建立一個子線程來進行收發。是以被阻塞的隻是子線程,而主線程總是可以随時建立子線程去幫它幹活。由于可能同時有很多個CSocket對象在工作,是以你一般還要建立一個清單來儲存這些CSocket對象的辨別,這樣你可能通過在清單中檢索辨別來區分各個CSocket對象,當然,由于記憶體位址的唯一性,對象指針本身就可以作為辨別。相對CAsyncSocket而言,CSocket的運作流程更直覺也更簡單。

l  CArchive

CArchive允許以一個永久二進制(通常為磁盤存儲)的形式儲存一個對象的複雜網絡,它可以在對象被删除時,還能永久儲存。可以從永久存儲中裝載對象,在記憶體中重新構造它們。使得資料永久保留的過程就叫作“串行化”。

CArchive不支援在資料報的Socket連接配接上序列化資料

l  CSocketFile

在#include <afxsock.h>中,一個CSocketFile對象是一個用來通過Windows Sockets 在網絡中發送和接收資料的CFile 對象。為了這個目的,你可以CSocketFile對象與一個CSocket  對象連接配接。你也可以将CSocketFile 對象與一個CArchive 對象連接配接,以使用MFC 系列來簡化發送和接收資料。

l  CSocket有關的函數

首先,CSocket比CAsyncSocket類的成員函數少得多,以下分别簡介:

1)       CSocket

是CSocket類的構造函數,它構造一個空的套接字對像,但是并沒有指定端口和IP,構造完成夠還必須調用Create函數建立SOCKET句柄。

2)       Create

該函數在構造函數之後使用将SOCKET句柄附到套接字上,并綁定套接字指定的位址。如果套接字是SOCK_DGRAM,不能和CArchive對象關聯使用。函數原型:

    BOOL Create (

          UINT nSocketPort ,                   //端口号

          int nSocketType = SOCK_STREAM,     //套接字類型

          LPCTSTR lpszSocketAddress,        //制定與套接字綁定的IP位址

);

傳回值:成功傳回非0,失敗傳回0。

3)       Attach

功能是将對象和資源句柄聯系起來,可以将一個SOCKET句柄附加到CSocket對象上。這個套接字句柄存放在對象資料成員m_hSocket中。函數原型:

    BOOL CSocket(SOCKET hSocket);

傳回值:成功傳回非0,失敗傳回0。

4)       CancleBlockingCall

終止正在進行的阻塞調用,但是終止出Accept以外的函數均會使套接字處于不确定狀态。函數原型:

    void CancelBlockingCall()

5)       OnMessagePending

可以再Windows派發消息時,處理自己感興趣的消息。函數原型:

           virtual BOOL OnMessagePending()

       傳回值:如果消息被處理傳回非0,否則傳回0。

6)       IsBlocking()函數:此函數可以确定目前是否在執行一個阻塞調用。

7)       FromHandle()函數:傳回一個指向 CSocket 對象的指針,其中存放了SOCKET句柄。使用SOCKET句柄,可以使用WinSocket API執行其他套接字函數。

8)       關于連接配接的三個函數

        CAsyncSocket::Accept

CAsyncSocket::Connect

CAsyncSocket::Listen

9)       接收發送

       使用CAsyncSocket的Receive和Send

10)    關聯CSocketFile和CSocket

        explicitCSocketFile( CSocket* pSocket, BOOL bArchiveCompatible = TRUE );

關聯CArchive和CSocketFile

        CArchive(

CFile* pFile,

UINT nMode,

int nBufSize =4096,

void* lpBuf =NULL );

使用CArchive對象的Read、Write等函數在客戶機與伺服器程序之間進行資料傳遞。

l  CSocket事件

同CAsyncSocket一樣使用的是響應六個FD_XXX事件。

l  使用CSocket程式設計思想

TCP通信圖示:

CArchive不能用于資料報套接字

通信的流程圖:

程式設計模型:

使用CSocket對象,需要建立CSocket對象,并将幾個MFC類對象關聯起來。下面的程式,伺服器Sockets和用戶端Sockets除了第(3)步其餘每步都必須執行,其中每種Sockets 類型需要不同的操作。當運作時,通常伺服器應用程式先啟動準備好并"監聽",然後用戶端應用程式發起連接配接。如果用戶端試圖連接配接時,伺服器沒有準備好,則需要用戶端程式稍後再試。使用CSocket在伺服器端Socket和用戶端Socket之間進行通信的流程圖如上圖所示。

(1)構造CSocket對象。

(2)使用CSocket::Create函數建立底層的SOCKET句柄。對于用戶端CSocket對象,使用預設參數調用Create()方法就可以。對于伺服器Csocket()對象,必須在Create()方法中指定端口。但是CArchive不能與資料報套接字一起使用。如果要以資料報套接字的方式使用CSocket,則不能使用檔案檔案。因為資料報是不可靠的,不能保證資料到達、資料不重複和順序,是以,不能通過檔案檔案與序列化相容。如果使用帶有CArchive對象的CSocket 類操作資料報套接字時,MFC會抛出錯誤。

(3)如果socket是伺服器,調用CAsyncSocket::Listen開始"監聽"用戶端的連接配接。如果socket

是用戶端,調用CAsyncSocket::Connect連接配接socket對象到伺服器socket。Connect()函數會首先檢查位址是否是IP位址的形式, 如果不是,則會将位址作為機器位址處理。當伺服器端收到連接配接請求,如果接收,則調用CAsyncSocket::Accept()函數。Accept 成員函數需要一個新socket的引用,即空CSocket對象。使用者在調用Accept()函數前,需要構造對象。如果 socket對象超出範圍,則連接配接會被關閉。MFC會連接配接新對象到SOCKET句柄。

(4)建立CSocketFile對象,使其與CSocket對象相連。

(5)建立用于接收或發送資料的CArchive對象,使其與CSocketFile對象相連。

(6)使用CArchive對象在用戶端和伺服器sockets之間傳輸資料。要注意,CArchive 對象隻能單方向存取資料。是以,有些情況下,需要使用兩個CArchive對象,一個用于發送資料,另外一個用于接收。在接收連接配接和建立檔案後,使用者可以通過驗證密碼等操作,保證資料傳輸的安全性。CArchive 類為 CSocket 類提供 IsBufferEmpty 成員函數確定接收所有資料。如果緩沖區中包含多條資料消息,則需要循環處理,讀取全部資料消息,并且清空緩沖區。否則,有資料可以接收的下條通知會不确定的延期。

(7)銷毀檔案對象和socket對象。關閉套接字和清除相關的對象:在使用完CSocket對象以後,應用程式應調用它的Close()成員函數來釋放套接字占用的系統資源,也可以調用它的ShutDown()成員函數來禁止套接字讀寫。而對于相應的CArchive對象、CSocketFile對象和CSocket對象,可以将它們銷毀;也可以不作處理,因為當應用程式終止時,會自動調用這些對象的析構函數,進而釋放這些對象占用的資源。

說明:

CSocket類使用基類CAsyncSocket的同名成員函數Connect()、Listen()、Accept()來建立伺服器和客戶機套接字之間的連接配接,使用方法相同。不同的是:CSocket類的Connect()和Accept()支援阻塞調用。比如:在調用Connect()函數時會發生阻塞,直到成功地建立了連接配接或有錯誤發生才傳回,CSocket對象從不調用OnConnect()事件處理函數。

發送和接收資料,在建立CSocket類對象後,對于資料報套接字,直接使用CSocket類的SendTo()、ReceiveFrom()成員函數來發送和接收資料。對于流式套接字,一般将CSocket類與CArchive類和CSocketFile類結合,來發送和接收資料,這将使程式設計更為簡單。CSocket對象從不調用OnSend()事件處理函數。

九、CAsyncSocket與CSocket的比較

前者是異步通信,後者是同步通信;前者是非阻塞模式,後者是阻塞模式。另外,異步非阻塞模式有時也被稱為長連接配接,同步阻塞模式則被稱為短連接配接。

為了更明白地講清楚兩者的差別,舉個例子:設想你是一位體育老師,需要測驗100位同學的400米成績。你當然不會讓100位同學一起起跑,因為當同學們傳回終點時,你根本來不及掐表記錄各位同學的成績。如果你每次讓一位同學起跑并等待他回到終點你記下成績後再讓下一位起跑,直到所有同學都跑完,這就是同步阻塞模式。你設計了一個函數,傳入參數是學生号和起跑時間,傳回值是到達終點的時間。你調用該函數100次,就能完成這次測驗任務。這個函數是同步的,因為隻要你調用它,就能得到結果;這個函數也是阻塞的,

因為你一旦調用它,就必須等待,直到它給你結果,不能去幹其他事情。如果你一邊每隔10秒讓一位同學起跑,直到所有同學出發完畢;另一邊每有一個同學回到終點就記錄成

績,直到所有同學都跑完,這就是異步非阻塞模式。你設計了兩個函數,其中一個函數記錄起跑時間和學生号,該函數你會主動調用100次;另一個函數記錄到達時間和學生号,該函數是一個事件驅動的callback函數,當有同學到達終點時,你會被動調用。你主動調用的函數是異步的,因為你調用它,它并不會告訴你結果;這個函數也是非阻塞的,因為你一

旦調用它,它就馬上傳回,你不用等待就可以再次調用它。但僅僅将這個函數調用100次,你并沒有完成你的測驗任務,你還需要被動等待調用另一個函數100次。

當然,你馬上就會意識到,同步阻塞模式的效率明顯低于異步非阻塞模式。那麼,誰還會使用同步阻塞模式呢?不錯,異步模式效率高,但更麻煩,你一邊要記錄起跑同學的資料,一邊要記錄到達同學的資料,而且同學們回到終點的次序與起跑的次序并不相同,是以你還要不停地在你的成績冊上查找學生号。忙亂之中你往往會張冠李戴。你可能會想出更聰明的辦法:你帶了很多塊秒表,讓同學們分組互相測驗,這是多線程同步模式。每個拿秒表的同學都可以獨立調用你的同步函數,這樣既不容易出錯,效率也大大提高,隻要秒表足夠多,同步的效率也能達到甚至超過異步。

既然多線程同步既快又好,異步模式還有存在的必要嗎?很遺憾,異步模式依然非常重要,因為在很多情況下,你拿不出很多秒表。你需要通信的對端系統可能隻允許你建立一個SOCKET連接配接,很多金融、電信行業的大型業務系統都如此要求。

一般CAsyncSocket用于在少量連接配接時,處理大批量無步驟依賴性的業務。CSocket

用于處理步驟依賴性業務,或在可多連接配接時配合多線程使用。

十、總結

l  程式設計步驟

無論使用MFC中的那種類型的套接字進行程式設計,在程式設計的時候都是從MFC 套接字類中繼承自己的類,然後在自己繼承的類中去重寫自己感興趣的事件即可。一般是按以下方式進行程式設計的:

伺服器端:

1、             繼承MFC 套接字的監聽類CListenSocket,獲得主視窗指針,編寫OnAccept()函數,進行監聽。

2、             繼承MFC 套接字的用戶端通信類CListenSocke,獲得主視窗指針,編寫OnReceive函數,進行接收消息。

用戶端:

1、             繼承MFC 套接字的用戶端通信類CListenSocke,獲得主視窗指針,編寫OnReceive函數,進行接收消息。

l  擷取主機IP位址

       charhostname[256];

       structhostent *host;

       LPCTSTRhostip;

       if(gethostname(hostname,sizeof(hostname)) == 0)

       {

              host= gethostbyname(hostname);

              for(inti = 0; host != NULL && host->h_addr_list[i] != NULL; i ++)

              {

                     hostip= inet_ntoa(*(struct in_addr*)host->h_addr_list[i]);

              }

       }

說明:hostname中放的是主機名稱。

         Hostip中放的是主機IP位址,而且可以直接付給CString類型的變量。

l  使用CPtrList存放CSocket指針,這也是存放指針的一個很好的連結清單。

1、             定義

       CPtrList m_userList;

2、             增加節點

       m_userList.AddTail(pSocket);//pSoxket是一個指針變量。

3、             搜尋符合條件的節點

         POSITIONpos;

      CClientSocket*pSock;

      for(pos =m_userList.GetHeadPosition(); pos != NULL; )

      {

             pSock= (CClientSocket *)m_userList.GetNext(pos);

             if(pSock->GetUserName()== name)

             {

                    returnFALSE;

             }

      }

4、             删除節點

       m_userList.RemoveAt(pos);//pos代表指定位置的節點