h5游戏中网络通信不在使用二进制协议,而是使用websocket(ws) 或者使用基于https的wss进行通信,但是已经自己实现过n次二进制协议通信了,所以也不想使用第三方库的实现。下面就介绍下如何使用c++自己实现一个ws服务器,下一篇文章在介绍wss实现。其实ws的实现只是在二进制协议的基础上增加了一层简单的封装而已,实现并不难。下面介绍ws相关的处理,不会详细介绍epoll的实现部分。

首先ws在连接服务器后第一个包是http中get方法的握手协议包,所以对于第一个包需要特殊处理,握手包的处理代码如下:

bool CWSClientSocket::WSHandshake()
{
    if (m_pRecvBuf->GetSize() < PACKHEADSIZE)return true;//包长不够
    NetBuffer arrBuf[2];
    m_pRecvBuf->GetReadEnable(arrBuf);
    uint32_t nContentPos = 0;
    uint32_t nContentLen = 0;
    int nRet = GetLength(arrBuf[0].buf, arrBuf[0].len, nContentPos, nContentLen);//检查http协议包是否完整
    if (nRet == -1)return false;//不支持的协议头解析
    if (0 == nRet) return true;//数据不全
    if (memcmp(arrBuf[0].buf, "GET ", 4) != 0)return false;//协议头错误
    auto pBegin = strstr(arrBuf[0].buf, "Sec-WebSocket-Key: ");
    if (nullptr == pBegin)return false;//数据错误
    pBegin += 19;
    auto pEnd = strstr(pBegin, "\r\n");
    if (nullptr == pEnd)return false;//数据错误
    SetStatus(enmSocketStatus_WSConnected);
    //构建http返回消息 完成握手处理
    std::string str("HTTP/1.1 101 Switching Protocols\r\nConnection: upgrade\r\nUpgrad: websocket\r\nSec-Websocket-Accept: ");
    uint32_t msg[5];
    //sha1加密
    CSHA1 sha;
    sha.Reset();
    sha.Input(pBegin, uint32_t(pEnd - pBegin));
    sha.Input("258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 36);
    sha.Result(msg);
    for (int i = 0; i < 5; ++i)
        msg[i] = htonl(msg[i]);
    Base64Encode((char*)msg, 20, str);//base 64加密
    str.append("\r\n\r\n");
    SendWebMsg(str.data(), (uint32_t)str.size());//发送数据
    m_pRecvBuf->Remove(nRet);//删除已经处理的数据
    return true;
}

握手成功之后就可以进行普通的数据通信了。ws数据分为协议头和数据两部分。下面直接使用二进制数据,也就是说只是将以前的二进制数据通信增加了一个协议头。收包处理代码如下:

bool CWSClientSocket::SpiltMsg()
{
    if (!IsWSConencted())//还没有握手成功
    {
        if (!WSHandshake())
            g_pSocketThread->AddKickSession(GetSessionIndex());
        return false;
    }
    static char buff[MaxOneMsg];
    char* pBuff = nullptr;
    NetBuffer arrBuf[2];
    uint32_t nSize = 0;
    for(uint32_t nCount = 0; nCount < 50;++nCount)
    {
        if (m_pRecvBuf->GetSize() < PACKHEADSIZE)return false;//数据不够
        m_pRecvBuf->GetReadEnable(arrBuf);
        if (char(0x82) != arrBuf[0].buf[0])
        {
            g_pSocketThread->AddKickSession(GetSessionIndex());
            return false;//不是数据包直接关闭socket
        }
        if (arrBuf[0].len >= 4)
            pBuff = arrBuf[0].buf;
        else
        {
            pBuff = buff;
            memcpy(pBuff, arrBuf[0].buf, arrBuf[0].len);
            memcpy(pBuff + arrBuf[0].len, arrBuf[1].buf, 4 - arrBuf[0].len);
        }
        nSize = pBuff[1] & 0x7f;
        if (127 == nSize || pBuff[1] > 0)
        {
            g_pSocketThread->AddKickSession(GetSessionIndex());
            return false; //不是数据包或者数据包太长直接关闭socket 超过64kws协议头中长度是8字节
        }
        uint32_t headLen = 2;
        if (126 == nSize)
        {
            nSize = uint8_t(pBuff[2]);
            nSize = (nSize << 8) | uint8_t(pBuff[2]);
            headLen = 4;
        }
        uint32_t nDataSize = nSize + headLen + 4;
        if (nDataSize > MaxOneMsg || nSize == 0)
        {
            g_pSocketThread->AddKickSession(GetSessionIndex());
            return false;//数据有问题直接关闭服务器
        }
        if (nDataSize > m_pRecvBuf->GetSize())return false;//数据不完整
        if (nDataSize <= arrBuf[0].len)
            pBuff = arrBuf[0].buf;
        else
        {
            pBuff = buff;
            memcpy(pBuff, arrBuf[0].buf, arrBuf[0].len);
            memcpy(pBuff + arrBuf[0].len, arrBuf[1].buf, (size_t)nDataSize - arrBuf[0].len);
        }
        const char* key = &pBuff[headLen];
#ifdef WIN32
#pragma  warning(push)
#pragma  warning(disable : 6385)
#endif
        for (uint32_t i = headLen + 4, j = 0; i < nDataSize; ++i)//数据解密
        {
            pBuff[i] = pBuff[i] ^ key[j];
            j = 3 == j ? 0 : j + 1;
        }
#ifdef WIN32
#pragma  warning(pop)
#endif
        DispatchMsg(pBuff + headLen, nSize);//消息处理
        m_pRecvBuf->Remove(nDataSize);
    }
    return true;
}

DispatchMsg函数中就可以直接使用以前二进制通信的组包方式获取通信数据了。最后在介绍下发包的处理函数:代码如下:

bool CWSClientSocket::SendMsg(const char* pMsg, uint32_t size)
{
    //假设msg前面4个字节是包长 只支持最大包长64k
    int len = size - 4;
    if (len > 0xffff)return false;
    char* msg = (char*)pMsg;
    if (len >= 126)
    {
        msg[0] = char(0x82);
        msg[1] = 126;
        msg[2] = char(len >> 8);
        msg[3] = char(0xff & len);
    }
    else
    {
        msg[2] = char(0x82);
        msg[3] = char(len);
        msg += 2;
        size -= 2;
    }
    SendWebMsg(msg, size);
    return true;
}

到这里websocket的相关处理就全部介绍完了,只需要将上面三个函数套在之前的二进制网络通信上面就可以实现ws通信了。

代码的完整实现传送地址如下:https://github.com/mouzey/c-project

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