SOCKET網絡編程
(WINDOWS SOCKET)
1.前言
網上看了很多Socket的資料,將理解的知識總結下,詳細介紹下VC下windows sockets編程,並結合服務器和客戶端的兩個實例(TCP/UDP)講解下。
2.SOCKET相關原理
在網絡編程中最常用的方案便是Client/Server (客戶機/服務器)模型。在這種方案中客戶應用程序向服務器程序請求服務。一個服務程序通常在一個眾所周知的地址監聽對服務的請求,也就是說,服務進程一直處於休眠狀態(簡單的說就是死循環),直到一個客戶向這個服務的地址提出了連接請求。在這個時刻,服務程序被"驚醒"並且為客戶提供服務-對客戶的請求作出適當的反應。
在VC中進行WINSOCK的API編程開發的時候,需要在項目中使用下面的三個文件,否則會出現編譯錯誤。
1.WINSOCK.H: 這是WINSOCK API的頭文件,需要包含在項目中。
2.WSOCK32.LIB: WINSOCK API連接庫文件。在使用中,一定要把它作為項目的非缺省的連接庫包含到項目文件中去。
3.WINSOCK.DLL: WINSOCK的動態連接庫,位於WINDOWS的安裝目錄下。
3.SOCKET API
下面講解的都是在實例中使用的接口,了解相關定義后我們結合實例講解。
1.WSAStartup()
此函數在應用程序中初始化Windows Sockets DLL ,只有此函數調用成功后,應用程序才可以再調用其他Windows Sockets DLL中的API函數。
int PASCAL FAR WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);
其返回值為整型,調用方式為PASCAL(即標准類型,PASCAL等於__stdcall),參數有兩個,第一個參數為WORD類型,指明了Socket的版本號,第二個參數為WSADATA類型的指針。
若返回值為0,則初始化成功,若不為0則失敗。
在程式中調用該函數的形式如下:WSAStartup(MAKEWORD(1,1), &WSAData),其中MAKEWORD(1,1)表示我們用的是WinSocket1.1版本,WSAata用來存儲系統傳回的關於WinSocket的資料。
(1) WORD類型、MAKEWORD、LOBYTE和HIBYTE宏
WORD類型是一個16位的無符號整型,在WTYPES.H中被定義為:
typedef unsigned short WORD;
其目的是提供兩個字節的存儲,在Socket中這兩個字節可以表示主版本號和副版本號。使用MAKEWORD宏可以給一個WORD類型賦值。例如要表示主版本號2,副版本號0,可以使用以下代碼:
WORD wVersionRequested;
wVersionRequested = MAKEWORD( 2, 0 );
注意低位內存存儲主版本號2,高位內存存儲副版本號0,其值為0x0002。使用宏LOBYTE可以讀取WORD的低位字節,HIBYTE可以讀取高位字節。在后面的實例中會應用。
(2)WSADATA類型和LPWSADATA類型
WSADATA類型是一個結構,描述了Socket庫的一些相關信息,其結構定義如下:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
} WSADATA;
typedef WSADATA FAR *LPWSADATA;
值得注意的就是wVersion字段,存儲了Socket的版本類型。LPWSADATA是WSADATA的指針類型。它們通過Socket的初始化函數WSAStartup讀取出來。
2.WSACleanup()
這是Socket環境的退出函數。返回值為0表示成功,SOCKET_ERROR表示失敗。
3.socket()
SOCKET是socket套接字類型,在WINSOCK2.H中有如下定義:
typedef unsigned int u_int;
typedef u_int SOCKET;
可知套接字實際上就是一個無符號整型,它將被Socket環境管理和使用。
初始化WinSock的動態連接庫后,需要在服務器端建立一個監聽的Socket,為此可以調用Socket()函數用來建立這個監聽的Socket,並定義此Socket所使用的通信協議。此函數調用成功返回Socket對象,失敗則返回INVALID_SOCKET(調用WSAGetLastError()可得知原因,所有WinSocket 的API函數都可以使用這個函數來獲取失敗的原因。
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。
l SOCK_STREAM:這個協議是按照順序的、可靠的、數據完整的基於字節流的連接。這是一個使用最多的socket類型,這個socket是使用TCP來進行傳輸。
l SOCK_DGRAM:這個協議是無連接的、固定長度的傳輸調用。該協議是不可靠的,使用UDP來進行它的連接。
套接字將被創建、設置、用來發送和接收數據,最后會被關閉。
4.bind()
接下來要為服務器端定義的這個監聽的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()函數來獲知其被設定的值。
5.sockaddr_in、in_addr類型,inet_addr、inet_ntoa函數,sockaddr
sockaddr_in定義了socket發送和接收數據包的地址,定義:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中in_addr的定義如下:
struct in_addr
{
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
首先闡述in_addr的含義,很顯然它是一個存儲ip地址的聯合體(union),有三種表達方式:
第一種用四個字節來表示IP地址的四個數字;
第二種用兩個雙字節來表示IP地址;
第三種用一個長整型來表示IP地址。
給in_addr賦值的一種最簡單方法是使用inet_addr函數,它可以把一個代表IP地址的字符串賦值轉換為in_addr類型,如
addrto.sin_addr.s_addr=inet_addr("192.168.0.2");
其反函數是inet_ntoa,可以把一個in_addr類型轉換為一個字符串。
sockaddr_in的含義比in_addr的含義要廣泛,其各個字段的含義和取值如下:
第一個字段short sin_family,代表網絡地址族,如前所述,只能取值AF_INET;
第二個字段u_short sin_port,代表IP地址端口,由程序員指定;
第三個字段struct in_addr sin_addr,代表IP地址;
第四個字段char sin_zero[8],是為了保證sockaddr_in與SOCKADDR類型的長度相等而填充進來的字段。
sockaddr類型是用來表示Socket地址的類型,同上面的sockaddr_in類型相比,sockaddr的適用范圍更廣,因為sockaddr_in只適用於TCP/IP地址。Sockaddr的定義如下:
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
可知sockaddr有16個字節,而sockaddr_in也有16個字節,所以sockaddr_in是可以強制類型轉換為sockaddr的。事實上也往往使用這種方法。
6.listen()
當服務器端的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) |
7.accept()
當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的長度
8.connect()
Connect用於客戶端想服務器發起連接。
格 式: int PASCAL FAR connect( SOCKET s,const struct sockaddr FAR *name,int namelen );
參 數: s Socket 的識別碼
name 此 Socket 想要連接的對方位址
namelen name的長度
傳回值:成功 - 0
失敗 - SOCKET_ERROR (呼叫WSAGetLastError()可得知原因)
說明: 此函式用來向對方要求建立連接。若是指定的對方位址為 0 的話,會傳回錯誤值。當連接建立完成後,使用者即可利用此一 Socket 來做傳送或接收資料之用了。
9.sendto(),send()函數
在Socket中有兩套發送和接收函數,一是sendto和recvfrom;二是send和recv。前一套在函數參數中要指明地址(UDP協議);而后一套需要先將套接字和一個地址綁定,然后直接發送和接收,不需綁定地址。
sendto的定義如下:
int PASCAL FAR sendto (SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR *to, int tolen);
第一個參數就是套接字;
第二個參數是要傳送的數據指針;
第三個參數是要傳送的數據長度(字節數);
第四個參數是傳送方式的標識,如果不需要特殊要求則可以設置為0,其它值請參考MSDN;
第五個參數是目標地址,注意這里使用的是sockaddr的指針;
第六個參數是地址的長度;
返回值為整型,如果成功,則返回發送的字節數,失敗則返回SOCKET_ERROR。
send函數有四個參數與sendto函數的前四個參數是一樣的。
10.recvfrom(),recv()函數
int WSAAPI recvfrom(IN SOCKET s,char FAR * buf,int len,int flags,sockaddr FAR * from,int * fromlen );
第一個參數就是套接字;
第二個參數是要接收消息的char數組的指針;
第三個參數是要接收的數據長度(字節數);
第四個參數是接收方式的標識,如果不需要特殊要求則可以設置為0;
第五個參數是接收地址的結構指針,存放接收到對方的的地址信息;
第六個參數是地址的長度的指針(&);
返回值為整型,如果成功,則返回接收的字節數,失敗則返回SOCKET_ERROR。
recv函數有四個參數與recvfrom函數的前四個參數是一樣的。
11.sleep()函數
線程掛起函數,表示線程掛起一段時間。Sleep(1000)表示掛起一秒。定義於WINBASE.H頭文件中。WINBASE.H又被包含於WINDOWS.H中,然后WINDOWS.H被WINSOCK2.H包含。
12.recvfrom(),recv()函數
int WSAAPI recvfrom(IN SOCKET s,char FAR * buf,int len,int flags,sockaddr FAR * from,int * fromlen );
第一個參數就是套接字;
第二個參數是要接收消息的char數組的指針;
第三個參數是要接收的數據長度(字節數);
第四個參數是接收方式的標識,如果不需要特殊要求則可以設置為0;
第五個參數是接收地址的結構指針,存放接收到對方的的地址信息;
第六個參數是地址的長度的指針(&);
返回值為整型,如果成功,則返回接收的字節數,失敗則返回SOCKET_ERROR。
recv函數有四個參數與recvfrom函數的前四個參數是一樣的。
13.closesocket()
關閉套接字,其參數為SOCKET類型。成功返回0,失敗返回SOCKET_ERROR。
4.TCP/UDP Socket流程圖
1.UDP流程圖
2.TCP流程圖
5.VC代碼實現
1.UDP實現
(1)udp server代碼
#include <stdio.h>
/* Windows socket頭文件 */
#include <Winsock2.h>
/* 網絡API的動態鏈接庫 */
#pragma comment(lib, "ws2_32.lib")
void main()
{
SOCKET uiFdSocket;
WSADATA wsaData;
char szbuffer[1024] = "\0";
struct sockaddr_in stServerAddr;
struct sockaddr_in stClientAddr;
int iAddrlen = sizeof(sockaddr_in);
/* 調用Windows Sockets DLL,成功后才能使用socket系列函數 */
if (0 != WSAStartup(MAKEWORD(2,1), &wsaData))
{
printf("Winsock init failed!\r\n");
WSACleanup();
return;
}
memset(&stServerAddr, 0, sizeof(stServerAddr));
memset(&stClientAddr, 0, sizeof(stClientAddr));
/* 服務器地址 */
stServerAddr.sin_family = AF_INET;
/* 監聽端口 */
stServerAddr.sin_port = htons(6000);
stServerAddr.sin_addr.s_addr = INADDR_ANY;
/* 服務器端創建socket, 報文模式(UDP)*/
uiFdSocket = socket(AF_INET, SOCK_DGRAM, 0);
/* 綁定端口號 */
bind(uiFdSocket, (struct sockaddr*)&stServerAddr, sizeof(sockaddr_in));
while(true)
{
printf("waiting client send msg now...\r\n");
if (SOCKET_ERROR != recvfrom(uiFdSocket,szbuffer,sizeof(szbuffer),0,(struct ockaddr*)&stClientAddr, &iAddrlen))
{
printf("Received datagram from %s--%s\n", inet_ntoa(stClientAddr.sin_addr), szbuffer);
sendto(uiFdSocket, szbuffer, sizeof(szbuffer), 0, (struct sockaddr*)&stClientAddr, iAddrlen);
}
}
closesocket(uiFdSocket);
}
(2)udp cilent代碼
void main()
{
SOCKET uiFdsocket;
WSADATA wsaData;
struct sockaddr_in stServerAddr;
int iAddrlen = sizeof(sockaddr_in);
char szbuffer[1024] = "\0";
if (0 != WSAStartup(MAKEWORD(2,1),&wsaData))
{
printf("Winsock init faied!\r\n");
WSACleanup();
return;
}
/* 服務器監聽的端口和地址 */
memset(&stServerAddr, 0, sizeof(stServerAddr));
stServerAddr.sin_family = AF_INET;
stServerAddr.sin_port = htons(6000);
stServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
printf("Now connecting the server...\r\n");
uiFdsocket = socket(AF_INET, SOCK_DGRAM, 0);
while(true)
{
printf("input message here...\r\n");
scanf("%s", szbuffer);
if(strcmp(szbuffer, "bye") == 0)
{
printf("exit\r\n");
Sleep(100);
closesocket(uiFdsocket);
break;
}
if(SOCKET_ERROR != sendto(uiFdsocket, szbuffer,sizeof(szbuffer), 0, (struct sockaddr*)&stServerAddr, iAddrlen))
{
Sleep(100);
if (SOCKET_ERROR != recvfrom(uiFdsocket, szbuffer, sizeof(szbuffer), 0, (struct sockaddr*)&stServerAddr, &iAddrlen))
{
printf("recive from server:%s\r\n", szbuffer);
}
}
}
closesocket(uiFdsocket);
return;
}
2.TCP實現
(1)tcp server代碼
#include <stdio.h>
/* Windows socket頭文件 */
#include <Winsock2.h>
/* 網絡API的動態鏈接庫 */
#pragma comment(lib, "ws2_32.lib")
void main()
{
SOCKET uiFdSerSocket;
SOCKET uiFdConnectSocket;
WSADATA wsaData;
char szSendbuffer[1024] = "\0";
char szRecvbuffer[1024] = "\0";
struct sockaddr_in stServerAddr;
struct sockaddr_in stClientAddr;
int iAddrlen = sizeof(sockaddr_in);
/* 調用Windows Sockets DLL,成功后才能使用socket系列函數 */
if (0 != WSAStartup(MAKEWORD(2,1), &wsaData))
{
printf("Winsock init failed!\r\n");
WSACleanup();
return;
}
if (LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 1)
{
printf("the socket version is error!\r\n");
WSACleanup();
return;
}
memset(&stServerAddr, 0, sizeof(stServerAddr));
memset(&stClientAddr, 0, sizeof(stClientAddr));
/* 服務器地址 */
stServerAddr.sin_family = AF_INET;
/* 監聽端口 */
stServerAddr.sin_port = htons(6000);
stServerAddr.sin_addr.s_addr = INADDR_ANY;
/* 服務器端創建socket, 流模式(TCP)*/
uiFdSerSocket = socket(AF_INET, SOCK_STREAM, 0);
/* 綁定端口號 */
bind(uiFdSerSocket, (struct sockaddr*)&stServerAddr, sizeof(sockaddr_in));
/* 服務器監聽 */
listen(uiFdSerSocket, 5);
while(true)
{
printf("waiting client send msg now...\r\n");
/* 接受客戶端連接,獲取客戶端的ip地址 */
uiFdConnectSocket = accept(uiFdSerSocket, (SOCKADDR*)&stClientAddr, &iAddrlen);
/* 組合發送消息,將IP地址整形轉化為字符串 */
sprintf(szSendbuffer, "Welcome %s to here!", inet_ntoa(stClientAddr.sin_addr));
/* 發送消息到客戶端 */
send(uiFdConnectSocket, szSendbuffer, strlen(szSendbuffer)+1, 0);
/* 接收客戶端消息 */
recv(uiFdConnectSocket, szRecvbuffer, sizeof(szRecvbuffer), 0);
printf("Received datagram from client %s->%s\n", inet_ntoa(stClientAddr.sin_addr), szRecvbuffer);
closesocket(uiFdConnectSocket);
}
closesocket(uiFdSerSocket);
}
(2)tcp client代碼
#include <stdio.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")
void main()
{
SOCKET uiFdClientsocket;
WSADATA wsaData;
struct sockaddr_in stServerAddr;
int iAddrlen = sizeof(sockaddr_in);
char szSendbuffer[1024];
char szRecvbuffer[1024];
if (0 != WSAStartup(MAKEWORD(2,1),&wsaData))
{
printf("Winsock init faied!\r\n");
WSACleanup();
return;
}
if (LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 1)
{
printf("the socket version is error!\r\n");
WSACleanup();
return;
}
/* 服務器監聽的端口和地址 */
memset(&stServerAddr, 0, sizeof(stServerAddr));
stServerAddr.sin_family = AF_INET;
stServerAddr.sin_port = htons(6000);
stServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
while(true)
{
szSendbuffer[0] = '\0';
szRecvbuffer[0] = '\0';
/* 創建SOCKET */
uiFdClientsocket = socket(AF_INET, SOCK_STREAM, 0);
/* 連接服務器 */
connect(uiFdClientsocket, (SOCKADDR*)&stServerAddr, sizeof(sockaddr_in));
printf("connect to server ok...\r\n");
/* 接收服務器端數據 */
recv(uiFdClientsocket, szRecvbuffer, sizeof(szRecvbuffer), 0);
printf("revive data from server->%s\r\nnow input msg here...`\r\n",szRecvbuffer);
scanf("%s", szSendbuffer);
if(strcmp(szSendbuffer, "bye") == 0)
{
printf("exit\r\n");
Sleep(100);
closesocket(uiFdClientsocket);
break;
}
/* 向服務器端發送數據 */
send(uiFdClientsocket, szSendbuffer, sizeof(szSendbuffer) + 1, 0);
closesocket(uiFdClientsocket);
}
closesocket(uiFdClientsocket);
return;
}
PS:有好的意見大家一起分享~~~感謝好基友!!