一、基本知識
1、Winsock,一種標准API,一種網絡編程接口,用於兩個或多個應用程序(或進程)之間通過網絡進行數據通信。具有兩個版本:
Winsock 1:
Windows CE平台支持。
頭文件:WinSock.h
庫:wsock32.lib
Winsock 2:
部分平台如Windows CE貌似不支持。通過前綴WSA可以區別於Winsock 1版本。個別函數如WSAStartup、WSACleanup、WSARecvEx、WSAGetLastError都屬於Winsock 1.1規范的函數;
頭文件:WinSock2.h
庫:ws2_32.lib
mswsock.h用於編程擴展,使用時必須鏈接mswsock.dll。
2、網絡協議:
IP (Internet Protocol) 網際協議,無連接協議;
TCP (Transmission Control Protocol) 傳輸控制協議;
UDP (User Datagram Protocol) 用戶數據協議;
FTP (File Transfer Protocol) 文件傳輸協議;
HTTP (Hypertext Transfer Protocol) 超文本傳輸協議;
3、字節存儲順序:
big_endian:大端存儲,存儲順序從高位到低位,地址指向最高有效字節。在網絡中將IP和端口指定為多字節時使用大端存儲,也稱為網絡字節順序(network_byte)。貌似MAC OS使用的是大端存儲方式;
little_endian:小端存儲,存儲順序從低位到高位,地址指向最低有效字節。本地主機存儲IP和端口制定的多字節時使用,也稱為主機字節順序(host_byte)。大多數系統都是小端存儲;
用下面的方式可以檢測是否為大端存儲:
bool IsBig_endian()
{
unsigned short test = 0x1122;
if ( *( (unsigned char*)&test ) == 0x11 ) //force unsigned short pointer to ussigned char pointer
{
return true;
}
else
{
return false;
}
}
此外有很多函數可以用來進行 主機字節和網絡字節之間的轉換,如:
u_long htonl( u_long hostlong );
int WSAHtonl( SOCKET s, u_long hostlong, u_long FAR *lpnetlong );
而有時網絡IP是點分法表示的,如:192.168.0.1,使用函數unsigned long inet_addr( const char FAR *cp ); 可以將點分法的IP字符串作為一個網絡字節順序的32位u_long返回。
二、快速了解
1、Winsock初始化:
首先確保包含對應版本的頭文件,然后保證鏈接對應的庫文件(可以在代碼中使用#pragma comment(lib, "WS2_32"),或在編譯器項目屬性中鏈接器->輸入->附加依賴項中添加ws2_32.lib);
通過調用WSAStartup函數來實現加載Winsock庫:
print?
int
WSAAPI
WSAStartup(
IN WORD wVersionRequested,
OUT LPWSADATA lpWSAData
);
其中參數wVersionRequested用來指定加載Winsock庫的版本,高位字節為次版本,低位字節為主版本,使用宏MAKEWORD(x,y)來生成一個WORD;
參數lpWSAData是指向WASDATA結構指針,加載的版本庫信息將會填充這個結構,詳細內容自查。
在使用Winsock后需要釋放資源,並取消應用程序掛起的Winsock操作。使用int WASCleanup();
2、錯誤處理:
如果已經加載了Winsock庫,則調用Winsock函數出錯后,通常會返回SOCKET_ERROR,而通過使用函數int WSAGetLastError() 可以獲得具體信息值,例如:
if ( SOCKET_ERROR == WSACleanup() )
{
cout << "WSACleanup error " << WSAGetLastError() << endl;
return 0;
}
根據獲取錯誤信息值,可以知道錯誤原因,並進行相應的處理。
3、尋址:
想要進行通信就需要知道彼此的地址,一般來說這個地址由IP和端口號來決定。在Winsock中使用SOCKADDR_IN結構來指定地址信息:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family字段通常設置為AF_INET,表示Winsock此時正在使用IP地址族;
sin_port用於標示TCP或UDP通信端口,部分端口是為一些服務保留的,如FTP和HTTP使用要注意;
sin_adr字段把地址(例如是IPv4地址)作為一個4字節的量來存儲起來,它是u_long類型,且是網絡字節順序的。可以使用inet_addr來處理點分法表示的IP地址;
sin_zero只充當填充項,以使SOCKADDR_IN結構和SOCKADDR結構長度一樣。
以下簡單的使用SOCKADDR_IN來指定地址:
print?
//創建一個地址
int serverPort = 5150;
char FAR serverIP[] = "192.168.1.102"; //本機ip,不知道就ipconfig
SOCKADDR_IN serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons( serverPort );
serverAddr.sin_addr.s_addr = inet_addr( serverIP );
int serverAddr_size = static_cast<int>( sizeof(serverAddr) );
有時作為一個連接通信的服務端來說,在設置監聽socket的地址結構時sin_addr.s_addr的值可以是htonl( INADDR_ANY ),INADDR_ANY允許將socket綁定到系統中所有可用的接口,以便傳到任意接口的連接(端口必須正確)都可以被監聽socket接受。
4、socket套接字:
套接字是通信時為傳輸提供的句柄,Winsock的操作都是基於套接字實現的。創建一個套接字有socket和WSASocket方法:
SOCKET
WSAAPI
socket(
IN int af, //協議的地址族,使用IPv4來描述Winsock,設置為AF_INET
IN int type, //套接字類型,TCP/IP設置為SOCK_STREAM,UDP/IP設置為SOCK_DGRAM
IN int protocol //用於給定地址族和類型具有多重入口的傳送限定,TCP設置為IPPROTO_TCP,UDP設置為IPPROTO_UDP
);
如果創建成功,函數會返回一個有效的SOCKET,否則會返回INVALID_SOCKET,可以用WSAGetLastError()函數獲得錯誤信息。
5、連接通信實現過程:
連結通信是基於TCP/IP實現的,進行數據傳輸前,通信雙方要進行連接。
服務端:
初始化Winsock后,創建一個監聽socket和一個要接受連接的地址結構;
使用bind將監聽socket與地址結構進行關聯;
int
WSAAPI
bind(
IN SOCKET s, //一個用於監聽的socket
IN const struct sockaddr FAR * name, //指向進行綁定的地址結構
IN int namelen //進行綁定的地址結構的大小
);
使用listen將bind成功的監聽socket狀態設置為監聽狀態;
int
WSAAPI
listen(
IN SOCKET s, //一個用於監聽的socket,已經進行bind
IN int backlog //允許掛起連接的隊列的最大長度,超過這個長度后,再有連接將會失敗
);
使用accept接受通過監聽socket獲取的連接,成功后將返回的新的連接socket進行保存以便數據傳輸;
print?
SOCKET
WSAAPI
accept(
IN SOCKET s, //處於監聽模式的socket
OUT struct sockaddr FAR * addr, //指向一個地址結構,用來接受連接后獲得對方地址信息
IN OUT int FAR * addrlen //指向一個整數,表示參數2指向地址結構的大小
);
客戶端:
初始化Winsock后,創建一個監聽socket和一個要連接的服務器地址結構;
使用connect將socket和服務器地址結構進行初始化連接,成功后將使用socket進行數據傳輸;
print?
int
WSAAPI
connect(
IN SOCKET s, //要建立連接的socket
IN const struct sockaddr FAR * name, //指向保存要建立連接信息的地址結構
IN int namelen //參數2指向地址結構的大小
);
連接成功后,使用send、recv來進行數據傳輸;
print?
int
WSAAPI
send(
IN SOCKET s, //進行連接的socket
IN const char FAR * buf, //指向發送數據的緩沖區
IN int len, //發送數據的字符數
IN int flags //一個標志位,可以是0、MSG_DONTROUTE、MSG_OOB還可以是他們的或運算結果
); //返回已經發送的數據長度
int
WSAAPI
recv(
IN SOCKET s, //進行連接的socket
OUT char FAR * buf, //指向接受數據的緩沖區
IN int len, //准備接受數據字節數或緩沖區的長度
IN int flags //可以是0、MSG_PEEK、MSG_OOB還可以是他們的或運算結果
); //返回已接受的數據長度
連接結束后,使用shutdown和closesocket來斷開連接和釋放資源;
print?
int
WSAAPI
shutdown(
IN SOCKET s, //要關閉的socket
IN int how //關閉標志:SD_RECEIVE、SD_SEND、SD_BOTH
);
6、無連接通信實現過程:
無連接通信是基於UDP/IP實現的,UDP不能確保可靠的數據傳輸,但能將數據發送到多個目標,或者接受多個源的數據。
初始化Winsock后,可以創建socket和用以進行通信任意地址結構;
使用recvfrom通過socket和通信的地址結構接受數據;
使用sendto通過socket和通信的地址結構發送數據;
print?
int
WSAAPI
recvfrom(
IN SOCKET s,
OUT char FAR * buf,
IN int len,
IN int flags,
OUT struct sockaddr FAR * from,
IN OUT int FAR * fromlen
);
int
WSAAPI
sendto(
IN SOCKET s,
IN const char FAR * buf,
IN int len,
IN int flags,
IN const struct sockaddr FAR * to,
IN int tolen
);
同樣通信結束后,使用shutdown和closesocket來斷開連接和釋放資源
上述使用函數都有多個版本,而且相關的一些標志位參數可以提供設置選項,另外,返回的錯誤處理等也有待於詳細研究;
7、select函數:
select()用於確定一個或多個套接口的狀態。對每一個套接口,調用者可查詢它的可讀性、可寫性及錯誤狀態信息。
print?
int
WSAAPI
select(
IN int nfds, //指集合中所有文件描述符的范圍,即所有文件描述符的最大值加1,在Windows中值無所謂。
IN OUT fd_set FAR * readfds, //可選指針,指向一組等待可讀性檢查的套接字。
IN OUT fd_set FAR * writefds, //可選指針,指向一組等待可寫性檢查的套接字。
IN OUT fd_set FAR *exceptfds, //可選指針,指向一組等待錯誤檢查的套接字。
IN const struct timeval FAR * timeout //select()最多等待時間,對阻塞操作則為NULL。
);
//用fd_set結構來表示一組等待檢查的套接口。在調用返回時,這個結構存有滿足一定條件的套接口組的子集:
typedef struct fd_set {
u_int fd_count; //其中set元素數目
SOCKET fd_array[FD_SETSIZE]; //保存set元素的數組
} fd_set;
fd_set set;
FD_ZERO(&set); /*將set清零使集合中不含任何fd*/
FD_SET(fd, &set); /*將fd加入set集合*/
FD_CLR(fd, &set); /*將fd從set集合中清除*/
FD_ISSET(fd, &set); /*測試fd是否在set集合中*/
select的返回值:
select()調用返回處於就緒狀態並且已經包含在fd_set結構中的描述字總數;
如果超時則返回0;否則的話,返回SOCKET_ERROR錯誤,通過WSAGetLastError獲取相應錯誤代碼。
當返回位0時,所有描述符集清0;
當返回為-1時,不修改任何描述符集;
當返回為非0時,在3個描述符集里,依舊是1的位就是准備好的描述符。這也就是為什么,每次用select后都要用FD_ISSET的原因。
參考:http://www.2cto.com/kf/201111/111568.html