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;//4void* _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;}//成功發送之后,釋放堆內存,移出msgm_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
總結
以上是生活随笔為你收集整理的DND是如何封装WinSock的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 让Win32窗口程序拥有控制台窗口
- 下一篇: WinSock的recv返回值处理