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