基於TCP(連線導向)的socket編程,分為用戶端和伺服器端。
用戶端的流程如下:
(1)建立通訊端(socket)
(2)向伺服器發出串連請求(connect)
(3)和伺服器端進行通訊(send/recv)
(4)關閉通訊端
伺服器端的流程如下:
(1)建立通訊端(socket)
(2)將通訊端綁定到一個本地地址和連接埠上(bind)
(3)將通訊端設為監聽模式,準備接收用戶端請求(listen)
(4)等待客戶請求到來;當請求到來後,接受串連請求,返回一個新的對應於此次串連的通訊端(accept)
(5)用返回的通訊端和用戶端進行通訊(send/recv)
(6)返回,等待另一個客戶請求。
(7)關閉通訊端。
下面通過一個具體例子講解一下具體的過程和相關的函數。
用戶端代碼,運行於vs2008
// ClientTest.cpp : 定義控制台應用程式的進入點。//#include "stdafx.h"#include <stdio.h>#include <stdlib.h>#include <winsock2.h>#define SERVER_PORT 5208 //偵聽連接埠int _tmain(int argc, _TCHAR* argv[]){WORD wVersionRequested;WSADATA wsaData;int ret;SOCKET sClient; //串連通訊端struct sockaddr_in saServer; //伺服器位址資訊char *ptr;BOOL fSuccess = TRUE;//WinSock初始化wVersionRequested = MAKEWORD(2, 2); //希望使用的WinSock DLL的版本ret = WSAStartup(wVersionRequested, &wsaData);//載入通訊端庫if(ret!=0){printf("WSAStartup() failed!\n");//return 0;}//確認WinSock DLL支援版本2.2if(LOBYTE(wsaData.wVersion)!=2 || HIBYTE(wsaData.wVersion)!=2){WSACleanup();//釋放為該程式分配的資源,終止對winsock動態庫的使用printf("Invalid WinSock version!\n");//return 0;}//建立Socket,使用TCP協議sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sClient == INVALID_SOCKET){WSACleanup();printf("socket() failed!\n");//return 0;}//構建伺服器位址資訊saServer.sin_family = AF_INET; //地址家族saServer.sin_port = htons(SERVER_PORT); //注意轉化為網路節序saServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//串連伺服器ret = connect(sClient, (struct sockaddr *)&saServer, sizeof(saServer));if (ret == SOCKET_ERROR){printf("connect() failed!\n");closesocket(sClient); //關閉通訊端WSACleanup();//return 0;}char sendMessage[]="ZhongXingPengYue"; ret = send (sClient, (char *)&sendMessage, sizeof(sendMessage), 0);if (ret == SOCKET_ERROR){printf("send() failed!\n");}elseprintf("client info has been sent!");char recvBuf[100];recv(sClient,recvBuf,100,0);printf("%s\n",recvBuf);closesocket(sClient); //關閉通訊端WSACleanup();getchar();//return 0;}
第一步,載入通訊端。使用WSAStartup 函數,如:ret = WSAStartup(wVersionRequested, &wsaData)。WSAStartup函數的原型為
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData)
第一參數wVersionRequested,用來指定準備載入的winsock庫的版本。利用MAKEWORD(x,y)宏來賦值。x是高位位元組,表示副版本號碼;y是低位位元組,表示主要版本號。MAKEWORD(2, 2)表示版本號碼為2.2。
第二個參數是指向WSADATA結構的指標,是一個傳回值,儲存了庫版本的有關資訊。
第二步,建立通訊端。使用socket函數,如:sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)。socket函數的原型為:
SOCKET socket(int af, int type, int protocol );
第一個參數,指定地址族,對於TCP/IP協議的通訊端,只能為AF_INET;
第二個參數,指定socket類型,SOCK_STREAM指產生流式通訊端,SOCK_DGRAM指產生資料通訊端,TCP/IP協議使用SOCK_STREAM。
第三個參數,與特定的地址家族相關的協議,TCP協議一般為IPPROTO_TCP。也可以寫0,那麼系統會根據地址格式和通訊端類別,自動選擇一個適合的協議。
如果socket建立成功,則返回一個新的SOCKET資料類型的通訊端描述符;若失敗,則返回INVALID_SOCKET,由此可以判斷是否建立成功。
第三步,串連伺服器。使用connect函數,如:ret = connect(sClient, (struct sockaddr *)&saServer, sizeof(saServer))。connect函數函數原型為
int connect(SOCKET s, const struct sockaddr FAR* name, int namelen);
第一個參數是將在上面建立串連的那個通訊端的描述符,即之前建立socket的傳回值sClient。
第二個參數是要已連線的服務器端的地址資訊。它是一個結構體類型struct sockaddr_in ,需要在調用connect函數之前構建伺服器位址資訊。sockaddr_in的定義如下:
struct sockaddr_in{short sin_family;unsigned short sin_port;struct in_addr sin_addr;char sin_zero[8]};
設定伺服器連接埠時,用到htons函數,該函數把一個u_short類型的值從主機位元組順序轉換為TCP/IP網路位元組順序,因為不同的電腦存放多位元組的順序不同(基於Intel CPU是高位元組存放在低地址,低位元組存放在高地址),所以網路中不同主機間通訊時,要統一採用網路位元組順序。設定伺服器IP地址時,使用到inet_addr函數,它是將點分十進位的IP地址的字串轉換成unsigned long型。inet_ntoa函數做相反的轉換。
第三個參數是伺服器端地址結構體的大小。
第四步,發送。使用send函數向伺服器發送資料,如:ret = send (sClient, (char *)&sendMessage, sizeof(sendMessage), 0)。send函數的原型為
int send(SOCKET s, const char FAR* buf, int len, int flags);
第一個參數,是一個與伺服器已經建立串連的通訊端。
第二個參數,指向包含要發送的資料的緩衝區的指標。
第三個參數,是所指向的緩衝區的長度。準確的說,應該是所要發送的資料的長度,因為不是緩衝區的所有資料都要同時發送。
第四個參數,它設定的值將影響函數的行為,一般將其設定為0即可。
如果發送失敗,send會返回SOCKET_ERROR,由此可以判斷髮送是否成功。
第五步,接收。使用recv函數接收伺服器發過來的資料,如recv(sClient,recvBuf,100,0)。recv函數的原型為
int recv(SOCKET s, const char FAR* buf, int len, int flags);
recv函數的參數的含義和send函數參數含義差不多,只是第二個參數是指向用來儲存接收資料的緩衝區的指標。recv函數的傳回值應該是所接收的資料的長度,如果返回SOCKET_ERROR表示接收失敗;返回0表示伺服器端關閉串連。
第六步,關閉socket,釋放資源。使用closesocket函數關閉通訊端,如closesocket(sClient);使用WSACleanup函數釋放為該程式分配的資源,終止對winsock動態庫的使用,如WSACleanup();
伺服器端代碼,運行於vs2008
// ServerTest.cpp : 定義控制台應用程式的進入點。//#include "stdafx.h"#include <stdio.h>#include <winsock2.h>#define SERVER_PORT 5208 //偵聽連接埠int _tmain(int argc, _TCHAR* argv[]){WORD wVersionRequested; WSADATA wsaData; int ret, nLeft, length; SOCKET sListen, sServer; //偵聽通訊端,串連通訊端 struct sockaddr_in saServer, saClient; //地址資訊 char *ptr;//用於遍曆資訊的指標 //WinSock初始化 wVersionRequested=MAKEWORD(2, 2); //希望使用的WinSock DLL 的版本 ret=WSAStartup(wVersionRequested, &wsaData); if(ret!=0) { printf("WSAStartup() failed!\n"); //return 0; } //建立Socket,使用TCP協議 sListen=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sListen == INVALID_SOCKET) { WSACleanup(); printf("socket() faild!\n"); //return 0; } //構建本地地址資訊 saServer.sin_family = AF_INET; //地址家族 saServer.sin_port = htons(SERVER_PORT); //注意轉化為網路位元組序 saServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //使用INADDR_ANY 指示任意地址 //綁定 ret = bind(sListen, (struct sockaddr *)&saServer, sizeof(saServer)); if (ret == SOCKET_ERROR) { printf("bind() faild! code:%d\n", WSAGetLastError()); closesocket(sListen); //關閉通訊端 WSACleanup(); //return 0; } //偵聽串連請求 ret = listen(sListen, 5); if (ret == SOCKET_ERROR) { printf("listen() faild! code:%d\n", WSAGetLastError()); closesocket(sListen); //關閉通訊端 //return 0; } printf("Waiting for client connecting!\n"); printf("Tips: Ctrl+c to quit!\n"); //阻塞等待接受用戶端串連while(1)//迴圈監聽用戶端,永遠不停止,所以,在本項目中,我們沒有心跳包。{length = sizeof(saClient);sServer = accept(sListen, (struct sockaddr *)&saClient, &length);if (sServer == INVALID_SOCKET){printf("accept() faild! code:%d\n", WSAGetLastError());closesocket(sListen); //關閉通訊端WSACleanup();return 0;} char sendMessage[]="hello client"; //發送資訊給用戶端send(sServer,sendMessage,strlen(sendMessage)+1,0);char receiveMessage[5000];nLeft = sizeof(receiveMessage);ptr = (char *)&receiveMessage;while(nLeft>0){//接收資料ret = recv(sServer, ptr, 5000, 0);if (ret == SOCKET_ERROR){printf("recv() failed!\n");return 0;}if (ret == 0) //用戶端已經關閉串連{printf("Client has closed the connection\n");break;}nLeft -= ret;ptr += ret;} printf("receive message:%s\n", receiveMessage);//列印我們接收到的訊息。} // closesocket(sListen); // closesocket(sServer); // WSACleanup();return 0;}
第一步,載入通訊端庫,和用戶端得載入通訊端一樣。
第二步,建立監聽通訊端,sListen=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);仍然使用的是socket函數。
第三步,綁定。使用bind函數,該函數的作用是將一個建立好的通訊端綁定到本地的某個地址和連接埠上,該函數的原型為:
int bind(SOCKET s, const struct sockaddr FAR* name, int namelen);
第一個參數,指定要綁定的通訊端;
第二個參數,指定該通訊端的地址資訊,這裡即伺服器的地址資訊,它仍是指向struct sockaddr_in類型的結構體的指標。這個結構體和用戶端調用connect函數之前構建伺服器位址資訊的一樣。其中INADDR_ANY 是指示任意地址,因為伺服器含有可能多個網卡,可能有多個IP地址,這邊指選擇一個任意可用的地址。
第三個參數,地址的資訊的長度。
第四步,監聽串連。使用listen函數,該函數是將指定的通訊端設定為監聽模式,如ret = listen(sListen, 5)。函數原型為
int listen(SOCKET s, int backlog);
第一個參數,是要設定為監聽的通訊端描述符。
第二個參數,是等待串連隊列的最大的長度。注意了,設定這個值是為了設定等待串連隊列的最大長度,而不是在一個連接埠上可以串連的最大數目。例如,設定為5,當有6個串連請求同時到達,前面5個串連請求會被放到等待請求串連隊列中,然後伺服器依次處理這些請求服務,但是第6個串連請求會被拒絕。
第五步,接受用戶端的串連請求。使用accept函數接受用戶端發送的串連請求,如sServer = accept(sListen, (struct sockaddr *)&saClient, &length);該函數的原型為
SOCKET accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
第一個參數,是一個已設為監聽模式的socket的描述符。
第二個參數,是一個傳回值,它指向一個struct sockaddr類型的結構體的變數,儲存了發起串連的用戶端得IP地址資訊和連接埠資訊。
第三個參數,也是一個傳回值,指向整型的變數,儲存了返回的地址資訊的長度。
accept函數傳回值是一個用戶端和伺服器串連的SOCKET類型的描述符,在伺服器端標識著這個用戶端。
第六、七步是發送和接收,分別使用send和recv函數,這裡和用戶端的一樣,不再重複。
accept函數是放在一個死迴圈中的,一直監聽客戶的請求。當伺服器關閉時要關閉所有的通訊端,和釋放資源。