C++ 使用 websocket 協(xié)議

開發(fā)了一款微信小游戲《約戰(zhàn)24點(diǎn)》 服務(wù)器是用 C++ 寫的,與客戶端之間采用的是 websocket 協(xié)議通信穆桂。C++ 中使用 websocket 需要對(duì)協(xié)議數(shù)據(jù)進(jìn)行處理才能使用。

1. websocket 協(xié)議數(shù)據(jù)格式詳解

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

?FIN
標(biāo)識(shí)是否為此消息的最后一個(gè)數(shù)據(jù)包,占1bit
?RSV1, RSV2, RSV3: 用于擴(kuò)展協(xié)議捣域,一般為0逾条,各占1bit

?Opcode
數(shù)據(jù)包類型(frame type)琢岩,占4bits
0x0:標(biāo)識(shí)一個(gè)中間數(shù)據(jù)包
0x1:標(biāo)識(shí)一個(gè)text類型數(shù)據(jù)包
0x2:標(biāo)識(shí)一個(gè)binary類型數(shù)據(jù)包
0x3-7:保留
0x8:標(biāo)識(shí)一個(gè)斷開連接類型數(shù)據(jù)包
0x9:標(biāo)識(shí)一個(gè)ping類型數(shù)據(jù)包
0xA:表示一個(gè)pong類型數(shù)據(jù)包
0xB-F:保留

?MASK:占1bits
用于標(biāo)識(shí)PayloadData是否經(jīng)過(guò)掩碼處理。如果是1师脂,Masking-key域的數(shù)據(jù)即是掩碼密鑰担孔,用于解碼PayloadData〕跃客戶端發(fā)出的數(shù)據(jù)幀需要進(jìn)行掩碼處理糕篇,所以此位是1。

?Payload length
Payload data的長(zhǎng)度酌心,占7bits拌消,7+16bits,7+64bits:
如果其值在0-125安券,則是payload的真實(shí)長(zhǎng)度墩崩。
如果值是126,則后面2個(gè)字節(jié)形成的16bits無(wú)符號(hào)整型數(shù)的值是payload的真實(shí)長(zhǎng)度完疫。注意泰鸡,網(wǎng)絡(luò)字節(jié)序,需要轉(zhuǎn)換壳鹤。
如果值是127盛龄,則后面8個(gè)字節(jié)形成的64bits無(wú)符號(hào)整型數(shù)的值是payload的真實(shí)長(zhǎng)度。注意,網(wǎng)絡(luò)字節(jié)序余舶,需要轉(zhuǎn)換啊鸭。
這里的長(zhǎng)度表示遵循一個(gè)原則,用最少的字節(jié)表示長(zhǎng)度(盡量減少不必要的傳輸)匿值。舉例說(shuō)赠制,payload真實(shí)長(zhǎng)度是124,在0-125之間挟憔,必須用前7位表示钟些;不允許長(zhǎng)度1是126或127,然后長(zhǎng)度2是124绊谭,這樣違反原則政恍。

?Payload data
應(yīng)用層數(shù)據(jù)

---------------------server解析client端的數(shù)據(jù)---------------------------
接收到客戶端數(shù)據(jù)后的解析規(guī)則如下:
?1byte
?1bit: frame-fin,x0表示該message后續(xù)還有frame达传;x1表示是message的最后一個(gè)frame
?3bit: 分別是frame-rsv1篙耗、frame-rsv2和frame-rsv3,通常都是x0
?4bit: frame-opcode宪赶,x0表示是延續(xù)frame宗弯;x1表示文本frame;x2表示二進(jìn)制frame搂妻;x3-7保留給非控制frame蒙保;x8表示關(guān) 閉連接;x9表示ping叽讳;xA表示pong追他;xB-F保留給控制frame

?2byte
?1bit: Mask,1表示該frame包含掩碼岛蚤;0表示無(wú)掩碼
?7bit邑狸、7bit+2byte、7bit+8byte: 7bit取整數(shù)值涤妒,若在0-125之間单雾,則是負(fù)載數(shù)據(jù)長(zhǎng)度;若是126表示她紫,后兩個(gè)byte取無(wú)符號(hào)16位整數(shù)值硅堆,是負(fù)載長(zhǎng)度;127表示后8個(gè) byte贿讹,取64位無(wú)符號(hào)整數(shù)值渐逃,是負(fù)載長(zhǎng)度
?3-6byte: 這里假定負(fù)載長(zhǎng)度在0-125之間,并且Mask為1民褂,則這4個(gè)byte是掩碼
?7-end byte: 長(zhǎng)度是上面取出的負(fù)載長(zhǎng)度茄菊,包括擴(kuò)展數(shù)據(jù)和應(yīng)用數(shù)據(jù)兩部分疯潭,通常沒有擴(kuò)展數(shù)據(jù);若Mask為1面殖,則此數(shù)據(jù)需要解碼竖哩,解碼規(guī)則為1 -4byte掩碼循環(huán)和數(shù)據(jù)byte做異或操作。

2. C++對(duì) websocket 封裝處理

WebSocket.h
//
// Description: WebSocket RFC6544 codec, written in C++.
//

