Websocket的C++服务端和JS客户端通信Demo

目录

1 Websocket简介

1.1 什么是WebSocket

1.2 WebSocket的优势和劣势

1.2.1 WebSocket的优势

1.2.1 WebSocket的劣势

1.3 WebSocket的生命周期

2 WebSocket协议解析

2.1 连接握手

2.1.1 客户端握手连接格式

2.1.2 服务端收到客户端连接后回复格式

2.2 数据传输

2 Websocket的C++服务端实现

3 Websocket的Javascript客户端实现

3.1 WebSocket 构造函数

3.2 WebSocket.onopen 事件

3.3 WebSocket.send() 方法

3.4 WebSocket.onmessage 事件

3.5 WebSocket.onerror 事件

3.6 WebSocket.onclose 事件

4 演示Demo

4.1 开发环境

4.2 功能介绍

4.3 下载地址


1 Websocket简介

1.1 什么是WebSocket

        WebSocket是一种协议,用于在Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接,这使得Web应用程序可以更加实时地传递数据。WebSocket协议最初由W3C开发,并于2011年成为标准。

1.2 WebSocket的优势和劣势

1.2.1 WebSocket的优势

  • 实时性: 由于WebSocket的持久化连接,它可以实现实时的数据传输,避免了Web应用程序需要不断地发送请求以获取最新数据的情况。

  • 双向通信: WebSocket协议支持双向通信,这意味着服务器可以主动向客户端发送数据,而不需要客户端发送请求。

  • 减少网络负载: 由于WebSocket的持久化连接,它可以减少HTTP请求的数量,从而减少了网络负载。

1.2.1 WebSocket的劣势

  • 需要浏览器和服务器都支持: WebSocket是一种相对新的技术,需要浏览器和服务器都支持。一些旧的浏览器和服务器可能不支持WebSocket。

  • 需要额外的开销: WebSocket需要在服务器上维护长时间的连接,这需要额外的开销,包括内存和CPU。

  • 安全问题: 由于WebSocket允许服务器主动向客户端发送数据,可能会存在安全问题。服务器必须保证只向合法的客户端发送数据。

1.3 WebSocket的生命周期

        WebSocket 生命周期描述了 WebSocket 连接从创建到关闭的过程。一个 WebSocket 连接包含以下四个主要阶段:

  • 连接建立阶段(Connection Establishment): 在这个阶段,客户端和服务器之间的 WebSocket 连接被建立。客户端发送一个 WebSocket 握手请求,服务器响应一个握手响应,然后连接就被建立了。

  • 连接开放阶段(Connection Open): 在这个阶段,WebSocket 连接已经建立并开放,客户端和服务器可以在连接上互相发送数据。

  • 连接关闭阶段(Connection Closing): 在这个阶段,一个 WebSocket 连接即将被关闭。它可以被客户端或服务器发起,通过发送一个关闭帧来关闭连接。

  • 连接关闭完成阶段(Connection Closed): 在这个阶段,WebSocket 连接已经完全关闭。客户端和服务器之间的任何交互都将无效。

需要注意的是,WebSocket 连接在任何时候都可能关闭,例如网络故障、服务器崩溃等情况都可能导致连接关闭。因此,需要及时处理 WebSocket 连接关闭的事件,以确保应用程序的可靠性和稳定性。

        下面是一个简单的 WebSocket 生命周期示意图:

2 WebSocket协议解析

        WebSocket实现了浏览器与服务器之间的全双工通讯。本质是前端 与服务器端建立一条TCP长连接,服务端可以随时向前端推送数据,前端也可以随时向服务端发送数据,实现了两者间双向数据实时传输。

        WebSocket协议的官方文档是RFC6455文件,下面对协议的核心部分做一个讲解,每一部分会列出关键注意点。

        协议分为:连接握手和数据传输

2.1 连接握手

2.1.1 客户端握手连接格式

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

        客户端连接格式下面几个关键点需要注意:

  • 请求行: 请求方法必须是GET, HTTP版本至少是1.1

  • 请求必须含有Host

  • 如果请求来自浏览器客户端, 必须包含Origin

  • 请求必须含有Connection, 其值必须含有"Upgrade"记号

  • 请求必须含有Upgrade, 其值必须含有"websocket"关键字

  • 请求必须含有Sec-Websocket-Version, 其值必须是13

  • 请求必须含有Sec-Websocket-Key, 用于提供基本的防护, 比如无意的连接

