Winsock編程基礎介紹 .
相信很多人都對網絡編程感興趣,下麵我們就來介紹,在網絡編程中應用最廣泛的編程接口Winsock API.
使用Winsock API的編程,應該了解一些TCP/IP的基礎知識.雖然你可以直接使用Winsock API來寫網絡應用程序,但是,要寫出優秀的網絡應用程序,還是必須對TCP/IP協議有一些了解的.
1. TCP/IP協議與Winsock網絡編程接口的關係.
在開始之前,我們先說一下Winsock和TCP/IP到底是什麼關係.
我碰到很多人問我:怎樣使用Winsock協議編程?其實,這話說的有點錯誤,Winsock並不是一種網絡協議,他隻是一個網絡編程接口,也就是說,他不是協議,但是他可以訪問很多種網絡協議,你可以把他當作一些協議的封裝.現在的Winsock已經基本上實現了與協議無關.你可以使用Winsock來調用多種協議的功能.
那麼,Winsock和TCP/IP協議到底是什麼關係呢?實際上,Winsock就是tcp/ip協議的一種封裝,你可以通過調用winsock的接口函數來調用tcp/ip的各種功能.例如我想用Tcp/ip協議發送數據,你就可以使用winsock的接口函數send()來調用tcp/ip的發送數據功能,至於具體怎麼發送數據,Winsock已經幫你封裝好了這種功能.
2.TCP/IP協議介紹
現在來介紹一些tcp/ip的原理.tcp/ip協議包含的範圍非常的廣,他是一種四層協議,包含了各種,硬件軟件需求的定義,我們這裏隻介紹軟件方麵的知識.tcp/ip協議確切的說法應該是tcp/udp/ip協議.
udp協議(User Datagram Protocol用戶數據報協議).是一種保護消息邊界的,不保障可靠數據的傳輸. tcp協議(Transmission Control Protocol 傳輸控製協議).是一種流傳輸的協議.他提供可靠的,有序的,雙向的,麵向連接的傳輸.
3.保護消息邊界和流
那麼什麼是保護消息邊界和流呢?
保護消息邊界,就是指傳輸協議把數據當作一條獨立的消息在網上傳輸,接收端隻能接收獨立的消息.也就是說存在保護消息邊界,接收端一次隻能接收發送端發出的一個數據包.而麵向流則是指無保護消息保護邊界的,如果發送端連續發送數據,接收端有可能在一次接收動作中,會接收兩個或者更多的數據包.
我們舉個例子來說,例如,我們連續發送三個數據包,大小分別是2k, 4k , 8k,這三個數據包,都已經到達了接收端的網絡堆棧中,如果使用UDP協議,不管我們使用多大的接收緩衝區去接收數據,我們必須有三次接收動作,才能夠把所有的數據包接收完.而使用TCP協議,我們隻要把接收的緩衝區大小設置在14k以上,我們就能夠一次把所有的數據包接收下來.隻需要有一次接收動作.
這就是因為UDP協議的保護消息邊界使得每一個消息都是獨立的.而流傳輸,卻把數據當作一串數據流,他不認為數據是一個一個的消息.
所以有很多人在使用tcp協議通訊的時候,並不清楚tcp是基於流的傳輸,當連續發送數據的時候,他們時常會認識tcp會丟包.其實不然,因為當他們使用的緩衝區足夠大時,他們有可能會一次接收到兩個甚至更多的數據包,而很多人往往會忽視這一點,隻解析檢查了第一個數據包,而已經接收的其他數據包卻被忽略了.所以大家如果要作這類的網絡編程的時候,必須要注意這一點.
4。Winsock編程簡單流程
下麵我們介紹一下Win32平台的Winsock編程方法.通訊則必須有服務器端,和客戶端.我們簡單介紹tcp服務器端的大體流程.
對於任何基於Winsock的編程首先我們必須要初始化Winsock DLL庫.
int WSAStarup( WORD wVersionRequested , LPWSADATA lpWsAData ).
wVersionRequested是我們要求使用的Winsock的版本.
調用這個接口函數可以幫我們初始化Winsock .然後我們必須創建一個套接字(socket). SOCKET socket( int af , int type , int protocol );
套接字可以說是Winsock通訊的核心.Winsock通訊的所有數據傳輸,都是通過套接字來完成的,套接字包含了兩個信息,一個是IP地址,一個是Port端口號,使用這兩個信息,我們就可以確定網絡中的任何一個通訊節點.
當我們調用了socket()接口函數創建了一個套接字後,我們必須把套接字與你需要進行通訊的地址建立聯係,我們可以通過綁定函數來實現這種聯係.
int bind(SOCKET s , const struct sockaddr FAR* name , int namelen ) ;
struct sockaddr_in{
short sin_family ;
u_short sin_prot ;
struct in_addr sin_addr ;
char sin_sero[8] ;
}
就包含了我們需要建立連接的本地的地址,包括,地址族,ip和端口信
息.sin_family字段我們必須把他設為AF_INET,這是告訴Winsock使
用的是IP地址族.sin_prot 就是我們要用來通訊的端口號.sin_addr
就是我們要用來通訊的ip地址信息.
在這裏,必須還得提一下有關'大頭(big-endian)'小頭(little-endian)'.
因為各種不同的計算機處理數據時的方法是不一樣的,Intel 86處理
器上是用'小頭'形勢來表示多字節的編號,就是把低字節放在前麵,
把高字節放在後麵,而互聯網標準卻正好相反,所以,我們必須把主機
字節轉換成網絡字節的順序.Winsock API提供了幾個函數.
把主機字節轉化成網絡字節的函數;
u_long htonl( u_long hostlong );
u_short htons( u_short hostshort );
把網絡字節轉化成主機字節的函數;
u_long ntohl( u_long netlong ) ;
u_short ntohs( u_short netshort ) ;
這樣,我們設置ip地址,和port端口時,就必須把主機字節轉化成網絡
字節後,才能用bind()函數來綁定套接字和地址.
當綁定完成之後,服務器端必須建立一個監聽的隊列來接收客戶端的
連接請求.
int listen( SOCKET s ,int backlog );
這個函數可以讓我們把套接字轉成監聽模式.
如果客戶端有了連接請求,我們還必須使用
int accept( SOCKET s , struct sockaddr FAR* addr , int FAR* addrlen );
來接受客戶端的請求.
現在我們基本上已經完成了一個服務器的建立,
而客戶端的建立的流程則是初始化WinSock ,然後創建socket套接字
,再使用
int connect( SOCKET s , const struct sockaddr FAR* name , int namelen ) ;
來連接服務端.
下麵是一個最簡單的創建服務器端和客戶端的例子:
服務器端的創建 :
WSADATA wsd ;
SOCKET sListen ;
SOCKET sclient ;
UINT port = 800 ;
int iAddrSize ;
struct sockaddr_in local , client ;
WSAStartup( 0x11 , &wsd );
sListen = socket ( AF_INET , SOCK_STREAM , IPPOTO_IP ) ;
local.sin_family = AF_INET ;
local.sin_addr = htonl( INADDR_ANY ) ;
local.sin_port = htons( port ) ;
bind( sListen , (struct sockaddr*)&local , sizeof( local ) ) ;
listen( sListen , 5 ) ;
sClient = accept( sListen , (struct sockaddr*)&client , &iAddrSize ) ;
客戶端的創建:
WSADATA wsd ;
SOCKET sClient ;
UINT port = 800 ;
char szIp[] = "127.0.0.1" ;
int iAddrSize ;
struct sockaddr_in server ;
WSAStartup( 0x11 , &wsd );
sClient = socket ( AF_INET , SOCK_STREAM , IPPOTO_IP ) ;
server.sin_family = AF_INET ;
server.sin_addr = inet_addr( szIp ) ;
server.sin_port = htons( port );
connect( sClient , (struct sockaddr*)&server , sizeof( server ) ) ;
當服務器端和客戶端建立連接以後,無論是客戶端,還是服務器端都
可以使用
int send( SOCKET s , const char FAR* buf , int len , int flags );
int recv( SOCKET s , char FAR* buf , int len , int flags );
函數來接收和發送數據,因為,TCP連接是雙向的.
當要關閉通訊連結的時候,任何一方都可以調用
int shutdown( SOCKET s , int how ) ;
來關閉套接字的指定功能。再調用
int closesocket( SOCKET s) ;
來關閉套接字句柄。這樣一個通訊過程就算完成了。
注意:上麵的代碼沒有任何檢查函數返回值,如果你作網絡編程就一定要
檢查任何一個Winsock API函數的調用結果,因為很多時候函數調用
並不一定成功.上麵介紹的函數,返回值類型是int的話,如果函數調
用失敗的話,返回的都是SOCKET_ERROR.
5。Winsock編程的五種模型
上麵介紹的僅僅是最簡單的winsock通訊的方法,而實際中很多網絡
通訊的卻很多難以解決的意外情況.
例如,Winsock提供了兩種套接字模式:鎖定和非鎖定.當我們使用鎖
定套接字的時候,我們使用的很多函數,例如accpet,send,recv等等,
如果沒有數據需要處理,這些函數都不會返回,也就是說,你的應用程
序會阻塞在那些函數的調用處.而 如果使用非阻塞模式,調用這些函
數,不管你有沒有數據到達,他都會返回,所以,有可能我們在非阻塞
模式裏,調用這些函數大部分的情況下會返回失敗,所以就需要我們
來處理很多的意外出錯.
這顯然不是我們想要看到的情況.我們可以采用Winsock的通訊模型
來避免這些情況的發生。
Winsock提供了五種套接字I/O模型來解決這些問題.他們分別是
select(選擇),WSAAsyncSelect(異步選擇),
WSAEventSelect (事件選擇), overlapped(重疊) , completion
port(完成端口) .
我們在這裏詳細介紹一下select,WSAASyncSelect兩種模型.
select模型是最常見的I/O模型.
使用
int select( int nfds , fd_set FAR* readfds , fd_set FAR* writefds , fd_set FAR* exceptfds ,
const struct timeval FAR * timeout ) ;
函數來檢查你要調用的socket套接字是否已經有了需要處理的數據.
select包含三個socket隊列,分別代表:
readfds ,檢查可讀性,writefds,檢查可寫性,exceptfds,例外數據.
timeout是select函數的返回時間.
例如,我們想要檢查一個套接字是否有數據需要接收,我們可以把套
接字句柄加入可讀性檢查隊列中,然後調用select,如果,該套接字沒
有數據需要接收,select函數會把該套接字從可讀性檢查隊列中刪除
掉,所以我們隻要檢查該套接字句柄是否還存在於可讀性隊列中,就
可以知道到底有沒有數據需要接收了.
Winsock提供了一些宏用來操作套接字隊列fd_set.
FD_CLR( s,*set) 從隊列set刪除句柄s.
FD_ISSET( s, *set) 檢查句柄s是否存在與隊列set中.
FD_SET( s,*set )把句柄s添加到隊列set中.
FD_ZERO( *set ) 把set隊列初始化成空隊列.
WSAAsyncSelect(異步選擇)模型:
WSAASyncSelect模型就是把一個窗口和套接字句柄建立起連接,套接
字的網絡事件發生時時候,就會把某個消息發送到窗口,然後可以在
窗口的消息響應函數中處理數據的接收和發送.
int WSAAsyncSelect( SOCKET s, HWND hWnd , unsigned int wMsg , long lEvent ) ;
這個函數可以把套接字句柄和窗口建立起連接,
wMsg 是我們必須自定義的一個消息.
lEvent就是製定的網絡事件.包括FD_READ , FD_WRITE , FD_ACCEPT
, FD_CONNECT , FD_CLOSE .
幾個事件.
例如,我需要接收FD_READ , FD_WRITE , FD_CLOSE的網絡事件.可
以調用
WSAAsyncSelect( s , hWnd , WM_SOCKET , FD_READ | FD_WRITE | FD_CLOSE ) ;
這樣,當有FD_READ , FD_WRITE或者
FD_CLOSE網絡事件時,窗口
hWnd將會收到WM_SOCKET消息,消息參數的lParam標誌了是什麼事件
發生.
其實大家應該見過這個模型,因為MFC的CSocket類,就是使用這個模
型.
最後更新:2017-04-03 15:21:44