#ifndef PROJECT_WEBSOCKET_H
#define PROJECT_WEBSOCKET_H

#include <string>
#include <vector>

enum WSFrameType {
    ERROR_FRAME=0xFF00,
    INCOMPLETE_FRAME=0xFE00,

    OPENING_FRAME=0x3300,
    CLOSING_FRAME=0x3400,

    INCOMPLETE_TEXT_FRAME=0x01,
    INCOMPLETE_BINARY_FRAME=0x02,

    TEXT_FRAME=0x81,
    BINARY_FRAME=0x82,

    PING_FRAME=0x19,
    PONG_FRAME=0x1A
};

enum WSStatus
{
    WS_STATUS_UNCONNECT = 1,
    WS_STATUS_CONNECT = 2,
};

class WebSocket
{
public:
    WebSocket();

    //解析 WebSocket 的握手?jǐn)?shù)據(jù)
    bool parseHandshake(const std::string& request);

    //應(yīng)答 WebSocket 的握手
    std::string respondHandshake();

    //解析 WebSocket 的協(xié)議具體數(shù)據(jù)脊僚,客戶端-->服務(wù)器
    int getWSFrameData(char* msg, int msgLen, std::vector<char>& outBuf, int* outLen);

    //封裝 WebSocket 協(xié)議的數(shù)據(jù)相叁,服務(wù)器-->客戶端
    int makeWSFrameData(char* msg, int msgLen, std::vector<char>& outBuf);

    //封裝 WebSocket 協(xié)議的數(shù)據(jù)頭(二進(jìn)制數(shù)據(jù))
    static int makeWSFrameDataHeader(int len, std::vector<char>& header);

private:
    std::string websocketKey_;//握手中客戶端發(fā)來(lái)的key
};


#endif //PROJECT_WEBSOCKET_H

2. WebSocket.cpp

#include "WebSocket.h"
#include "BaseFunc.h"
#include <openssl/sha.h>  //for SHA1
#include <arpa/inet.h>    //for ntohl
#include <string.h>


WebSocket::WebSocket()
{
}

bool WebSocket::parseHandshake(const std::string& request)
{
    // 解析WEBSOCKET請(qǐng)求頭信息
    bool ret = false;
    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;
    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 = true;
                websocketKey_ = value;
                break;
            }
        }
    }

    return ret;
}