2.1.2 服务端收到客户端连接后回复格式

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
  • 响应行: HTTP/1.1 101 Switching Protocols

  • 响应必须含有Upgrade, 其值为"weboscket"

  • 响应必须含有Connection, 其值为"Upgrade"

  • 响应必须含有Sec-Websocket-Accept, 根据请求首部的Sec-Websocket-key计算出来

        服务端回复中关键点在于Sec-Websocket-Accept值的计算,具体计算方式如下:

  1. 将客户端送来的Sec-Websocket-Key的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接

  2. 258EAFA5-E914-47DA-95CA-C5AB0DC85B11是一个magic key,是RFC6455 Page24页中定义的一个固定值,直接用即可。

  3. 通过SHA1计算出摘要, 并转成base64字符串

        至此,一来一回,客户端和服务器端已经完成WebSocket 握手,连接建立,下一步就是传输数据了。

2.2 数据传输

        RFC6455中定义了数据帧的格式,如下:

        数据帧的组成结构和其他协议类似,归纳起来:数据头+载荷

        WebSocket的数据头长度是可变的,有两个因素影响:

  1. 载荷长度的数值大小,Payload length: 占7或7+16或7+64bit,具体看下面详解。

  2. 是否有maks key,有的话头部多4个字节

        数据帧格式如下:

