DND是如何封裝WinSock的?

DND是如何封裝WinSock的?

文章簡介:

本文章講述在WinSock的基礎上封裝一層框架後,將網絡通訊變得簡單和具有實用價值。
這個框架使用多線程阻塞模型,使用TCP協議,最終封裝後爲一個服務器對多個客戶端的C/S模式,這種模式比較適合遊戲。
完整的代碼在這個地方:
https://github.com/Lveyou/DND

WinSock的配置:

本框架使用WinSock 2.2版本(目前都用這個),只要包含了WindowsSDK的頭文件和庫文件目錄,就可以直接使用頭文件WinSock2.h,然後配置附加依賴項ws2_32.lib。(vs創建的項目會自動包含WindowsSDK目錄,不然你怎麼能直接包含windows.h呢)
由於和老版本的WinSock會發生衝突,需要定義一個宏_WINSOCK2API_。我建議是放在【項目配置】中的【C/C++】中的【預處理器】的【預處理命令】中,這樣對整個項目都有效。

具體實現:

Net靜態類

class DLL_API Net
{
public:
    static Client* GetClient();
    static Server* GetServer();
};

用戶用它直接返回Client或者Server對象,同時初始化WinSock庫。初始化WinSock一般像下面這樣寫(爲啥後面的字會變綠?)。

WSADATA wsaData;
WORD scokVersion = MAKEWORD(2, 2);
assert(!WSAStartup(scokVersion, &wsaData));

PS:對於錯誤處理,我認爲像這種錯誤,就直接assert好了,因爲已經沒有理由讓程序繼續運行,及時發現錯誤及時處理纔好,因爲邏輯上它是不會失敗的,如果失敗了就說明有問題,就應該及時解決,而不是將錯誤隱藏起來。

NetMsg消息類

class DLL_API NetMsg
{
public:
    template<typename T>
    static MetMsg Build(T* p)
    {
        NetMsg ret;
        ret._type = GetClassType<T>();
        ret._data = (void*)p;
        ret._size = sizeof(T);
        return ret;
    }
    UINT32 GetType()
    {
        return _type;
    }
    template<typename T>
    T* UnBuild()
    {
        dnd_assert(_type == GetClassType<T>(), ERROR_00050);
        return (T*)_data;
    }
private:
    UINT32 _type;//4 
    UINT32 _size;//4
    void* _data;
};

NetMsg類的用途是將普通的結構體轉化成可收發的消息。例如用戶定義一個登錄消息的結構體:

struct cs_Login
{
    WCHAR username[16];//賬號
    WCHAR passkey[16];//密碼
};

如果需要發送一個cs_Login消息,就用NetMsg的靜態函數Build一個NetMsg對象,然後通過Client的Send接口作爲參數發送,例如下面這樣:

//構造一個登錄消息結構體
cs_Login msg;
wcscpy_s(msg.username, 16, L"略遊的ID");
wcscpy_s(msg.passkey, 16, L"123456");
//構造一個臨時NetMsg,然後發送
client->Send(NetMsg::Build<cs_Login>(&msg));

NetMsg具有三個成員變量,分別是消息的類型、長度、和內存地址。類型通過函數GetClassType<T>()獲得,爲了避免開銷可以通過constexpr關鍵字使其類型的類型值在編譯期之前就確定,但vs2010並不支持這個語法,於是我採用了下面的辦法,讓一個類型的類型值計算降低爲1次(type_info::hash_code())。

template<typename T>
class ClassType
{
public:
    UINT32 _code;
    ClassType()
    {
        _code = typeid(T).hash_code();
    }
};

template<typename T>
inline UINT32 GetClassType()
{
    static ClassType<T> type;
    return type._code;
}

其中typeid可獲得類型相關的信息,返回一個type_info對象,其中==操作符被重載爲strcmp判斷字符串是否相等,也是就是判斷類型的名字是否相等。所以其效率會比較低,如果要記錄類型還需要記錄整個名字的字符串。而它的hash_code函數會根據這個字符串產生一個32位值,但如果反覆調用效率就特別低,所以通過上面的辦法就解決了問題。長度爲sizeof(T)的結果,理論上在編譯期就確定了值。最後的指針一般指向臨時構造的結構體變量的地址,但是傳給Send後Client會拷貝一份內存,所以也不需要擔心它的指針失效(賦值構造函數和=操作符默認爲淺拷貝)。

Client類

class Client_imp : public Client, public Thread
{
public:
    //嘗試向指定服務器地址和端口連接
    virtual void Connect(const String& ip, const int port) override;
    //發送一個消息
    virtual void Send(const NetMsg& msg) override;
    //取一個消息進行處理
    virtual NetMsg Recv() override;
    //線程函數
    void _run();

    list<NetMsg> m_sends;
    list<NetMsg> m_recvs;

    ~Client_imp();
    //其他細節...
};

Client類繼承了Thread類,Thread具有開闢一個線程的功能。其中重寫的_run函數會被新線程調用,類似於線程函數的效果。Thread類的封裝很簡單,如下:

//.h
#ifndef _DND_THREAD_H_
#define _DND_THREAD_H_

#include "DNDDLL.h"
#include <process.h>
#include "DNDTypedef.h"

namespace DND
{
    void __cdecl _thread_func(void *);//線程函數

    enum ThreadState
    {
        THREAD_START = 0,
        THREAD_RUN,
        THREAD_END
    };

    class DLL_API Thread
    {
    public:
        friend void __cdecl _thread_func(void*);
        Thread() { m_state = THREAD_START; _beginthread(_thread_func, 0, this); }
        UINT32 Get_State();
        void Start();
    private:
        UINT32 m_state;
        virtual void _run() = 0;
    };
}

