zoukankan      html  css  js  c++  java
  • (转)服务端使用c++实现websocket协议解析及通信

    转自:http://blog.csdn.net/grafx/article/details/54234518

           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 != " ")
        {
            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 ";
        response += "Upgrade: websocket ";
        response += "Connection: upgrade ";
        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)) + " ";
        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] = '';
        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
    };
           参考资料:
           https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server
           https://www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/
  • 相关阅读:
    Minimum Depth of Binary Tree leetcode java
    Maximum Depth of Binary Tree leetcode java
    Symmetric Tree leetcode java
    Same Tree leetcode java
    Binary Tree Postorder Traversal leetcode java
    Binary Tree Preorder Traversal leetcode java
    Binary Tree Inorder Traversal leetcode java
    Combinations leetcode java
    一键清除Centos iptables 防火墙所有规则
    阿里云centos7.7x64安装open,并配置ip转发和nat伪装
  • 原文地址:https://www.cnblogs.com/xj2015/p/7419326.html
Copyright © 2011-2022 走看看