/*-------------------------------------------------------------------
 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: 占1bit

    • 0表示不是消息的最后一个分片

    • 1表示是消息的最后一个分片

  • RSV1, RSV2, RSV3: 各占1bit, 一般情况下全为0, 与Websocket拓展有关, 如果出现非零的值且没有采用WebSocket拓展, 连接出错

  • Opcode: 占4bit

    • %x0: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片

    • %x1: 表示这是一个文本帧

    • %x2: 表示这是一个二进制帧

    • %x3-7: 保留的操作代码, 用于后续定义的非控制帧

    • %x8: 表示连接断开

    • %x9: 表示这是一个心跳请求(ping)

    • %xA: 表示这是一个心跳响应(pong)

    • %xB-F: 保留的操作代码, 用于后续定义的非控制帧

  • Mask: 占1bit

    • 0表示不对数据载荷进行掩码异或操作

    • 1表示对数据载荷进行掩码异或操作

  • Payload length: 占7或7+16或7+64bit

    • 0~125: 数据长度等于该值

    • 126: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度

    • 127: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度

  • Masking-key: 占0或4bytes

    • 1: 携带了4字节的Masking-key

    • 0: 没有Masking-key

  • payload data: 载荷数据

        这里有几个关键点需要注意:

  1. Fin为0,表示一个完整的消息被分片成多个数据帧中传输的,需要一直等待接到Fin为1的数据帧之后,才算收到一个完整的消息。websocketfiles 中的recv_dataframe已经考虑到这一点,因此为此返回给上层的数据都是一个完整的消息包。

  2. 只有客户端给服务器端发送数据时才会有masking key,服务器端给客户端发送数据不需要masking key

  3. mask掩码计算可以查看文档,或者直接阅读 pack_dataframe代码。

2 Websocket的C++服务端实现

#define SHA_DIGEST_LENGTH 20
#define BUFFER_SIZE 1024
#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

typedef struct _frame_head {
    char fin;
    char opcode;
    char mask;
    unsigned long payload_length;
    char masking_key[4];
}frame_head;

int _readline(char* allbuf, int level, char* linebuf)
{
    int len = strlen(allbuf);
    for (;level<len;++level)
    {
        if(allbuf[level]=='\r' && allbuf[level+1]=='\n')
                {
            return level+2;
                }
        else
                {
            *(linebuf++) = allbuf[level];
                }
    }
    return -1;
}

int shakehands(int cli_fd)
{
    int level = 0;
    char buffer[BUFFER_SIZE] = {0};
    char linebuf[256] = {0};
    char sec_accept[32] = {0};
    unsigned char sha1_data[SHA_DIGEST_LENGTH]={0};
    char head[BUFFER_SIZE] = {0};

    if (recv(cli_fd, buffer, sizeof(buffer), 0)<=0)
        {
                WriteInfoLog("Handshake, accept data error.");
                return -1;
        }

        WriteInfoLog("buffer:%s", buffer);
    do {
        memset(linebuf, 0, sizeof(linebuf));
        level = _readline(buffer, level, linebuf);

        if (strstr(linebuf,"Sec-WebSocket-Key")!=NULL)
        {
                        WriteInfoLog("key1:%s", linebuf);
            strcat_s(linebuf, GUID);
                        WriteInfoLog("key2:%s", linebuf);
            sha1_hash((unsigned char*)&linebuf+19, strlen(linebuf+19),
                                (unsigned char*)sha1_data);
                        B64Encode(sha1_data, 20, (char*)sec_accept, 32);
                        WriteInfoLog("key3:%s", sec_accept);

            sprintf_s(head, BUFFER_SIZE, "HTTP/1.1 101 Switching Protocols\r\n" \
                                "Upgrade: websocket\r\n" \
                "Connection: Upgrade\r\n" \
                                "Sec-WebSocket-Accept: %s\r\n" \
                "\r\n", sec_accept);  

                        WriteInfoLog("rsp head:%s", head);
            if (send(cli_fd, head, strlen(head), 0) != strlen(head))
                        {
                                WriteInfoLog("Handshake,sending data error.");
                                return -2;
                        }
            break;
        }
    }while((buffer[level]!='\r' || buffer[level+1]!='\n') && level!=-1);
    return 0;
}

int recv_frame_head(int fd, frame_head* head)
{
    char one_char;
    /*read fin and op code*/
    if (recv(fd,&one_char,1, 0)<=0)
    {
                WriteInfoLog("Accept frame header,accept fin error.");
        return -1;
    }
    head->fin = (one_char & 0x80) == 0x80;
    head->opcode = one_char & 0x0F;
    if (recv(fd, &one_char, 1, 0)<=0)
    {
                WriteInfoLog("Accept frame header,accept mask error.");
        return -1;
    }
    head->mask = (one_char & 0x80) == 0X80;

    /*get payload length*/
    head->payload_length = one_char & 0x7F;
    if (head->payload_length == 126)
    {
        char extern_len[2];
        if (recv(fd, extern_len, 2, 0) != 2)
        {
                        WriteInfoLog("Accept frame header,accept extern_len error.");
            return -1;
        }
        head->payload_length = (extern_len[0]&0xFF) << 8 | (extern_len[1]&0xFF);
    }
        else if(head->payload_length == 127)
        {
                char extern_len[8];
                if (recv(fd, extern_len, 8, 0) != 8)
                {
                        WriteInfoLog("Accept frame header,accept extern_len error.");
                        return -1;
                }
                head->payload_length = (extern_len[4]&0xFF)<<24 | (extern_len[5]&0xFF)<<16
                        | (extern_len[6]&0xFF)<<8 | (extern_len[7]&0xFF);
        }

    /*read masking-key*/
    if (recv(fd, head->masking_key, 4, 0) != 4)
    {
                WriteInfoLog("Accept frame header,accept masking_key error.");
        return -1;
    }

    return 0;
}

void umask(char *data, int len, char *mask)
{
    int i;
    for (i=0; i<len; ++i)
        {
        *(data+i) ^= *(mask+(i%4));
        }
}

int send_frame_head(int fd, char opcode, int payload_length)
{
    char response_head[20];
    int head_length = 0;
    if(payload_length < 126)
    {
        response_head[0] = 0x80+opcode;
        response_head[1] = payload_length;
        head_length = 2;
    }
    else if(payload_length>=126 && payload_length<0xffff)
    {
        response_head[0] = 0x80+opcode;
        response_head[1] = 126;
        response_head[2] = (payload_length >> 8 & 0xFF);
        response_head[3] = (payload_length & 0xFF);
        head_length = 4;
    }
        else
        {
                response_head[0] = 0x80+opcode;
                response_head[1] = 127;
                response_head[2] = 0;
                response_head[3] = 0;
                response_head[4] = 0;
                response_head[5] = 0;
                response_head[6] = (payload_length >> 24 & 0xFF);
                response_head[7] = (payload_length >> 16 & 0xFF);
                response_head[8] = (payload_length >> 8 & 0xFF);
                response_head[9] = (payload_length & 0xFF);
                head_length = 10;
        }
        
    if(send(fd, response_head, head_length, 0)<=0)
    {
                WriteInfoLog("Send head error.");
        return -1;
    }
        
    return 0;
}