#endif 
//.cpp
#include "DNDThread.h"
#include <windows.h>

namespace DND
{
    void __cdecl _thread_func(void* p)
    {
        Thread* thread = (Thread*)p;
        while (thread->m_state == THREAD_START)
            Sleep(500);//延時半秒,防止佔據大量資源
        thread->_run();
        thread->m_state = THREAD_END;
    }

    UINT32 Thread::Get_State()
    {
        return m_state;
    }

    void Thread::Start()
    {
        m_state = THREAD_RUN;
    }
}

Client類調用Connect後,會設置要連接的服務器信息,並開啓線程函數做實際的操作(我刪掉了一些線程同步的代碼):

void Client_imp::_run()
{
    char buffer[BUFFER_SIZE];
re2://斷線重連
    //創建套接字
    m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (m_socket == INVALID_SOCKET)
    {
        debug_err(L"DND:Client 創建套接字失敗。");
        return;
    }

    //連接服務器
    SOCKADDR_IN server_ip;
    server_ip.sin_family = AF_INET;
    m_server_ip.GetMultiByteStr(buffer, BUFFER_SIZE);
    //inet_pton(AF_INET, buffer, (void*)&server_ip);
    server_ip.sin_addr.s_addr = inet_addr(buffer);
    server_ip.sin_port = htons((short)m_port);

re: 
    int ret = connect(m_socket, (LPSOCKADDR)&server_ip, sizeof(server_ip));
    if (ret == SOCKET_ERROR)
    {
        state = -1;//失敗
        InterlockedExchange(&m_state, state);
        debug_warn(L"DND: Clinet連接服務器失敗。");
        Sleep(3000);//3秒後重連
        goto re;
    }

    debug_notice(L"DND: Clinet連接服務器成功。");

    //請求接受循環
    while (true)
    {
        //如果沒有消息發送 ,就sleep線程
        if (m_sends.size() == 0)
        {
            Sleep(100);
            continue;
        }
        //從隊列取出一個消息
        NetMsg msg = m_sends.front();
        //NetMsg轉換爲字節流
        memcpy(buffer, &msg._type, sizeof(msg._type));
        memcpy(buffer + sizeof(msg._type), &msg._size, sizeof(msg._size));
        memcpy(buffer + sizeof(msg._type) + sizeof(msg._size),
            msg._data, msg._size);

        ret = send(m_socket, buffer, sizeof(msg._type) + sizeof(msg._size) + msg._size, 0);


        if (ret == SOCKET_ERROR)
        {
            debug_err(L"DND:Clinet 發送數據失敗。");
            closesocket(m_socket);
            goto re2;
        }
        //成功發送之後,釋放堆內存,移出msg
        m_sends.pop_front();
        delete[] msg._data;

        //接收服務器返回的消息
        ret = recv(m_socket, buffer, BUFFER_SIZE, 0);
        if (ret == SOCKET_ERROR)
        {
            debug_err(L"DND:Clinet 接收數據失敗。");
            closesocket(m_socket);
            goto re2;
        }

        //根據收到的消息構造一個NetMsg,用戶Unbuild後釋放堆內存
        NetMsg msg2;
        memcpy(&msg2._type, buffer, sizeof(msg2._type));
        memcpy(&msg2._size, buffer + sizeof(msg2._type), sizeof(msg2._size));
        msg2._data = new BYTE[msg2._size];
        memcpy(msg2._data, buffer + sizeof(msg2._type) + sizeof(msg2._size), msg2._size);

        m_recvs.push_back(msg2);
    }
}

簡而言之Client的Send往發送隊列中添加消息,調用Recv會從接收隊列中取得一個消息。然後用戶對取得的消息做相應處理(通過GetType判斷類型來調相應的處理函數)。我給出了兩個宏來簡化這個操作:

#define DND_CLIENT_MSG_HEAD() \
    UINT32 type = msg.GetType();\
    if(type == 0)\
        return;

#define DND_CLIENT_ON_MSG(name) \
    if(type == GetClassType<name>())\
        {OnMsg_##name(msg.UnBuild<name>());return;} 

在實際應用中就可以這麼寫:

void update()
{
    //幀函數內取得一個消息(你也可以用while在一幀就處理完所有的消息)
    NetMsg net_msg;
    net_msg = client->Recv();
    OnMsg(net_msg);
    //其餘代碼...
}

void DNDBird::OnMsg(NetMsg msg)
{
    DND_CLIENT_MSG_HEAD()
    DND_CLIENT_ON_MSG(sc_Ok)
    DND_CLIENT_ON_MSG(sc_Beat)
    //更多的消息處理...
}
//固定函數名的格式(OnMsg_+類型名)
void DNDBird::OnMsg_sc_Ok(sc_Ok* msg)
{
    debug_msg(L"接收到一個空返回。");
}

Server類

Server類有一個線程,用於監聽新客戶端的連接,然後爲每一個客戶端創建一個單獨的線程處理數據傳輸。但服務器不能每一幀返回一個消息,因爲客戶端有千萬個,應當將邏輯適應給每一個客戶端。由於客戶端的線程在send後,處於recv阻塞狀態,需要服務器返回消息。所以服務器要做的就是接收到客戶端的消息後,馬上處理後再返回一條消息。我這裏是Server指定一個消息分發器函數,當有消息時就會回調此函數,而不是客戶端那種主動的取消息進行處理。

結語

詳細的源碼請看開頭給出的github地址,相關代碼在:
include\ DNDNet.h
src\ DNDNet_imp.hDNDNet_imp.cpp

另還有兩個簡單的例子:
DNDBird
DNDBirdServer

略遊 於 2017-09-08

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章