7 UDP
用最通俗的話講,所謂UDP,就是發送出去就不管的一種網路通訊協定。因此UDP編程的發送端只管發送就可以了,不用檢查網路連接狀態。下面用例子來說明怎樣編寫UDP,並會詳細解釋每個API和資料類型。
7.1 UDP廣播發送程式
下面是一個用UDP發送廣播報文的例子。
#include <winsock2.h>
#include <iostream.h>
void main()
{
SOCKET sock; //socket通訊端
char szMsg[] = "this is a UDP test package";//被發送的欄位
//1.啟動SOCKET庫,版本為2.0
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 0 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( 0 != err ) //檢查Socket初始化是否成功
{
cout<<"Socket2.0初始化失敗,Exit!";
return;
}
//檢查Socket庫的版本是否為2.0
if (LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 0 )
{
WSACleanup( );
return;
}
//2.建立socket,
sock = socket(
AF_INET, //internetwork: UDP, TCP, etc
SOCK_DGRAM, //SOCK_DGRAM說明是UDP類型
0 //protocol
);
if (INVALID_SOCKET == sock ) {
cout<<"Socket 建立失敗,Exit!";
return;
}
//3.設定該通訊端為廣播類型,
bool opt = true;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<char FAR *>(&opt), sizeof(opt));
//4.設定發往的地址
sockaddr_in addrto; //發往的地址
memset(&addrto,0,sizeof(addrto));
addrto.sin_family = AF_INET; //地址類型為internetwork
addrto.sin_addr.s_addr = INADDR_BROADCAST; //設定ip為廣播位址
addrto.sin_port = htons(7861); //連接埠號碼為7861
int nlen=sizeof(addrto);
unsigned int uIndex = 1;
while(true)
{
Sleep(1000); //程式休眠一秒
//向廣播位址發送訊息
if( sendto(sock, szMsg, strlen(szMsg), 0, (sockaddr*)&addrto,nlen)
== SOCKET_ERROR )
cout<<WSAGetLastError()<<endl;
else
cout<<uIndex++<<":an UDP package is sended."<<endl;
}
if (!closesocket(sock)) //關閉通訊端
{
WSAGetLastError();
return;
}
if (!WSACleanup()) //關閉Socket庫
{
WSAGetLastError();
return;
}
}
編譯命令:
CL /c UDP_Send_Broadcast.cpp
連結命令(注意如果找不到該庫,則要在後面的/LIBPATH參數後加上庫的路徑):
link UDP_Send_Broadcast.obj ws2_32.lib
執行命令:
D:"Code"成品代碼"Socket"socket_src>UDP_Send_Broadcast.exe
1:an UDP package is sended.
2:an UDP package is sended.
3:an UDP package is sended.
4:an UDP package is sended.
^C
下面一一解釋代碼中出現的資料類型與API函數。有耐心的可以仔細看看,沒耐心的依葫蘆畫瓢也可以寫程式了。
7.2 SOCKET類型
SOCKET是socket通訊端類型,在WINSOCK2.H中有如下定義:
typedef unsigned int u_int;
typedef u_int SOCKET;
可知通訊端實際上就是一個無符號整型,它將被Socket環境管理和使用。通訊端將被建立、設定、用來發送和接收資料,最後會被關閉。
7.3 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可以讀取高位位元組。
7.4 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讀取出來。
7.5 WSAStartup函數
WSAStartup函數被用來初始化Socket環境,它的定義如下:
int PASCAL FAR WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);
其傳回值為整型,調用方式為PASCAL(即標準類型,PASCAL等於__stdcall),參數有兩個,第一個參數為WORD類型,指明了Socket的版本號碼,第二個參數為WSADATA類型的指標。
若傳回值為0,則初始化成功,若不為0則失敗。
7.6 WSACleanup函數
這是Socket環境的退出函數。傳回值為0表示成功,SOCKET_ERROR表示失敗。
7.7 socket函數
socket的建立函數,其定義為:
SOCKET PASCAL FAR socket (int af, int type, int protocol);
第一個參數為int af,代表網路地址族,目前只有一種取值是有效,即AF_INET,代表internet地址族;
第二個參數為int type,代表網路通訊協定類型,SOCK_DGRAM代表UDP協議,SOCK_STREAM代表TCP協議;
第三個參數為int protocol,指定網路地址族的特殊協議,目前無用,賦值0即可。
傳回值為SOCKET,若返回INVALID_SOCKET則失敗。
7.8 setsockopt函數
這個函數用來設定Socket的屬性,若不能正確設定socket屬性,則資料的發送和接收會失敗。定義如下:
int PASCAL FAR setsockopt (SOCKET s, int level, int optname,
const char FAR * optval, int optlen);
其傳回值為int類型,0代表成功,SOCKET_ERROR代表有錯誤發生。
第一個參數SOCKET s,代表要設定的通訊端;
第二個參數int level,代表要設定的屬性所處的層次,層次包含以下取值:SOL_SOCKET代表通訊端層次;IPPROTO_TCP代表TCP協議層次,IPPROTO_IP代表IP協議層次(後面兩個我都沒有用過);
第三個參數int optname,代表設定參數的名稱,SO_BROADCAST代表允許發送廣播資料的屬性,其它屬性可參考MSDN;
第四個參數const char FAR * optval,代表指向儲存參數數值的指標,注意這裡可能要使用reinterpret_cast類型轉換;
第五個參數int optlen,代表格儲存體參數數值變數的長度。
7.9 sockaddr_in、in_addr類型,inet_addr、inet_ntoa函數
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含義的請看c++書),有三種表達方式:
第一種用四個位元組來表示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類型的長度相等而填充進來的欄位。
以下代表指明了廣播位址,連接埠號碼為7861的一個地址:
sockaddr_in addrto; //發往的地址
memset(&addrto,0,sizeof(addrto));
addrto.sin_family = AF_INET; //地址類型為internetwork
addrto.sin_addr.s_addr = INADDR_BROADCAST; //設定ip為廣播位址
addrto.sin_port = htons(7861); //連接埠號碼為7861
7.10 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的。事實上也往往使用這種方法。
7.11 Sleep函數
線程掛起函數,表示線程掛起一段時間。Sleep(1000)表示掛起一秒。定義於WINBASE.H標頭檔中。WINBASE.H又被包含於WINDOWS.H中,然後WINDOWS.H被WINSOCK2.H包含。所以在本例中使用Sleep函數不需要包含其它標頭檔。
7.12 sendto函數
在Socket中有兩套發送和接收函數,一是sendto和recvfrom;二是send和recv。前一套在函數參數中要指明地址;而後一套需要先將通訊端和一個地址綁定,然後直接發送和接收,不需綁定地址。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。
7.13 WSAGetLastError函數
該函數用來在Socket相關API失敗後讀取錯誤碼,根據這些錯誤碼可以對照查出錯誤原因。
7.14 closesocket
關閉通訊端,其參數為SOCKET類型。成功返回0,失敗返回SOCKET_ERROR。
8 TCP
TCP與UDP最大的不同之處在於TCP是一個連線導向的協議,在進行資料收發之前TCP必須進行串連,並且在收發的時候必須保持該串連。
發送方的步驟如下(省略了Socket環境的初始化、關閉等內容):
1. 用socket函數建立一個通訊端sock;
2. 用bind將sock綁定到本地地址;
3. 用listen偵聽sock通訊端;
4. 用accept函數接收客戶方的串連,返回客戶方通訊端clientSocket;
5. 在客戶方通訊端clientSocket上使用send發送資料;
6. 用closesocket函數關閉通訊端sock和clientSocket;
而接收方的步驟如下:
1. 用socket函數建立一個通訊端sock;
2. 建立一個指向服務方的遠程地址;
3. 用connect將sock串連到服務方,使用遠程地址;
4. 在通訊端上使用recv接收資料;
5. 用closesocket函數關閉通訊端sock;
值得注意的是,在服務方有兩個地址,一個是本地地址myaddr,另一個是目標地址addrto。本地地址myaddr用來和本地通訊端sock綁定,目標地址被sock用來accept客戶方通訊端clientSocket。這樣sock和clientSocket串連成功,這兩個地址也串連上了。在服務方使用clientSocket發送資料,則會從本地地址傳送到目標地址。
在客戶方只有一個地址,即來源地址addrfrom。這個地址被用來connect遠端服務方通訊端,connect成功則本地通訊端與遠端來源地址串連了,因此可以使用該通訊端接收遠端資料。其實這時客戶方通訊端已經被隱性的綁定了本地地址,所以不需要顯式調用bind函數,即使調用也不會影像結果。
具體原始碼見TCP_Send.cpp和TCP_Recv.cpp。注意將原始碼中的IP地址修改為符合自己需要的IP。為了減少代碼複雜性,沒有使用讀取本機IP的代碼,後續例子程式中含有此功能代碼。
8.1 bind函數
bind函數用來將一個通訊端綁定到一個IP地址。一般只在服務方(即資料發送方)調用,很多函數會隱式的調用bind函數。
8.2 listen函數
從服務方監聽客戶方的串連。同一個通訊端可以多次監聽。
8.3 connect和accept函數
connect是客戶方串連服務方的函數,而accept是服務方同意客戶方串連的函數。這兩個配套函數分別在各自的程式中被成功調用後就可以收發資料了。
8.4 send和recv函數
send和recv是用來發送和接收資料的兩個重要函數。send只能在已經串連的狀態下使用,而recv可以連線導向和非串連的狀態下使用。
send的定義如下:
int WSAAPI send(
SOCKET s,
const char FAR * buf,
int len,
int flags
);
其參數的含義和sendto中的前四個參數一樣。而recv的定義如下:
int WSAAPI recv(
SOCKET s,
char FAR * buf,
int len,
int flags
);
其參數含義與send中的參數含義一樣。