一、基本知識
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) 超文字傳輸通訊協定 (HTTP);
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 ){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庫:
intWSAAPIWSAStartup( 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來指定地址:
//建立一個地址int serverPort= 5150;char FAR serverIP[] = "192.168.1.102"; //本機ip,不知道就ipconfigSOCKADDR_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方法:
SOCKETWSAAPIsocket( 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與地址結構進行關聯;
intWSAAPIbind( IN SOCKET s,//一個用於監聽的socket IN const struct sockaddr FAR * name,//指向進行綁定的地址結構 IN int namelen//進行綁定的地址結構的大小 );
使用listen將bind成功的監聽socket狀態設定為監聽狀態;
intWSAAPIlisten( IN SOCKET s,//一個用於監聽的socket,已經進行bind IN int backlog//允許掛起串連的隊列的最大長度,超過這個長度後,再有串連將會失敗 );
使用accept接受通過監聽socket擷取的串連,成功後將返回的新的串連socket進行儲存以便資料轉送;
SOCKETWSAAPIaccept( IN SOCKET s,//處於監聽模式的socket OUT struct sockaddr FAR * addr,//指向一個地址結構,用來接受串連後獲得對方地址資訊 IN OUT int FAR * addrlen//指向一個整數,表示參數2指向地址結構的大小 );
用戶端:
初始化Winsock後,建立一個監聽socket和一個要已連線的服務器地址結構;
使用connect將socket和伺服器位址結構進行初始化串連,成功後將使用socket進行資料轉送;
intWSAAPIconnect(IN SOCKET s,//要建立串連的socketIN const struct sockaddr FAR * name,//指向儲存要建立串連資訊的地址結構IN int namelen//參數2指向地址結構的大小);
串連成功後,使用send、recv來進行資料轉送;
intWSAAPIsend( IN SOCKET s,//進行串連的socket IN const char FAR * buf,//指向發送資料的緩衝區 IN int len,//發送資料的字元數 IN int flags//一個標誌位,可以是0、MSG_DONTROUTE、MSG_OOB還可以是他們的或運算結果 );//返回已經發送的資料長度intWSAAPIrecv( IN SOCKET s,//進行串連的socket OUT char FAR * buf,//指向接受資料的緩衝區 IN int len,//準備接受資料位元組數或緩衝區的長度 IN int flags//可以是0、MSG_PEEK、MSG_OOB還可以是他們的或運算結果 );//返回已接受的資料長度
串連結束後,使用shutdown和closesocket來中斷連線和釋放資源;
intWSAAPIshutdown( IN SOCKET s,//要關閉的socket IN int how//關閉標誌:SD_RECEIVE、SD_SEND、SD_BOTH );
6、無串連通訊實現過程:
無串連通訊是基於UDP/IP實現的,UDP不能確保可靠的資料轉送,但能將資料發送到多個目標,或者接受多個源的資料。
初始化Winsock後,可以建立socket和用以進行通訊任意地址結構;
使用recvfrom通過socket和通訊的地址結構接受資料;
使用sendto通過socket和通訊的地址結構發送資料;
intWSAAPIrecvfrom( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags, OUT struct sockaddr FAR * from, IN OUT int FAR * fromlen );intWSAAPIsendto( 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()用於確定一個或多個套介面的狀態。對每一個套介面,調用者可查詢它的可讀性、可寫性及錯誤狀態資訊。
intWSAAPIselect( 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的原因。
三、簡單實踐
利用上述內容,實現一個基於TCP/IP的串連通訊。
服務端:
//******************************************************************
#include "stdafx.h"#include <iostream>#include <WinSock2.h>#pragma comment(lib, "WS2_32")using namespace std;# define REQUEST_BACKLOG 5//******************************//好吧不用寫這些糾結的函數,就是看著清晰些//初始化Winsockbool InitWSA( const WORD &wVersion, WSADATA *wsadata ){int Ret = 0;if ( ( Ret = WSAStartup( wVersion,wsadata ) ) != 0 ){cout << "WSAStartup failed, error " << Ret << endl;return false;}return true;}//結束Winsockvoid cleanWSA(){if ( WSACleanup() == SOCKET_ERROR ){cout << "WSACleanup failed, error " << WSAGetLastError() << endl;}}//IPv4定址,通過ip填充SOCKADDR_IN結構void InitSockAddrByIP( SOCKADDR_IN *pSockAddr, const char FAR *strIP, const INT &nPortID ){pSockAddr->sin_family= AF_INET;pSockAddr->sin_port= htons( nPortID );if ( 0 != strlen(strIP) ){pSockAddr->sin_addr.s_addr= inet_addr( strIP );}else{pSockAddr->sin_addr.s_addr= htonl( INADDR_ANY );}}//bindbool bindAddr( const SOCKADDR_IN *pSockAddr, SOCKET *pSocket ){int bindResult = bind( *pSocket, (sockaddr *)(pSockAddr), sizeof(*pSockAddr));if ( SOCKET_ERROR == bindResult ){cout << "bind error :" << WSAGetLastError() << endl;return false;} return true;}//listenbool setListener( SOCKET *pSocket, int backlog ){int nResult = listen( *pSocket, backlog );if ( SOCKET_ERROR == nResult ){cout << "listen error :" << WSAGetLastError() << endl;return false;} return true;}//******************************//程式入口int _tmain(int argc, _TCHAR* argv[]){//初始化WinsockWSADATA wsadata;if ( !InitWSA( MAKEWORD(2,2), &wsadata ) ){return 0;}//指定串連ip地址和伺服器口SOCKADDR_IN InternetAddr;//char FAR strIP[]= "198.0.0.0";char FAR strIP[]= "";INTnPortID= 5150;InitSockAddrByIP( &InternetAddr, strIP, nPortID );//建立listener_socketSOCKET listener_socket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );if ( INVALID_SOCKET == listener_socket ){cout << "listener_socket creat failed " << endl;return 0;}//bindif ( !bindAddr( &InternetAddr, &listener_socket ) ){return 0;}//監聽if ( !setListener( &listener_socket, REQUEST_BACKLOG ) ){return 0;}cout << "server started~~~ " << endl;//建立 socket儲存結構fd_set fdSocket;FD_ZERO( &fdSocket );FD_SET( listener_socket, &fdSocket );//尋找可讀的 socketwhile( true ){fd_set fdSocket_temp;fdSocket_temp = fdSocket;fd_set fdRead;fdRead = fdSocket;fd_set fdExceptds;fdExceptds = fdSocket;int nResult_select = select( 0, &fdRead, NULL, &fdExceptds, NULL );if ( 0 < nResult_select ){unsigned int socket_count = fdSocket_temp.fd_count;for ( unsigned int i=0; i< socket_count; i++ ){//可讀的if ( FD_ISSET( fdSocket_temp.fd_array[i], &fdRead ) ){//找到所有可讀串連if ( fdSocket_temp.fd_array[i] == listener_socket ){if ( fdSocket.fd_count < FD_SETSIZE ){//接受新的串連SOCKADDR_IN ClientAddr;int addrlen = static_cast<int>(sizeof(ClientAddr));//一定要賦值SOCKET newClient_socket = accept( listener_socket, (sockaddr *)&ClientAddr, &addrlen );if ( INVALID_SOCKET == newClient_socket ){cout << " accept error " << WSAGetLastError() << endl;}else{FD_SET( newClient_socket, &fdSocket );cout << "find new connect: " << inet_ntoa( ClientAddr.sin_addr ) << endl;}}else{cout<<"too much connections"<<endl;continue;}} else{//接收資料char recvbuff[1024];int ret = 0;ret = recv( fdSocket_temp.fd_array[i], recvbuff, static_cast<int>( strlen(recvbuff) ), 0 );if ( 0 < ret ){recvbuff[ret] = '\0';cout << "recv : " << recvbuff << endl;//回複char backbuf[1024] = "receive info!";send( fdSocket_temp.fd_array[i], backbuf, static_cast<int>( strlen(backbuf) ), 0 );}else{//該串連斷開closesocket( fdSocket_temp.fd_array[i] );FD_CLR( fdSocket_temp.fd_array[i], &fdSocket );}}}else if( fdSocket_temp.fd_array[i] != listener_socket ){//該串連斷開closesocket( fdSocket_temp.fd_array[i] );FD_CLR( fdSocket_temp.fd_array[i], &fdSocket );}if ( FD_ISSET( fdSocket_temp.fd_array[i], &fdExceptds ) && (fdSocket_temp.fd_array[i] != listener_socket) ){//該串連斷開closesocket( fdSocket_temp.fd_array[i] );FD_CLR( fdSocket_temp.fd_array[i], &fdSocket );}}//end for}else if( SOCKET_ERROR == nResult_select ){cout << "select error : " << WSAGetLastError() << endl;return 0;}Sleep( 50 );//不要挑戰你的機器}//end whileclosesocket( listener_socket );cleanWSA();return 0;}
用戶端:
//******************************************************************
#include "stdafx.h"#include <WinSock2.h>#include <iostream>#pragma comment(lib, "WS2_32")using namespace std;int _tmain(int argc, _TCHAR* argv[]){int result = 0;//初始化winsockWSADATA wsadata;result = WSAStartup( MAKEWORD(2,2), &wsadata );if ( 0 != result ){cout << "WSAStartup error " << result << endl;return 0;}//建立一個地址int serverPort= 5150;char FAR serverIP[] = "192.168.1.102"; //本機ip,不知道就ipconfigSOCKADDR_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) );//建立一個socketSOCKET socket_toServer;socket_toServer = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );if ( INVALID_SOCKET == socket_toServer ){cout << "socket_toServer creat failed " << WSAGetLastError() << endl;return 0;}//串連result = connect( socket_toServer, (sockaddr *)&serverAddr, serverAddr_size );if ( SOCKET_ERROR == result ){cout << "connect error :" << WSAGetLastError() << endl;return 0;}char sendbuff[2048];char recvbuff[2048];while( true ){cout << "input send info:" << endl;cin >> sendbuff;int ret = send( socket_toServer, sendbuff, static_cast<int>( strlen(sendbuff) ), 0 );if ( SOCKET_ERROR == ret ){cout << "send error " << WSAGetLastError() << endl;break;}//處理接受的訊息,由於之前沒有accept和listen,這裡使用recvfrom來接受int nRecv = 0;nRecv = recvfrom( socket_toServer, recvbuff, static_cast<int>( strlen(recvbuff) ), 0, (sockaddr *)&serverAddr, &serverAddr_size );if ( 0 < nRecv ){recvbuff[nRecv] = '\0';cout << "receive : " << recvbuff << endl;cout << "from : " << inet_ntoa( serverAddr.sin_addr ) << endl;cout << " " << endl;}}//清除各種資料和連結closesocket( socket_toServer );if ( SOCKET_ERROR == WSACleanup() ){cout << "WSACleanup error " << WSAGetLastError() << endl;return 0;}return 0;}
如題,上述內容只是協助快速入門,要掌握Winsock還要經過大量的學習和實踐。