//栈上空间不宜过大(最大10M),会放不下,改成全局
char g_payload_data[IMAGE_X*IMAGE_Y * 2]      = { 0 };
char g_szJsonData[IMAGE_X*IMAGE_Y * 2 + 1024] = { 0 };

int  DealCommEvent(SOCKET hCommSock)
{
        if (shakehands(hCommSock) != 0)
        {
                return 0;
        }
    
        if(WaitEvent(hCommSock,500) <= 0) 
        {
                WriteInfoLog("Timeout waiting for driving fingerprint data.");
                return 0;
        }

        frame_head head;
        int rul = recv_frame_head(hCommSock, &head);
        if (rul < 0)
        {
                return 0;
        }
        
        int size = 0;
        do {
                int rul;
                rul = recv(hCommSock, g_payload_data+size, 1024, 0);
                if (rul <= 0)
                {
                        WriteInfoLog("Accept Client Data Error.");
                        return 0;
                }
                size += rul;
        }while(size<head.payload_length);
        
        umask(g_payload_data, size, head.masking_key);
        int nstlenl=strlen(g_payload_data);
        WriteInfoLog("Receive client data:%d[%s]", head.payload_length, g_payload_data);

        // 解析请求数据 & 生成响应应答数据
        Requst2Respond(g_payload_data, g_szJsonData);
        WriteInfoLog("Respond g_szJsonData=%s", g_szJsonData);

        //响应应答
        send_frame_head(hCommSock,1, strlen(g_szJsonData));
        int len=send(hCommSock, g_szJsonData, strlen(g_szJsonData), 0);
        // 接收客户端发起的关闭请求
        if(WaitEvent(hCommSock, 200) <= 0)
        {
                WriteInfoLog("Timeout waiting for client to initiate shutdown request.");
                return 0;
        }
        memset(g_payload_data, 0, 1024);
        recv(hCommSock, g_payload_data, 1024, 0);
        send_frame_head(hCommSock, 8, 0);
        return 0;
}

3 Websocket的Javascript客户端实现

        WebSocket API 是用于在 Web 应用程序中创建和管理 WebSocket 连接的接口集合。WebSocket API 由浏览器原生支持,无需使用额外的 JavaScript 库或框架,可以直接在 JavaScript 中使用。

下面是一些常用的 WebSocket API:

3.1 WebSocket 构造函数

        WebSocket 构造函数用于创建 WebSocket 对象。它接受一个 URL 作为参数,表示要连接的 WebSocket 服务器的地址。例如:

var ws = new WebSocket('ws://localhost:12345/demo');

3.2 WebSocket.onopen 事件

  WebSocket.onopen 事件在 WebSocket 连接成功建立时触发。例如:

ws.onopen = function() {
  console.log('WebSocket 连接已经建立。');
};

3.3 WebSocket.send() 方法

  WebSocket.send() 方法用于向服务器发送数据。它接受一个参数,表示要发送的数据。数据可以是字符串、Blob 对象或 ArrayBuffer 对象。例如:

ws.send('Hello, server!');

3.4 WebSocket.onmessage 事件

  WebSocket.onmessage 事件在接收到服务器发送的消息时触发。它的 event 对象包含一个 data 属性,表示接收到的数据。例如:

ws.onmessage = function(event) {
  console.log('收到服务器消息:', event.data);
};

3.5 WebSocket.onerror 事件

  WebSocket.onerror 事件在 WebSocket 连接出现错误时触发。例如:

ws.onerror = function(event) {
  console.error('WebSocket 连接出现错误:', event);
};

3.6 WebSocket.onclose 事件

  WebSocket.onclose 事件在 WebSocket 连接被关闭时触发。例如:

ws.onclose = function() {
  console.log('WebSocket 连接已经关闭。');
};

4 演示Demo

4.1 开发环境

  • Visual Studio 2015

  • Javascript

  • Windows 10 Pro x64

4.2 功能介绍

        演示Websocket的C++服务端和JS客户端通信。

4.3 下载地址

        开发环境:

  • Visual Studio 2015

  • Javascript

  • Visual Studio 2015

        下载地址:Websocket的C++服务端和JS客户端通信DemoV2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值