前置知識
1.有一定C++編程基礎
2.套接字的主要類型
有兩種:一種是流式套接字(SOCK_STREAM),一種是數據報套接字(SOCK_DGRAM),
分別用於TCP/IP協議和UDP協議進行通訊的應用程序中。
3.TCP/IP尋址方式
用戶使用IP地址和端口號進行確定通信雙方。
-----------------------------------------------------------------------------
一、Winsock編程流程
1.配置編程環境
由於所有的Winsock函數均是從動態鏈接庫WS2_32.DLL中導出的,而VC默認是沒有與該庫進行連接,所以我們需要進行編程環境設置。添加方法是選擇“工程--->設置--->連接”
2.初始化套接字庫
調用WSAStartup()對該庫進行初始化,查詢MSDN後得知原型如下
int WSAStartup(WORD wVersionRequested , LPWSADATA lpWSAData);
該函數調用成功,將返回0。
參數wVersionRequested表示當前套接字庫的版本號。
例如當前套接字版本號爲2.0,則將該參數設置爲2.0,代碼如下:
<pre class="cpp" name="codeWORD wVersionRequested=MAKEWORD(2,0);參數lpWSAData指向結構體WSADATA的指針變量,表示獲取到的套接字庫詳細信息。
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, *LPWSADATA;
例1:有了上述知識,就可以開始初始化套接字庫了,代碼如下:
WSADATA data; //定義WSAData指針對象
WORD w=MAKEWORD(2,0); //定義當前套接字庫版本號
::WSAStartup(w,&data); //初始化套接字庫,爲默認參數
3.創建套接字句柄
創建套接字句柄的函數是socket(),查詢MSDN後得知該函數原型如下
SOCKET socket(
int af, //指定套接字所使用的地址格式,TCP/IP設置爲AF_INET
int type, //套接字類型,見前置知識第2條。
int protocol //一般設置爲0,想深究,請百度。
);
該函數執行成功,將返回新創建的套接字句柄。否則,將返回INVALID_SOCKET。
例2:創建流式套接字(TCP/IP)句柄的代碼如下:
SOCKET s; //定義套接字句柄對象
s=::socket(AF_INET,SOCK_STREAM,0); //創建並返回套接字句柄
4.構造套接字地址結構
sockaddr_in是TCP/IP地址家族中統一的套接字地址結構,查詢MSDN得知原型如下
struct sockaddr_in{
short sin_family; //指定地址家族,TCP/IP設置爲AF_INET
unsigned short sin_port; //端口
struct in_addr sin_addr; // IP地址
char sin_zero[8]; //一般設置爲0
};
該結構成員變量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;
};
通常,我們在編寫網絡程序時,使用一個u_long類型的字符串進行描述IP地址。
例3:127.0.0.1是回送地址,指向本機,一般可以用來測試,代碼如下:
sockaddr_in addr; //定義結構對象
addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
編程知識普及:字節順序
字節順序分爲:Big-Endian和Little-Endian。以unsigned int val=0x12345678爲例
Intel CPU(Little-Endian)存放的字節順序爲0x78、0x56、0x34、0x12。
而網絡的字節順序爲(Big-Endian) 0x12、0x34、0x56、0x78。正好相反,但更符合人
們的閱讀情況。所以編程人員將數據通過網絡發送時,需要將存儲在本地計算機上的數
據轉換成以網絡字節順序排列的數據。從數據的角度來說,網絡字節順序將最重要的數
據先進行存儲,而Intel CPU字節順序則將不重要的字節首先儲存。
字節順序轉換函數
u_short htons(u_short hostshort);
//將一個u_short類型的端口號從主機字節順序轉換到網絡字節順序。
u_short ntohs(u_short netshort);
//將一個u_short類型的端口號從網絡字節順序轉換到主機字節順序。
u_long htonl(u_long hostlong);
//將一個u_long類型的IP地址從主機字節順序轉換到網絡字節順序。
u_long ntohl(u_long netlong);
//將一個u_long類型的IP地址從網絡字節順序轉換到主機字節順序。
unsigned long inet_addr(const char FAR *cp);
//將一個字符串IP轉換到以網絡字節順序排列的IP地址。
char FAR * inet_ntoa(struct in_addr in);
//將一個以網絡字節順序排列的IP地址轉化爲一個字符串IP。
例4:在構造套接字地址結構中使用字節順序轉換函數
sockaddr_in addr; //定義套接字地址結構對象
addr.sin_family=AF_INET; //指定地址家族爲TCP/IP
addr.sin_port=htons(80); //指定端口號
addr.sin_addr.S_un.S_addr="127.0.0.1"; //將字符串IP轉換爲網絡字節順序排列的IP
char addRes[]=inet_ntoa(addr.sin_addr.S_un.S_addr); //將網絡字節順序排列的IP轉換爲字符串IP
5.綁定地址信息(服務端)
對於服務器,套接字創建成功後,還應該使用bind()函數將套接字與地址結構信息相
關聯,bind()函數原型如下:
int bind(
SOCKET s, //套接字句柄
const struct sockaddr FAR *name, //地址結構信息
int namelen //地址結構大小
);
該函數調用成功,則返回0。
例5:將套接字句柄綁定到本地地址,代碼如下:
sockaddr_in addr; //定義套接字地址結構對象
addr.sin_family=AF_INET; //指定地址家族爲TCP/IP
addr.sin_port=htons(80); //指定端口號
addr.sin_addr.S_un.S_addr=INADDR_ANY; //服務器能接受任何計算機發來的請求
::bind(s,(sockaddr*)&addr,sizeof(addr));//綁定套接字到指定地址結構
6.監聽端口信息(服務端)
服務器程序可以調用listen()函數實現監聽端口的功能,函數原型如下:
int listen(
SOCKET s, //實現監聽功能的套接字句柄
int backlog //指定監聽的最大連接數量
);
如果多個客戶端同時向服務器發出連接請求,並且超過了最大連接數,客戶端將返回錯誤代碼。
例6:在套接字上進行監聽,並且將最大監聽數量定義爲5:
::listen(s,5);
7.連接(客戶端)
客戶端連接服務端需要用到connect()函數,函數原型如下:
int connect(
SOCKET s, //實現連接功能的套接字句柄
const struct sockaddr FAR *name, //將要連接的服務器地址信息的結構指針
int namelen //服務器地址信息結構體長度
);
例7:連接地址爲"127.0.0.1",端口爲80的服務器。代碼如下:
sockaddr_in addr; //定義套接字地址結構對象
addr.sin_family=AF_INET; //指定服務器地址家族爲TCP/IP
addr.sin_port=htons(80); //指定要連接的端口號
addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//指定服務器IP地址
::connect(s,(sockaddr*)&addr,sizeof(addr)); //連接服務器
8.接受連接請求(服務端)
當服務端收到客戶端的連接請求時,則可以調用accept()函數接受該請求,函數原型:
SOCKET accept(
SOCKET s, //套接字句柄
struct sockaddr FAR *addr, //保存客戶端的地址信息於addr中
int FAR *addrlen //保存客戶端的地址信息長度於addrlen中
);
例8:服務端套接字句柄爲s,使用accept()函數接收客戶端信息,代碼如下:
SOCKET s1;
sockaddr_in addrConnect;
int n=sizeof(addrConnect);
s1=::accept(s,(sockaddr*)&addrConnect,&n);
9.數據收發
當服務器和客戶端成功連接時,我們可以使用send()和recv()函數實現數據的發送和接收功能,函數原型如下:
int send(SOCKET s, const char FAR *buf, int len, int flags);
int recv(SOCKET s, char FAR *buf, int len, int flags);
兩個函數的各個參數以及表示的意義都是相同的,參數*buf是指向數據緩衝區的指針變量,參數len是數據的長度,參數flags一般設置爲0。
注意:對於服務端,參數s應該爲接收客戶端連接請求後,返回的新套接字句柄。
對於客戶端,參數s應該爲客戶端創建的套接字句柄。
例9:使用send()、recv()函數來發送、接收數據,代碼如下:
char szText[]="Hello C++ Socket World"; //用於儲存發送的數據
::send(s1,szText,sizeof(szText),0); //發送數據給客戶端s1
char szText2[100]={0}; //用於保存接收到的數據
::recv(s,szText2,sizeof(szText2),0); //接收服務端發來的數據
10.關閉套接字
當套接字使用完畢或者程序退出時,應該調用函數closesocket()關閉套接字句柄,函數原型如下:
int closesocket(
SOCKET s //需要關閉的套接字句柄
);
例10:關閉之前創建的套接字句柄s,代碼如下:
::closesocket(s);
11.釋放套接字庫
例11:當程序退出時,還應該記得調用WSACleanup()函數釋放該套接字庫,代碼如下:
::WSACleanup();
二、Winsock編寫過程
服務端編寫過程: 客戶端編寫過程:
1.初始化套接字庫:WSAStartup() 1.初始化套接字庫:WSAStartup()
2.創建套接字句柄:socket() 2.創建套接字句柄:socket()
3.構造套接字地址結構:sockaddr_in 3.構造套接字地址結構:sockaddr_in
4.綁定地址信息:bind() 4.連接服務端:connect
5.監聽端口信息:listen() 5.數據收發:send()、recv()
6.接受連接請求:accept() 6.關閉套接字:closesocket()
7.數據收發:send()、recv() 7.釋放套接字庫:WSACleanup()
8.關閉套接字:closesocket()
9.釋放套接字庫:WSACleanup()