1.Soket 發展史以及它和 tcp/ip 的關系
七十年代中,美國國防部高研署
(DARPA)
將
TCP/IP
的軟件提供給加利福尼亞大學
Berkeley
分校后,
TCP/IP
很快被集成到
Unix
中,同時出現了許多成熟的
TCP/IP
應用程序接口
(API)
。這個
API
稱為
Socket
接口。今天,
SOCKET
接口是
TCP/IP
網絡最為 通用的
API
,也是在
INTERNET
上進行應用開發最為通用的
API
。
九十年代初,由
Microsoft
聯合了其他幾家公司共同制定了一套
WINDOWS
下的網絡編程接口,即
Windows Sockets
規范。它是
Berkeley Sockets
的重要擴充,主要是增加了一些異步函數,并增加了符合
Windows
消息驅動特性的網絡事件異步選擇機制。
Windows Sockets
規范是一套開放的、支持多種協議的
Windows
下的網絡編程接口。目前,在實際應用中的
Windows Sockets
規范主要有
1.1
版和
2.0
版。兩者的最重要區別是
1.1
版只支持
TCP/IP
協議,而
2.0
版可以支持多協議,
2.0
版有良好的向后兼容 性,目前,
Windows
下的
Internet
軟件都是基于
WinSock
開發的。
Socket
實際在計算機中提供了一個通信端口,可以通過這個端口與任何一個具有
Socket
接口的計算機通信。應用程序在網絡上傳輸,接收的信息都通過這個
Socket
接口來實現。在應用開發中就像使用文件 句柄一樣,可以對
Socket
句柄進行讀、寫操作。套接字是網絡的基本構件。它是可以被命名和尋址的通信端點,使用中的每一個套接字都有其類型和一個與之相連進程。套接字存在通信區域(通信區域又稱地址簇)中。套接字只與同一區域中的套接字交換數據(跨區域時,需要執行某和轉換進程才能實現)。
WINDOWS
中的套接字只支持一個域
——
網際域。套接字具有類型。我們將
Socket
翻譯為套接字,套接字分為以下三種類型:
字節流套接字
(Stream Socket)
是最常用的套接字類型,
TCP/IP
協議族中的
TCP
協議使用此類接口。字節流套接口提供面向連接的
(
建立虛電路
)
、無差錯的、發送先后順序一致的、無記錄邊界和非重復的網絡信包傳輸。
數據報套接字
(Datagram Socket)
TCP/IP
協議族中的
UDP
協議使用此類接口,它是無連接的服務,它以獨立的信包進行網絡傳輸,信包最大長度為
32KB
,傳輸不保證順 序性、可靠性和無重復性,它通常用于單個報文傳輸或可靠性不重要的場合。數據報套接口的一個重要特點是它保留了記錄邊界。對于這一特點。數據報套接口采用了與現在許多包交換網絡
(
例如以太網
)
非常類似的模型。
原始數據報套接字
(Raw Socket)
提供對網絡下層通訊協議
(
如
IP
協議
)
的直接訪問,它一般不是提供給普通用戶的,主要用于開發新的協議或用于提取協議較隱蔽的功能。
2 、 socket 通信概念
端口
網絡中可以被命名和尋址的通信端口,是操作系統可分配的一種資源。
按照
OSI
七層協議的描述,傳輸層與網絡層在功能上的最大區別是傳輸層提供進程通信能力。
從這個意義上講,網絡通信的最終地址就不僅僅是主機地址了,還包括可以描述進程的某種標識符。為此,
TCP/IP
協議提出了協議端口(
protocol port
,簡稱端口)的概念,用于標識通信的進程。
端口是一種抽象的軟件結構(包括一些數據結構和
I/O
緩沖區)。應用程序(即進程)通過系統調
用與某端口建立連接(
binding
)后,傳輸層傳給該端口的數據都被相應進程所接收,相應進程發給傳輸層的數據都通過該端口輸出。在
TCP/IP
協議的實現中,端口操作類似于一般的
I/O
操作,進程獲取一個端口,相當于獲取本地唯一的
I/O
文件,可以用一般的讀寫原語訪問之。
類似于文件描述符,每個端口都擁有一個叫端口號(
port number
)的整數型標識符,用于區別不同端口。由于
TCP/IP
傳輸層的兩個協議
TCP
和
UDP
是完全獨立的兩個軟件模塊,因此各自的端口號也相互獨立,如
TCP
有一個
255
號端口,
UDP
也可以有一個
255
號端口,二者并不沖突。
地址
網絡通信中通信的兩個進程分別在不同的機器上。在互連網絡中,兩臺機器可能位于不同的網絡,這些網絡通過網絡互連設備(網關,網橋,路由器等)連接。因此需要三級尋址:
某一主機可與多個網絡相連,必須指定一特定網絡地址;
網絡上每一臺主機應有其唯一的地址;
每一主機上的每一進程應有在該主機上的唯一標識符。
通常主機地址由網絡
ID
和主機
ID
組成,在
TCP/IP
協議中用
32
位整數值表示;
TCP
和
UDP
均使用
16
位端口號標識用戶進程。
網絡字節順序
不同的計算機存放多字節值的順序不同,有的機器在起始地址存放低位字節(低價先存),有的存高位字節(高價先存)。為保證數據的正確性,在網絡協議中須指定網絡字節順序。
TCP/IP
協議使用
16
位整數和
32
位整數的高價先存格式,它們均含在協議頭文件中。
面向連接
可靠的報文流、可靠的字節流、可靠的連接,如:文件傳輸(
FTP
)、遠程登錄(
Telnet
)
數字話音。
無連接
不可靠的數據報、有確認的數據報、請求-應答,如:電子郵件( E-mail )、電子郵件中的掛號信、網絡數據庫查詢。
順序
在網絡傳輸中,兩個連續報文在端-端通信中可能經過不同路徑,這樣到達目的地時的順序
可能會與發送時不同。 " 順序 " 是指接收數據順序與發送數據順序相同。 TCP 協議提供這項服務。
差錯控制
保證應用程序接收的數據無差錯的一種機制。檢查差錯的方法一般是采用檢驗、檢查和 Checksum 的方法。而保證傳送無差錯的方法是雙方采用確認應答技術。 TCP 協議提供這項服務。
流控制
在數據傳輸過程中控制數據傳輸速率的一種機制,以保證數據不被丟失。
TCP
協議提供這項服務。
字節流
字節流方式指的是僅把傳輸中的報文看作是一個字節序列,不提供數據流的任何邊界。
TCP
協議提供字節流服務。
報文
接收方要保存發送方的報文邊界。
UDP
協議提供報文服務。
全雙工
/
半雙工
端-端間數據同時以兩個方向
/
一個方向傳送。
緩存
/
帶外數據
在字節流服務中,由于沒有報文邊界,用戶進程在某一時刻可以讀或寫任意數量的字節。為保證傳輸正確或采用有流控制的協議時,都要進行緩存。但對某些特殊的需求,如交互式應用程序,又會要求取消這種緩存。
客戶
/
服務器模式
在
TCP/IP
網絡應用中,通信的兩個進程間相互作用的主要模式是客戶
/
服務器模式(
Client/Server model
),即客戶向服務器發出服務請求,服務器接收到請求后,提供相應的服務。客戶
/
服務器模式的建立基于以下兩點:首先,建立網絡的起因是網絡中軟硬件資源、運算能力和信息不均等,需要共享,從而造就擁有眾多資源的主機提供服務,資源較少的客戶請求服務這一非對等作用。其次,網間進程通信完全是異步的,相互通信的進程間既不存在父子關系,又不共享內存緩沖區,因此需要一種機制為希望通信的進程間建立聯系,為二者的數據交換提供同步,這就是基于客戶
/
服務器模式的
TCP/IP
。
客戶
/
服務器模式在操作過程中采取的是主動請求方式:
首先服務器方要先啟動,并根據請求提供相應服務:
打開一通信通道并告知本地主機,它愿意在某一公認地址上(周知口,如 FTP 為 21 )接收客戶請求;
等待客戶請求到達該端口;
接收到重復服務請求,處理該請求并發送應答信號。接收到并發服務請求,要激活一新進程來處理這個客戶請求(如 UNIX 系統中用 fork 、 exec )。新進程處理此客戶請求,并不需要對其它請求作出應答。服務完成后,關閉此新進程與客戶的通信鏈路,并終止。
返回第二步,等待另一客戶請求。
關閉服務器
客戶方:
打開一通信通道,并連接到服務器所在主機的特定端口;
向服務器發服務請求報文,等待并接收應答;繼續提出請求 ......
請求結束后關閉通信通道并終止。
從上面所描述過程可知:
客戶與服務器進程的作用是非對稱的,因此編碼不同。
服務進程一般是先于客戶請求而啟動的。只要系統運行,該服務進程一直存在,直到正?;驈娖冉K止。
3.socket 通信五元組
SOCKET PASCAL FAR socket(int af, int type, int protocol)
該調用要接收三個參數:
af
、
type
、
protocol
。參數
af
指定通信發生的區域,
UNIX
系統支持的地址族有:
AF_UNIX
、
AF_INET
、
AF_NS
等,而
DOS
、
WINDOWS
中僅支持
AF_INET
,它是網際網區域。因此,地址族與協議族相同。參數
type
描述要建立的套接字的類型。參數
protocol
說明該套接字使用的特定協議,如果調用者不希望特別指定使用的協議,則置為
0
,使用默認的連接模式。根據這三個參數建立一個套接字,并將相應的資源分配給它,同時返回一個整型套接字號。因此,
socket()
系統調用實際上指定了相關五元組中的
"
協議
"
這一元。
TCP/IP 的 socket 提供下列三種類型套接字。
流式套接字(
SOCK_STREAM
)
提供了一個面向連接、可靠的數據傳輸服務,數據無差錯、無重復地發送,且按發送順序接收。內設流量控制,避免數據流超限;數據被看作是字節流,無長度限制。文件傳送協議(
FTP
)即使用流式套接字。
數據報式套接字(
SOCK_DGRAM
)
提供了一個無連接服務。數據包以獨立包形式被發送,不提供無錯保證,數據可能丟失或重復,并且接收順序混亂。網絡文件系統(
NFS
)使用數據報式套接字。
原始式套接字(
SOCK_RAW
)
該接口允許對較低層協議,如
IP
、
ICMP
直接訪問。常用于檢驗新的協議實現或訪問現有服務中配置的新設備。
int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen)
參數
s
是由
socket()
調用返回的并且未作連接的套接字描述符
(
套接字號
)
。參數
name
是賦給套接字
s
的本地地址(名字),其長度可變,結構隨通信域的不同而不同。
namelen
表明了
name
的長度。
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
參數
s
是欲建立連接的本地套接字描述符。參數
name
指出說明對方套接字地址結構的指針。對方套接字地址長度由
namelen
說明。
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
參數
s
為本地套接字描述符,在用做
accept()
調用的參數前應該先調用過
listen()
。
addr
指向客戶方套接字地址結構的指針,用來接收連接實體的地址。
addr
的確切格式由套接字創建時建立的地址族決定。
addrlen
為客戶方套接字地址的長度(字節數)。如果沒有錯誤發生,
accept()
返回一個
SOCKET
類型的值,表示接收到的套接字的描述符。否則返回值
INVALID_SOCKET
。
調用 accept() 后,服務器等待從編號為 s 的套接字上接受客戶連接請求,而連接請求是由客戶方的 connect() 調用發出的。當有連接請求到達時, accept() 調用將請求連接隊列上的第一個客戶方套接字地址及長度放入 addr 和 addrlen ,并創建一個與 s 有相同特性的新套接字號。新的套接字可用于處理服務器并發請求。
監聽連接
──
listen()
此調用用于面向連接服務器,表明它愿意接收連接。
listen()
需在
accept()
之前調用,其調用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
參數
s
標識一個本地已建立、尚未連接的套接字號,服務器愿意從它上面接收請求。
backlog
表示請求連接隊列的最大長度,用于限制排隊請求的個數,目前允許的最大值為
5
。如果沒有錯誤發生,
listen()
返回
0
。否則它返回
SOCKET_ERROR
。
listen()
在執行調用過程中可為沒有調用過
bind()
的套接字
s
完成所必須的連接,并建立長度為
backlog
的請求連接隊列。
調用
listen()
是服務器接收一個連接請求的四個步驟中的第三步。它在調用
socket()
分配一個流套接字,且調用
bind()
給
s
賦于一個名字之后調用,而且一定要在
accept()
之前調用。
recv()
調用用于在參數
s
指定的已連接的數據報或流套接字上接收輸入數據,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
參數
s
為已連接的套接字描述符。
buf
指向接收輸入數據緩沖區的指針,其長度由
len
指定。
flags
指定傳輸控制方式,如是否接收帶外數據等。如果沒有錯誤發生,
recv()
返回總共接收的字節數。如果連接被關閉,返回
0
。否則它返回
SOCKET_ERROR
。
send()
調用用于在參數
s
指定的已連接的數據報或流套接字上發送輸出數據,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
參數
s
為已連接的本地套接字描述符。
buf
指向存有發送數據的緩沖區的指針,其長度由
len
指定。
flags
指定傳輸控制方式,如是否發送帶外數據等。如果沒有錯誤發生,
send()
返回總共發送的字節數。否則它返回
SOCKET_ERROR
。
輸入
/
輸出多路復用
──
select()
select()
調用用來檢測一個或多個套接字的狀態。對每一個套接字來說,這個調用可以請求讀、寫或錯誤狀態方面的信息。請求給定狀態的套接字集合由一個
fd_set
結構指示。在返回時,此結構被更新,以反映那些滿足特定條件的套接字的子集,同時,
select()
調用返回滿足條件的套接字的數目,其調用格式如下:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
參數
nfds
指明被檢查的套接字描述符的值域,此變量一般被忽略。
參數
readfds
指向要做讀檢測的套接字描述符集合的指針,調用者希望從中讀取數據。參數
writefds
指向要做寫檢測的套接字描述符集合的指針。
exceptfds
指向要檢測是否出錯的套接字描述符集合的指針。
timeout
指向
select()
函數等待的最大時間,如果設為
NULL
則為阻塞操作。
select()
返回包含在
fd_set
結構中已準備好的套接字描述符的總數目,或者是發生錯誤則返回
SOCKET_ERROR
。
select()
的機制中提供一
fd_set
的數據結構,實際上是一
long
類型的數組,每一個數組元素都能與一打開的文件句柄(不管是
Socket
句柄
,
還是其他文件或命名管道或設備句柄)建立聯系,建立聯系的工作由程序員完成,當調用
select()
時,由內核根據
IO
狀態修改
fd_set
的內容,由此來通知執行了
select()
的進程哪一
Socket
或文件可讀,下面具體解釋:
#include<sys/types.h>
#include<sys/times.h>
#include<sys/select.h>
intselect(nfds,readfds,writefds,exceptfds,timeout)
intnfds;
fd_set*readfds,*writefds,*exceptfds;
structtimeval*timeout;
ndfs
:
select
監視的文件句柄數,視進程中打開的文件數而定
,
一般設為呢要監視各文件中的最大文件號加一。
readfds
:
select
監視的可讀文件句柄集合。
writefds:select
監視的可寫文件句柄集合。
exceptfds
:
select
監視的異常文件句柄集合。
timeout
:本次
select()
的超時結束時間。(見
/usr/sys/select.h
,可精確至百萬分之一秒?。?
當
readfds
或
writefds
中映象的文件可讀或可寫或超時,本次
select()
就結束返回。程序員利用一組系統提供的宏在
select()
結束時便可判
斷哪一文件可讀或可寫。對
Socket
編程特別有用的就是
readfds
。
幾只相關的宏解釋如下:
FD_ZERO(fd_set*fdset)
:清空
fdset
與所有文件句柄的聯系。
FD_SET(intfd,fd_set*fdset)
:建立文件句柄
fd
與
fdset
的聯系。
FD_CLR(intfd,fd_set*fdset)
:清除文件句柄
fd
與
fdset
的聯系。
FD_ISSET(intfd,fdset*fdset)
:檢查
fdset
聯系的文件句柄
fd
是否可讀寫,
>0
表示可讀寫。
(關于
fd_set
及相關宏的定義見
/usr/include/sys/types.h
)
這樣,你的
socket
只需在有東東讀的時候才讀入,大致如下:
...
intsockfd;
fd_setfdR;
structtimevaltimeout=..;
...
for(;;){
FD_ZERO(&fdR);
FD_SET(sockfd,&fdR);
switch(select(sockfd+1,&fdR,NULL,&timeout)){
case-1:
errorhandledbyu;
case0:
timeouthanledbyu;
default:
if(FD_ISSET(sockfd)){
nowureadorrecvsomething;
/*ifsockfdisfatherand
serversocket,ucannow
accept()*/
}
}
}
所以一個
FD_ISSET(sockfd)
就相當通知了
sockfd
可讀。
至于
structtimeval
在此的功能,請
manselect
。不同的
timeval
設置使使
select()
表現出超時結束、無超時阻塞和輪詢三種特性。由于
timeval
可精確至百萬分之一秒,所以
Windows
的
SetTimer()
根本不算什么。你可以用
select()
做一個超級時鐘。
服務器方程序:
/* File Name: streams.c */
#include
#include
#define TRUE 1
/*
這個程序建立一個套接字,然后開始無限循環;每當它通過循環接收到一個連接,則打印出一個信息。當連接斷開,或接收到終止信息,則此連接結束,程序再接收一個新的連接。命令行的格式是:
streams */
main( )
{
int sock, length;
struct sockaddr_in server;
struct sockaddr tcpaddr;
int msgsock;
char buf[1024];
int rval, len;
/*
建立套接字
*/
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("opening stream socket");
exit(1);
}
/*
使用任意端口命名套接字
*/
server.sin_family = AF_INET;
server.sin_port = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("binding stream socket");
exit(1);
}
/*
找出指定的端口號并打印出來
*/
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
perror("getting socket name");
exit(1);
}
printf("socket port #%d/n", ntohs(server.sin_port));
/*
開始接收連接
*/
listen(sock, 5);
len = sizeof(struct sockaddr);
do {
msgsock = accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
if (msgsock == -1)
perror("accept");
else do{
memset(buf, 0, sizeof(buf));
if ((rval = recv(msgsock, buf, 1024)) < 0)
perror("reading stream message");
if (rval == 0)
printf("ending connection /n");
else
printf("-->%s/n", buf);
}while (rval != 0);
closesocket(msgsock);
} while (TRUE);
/*
因為這個程序已經有了一個無限循環,所以套接字
"sock"
從來不顯式關閉。然而,當進程被殺死或正常終止時,所有套接字都將自動地被關閉。
*/
exit(0);
}
客戶方程序:
/* File Name: streamc.c */
#include
#include
#define DATA "half a league, half a league ..."
/*
這個程序建立套接字,然后與命令行給出的套接字連接;連接結束時,在連接上發送
一個消息,然后關閉套接字。命令行的格式是:
streamc
主機名 端口號
端口號要與服務器程序的端口號相同
*/
main(argc, argv)
int argc;
char *argv[ ];
{
int sock;
struct sockaddr_in server;
struct hostent *hp, *gethostbyname( );
char buf[1024];
/*
建立套接字
*/
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("opening stream socket");
exit(1);
}
/*
使用命令行中指定的名字連接套接字
*/
server.sin_family = AF_INET;
hp = gethostbyname(argv[1]);
if (hp == 0) {
fprintf(stderr, "%s: unknown host /n", argv[1]);
exit(2);
}
memcpy((char*)&server.sin_addr, (char*)hp->h_addr, hp->h_length);
sever.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
perror("connecting stream socket");
exit(3);
}
if (send(sock, DATA, sizeof(DATA)) < 0)
perror("sending on stream socket");
closesocket(sock);
exit(0);
}
4.Windows socket 程序設計
Windows Sockets 是從 Berkeley Sockets 擴展而來的,其在繼承 Berkeley Sockets 的基礎上,又進行了新的擴充。這些擴充主要是提供了一些異步函數,并增加了符合 WINDOWS 消息驅動特性的網絡事件異步選擇機制。
Windows Sockets
由兩部分組成:開發組件和運行組件。
開發組件:
Windows Sockets
實現文檔、應用程序接口
(API)
引入庫和一些頭文件。
運行組件:
Windows Sockets
應用程序接口的動態鏈接庫
(WINSOCK.DLL)
。
在 Microsoft Windows 下開發 Windows Sockets 網絡程序與在 UNIX 環境下開發 Berkeley Sockets 網絡程序有一定的差別,這主要時因為 Windows 是非搶先多任務環境,各任務之間的切換是通過消息驅動的。因此,在 Windows 下開發 Sockets 網絡程序要盡量避開阻塞工作方式,而使用 Windows Sockets 提供的基于消息機制的網絡事件異步存取接口。
Windows Sockets 為了支持 Windows 消息驅動機制,使應用程序開發者能夠方便地處理網絡通信,它對網絡事件采用了基于消息的異步存取策略?;谶@一策略, Windows Sockets 在如下方面作了擴充:
異步選擇機制
UNIX 下 Sockets 對于異步事件的選擇是靠調用 select() 函數來查詢的,這種方式對于 Windows 應用程序來說是難以接受的。 Windows Sockets 的異步選擇函數提供了消息機制的網絡事件選擇,當使用它登記的網絡事件發生時, Windows 應用程序相應的窗口函數將收到一個消息,消息中指示了發生的網絡事件,以及與事件相關的一些信息。
異步請求函數
在標準 Berkeley Sockets 中,請求服務是阻塞的。 Windows Sockets 除了支持這一類函數外,還增加了相應的異步請求服務函數 WSAASyncGetXByY() 。這些異步請求函數允許應用程序采用異步方式獲取請求信息,并且在請求的服務完成時給應用程序相應的窗口函數發送一個消息。
阻塞處理方法
Windows Sockets 為了實現當一個應用程序的套接字調用處于阻塞時,能夠放棄 CPU 讓其它應用程序運行,它在調用處于阻塞時便進入一個叫“ HOOK ”的例程,此例程負責接收和分配 Windows 消息,這使得其它應用程序仍然能夠接收到自己的消息并取得控制權。 Windows Sockets 還提供了兩個函數 (WSASetBlockingHook() 和 WSAUnhookBlockingHook()) 讓用戶設置和取消自己的阻塞處理例程,以支持要求復雜消息處理的應用程序(如多文檔界面)。
出錯處理
Windows Sockets 為了和以后多線程環境(如 Windows/NT )兼容,它提供了兩個出錯處理函數 WSAGetLastError() 和 WSASetLastError() 來獲取和設置當前線程的最近錯誤號,而不使用 Berkeley Sockets 中的全局變量 errno 和 h_errno 。
啟動與終止
對于所有在 Windows Sockets 上開發的應用程序,在它使用任何 Windows Sockets API 調用之前,必須先調用啟動函數 WSAStartup() ,它完成 Windows Sockets DLL 的初始化;協商版本支持,分配必要的資源。在應用程序完成了對 Windows Sockets 的使用之后,它必須調用函數 WSACleanup() 來從 Windows Sockets 實現中注銷自己,并允許實現釋放為其分配的任何資源。
服務器端操作
socket
(套接字)
在初始化階段調用
WSAStartup()
此函數在應用程序中初始化
Windows Sockets DLL
,只有此函數調用成功后,應用程序才可以再調用其他
Windows Sockets DLL
中的
API
函數。在程式中調用該函數的形式如下:
WSAStartup((WORD)((1<<8|1)
,(
LPWSADATA
)
&WSAData)
,其中
(1<<8|1)
表示我們用的是
WinSocket1.1
版本,
WSAata
用來存儲系統傳回的關于
WinSocket
的資料。
建立
Socket
初始化
WinSock
的動態連接庫后,需要在服務器端建立一個 監聽的
Socket
,為此可以調用
Socket()
函數用來建立這個監聽的
Socket
,并定義此
Socket
所使用的通信協議。此函數調用成功返回
Socket
對象,失敗則返回
INVALID_SOCKET(
調用
WSAGetLastError()
可得知原因,所有
WinSocket
的函數都可以使用這個函數來獲取失敗的原因
)
。
SOCKET PASCAL FAR socket( int af, int type, int protocol )
參數
: af:
目前只提供
PF_INET(AF_INET)
;
type
:
Socket
的類型
(SOCK_STREAM
、
SOCK_DGRAM)
;
protocol
:通訊協定
(
如果使用者不指定則設為
0)
;
如果要建立的是遵從
TCP/IP
協議的
socket
,第二個參數
type
應為
SOCK_STREAM
,如為
UDP
(數據報)的
socket
,應為
SOCK_DGRAM
。
綁定端口
接下來要為服務器端定義的這個監聽的
Socket
指定一個地址及端口(
Port
),這樣客戶端才知道待會要連接哪一個地址的哪個端口,為此我們要調用
bind()
函數,該函數調用成功返回
0
,否則返回
SOCKET_ERROR
。
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );
參 數:
s
:
Socket
對象名;
name
:
Socket
的地址值,這個地址必須是執行這個程式所在機器的
IP
地址;
namelen
:
name
的長度;
如果使用者不在意地址或端口的值,那么可以設定地址為
INADDR_ANY
,及
Port
為
0
,
Windows Sockets
會自動將其設定適當之地址及
Port (1024
到
5000
之間的值
)
。此后可以調用
getsockname()
函數來獲知其被設定的值。
監聽
當服務器端的
Socket
對象綁定完成之后
,
服務器端必須建立一個監聽的隊列來接收客戶端的連接請求。
listen()
函數使服務器端的
Socket
進入監聽狀態,并設定可以建立的最大連接數
(
目前最大值限制為
5,
最小值為
1)
。該函數調用成功返回
0
,否則返回
SOCKET_ERROR
。
int PASCAL FAR listen( SOCKET s, int backlog );
參 數:
s
:需要建立監聽的
Socket
;
backlog
:最大連接個數;
異步選擇
服務器端的
Socket
調用完
listen
()后,如果此時客戶端調用
connect
()函數提出連接申請的話,
Server
端必須再調用
accept()
函數,這樣服務器端和客戶端才算正式完成通信程序的連接動作。為了知道什么時候客戶端提出連接要求,從而服務器端的
Socket
在恰當的時候調用
accept()
函數完成連接的建立,我們就要使用
WSAAsyncSelect
()函數,讓系統主動來通知我們有客戶端提出連接請求了。該函數調用成功 返回
0
,否則返回
SOCKET_ERROR
。
int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent );
參數:
s
:
Socket
對象;
hWnd
:接收消息的窗口句柄;
wMsg
:傳給窗口的消息;
lEvent
: 被注冊的網絡事件,也即是應用程序向窗口發送消息的網路事件,該值為下列值
FD_READ
、
FD_WRITE
、
FD_OOB
、
FD_ACCEPT
、
FD_CONNECT
、
FD_CLOSE
的組合,各個值的具體含意為
FD_READ
:希望在套接字
S
收到數據時收到消息;
FD_WRITE
:希望在套接字
S
上可以發送數據時收到消息;
FD_ACCEPT
:希望在套接字
S
上收到連接請求時收到消息;
FD_CONNECT
:希望在套接字
S
上連接成功時收到消 息;
FD_CLOSE
:希望在套接字
S
上連接關閉時收到消息;
FD_OOB
:希望在套接字
S
上收到帶外數據時收到消息。
具體應用時,
wMsg
應是在應用程序中定義的消息名稱,而消息結構中的
lParam
則為以上各種網絡事件名稱。所以,可以在窗口處理自定義消息函數中使用以下結構來響應
Socket
的不同事件:
switch(lParam)
{
case FD_READ:
…
break;
case FD_WRITE
、
…
break;
…
}
服務器端接受客戶端的連接請求
當
Client
提出連接請求時,
Server
端
hwnd
視窗會收到
Winsock Stack
送來我們自定義的一 個消息,這時,我們可以分析
lParam
,然后調用相關的函數來處理此事件。為了使服務器端接受客戶端的連接請求,就要使用
accept()
函數,該函數新建一
Socket
與客戶端的
Socket
相通,原先監聽之
Socket
繼續進入監聽狀態,等待他人的連接要求。該函數調用成功返回一個新產生的
Socket
對象,否則返回
INVALID_SOCKET
。
SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen );
參數:
s
:
Socket
的識別碼;
addr
:存放來連接的客戶端的地址;
addrlen
:
addr
的長度
結束
socket
連接
結束服務器和客戶端的通信連接是很簡單的,這一過程可以由服務器或客戶機的任一端啟動,只要調用
closesocket()
就可以了,而要關閉
Server
端監聽狀態的
socket
,同樣也是利用此函數。另外,與程序啟動時調用
WSAStartup()
憨數相對應,程式結束前,需要調用
WSACleanup()
來通知
Winsock Stack
釋放
Socket
所占用的資源。這兩個函數都是調用成功返回
0
,否則返回
SOCKET_ERROR
。
int PASCAL FAR closesocket( SOCKET s );
參 數:
s
:
Socket
的識別碼;
int PASCAL FAR WSACleanup( void );
參 數: 無
客戶端
Socket
的操作
建立客戶端的
Socket
客戶端應用程序首先也是調用
WSAStartup()
函數來與
Winsock
的動態連接庫建立關系,然后同樣調用
socket()
來建立一個
TCP
或
UDP socket
(相同協定的
sockets
才能相通,
TCP
對
TCP
,
UDP
對
UDP
)。與服務器端的
socket
不同的是,客戶端的
socket
可以調用
bind()
函數,由自己來指定
IP
地址及
port
號碼;但是也可以不調用
bind()
,而由
Winsock
來自動設定
IP
地址及
port
號碼。
提出連接申請
客戶端的
Socket
使用
connect()
函數來提出與服務器端的
Socket
建立連接的申請,函數調用成功返回
0
,否則返回
SOCKET_ERROR
。
int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
參 數:
s
:
Socket
的識別碼;
name
:
Socket
想要連接的對方地址;
namelen
:
name
的長度
數據的傳送
雖然基于
TCP/IP
連接協議(流套接字)的服務是設計客戶機
/
服務器應用程序時的主流標準,但有些服務也是可以通過無連接協議(數據報套接字)提供的。先介紹一下
TCP socket
與
UDP socket
在傳送數據時的特性:
Stream (TCP) Socket
提供雙向、可靠、有次序、不重復的資料傳送。
Datagram (UDP) Socket
雖然提供雙向的通信,但沒有可靠、有次序、不重復的保證,所以
UDP
傳送數據可能會收到無次序、重復的資料,甚至資料在傳輸過程中出現遺漏。由于
UDP Socket
在傳送資料時,并不保證資料能完整地送達對方,所以絕大多數應用程序都是采用
TCP
處理
Socket
,以保證資料的正確性。一般情況下
TCP Socket
的數據發送和接收是調用
send()
及
recv()
這兩個函數來達成,而
UDP Socket
則是用
sendto()
及
recvfrom()
這兩個函數,這兩個函數調用成功發揮發送或接收的資料的長度,否則返回
SOCKET_ERROR
。
int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags );
參數:
s
:
Socket
的識別碼
buf
:存放要傳送的資料的暫存區
len buf
:的長度
flags
:此函數被調用的方式
對于
Datagram Socket
而言,若是
datagram
的大小超過限制,則將不會送出任何資料,并會傳回錯誤值。對
Stream Socket
言,
Blocking
模式下,若是傳送系統內的儲存空間不夠存放這些要傳送的資料,
send()
將會被
block
住,直到資料送完為止;如果該
Socket
被設定為
Non-Blocking
模式,那么將視目前的
output buffer
空間有多少,就送出多少資料,并不會被
block
住。
flags
的值可設為
0
或
MSG_DONTROUTE
及
MSG_OOB
的組合。
int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );
參數:
s
:
Socket
的識別碼
buf
:存放接收到的資料的暫存區
len buf
:的長度
flags
:此函數被調用的方式
對
Stream Socket
言,我們可以接收到目前
input buffer
內有效的資料,但其數量不超過
len
的大小。
自定義的
CMySocket
類的實現代碼:
根據上面的知識,自定義了一個簡單的
CMySocket
類,下面是定義的該類的部分實現代碼:
CMySocket::CMySocket() : file://
類的構造函數
{
WSADATA wsaD;
memset( m_LastError, 0, ERR_MAXLENGTH );
// m_LastError
是類內字符串變量
,
初始化用來存放最后錯誤說明的字符串;
//
初始化類內
sockaddr_in
結構變量,前者存放客戶端地址,后者對應于服務器端地址
;
memset( &m_sockaddr, 0, sizeof( m_sockaddr ) );
memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
int result = WSAStartup((WORD)((1<<8|1)
,
&wsaD);//
初始化
WinSocket
動態連接庫
;
if( result != 0 ) //
初始化失敗;
{ set_LastError( "WSAStartup failed!", WSAGetLastError() );
return;
}
}
//////////////////////////////
CMySocket::~CMySocket() { WSACleanup(); }//
類的析構函數;
////////////////////////////////////////////////////
int CMySocket::Create( void )
{// m_hSocket
是類內
Socket
對象,創建一個基于
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 )//
關閉
Socket
對象;
{
if ( closesocket( m_hSocket ) == SOCKET_ERROR )
{
set_LastError( "closesocket() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
file://
重置
sockaddr_in
結構變量;
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 )//
定義連接函數;
{
if( strlen( strRemote ) == 0 || iPort == 0 )
return ERR_BADPARAM;
hostent *hostEnt = NULL;
long lIPAddress = 0;
hostEnt = gethostbyname( strRemote );//
根據計算機名得到該計算機的相關內容;
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 )//
綁定函數;
{
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 )//
建立連接函數,
S
為監聽
Socket
對象名;
{
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://
事件選擇函數;
{
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 )//
監聽函數;
{
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 )//
數據發送函數;
{
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 )//
數據接收函數;
{
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
操作錯誤字符串設置函數;
{
memset( m_LastError, 0, ERR_MAXLENGTH );
memcpy( m_LastError, newError, strlen( newError ) );
m_LastError[strlen(newError)+1] = '/0';
}
有了上述類的定義,就可以在網絡程序的服務器和客戶端分別定義
CMySocket
對象,建立連接,傳送數據了。例如,為了在服務器和客戶端發送數據,需要在服務器端定義兩個
CMySocket
對象
ServerSocket1
和
ServerSocket2
,分別用于監聽和連接,客戶端定義一個
CMySocket
對象
ClientSocket
,用于發送或接收數據,如果建立的連接數大于一,可以在服務器端再定義
CMySocket
對象,但要注意 連接數不要大于五。
在
VC
中進行
WINSOCK
的
API
編程開發的時候,需要在項目中使用下面三個文件,否則會出現編譯錯誤。
1
.
WINSOCK.H:
這是
WINSOCK API
的頭文件,需要包含在項目中。
2
.
WSOCK32.LIB: WINSOCK API
連接庫文件。在使用中,一定要把它作為項目的非缺省的連接庫包含到項目文件中去。
3
.
WINSOCK.DLL: WINSOCK
的動態連接庫,位于
WINDOWS
的安裝目錄下。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
