(轉)服務端使用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 != "\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
};
       參考資料:
       https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server
       https://www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM