基於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.2
if(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");
}
else
printf("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函數是放在一個死循環中的,一直監聽客戶的請求。當服務器關閉時要關閉所有的套接字,和釋放資源。