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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值