一個簡單的Windows Socket可復用框架


一個簡單的Windows Socket可復用框架

 

說起網絡編程,無非是建立連接,發送數據,接收數據,關閉連接。曾經學習網絡編程的時候用Java寫了一些小的聊天程序,Java對網絡接口函數的封裝還是很簡單實用的,但是在Windows下網絡編程使用的Socket就顯得稍微有點繁瑣。這里介紹一個自己封裝的一個簡單的基於Windows Socket的一個框架代碼,主要目的是為了方便使用Windows Socket進行編程時的代碼復用,閑話少說,上代碼。

熟悉Windows Socket的都知道進行Windows網絡編程必須引入頭文件和庫:

#pragma once
/* *******************公用數據預定義************************** */

// 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)       getProtogetIPgetPort分別提取服務器信息。

(7)       sendData向服務器發送指定緩沖區的數據。

(8)       getData從服務器接收數據保存到指定緩沖區。

(9)   析構函數使用closesocket(m_socket)關閉套接字,WSACleanup卸載WinSock DLL

Client類的實現如下:

1)對於init,實現代碼為:

void Client::init( int inet_type, char*addr,unsigned  short port)
{
     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發送到服務器,注意TCPsend的套接字參數是本地創建的套接字,和服務器的信息無關。而對於UDP,需要額外指定服務器的地址信息serverAddr,因為UDP是面向無連接的。

3)若客戶端需要接收數據,使用getData:

void Client::getData( char * buff, const  int len)
{
     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);
}

 

根據不同的通信協議使用recvrecvfrom接收服務器返回的數據,和發送數據參數類似。

4)有時需要獲取客戶端連接的服務器信息,這里封裝的三個函數實現如下:

char* Client::getProto()
{
     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功能相反,ntohshtons功能相反。

5)構造函數和析構函數的具體代碼如下:

Client::Client()
{
    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_socketserverAddrm_type,這里引入clientAddrs保存客戶端的信息列表,用addClientdelClient維護這個列表。

(2)              CRITICAL_SECTION *cs記錄服務器的臨界區對象,用於保持線程處理函數內的同步。

(3)       構造函數和析構函數與Client功能類似,getProtogetIPgetPort允許獲取服務器和客戶端的地址信息。

(4)              init初始化服務器參數,start啟動服務器。

(5)              connectprocRequestdisConnect用於實現用戶自定義的服務器行為。

(6)       友元函數threadProc是線程處理函數。

具體實現如下:

(1)       init具體代碼為:

void Server::init( int inet_type, char*addr,unsigned  short port)
{
     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啟動服務器:

void Server::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)相比UDPTCP數據處理稍顯復雜:

DWORD WINAPI threadProc(LPVOID lpParam) // 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!為了保證線程的並發控制,使用EnterCriticalSectionLeaveCriticalSection保證,中間的請求處理函數和UDP使用的相同。另外,線程的退出表示客戶端的連接斷開,這里更新客戶端列表並調用disConnect允許服務器做最后的處理。和connect類似,這一對函數調用只針對TCP通信,對於UDP通信不存在調用關系。

4connectprocRequestdisConnect函數形式如下:

/* ******************用戶自定義************************* */
// 用戶自定義服務器處理功能函數:連接請求,請求處理,連接關閉

/* **
    以下三個函數的功能由使用者自行定義,頭文件包含自行設計
**
*/
#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)剩余的函數實現如下:

Server::Server()
{
    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的文件里,在需要使用類似功能的程序中只需要引入這個頭文件即可。

下面通過構造一個測試用例來體會這種框架的簡潔性:

首先測試服務器代碼:

void testServer()
{
     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();
}

然后是測試客戶端代碼:

void testClient()
{
     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  " Inet.h "
#include <iostream>
using  namespace std;

int main()
{
     int flag;
     cout<< " 構建服務器/客戶端(0-服務器|1-客戶端): ";
    cin>>flag;
     if(flag== 0)
        testServer();
     else
        testClient();
     return  0;
}

對於TCP測試結果如下:

對於UDP測試結果如下:

通過測試程序的簡潔性和結果可以看出框架的設計還是比較合理的,當然,這里肯定還有很多的不足,希望讀者能提出更好的設計建議。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM