C/C++知识点之服务端使用c++实现websocket协议解析及通信
小标 2018-12-03 来源 : 阅读 3241 评论 0

摘要:本文主要向大家介绍了 C/C++知识点之服务端使用c++实现websocket协议解析及通信,通过具体的内容向大家展示,希望对大家学习C/C++知识点有所帮助。

本文主要向大家介绍了 C/C++知识点之服务端使用c++实现websocket协议解析及通信,通过具体的内容向大家展示,希望对大家学习C/C++知识点有所帮助。

WebSocket 设计出来的目的就是要使客户端浏览器具备像 C/S 架构下桌面系统的实时通讯能力。 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。因为 WebSocket 连接本质上就是一个 TCP 连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮询以及 Comet 技术比较,具有很大的性能优势。下面是一个简单 Web 应用分别用轮询方式和 WebSocket 方式来实现,下面是测试结果图:

                                                 

       通过这张图可以清楚的看出,在流量和负载增大的情况下,WebSocket 方案相比传统的 Ajax 轮询方案有极大的性能优势。
       好了不过多介绍 WebSocket 了,更多介绍大家可以点击参考资料引用的链接查看,还是回到解析协议及通信上来。解析协议这种事,就得耐着性子,一个字节一个字节解析,按步骤一点一点写程序。不过读懂了文档,知道了每个字节的属性意义后,解析起来还是挺简单的。按照协议说明,一旦完成数据解码,那么编码就稍微容易一些,差不多就是解码的逆向操作了。服务端使用c++完成 WebSocket 通信,主要需要完成以下三方面编程:
       1. 服务端与h5客户端发起的 WebSocket 连接握手:

    int wsHandshake(string &request, string &response)
    {
        // 解析http请求头信息
        int ret = WS_STATUS_UNCONNECT;
        std::istringstream stream(request.c_str());
        std::string reqType;
        std::getline(stream, reqType);
        if (reqType.substr(0, 4) != "GET ")
        {
            return ret;
        }
     
        std::string header;
        std::string::size_type pos = 0;
        std::string websocketKey;
        while (std::getline(stream, header) && header != "\r")
        {
            header.erase(header.end() - 1);
            pos = header.find(": ", 0);
            if (pos != std::string::npos)
            {
                std::string key = header.substr(0, pos);
                std::string value = header.substr(pos + 2);
                if (key == "Sec-WebSocket-Key")
                {
                    ret = WS_STATUS_CONNECT;
                    websocketKey = value;
                    break;
                }
            }
        }
     
        if (ret != WS_STATUS_CONNECT)
        {
            return ret;
        }
     
        // 填充http响应头信息
        response = "HTTP/1.1 101 Switching Protocols\r\n";
        response += "Upgrade: websocket\r\n";
        response += "Connection: upgrade\r\n";
        response += "Sec-WebSocket-Accept: ";
     
        const std::string magicKey("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
        std::string serverKey = websocketKey + magicKey;
     
        char shaHash[32];
        memset(shaHash, 0, sizeof(shaHash));
        sha1::calc(serverKey.c_str(), serverKey.size(), (unsigned char *) shaHash);
        serverKey = base64::base64_encode(std::string(shaHash)) + "\r\n\r\n";
        string strtmp(serverKey.c_str());
        response += strtmp;
     
        return ret;
    }

       2. 完成握手后连接就建立了。然后就是接收h5客户端通过 WebSocket 发过来的数据帧并解码:

    int wsDecodeFrame(string inFrame, string &outMessage)
    {
        int ret = WS_OPENING_FRAME;
        const char *frameData = inFrame.c_str();
        const int frameLength = inFrame.size();
        if (frameLength < 2)
        {
            ret = WS_ERROR_FRAME;
        }
     
        // 检查扩展位并忽略
        if ((frameData[0] & 0x70) != 0x0)
        {
            ret = WS_ERROR_FRAME;
        }
     
        // fin位: 为1表示已接收完整报文, 为0表示继续监听后续报文
        ret = (frameData[0] & 0x80);
        if ((frameData[0] & 0x80) != 0x80)
        {
            ret = WS_ERROR_FRAME;
        }
     
        // mask位, 为1表示数据被加密
        if ((frameData[1] & 0x80) != 0x80)
        {
            ret = WS_ERROR_FRAME;
        }
     
        // 操作码
        uint16_t payloadLength = 0;
        uint8_t payloadFieldExtraBytes = 0;
        uint8_t opcode = static_cast<uint8_t >(frameData[0] & 0x0f);
        if (opcode == WS_TEXT_FRAME)
        {
            // 处理utf-8编码的文本帧
            payloadLength = static_cast<uint16_t >(frameData[1] & 0x7f);
            if (payloadLength == 0x7e)
            {
                uint16_t payloadLength16b = 0;
                payloadFieldExtraBytes = 2;
                memcpy(&payloadLength16b, &frameData[2], payloadFieldExtraBytes);
                payloadLength = ntohs(payloadLength16b);
            }
            else if (payloadLength == 0x7f)
            {
                // 数据过长,暂不支持
                ret = WS_ERROR_FRAME;
            }
        }
        else if (opcode == WS_BINARY_FRAME || opcode == WS_PING_FRAME || opcode == WS_PONG_FRAME)
        {
            // 二进制/ping/pong帧暂不处理
        }
        else if (opcode == WS_CLOSING_FRAME)
        {
            ret = WS_CLOSING_FRAME;
        }
        else
        {
            ret = WS_ERROR_FRAME;
        }
     
        // 数据解码
        if ((ret != WS_ERROR_FRAME) && (payloadLength > 0))
        {
            // header: 2字节, masking key: 4字节
            const char *maskingKey = &frameData[2 + payloadFieldExtraBytes];
            char *payloadData = new char[payloadLength + 1];
            memset(payloadData, 0, payloadLength + 1);
            memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength);
            for (int i = 0; i < payloadLength; i++)
            {
                payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
            }
     
            outMessage = payloadData;
            delete[] payloadData;
        }
     
        return ret;
    }

       3. 解码完数据帧,服务端做出相应处理后将结果按照 WebSocket 协议编码,然后发给h5客户端:

    int wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType)
    {
        int ret = WS_EMPTY_FRAME;
        const uint32_t messageLength = inMessage.size();
        if (messageLength > 32767)
        {
            // 暂不支持这么长的数据
            return WS_ERROR_FRAME;
        }
     
        uint8_t payloadFieldExtraBytes = (messageLength <= 0x7d) ? 0 : 2;
        // header: 2字节, mask位设置为0(不加密), 则后面的masking key无须填写, 省略4字节
        uint8_t frameHeaderSize = 2 + payloadFieldExtraBytes;
        uint8_t *frameHeader = new uint8_t[frameHeaderSize];
        memset(frameHeader, 0, frameHeaderSize);
        // fin位为1, 扩展位为0, 操作位为frameType
        frameHeader[0] = static_cast<uint8_t>(0x80 | frameType);
     
        // 填充数据长度
        if (messageLength <= 0x7d)
        {
            frameHeader[1] = static_cast<uint8_t>(messageLength);
        }
        else
        {
            frameHeader[1] = 0x7e;
            uint16_t len = htons(messageLength);
            memcpy(&frameHeader[2], &len, payloadFieldExtraBytes);
        }
     
        // 填充数据
        uint32_t frameSize = frameHeaderSize + messageLength;
        char *frame = new char[frameSize + 1];
        memcpy(frame, frameHeader, frameHeaderSize);
        memcpy(frame + frameHeaderSize, inMessage.c_str(), messageLength);
        frame[frameSize] = '\0';
        outFrame = frame;
     
        delete[] frame;
        delete[] frameHeader;
        return ret;
    }

       4. 握手只需一次,随后反复执行第2步及第3步,就完成了服务端与h5客户端通信。这个只是c++版本的,可以很容易改成java版本的。下面是上述方法用到的一些枚举:

    enum WS_Status
    {
        WS_STATUS_CONNECT = 0,
        WS_STATUS_UNCONNECT = 1,
    };
     
    enum WS_FrameType
    {
        WS_EMPTY_FRAME = 0xF0,
        WS_ERROR_FRAME = 0xF1,
        WS_TEXT_FRAME   = 0x01,
        WS_BINARY_FRAME = 0x02,
        WS_PING_FRAME = 0x09,
        WS_PONG_FRAME = 0x0A,
        WS_OPENING_FRAME = 0xF3,
        WS_CLOSING_FRAME = 0x08
    };

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标编程语言C/C+频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 5 不喜欢 | 0
看完这篇文章有何感觉?已经有5人表态,100%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程