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.hDNDNet_imp.cpp

另还有两个简单的例子:
DNDBird
DNDBirdServer

略游 于 2017-09-08

DND是如何封装WinSock的?相关推荐

  1. [WinSock]封装WSAAsyncSelect!

    封装目标: 最终目标是封装WinSock的WSAAsyncSelect IO模型. 封装原则: 耦合性[减少各种依赖,包括classes之间,编译模块之间.],小粒度增加可复用性. 依赖ATL/WTL ...

  2. MFC 教程【14_SOCKET类的设计和实现】

    SOCKET类的设计和实现 WinSock基本知识 这里不打算系统地介绍socket或者WinSock的知识.首先介绍WinSock API函数,讲解阻塞/非阻塞的概念:然后介绍socket的使用. ...

  3. SOCKET类的设计和实现

    WinSock基本知识 这里不打算系统地介绍socket或者WinSock的知识.首先介绍WinSock API函数,讲解阻塞/非阻塞的概念:然后介绍socket的使用. WinSock API So ...

  4. 一个对Winsock完成端口模型封装的类

    转载请按如下方式显示标明原创作者及出处,以示尊重!! 原创作者:elssann 联系方式:PPP elssann@hotmail.com 在Windows下进行网络服务端程序开发,毫无疑问,Winso ...

  5. VB6+Winsock编写的websocket服务端

    2017/07/08 - 最新的封装模块在:http://www.cnblogs.com/xiii/p/7135233.html,这篇可以忽略了 早就写好了,看这方面资料比较少,索性贴出来.只是一个D ...

  6. Winsock编程原理——面向连接

    Winsock编程原理--面向连接 Windows Sockets使用套接字进行编程,套接字编程是面向客户端/服务器模型而设计的,因此系统中需要客户端和服务器两个不同类型的进程,根据连接类型的不同,对 ...

  7. Winsock开发网络通信程序的经典入门

    Winsock开发网络通信程序的经典入门 对于许多初学者来说,网络通信程序的开发,普遍的一个现象就是觉得难以入手.许多概念,诸如:同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Un ...

  8. 1.封装WinMain至动态链接库

    1.封装WinMain至动态链接库 DND的前言: DND是定位于Windows平台的2d游戏引擎,使用C++和DirectX 11实现,编译器使用vs2015.保留了一些3d功能,适合做一些pc上的 ...

  9. WinSock编程基础

    在上一篇中,我们具体介绍了socket的相关概念,本节将概述套接字规范及操作的一些基础性知识.   一.套接字的一些基础知识 1.Windows通信相关驱动 netio.sys(Network I/O ...

最新文章

  1. centos8编译openssl-1.0.2u、openssl-1.1.1k
  2. 5.10. Web Tools
  3. ctex 图片裁剪 盖住文字_新媒体运营们不可或缺的图片编辑神器!
  4. hdu5025 状态压缩广搜
  5. 【PC工具】速度最快最好用的文件搜索工具:everything,更新文件内容搜索方法...
  6. tf.keras.preprocessing.image_dataset_from_directory() 简介
  7. python css selector_Python爬虫之Selector的用法
  8. 关于eclipse中web项目tomcat报错Server Tomcat v9.0 Server at localhost failed to start问题解决
  9. golang 远程传输文件
  10. sequelize模型关联_关于Sequelize连接查询时inlude中model和association的区别详解
  11. Spring Boot中静态文件获得Thymeleaf支持(配置porm.xml)
  12. 如何在TensorFlow中通过深度学习构建年龄和性别的多任务预测器
  13. JAVA中当子类覆盖一个父类的_java – 当子类中的某个方法被覆盖时,父类如何运行?...
  14. 三万字带你了解那些年面过的Java八股文
  15. @JsonView注解的使用
  16. 计算机操作系统|汤小丹|第四版|习题答案(七)
  17. ubuntu 10.10 安装google拼音输入法 并实现光标跟随
  18. ctrl+鼠标滚轮 设置pycharm字体大小
  19. 高数_第2章多元函数微分学__偏导数的几何应用_空间曲线的切线与法平面
  20. QT小项目练手——用QTimer做一个倒计时程序

热门文章

  1. 我的数据分析全系列教程,记录着那些大学奋斗的时光
  2. 九、Golang并发和线程模型
  3. 三十、深入Python中的Pickle和Json模块
  4. ICCV 2021 | G-SFDA:无需源数据的领域自适应方法
  5. 今日arXiv精选 | Survey/ICCV/ACM MM/ICML/CIKM/SIGIR/RecSys/IROS
  6. 比赛报名 | 第二届ChineseCSCW恒电杯大数据竞赛
  7. 算法那么重要,你还不会?ACM金牌选手教你学习数据结构与算法
  8. 直播预告 | 从编码器与解码器端改进生成式句子摘要
  9. POJ 3984 迷宫问题 BFS求最短路线+路径记录
  10. 广西大学计算机技术复试题库,2018年广西大学计算机与电子信息学院408计算机学科专业基础综合之计算机操作系统考研基础五套测试题...