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.h、DNDNet_imp.cpp
另還有兩個簡單的例子:
DNDBird
DNDBirdServer
略遊 於 2017-09-08