相信很多人都對網絡程式設計感興趣,下面我們就來介紹,在網絡程式設計中應用最廣泛的程式設計接口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 ) ;
用戶端的建立:
char szip[] = "127.0.0.1" ;
struct sockaddr_in server ;
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類,就是使用這個模
型.