一個簡單的Windows Socket可復用框架
說起網絡編程,無非是建立連接,發送數據,接收數據,關閉連接。曾經學習網絡編程的時候用Java寫了一些小的聊天程序,Java對網絡接口函數的封裝還是很簡單實用的,但是在Windows下網絡編程使用的Socket就顯得稍微有點繁瑣。這里介紹一個自己封裝的一個簡單的基於Windows Socket的一個框架代碼,主要目的是為了方便使用Windows Socket進行編程時的代碼復用,閑話少說,上代碼。
熟悉Windows Socket的都知道進行Windows網絡編程必須引入頭文件和庫:
/* *******************公用數據預定義************************** */
// WinSock必須的頭文件和庫
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
在網絡編程中需要對很多API進行返回值檢測,這里使用assert斷言來處理錯誤,另外還有一些公用的宏定義,如下:
#include <assert.h>
// 網絡數據類型
#define TCP_DATA 1
#define UDP_DATA 2
// TCP連接限制
#define MAX_TCP_ CONNECT 10
// 緩沖區上限
#define MAX_BUFFER_LEN 1024
接下來從簡單的開始,封裝一個Client類,用於創建一個客戶端,類定義如下:
// 客戶端類
class Client
{
int m_type; // 通信協議類型
SOCKET m_socket; // 本地套接字
sockaddr_in serverAddr; // 服務器地址結構
public:
Client();
void init( int inet_type, char*addr,unsigned short port); // 初始化通信協議,地址,端口
char*getProto(); // 獲取通信協議類型
char*getIP(); // 獲取IP地址
unsigned short getPort(); // 獲取端口
void sendData( const char * buff, const int len); // 發送數據
void getData( char * buff, const int len); // 接收數據
virtual ~Client( void);
};
(1) 字段m_type標識通信協議是TCP還是UDP。
(2) m_socket保存了本地的套接字,用於發送和接收數據。
(3) serverAddr記錄了連接的服務器的地址和端口信息。
(4) 構造函數使用WSAStartup(WINSOCK_VERSION,&wsa)加載WinSock DLL。
(5) init函數初始化客戶端進行通信的服務器協議類型,IP和端口。
(6) getProto,getIP,getPort分別提取服務器信息。
(7) sendData向服務器發送指定緩沖區的數據。
(8) getData從服務器接收數據保存到指定緩沖區。
(9) 析構函數使用closesocket(m_socket)關閉套接字,WSACleanup卸載WinSock DLL。
Client類的實現如下:
(1)對於init,實現代碼為:
{
int rslt;
m_type=inet_type;
if(m_type==TCP_DATA) // TCP數據
m_socket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // 創建TCP套接字
else if(m_type==UDP_DATA) // UDP數據
m_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); // 創建UDP套接字
assert(m_socket!=INVALID_SOCKET);
serverAddr.sin_family=AF_INET;
serverAddr.sin_addr.S_un.S_addr=inet_addr(addr);
serverAddr.sin_port=htons(port);
memset(serverAddr.sin_zero, 0, 8);
if(m_type==TCP_DATA) // TCP數據
{
rslt= connect(m_socket,(sockaddr*)&serverAddr, sizeof(sockaddr)); // 客戶端連接請求
assert(rslt== 0);
}
}
首先,Client根據不同的協議類型創建不同的套接字m_socket,然后填充serverAddr結構,其中inet_addr是將字符串IP地址轉化為網絡字節序的IP地址,htons將整形轉化為網絡字節順序,對於短整型,相當於高低字節交換。如果通信是TCP協議,那么還需要客戶端主動發起connect連接,UDP不需要做。
(2)初始化連接后就可以發送數據了,sendData實現如下:
這里根據不同的通信類型將數據使用send或者sendto發送到服務器,注意TCP下send的套接字參數是本地創建的套接字,和服務器的信息無關。而對於UDP,需要額外指定服務器的地址信息serverAddr,因為UDP是面向無連接的。
(3)若客戶端需要接收數據,使用getData:
{
int rslt;
int addrLen= sizeof(sockaddr_in);
memset(buff, 0,len);
if(m_type==TCP_DATA) // TCP數據
{
rslt=recv(m_socket,buff,len, 0);
}
else if(m_type==UDP_DATA) // UDP數據
{
rslt=recvfrom(m_socket,buff,len, 0,(sockaddr*)&serverAddr,&addrLen);
}
assert(rslt> 0);
}
根據不同的通信協議使用recv和recvfrom接收服務器返回的數據,和發送數據參數類似。
(4)有時需要獲取客戶端連接的服務器信息,這里封裝的三個函數實現如下:
{
if(m_type==TCP_DATA)
return " TCP ";
else if(m_type==UDP_DATA)
return " UDP ";
else
return "";
}
char* Client::getIP()
{
return inet_ntoa(serverAddr.sin_addr);
}
unsigned short Client::getPort()
{
return ntohs(serverAddr.sin_port);
}
需要額外說明的是,inet_ntoa將網絡字節序的IP地址轉換為字符串IP,和前邊inet_addr功能相反,ntohs和htons功能相反。
(5)構造函數和析構函數的具體代碼如下:
{
WSADATA wsa;
int rslt=WSAStartup(WINSOCK_VERSION,&wsa); // 加載WinSock DLL
assert(rslt== 0);
}
Client::~Client( void)
{
if(m_socket!=INVALID_SOCKET)
closesocket(m_socket);
WSACleanup(); // 卸載WinSock DLL
}
(6)如果需要對客戶端的功能進行增強,可以進行復用Client類。
服務器類Server比客戶端復雜一些,首先服務器需要處理多個客戶端連接請求,因此需要為每個客戶端開辟新的線程(UDP不需要),Server的定義如下:
// 服務器類
#include <list>
using namespace std;
class Server
{
CRITICAL_SECTION *cs; // 臨界區對象
int m_type; // 記錄數據包類型
SOCKET m_socket; // 本地socket
sockaddr_in serverAddr; // 服務器地址
list<sockaddr_in*> clientAddrs; // 客戶端地址結構列表
sockaddr_in* addClient(sockaddr_in client); // 添加客戶端地址結構
void delClient(sockaddr_in *client); // 刪除客戶端地址結構
friend DWORD WINAPI threadProc(LPVOID lpParam); // 線程處理函數作為友元函數
public:
Server();
void init( int inet_type, char*addr,unsigned short port);
void start(); // 啟動服務器
char* getProto(); // 獲取協議類型
char* getIP(sockaddr_in*serverAddr=NULL); // 獲取IP
unsigned short getPort(sockaddr_in*serverAddr=NULL); // 獲取端口
virtual void connect(sockaddr_in*client); // 連接時候處理
virtual int procRequest(sockaddr_in*client, const char* req, int reqLen, char*resp); // 處理客戶端請求
virtual void dis Connect(sockaddr_in*client); // 斷開時候處理
virtual ~Server( void);
};
(1) 和Client類似,Server也需要字段m_socket,serverAddr和m_type,這里引入clientAddrs保存客戶端的信息列表,用addClient和delClient維護這個列表。
(2) CRITICAL_SECTION *cs記錄服務器的臨界區對象,用於保持線程處理函數內的同步。
(3) 構造函數和析構函數與Client功能類似,getProto,getIP,getPort允許獲取服務器和客戶端的地址信息。
(4) init初始化服務器參數,start啟動服務器。
(5) connect,procRequest,disConnect用於實現用戶自定義的服務器行為。
(6) 友元函數threadProc是線程處理函數。
具體實現如下:
(1) init具體代碼為:
{
int rslt;
m_type=inet_type;
if(m_type==TCP_DATA) // TCP數據
m_socket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // 創建TCP套接字
else if(m_type==UDP_DATA) // UDP數據
m_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); // 創建UDP套接字
assert(m_socket!=INVALID_SOCKET);
serverAddr.sin_family=AF_INET;
serverAddr.sin_addr.S_un.S_addr=inet_addr(addr);
serverAddr.sin_port=htons(port);
memset(serverAddr.sin_zero, 0, 8);
rslt=bind(m_socket,(sockaddr*)&serverAddr, sizeof(serverAddr)); // 綁定地址和端口
assert(rslt== 0);
if(m_type==TCP_DATA) // TCP需要偵聽
{
rslt=listen(m_socket,MAX_TCP_ CONNECT); // 監聽客戶端連接
assert(rslt== 0);
}
}
首先根據通信協議類型創建本地套接字m_socket,填充地址serverAddr,使用bind函數綁定服務器參數,對於TCP通信,需要listen進行服務器監聽。
(2) 初始化服務器后使用start啟動服務器:
{
int rslt;
sockaddr_in client; // 客戶端地址結構
int addrLen= sizeof(client);
SOCKET clientSock; // 客戶端socket
char buff[MAX_BUFFER_LEN]; // UDP數據緩存
while( true)
{
if(m_type==TCP_DATA) // TCP數據
{
clientSock=accept(m_socket,(sockaddr*)&client,&addrLen); // 接收請求
if(clientSock==INVALID_SOCKET)
break;
assert(clientSock!=INVALID_SOCKET);
sockaddr_in*pc=addClient(client); // 添加一個客戶端
connect(pc); // 連接處理函數
SockParam sp(clientSock,pc, this); // 參數結構
HANDLE thread=CreateThread(NULL, 0,threadProc,(LPVOID)&sp, 0,NULL); // 創建連接線程
assert(thread!=NULL);
CloseHandle(thread); // 關閉線程
}
else if(m_type==UDP_DATA) // UDP數據
{
memset(buff, 0,MAX_BUFFER_LEN);
rslt=recvfrom(m_socket,buff,MAX_BUFFER_LEN, 0,(sockaddr*)&client,&addrLen);
assert(rslt> 0);
char resp[MAX_BUFFER_LEN]={ 0}; // 接收處理后的數據
rslt=procRequest(&client,buff,rslt,resp); // 處理請求
rslt=sendto(m_socket,resp,rslt, 0,(sockaddr*)&client,addrLen); // 發送udp數據
}
}
}
TCP服務器不斷的監聽新的連接請求,使用accept接收請求,獲得客戶端的地址結構和socket,然后更新客戶端列表,調用connect進行連接時候的處理,使用CreateThread創建一個TCP客戶端線程,線程參數傳遞了客戶端socket和地址,以及服務器對象的指針,交給procThread處理數據的接收和發送。參數結構如下:
struct SockParam
{
SOCKET rsock; // 遠程的socket
sockaddr_in *raddr; // 遠程地址結構
Server*pServer; // 服務器對象指針
SockParam(SOCKET rs,sockaddr_in*ra,Server*ps)
{
rsock=rs;
raddr=ra;
pServer=ps;
}
};
但是對於UDP服務器,只需要不斷使用recvfrom檢測接收新的數據,直接處理即可,請求處理函數proRequest功能可以由用戶自定義。處理后的數據使用sendto發送給客戶端。
(3)相比UDP,TCP數據處理稍顯復雜:
{
SockParam sp=*(SockParam*)lpParam;
Server*s=sp.pServer;
SOCKET sock=s->m_socket;
SOCKET clientSock=sp.rsock;
sockaddr_in *clientAddr=sp.raddr;
CRITICAL_SECTION*cs=s->cs;
int rslt;
char req[MAX_BUFFER_LEN+ 1]={ 0}; // 數據緩沖區,多留一個字節,方便輸出
do
{
rslt=recv(clientSock,req,MAX_BUFFER_LEN, 0); // 接收數據
if(rslt<= 0)
break;
char resp[MAX_BUFFER_LEN]={ 0}; // 接收處理后的數據
EnterCriticalSection(cs);
rslt=s->procRequest(clientAddr,req,rslt,resp); // 處理后返回數據的長度
LeaveCriticalSection(cs);
assert(rslt<=MAX_BUFFER_LEN); // 不會超過MAX_BUFFER_LEN
rslt=send(clientSock,resp,rslt, 0); // 發送tcp數據
}
while(rslt!= 0||rslt!=SOCKET_ERROR);
s->delClient(clientAddr);
s->dis Connect(clientAddr); // 斷開連接后處理
return 0;
}
線程處理函數使用傳遞的服務器對象指針pServer獲取服務器socket,地址和臨界區對象。和客戶端不同的是,服務接收發送數據使用的socket不是本地socket而是客戶端的socket!為了保證線程的並發控制,使用EnterCriticalSection和LeaveCriticalSection保證,中間的請求處理函數和UDP使用的相同。另外,線程的退出表示客戶端的連接斷開,這里更新客戶端列表並調用disConnect允許服務器做最后的處理。和connect類似,這一對函數調用只針對TCP通信,對於UDP通信不存在調用關系。
(4)connect,procRequest,disConnect函數形式如下:
// 用戶自定義服務器處理功能函數:連接請求,請求處理,連接關閉
/* **
以下三個函數的功能由使用者自行定義,頭文件包含自行設計
** */
#include <iostream>
void Server:: connect(sockaddr_in*client)
{
cout<< " 客戶端 "<<getIP(client)<< " [ "<<getPort(client)<< " ] "<< " 連接。 "<<endl;
}
int Server::procRequest(sockaddr_in*client, const char* req, int reqLen, char*resp)
{
cout<<getIP(client)<< " [ "<<getPort(client)<< " ]: "<<req<<endl;
if(m_type==TCP_DATA)
strcpy(resp, " TCP回復 ");
else if(m_type==UDP_DATA)
strcpy(resp, " UDP回復 ");
return 10;
}
void Server::dis Connect(sockaddr_in*client)
{
cout<< " 客戶端 "<<getIP(client)<< " [ "<<getPort(client)<< " ] "<< " 斷開。 "<<endl;
}
這里為了測試,進行了一下簡單的輸出,實際功能可以自行修改。
(5)剩余的函數實現如下:
{
cs= new CRITICAL_SECTION();
InitializeCriticalSection(cs); // 初始化臨界區
WSADATA wsa;
int rslt=WSAStartup(WINSOCK_VERSION,&wsa); // 加載WinSock DLL
assert(rslt== 0);
}
char* Server::getProto()
{
if(m_type==TCP_DATA)
return " TCP ";
else if(m_type==UDP_DATA)
return " UDP ";
else
return "";
}
char* Server::getIP(sockaddr_in*addr)
{
if(addr==NULL)
addr=&serverAddr;
return inet_ntoa(addr->sin_addr);
}
unsigned short Server::getPort(sockaddr_in*addr)
{
if(addr==NULL)
addr=&serverAddr;
return htons(addr->sin_port);
}
sockaddr_in* Server::addClient(sockaddr_in client)
{
sockaddr_in*pc= new sockaddr_in(client);
clientAddrs.push_back(pc);
return pc;
}
void Server::delClient(sockaddr_in *client)
{
assert(client!=NULL);
delete client;
clientAddrs.remove(client);
}
Server::~Server( void)
{
for(list<sockaddr_in*>::iterator i=clientAddrs.begin();i!=clientAddrs.end();++i) // 清空客戶端地址結構
{
delete *i;
}
clientAddrs.clear();
if(m_socket!=INVALID_SOCKET)
closesocket(m_socket); // 關閉服務器socket
WSACleanup(); // 卸載WinSock DLL
DeleteCriticalSection(cs);
delete cs;
}
以上是整個框架的代碼,整體看來我們可以總結如下:
(1) 使用協議類型,IP,端口初始化客戶端后,可以自由的收發數據。
(2) 使用協議類型,IP,端口初始化服務器后,可以自由的處理請求數據和管理連接,並且功能可以由使用者自行定義。
(3) 復用這塊代碼時候可以直接使用或者繼承Client類和Server進行功能擴展,不需要直接修改類的整體設計。
將上述所有的代碼整合到一個Inet.h的文件里,在需要使用類似功能的程序中只需要引入這個頭文件即可。
下面通過構造一個測試用例來體會這種框架的簡潔性:
首先測試服務器代碼:
{
int type;
cout<< " 選擇通信類型(TCP=0/UDP=1): ";
cin>>type;
Server s;
if(type== 1)
s.init(UDP_DATA, " 127.0.0.1 ", 90);
else
s.init(TCP_DATA, " 127.0.0.1 ", 80);
cout<<s.getProto()<< " 服務器 "<<s.getIP()<< " [ "<<s.getPort()<< " ] "<< " 啟動成功。 "<<endl;
s.start();
}
然后是測試客戶端代碼:
{
int type;
cout<< " 選擇通信類型(TCP=0/UDP=1): ";
cin>>type;
Client c;
if(type== 1)
c.init(UDP_DATA, " 127.0.0.1 ", 90);
else
c.init(TCP_DATA, " 127.0.0.1 ", 80);
cout<< " 客戶端發起對 "<<c.getIP()<< " [ "<<c.getPort()<< " ]的 "<<c.getProto()<< " 連接。 "<<endl;
char buff[MAX_BUFFER_LEN];
while( true)
{
cout<< " 發送 "<<c.getProto()<< " 數據到 "<<c.getIP()<< " [ "<<c.getPort()<< " ]: ";
cin>>buff;
if(strcmp(buff, " q ")== 0)
break;
c.sendData(buff,MAX_BUFFER_LEN);
c.getData(buff,MAX_BUFFER_LEN);
cout<< " 接收 "<<c.getProto()<< " 數據從 "<<c.getIP()<< " [ "<<c.getPort()<< " ]: "<<buff<<endl;
}
}
最后我們把這個測試程序整合在一塊:
#include <iostream>
using namespace std;
int main()
{
int flag;
cout<< " 構建服務器/客戶端(0-服務器|1-客戶端): ";
cin>>flag;
if(flag== 0)
testServer();
else
testClient();
return 0;
}
對於TCP測試結果如下:
對於UDP測試結果如下:
通過測試程序的簡潔性和結果可以看出框架的設計還是比較合理的,當然,這里肯定還有很多的不足,希望讀者能提出更好的設計建議。