std::string WebSocket::respondHandshake()
{
    // 算出WEBSOCKET響應(yīng)信息
    std::string response = "HTTP/1.1 101 Switching Protocols\r\n";
    response += "Upgrade: websocket\r\n";
    response += "Connection: upgrade\r\n";
    response += "Sec-WebSocket-Accept: ";

    //使用請(qǐng)求傳過(guò)來(lái)的KEY+協(xié)議字符串,先用SHA1加密然后使用base64編碼算出一個(gè)應(yīng)答的KEY
    const std::string magicKey("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
    std::string serverKey = websocketKey_ + magicKey;
    //LOG_INFO << "serverKey:" << serverKey;

    //SHA1
    unsigned char digest[SHA_DIGEST_LENGTH];
    SHA1((unsigned char*)serverKey.c_str(), serverKey.length(), (unsigned char*)&digest);
    //printf("DIGEST:"); for(int i=0; i<20; i++) printf("%02x ",digest[i]); printf("\n");

    //Base64
    char basestr[1024] = {0};
    base64_encode((char*)digest, SHA_DIGEST_LENGTH, basestr);

    //完整的握手應(yīng)答
    response = response + std::string(basestr) + "\r\n";
    //LOG_INFO << "RESPONSE:" << response;

    return response;
}


int WebSocket::getWSFrameData(char* msg, int msgLen, std::vector<char>& outBuf, int* outLen)
{
    if(msgLen < 2)
        return INCOMPLETE_FRAME;

    uint8_t fin_ = 0;
    uint8_t opcode_ = 0;
    uint8_t mask_ = 0;
    uint8_t masking_key_[4] = {0,0,0,0};
    uint64_t payload_length_ = 0;
    int pos = 0;
    //FIN
    fin_ = (unsigned char)msg[pos] >> 7;
    //Opcode
    opcode_ = msg[pos] & 0x0f;
    pos++;
    //MASK
    mask_ = (unsigned char)msg[pos] >> 7;
    //Payload length
    payload_length_ = msg[pos] & 0x7f;
    pos++;
    if(payload_length_ == 126)
    {
        uint16_t length = 0;
        memcpy(&length, msg + pos, 2);
        pos += 2;
        payload_length_ = ntohs(length);
    }
    else if(payload_length_ == 127)
    {
        uint32_t length = 0;
        memcpy(&length, msg + pos, 4);
        pos += 4;
        payload_length_ = ntohl(length);
    }
    //Masking-key
    if(mask_ == 1)
    {
        for(int i = 0; i < 4; i++)
            masking_key_[i] = msg[pos + i];
        pos += 4;
    }
    //取出消息數(shù)據(jù)
    if (msgLen >= pos + payload_length_ )
    {
        //Payload data
        *outLen = pos + payload_length_;
        outBuf.clear();
        if(mask_ != 1)
        {
            char* dataBegin = msg + pos;
            outBuf.insert(outBuf.begin(), dataBegin, dataBegin+payload_length_);
        }
        else
        {
            for(uint i = 0; i < payload_length_; i++)
            {
                int j = i % 4;
                outBuf.push_back(msg[pos + i] ^ masking_key_[j]);
            }
        }
    }
    else
    {
        return INCOMPLETE_FRAME;
    }

//    printf("WEBSOCKET PROTOCOL\n"
//            "FIN: %d\n"
//            "OPCODE: %d\n"
//            "MASK: %d\n"
//            "PAYLOADLEN: %d\n"
//            "outLen:%d\n",
//            fin_, opcode_, mask_, payload_length_, *outLen);

    //斷開連接類型數(shù)據(jù)包
    if ((int)opcode_ == 0x8)
        return -1;

    return 0;
}


int WebSocket::makeWSFrameData(char* msg, int msgLen, std::vector<char>& outBuf)
{
    std::vector<char> header;
    makeWSFrameDataHeader(msgLen, header);
    outBuf.insert(outBuf.begin(), header.begin(), header.end());
    outBuf.insert(outBuf.end(), msg, msg+msgLen);
    return 0;
}

int WebSocket::makeWSFrameDataHeader(int len, std::vector<char>& header)
{
    header.push_back((char)BINARY_FRAME);
    if(len <= 125)
    {
        header.push_back((char)len);
    }
    else if(len <= 65535)
    {
        header.push_back((char)126);//16 bit length follows
        header.push_back((char)((len >> 8) & 0xFF));// leftmost first
        header.push_back((char)(len & 0xFF));
    }
    else // >2^16-1 (65535)
    {
        header.push_back((char)127);//64 bit length follows

        // write 8 bytes length (significant first)
        // since msg_length is int it can be no longer than 4 bytes = 2^32-1
        // padd zeroes for the first 4 bytes
        for(int i=3; i>=0; i--)
        {
            header.push_back((char)0);
        }
        // write the actual 32bit msg_length in the next 4 bytes
        for(int i=3; i>=0; i--)
        {
            header.push_back((char)((len >> 8*i) & 0xFF));
        }
    }

    return 0;
}

3. BaseFunc.h

#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>


// base64 編碼
int base64_encode(char *in_str, int in_len, char *out_str)
{
    BIO *b64, *bio;
    BUF_MEM *bptr = NULL;
    size_t size = 0;

    if (in_str == NULL || out_str == NULL)
        return -1;

    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new(BIO_s_mem());
    bio = BIO_push(b64, bio);

    BIO_write(bio, in_str, in_len);
    BIO_flush(bio);

    BIO_get_mem_ptr(bio, &bptr);
    memcpy(out_str, bptr->data, bptr->length);
    out_str[bptr->length] = '\0';
    size = bptr->length;

    BIO_free_all(bio);
    return 0;
}

// base64 解碼
int base64_decode(char *in_str, int in_len, char *out_str)
{
    BIO *b64, *bio;
    BUF_MEM *bptr = NULL;
    //int counts;
    int size = 0;

    if (in_str == NULL || out_str == NULL)
        return -1;

    b64 = BIO_new(BIO_f_base64());
    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);

    bio = BIO_new_mem_buf(in_str, in_len);
    bio = BIO_push(b64, bio);

    size = BIO_read(bio, out_str, in_len);
    out_str[size] = '\0';

    BIO_free_all(bio);
    return 0;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辽幌,一起剝皮案震驚了整個(gè)濱河市增淹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌乌企,老刑警劉巖埠通,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異逛犹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)梁剔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門虽画,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人荣病,你說(shuō)我怎么就攤上這事码撰。” “怎么了个盆?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵脖岛,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我颊亮,道長(zhǎng)柴梆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任终惑,我火速辦了婚禮绍在,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雹有。我一直安慰自己偿渡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布霸奕。 她就那樣靜靜地躺著溜宽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪质帅。 梳的紋絲不亂的頭發(fā)上适揉,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天留攒,我揣著相機(jī)與錄音,去河邊找鬼涡扼。 笑死稼跳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吃沪。 我是一名探鬼主播汤善,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼票彪!你這毒婦竟也來(lái)了红淡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤降铸,失蹤者是張志新(化名)和其女友劉穎在旱,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體推掸,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡桶蝎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谅畅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片登渣。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖毡泻,靈堂內(nèi)的尸體忽然破棺而出胜茧,到底是詐尸還是另有隱情,我是刑警寧澤仇味,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布呻顽,位于F島的核電站,受9級(jí)特大地震影響丹墨,放射性物質(zhì)發(fā)生泄漏廊遍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一贩挣、第九天 我趴在偏房一處隱蔽的房頂上張望昧碉。 院中可真熱鬧,春花似錦揽惹、人聲如沸被饿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)狭握。三九已至,卻和暖如春疯溺,著一層夾襖步出監(jiān)牢的瞬間论颅,已是汗流浹背哎垦。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恃疯,地道東北人漏设。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像今妄,于是被迫代替她去往敵國(guó)和親郑口。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349