云计算百科
云计算领域专业知识百科平台

linux网络编程4——WebSocket协议及服务器的简易实现

文章目录

    • 1. WebSocket服务器介绍
        • 1.1 WebSocket 协议的特点
        • 1.2 WebSocket 与 HTTP 的区别:
        • 1.3 WebSocket 的应用场景:
    • 2. WebSocket握手协议详解
    • 3. 可能出现的错误
    • 4. 握手协议编码实现
    • 5. websocket传输协议实现
        • 5.1 websocket帧格式
        • 5.2 解包客户端数据
        • 5.3 服务端发包
    • 学习参考

1. WebSocket服务器介绍

本文详细介绍了WebSocket协议的特点、与HTTP的区别以及应用场景;然后分析了WebSocket协议的主要内容;最后借助前面的底层reactor的代码实现了一个WebSocket协议的Web服务器。

完整项目代码参考:我的github项目

WebSocket 是一种在客户端(通常是浏览器)和服务器之间建立双向通信通道的协议,允许它们通过一个持久的 TCP 连接进行实时数据交换。与传统的 HTTP 请求-响应模型不同,WebSocket 提供了全双工(full-duplex)的通信,即客户端和服务器都可以在任何时间向对方发送消息,而无需等待响应。

1.1 WebSocket 协议的特点
  • 持久连接:WebSocket 建立连接后,它保持打开状态,客户端和服务器之间可以持续交换数据,直到连接被一方主动关闭。
  • 全双工通信:双向通信通道可以同时发送和接收数据。服务器可以在不依赖客户端请求的情况下推送数据。
  • 减少网络开销:WebSocket 通过升级一次 HTTP 请求来建立连接,之后的数据交换只通过轻量的 WebSocket 帧格式,而不像 HTTP 需要额外的请求头部信息,因而大大减少了网络开销。
  • 实时数据传输:适合实时应用,如在线聊天、股票行情、游戏、物联网数据传输等。
  • WebSocket协议是通过HTTP1.1协议的握手过程建立的,但连接建立后两者的通信机制完全不同。

    WebSocket握手头部:

    GET /chat HTTP/1.1
    Host: example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13

    WebSocket握手成功后升级连接:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    1.2 WebSocket 与 HTTP 的区别:
    • 双向通信 vs 单向请求响应:HTTP 是单向的请求-响应模型,客户端必须发起请求,服务器响应。而 WebSocket 是双向的,双方可以随时发送数据。
    • 持久连接:HTTP 需要每次发起新的连接请求(即使是 HTTP/1.1,也需要保持连接),而 WebSocket 在建立连接后,连接是持久的,直到主动关闭。
    • 协议头部大小:HTTP 请求和响应头部信息较多,而 WebSocket 帧的协议头部相对较少,减少了数据传输的开销。
    1.3 WebSocket 的应用场景:

    Http协议和WebSocket协议常常结合使用,例如HTTP用于初始的页面加载和静态资源获取,WebSocket用于需要长时间实时交互的场景。

  • 实时聊天:像 Slack、微信、Facebook Messenger 这样需要实时通信的应用。
  • 实时股票行情:股票交易平台、加密货币交易所等,需要不断推送最新的市场数据。
  • 多人在线游戏:游戏服务器需要与每个客户端频繁、实时交换数据。
  • 实时通知系统:例如社交网络中的通知,或电子商务中的订单更新。
  • IoT 设备管理:物联网应用可以使用 WebSocket 实时管理和监控设备的状态。
  • 2. WebSocket握手协议详解

    主要介绍握手协议,在握手阶段,客户端会在请求头中发送一个sec-websocket-key

    sec-websocket-key: e3bLzpFK7Li8RHh8DZL87A==

    服务器需要拿到这个key值,然后进行如下计算

    • 将key与一个GUID连结,该GUID值为258EAFA5-E914-47DA-95CA-C5AB0DC85B11,得到input
    • 使用SHA-1算法计算input得到input2
    • 使用base64算法计算input2得到ouput

    最后在响应头中发送sec-websocket-Accpet头即可

    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    之后,双方可以保持连接,进行实时的全双工的交互。

    3. 可能出现的错误

  • 编译时链接器显示找不到一些符号的定义

    记得链接ssl库和crypto库

    gcc -o xxx xxx1.c xxx2.c -lssl -lcrypto

  • 客户端发送”unknown opcode",并主动关闭连接

  • ​ 一定是服务端发送的数据不符合协议,或者Sec-WebSocket-Accept值有误。

    4. 握手协议编码实现

    这里只实现了建立握手协议这一环节,连接建立后发送的消息都应该遵守websocket协议的格式。完整的websocket回声服务器代码可参考完整项目代码参考:我的github项目。

    完整的websocket协议请参考rfc6455。

    #include <string.h>

    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/epoll.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <openssl/sha.h>
    #include <openssl/bio.h>
    #include <openssl/evp.h>
    #include <openssl/buffer.h>

    #include "webserver.h"
    #include "websocket.h"

    #define DEBUG

    #define WEBSOCKET_KEY_LENGTH 256

    // 负责按照rfc6455的规定输出Sec-WebSocket-Accept的值
    static int encode_key(unsigned char *key, size_t n, unsigned char *output)
    {
    unsigned char hash[SHA_DIGEST_LENGTH];
    SHA1(key, n, hash);

    BIO *bmem, *b64;
    BUF_MEM *bptr;

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

    BIO_write(b64, hash, SHA_DIGEST_LENGTH);
    BIO_flush(b64);
    BIO_get_mem_ptr(b64, &bptr);
    memcpy(output, bptr->data, bptr->length);
    // 这里切记是bptr->length字符数组的长度
    output[bptr->length 1] = 0;

    BIO_free_all(b64);

    return 0;
    }

    int handshake(struct Conn *conn)
    {
    // handshake
    unsigned char output[WEBSOCKET_KEY_LENGTH] = {0};
    unsigned char input[WEBSOCKET_KEY_LENGTH] = {0};
    char *key = strstr(conn->rbuffer, "Sec-WebSocket-Key:");
    if (!key)
    {
    conn->wlength = 0;
    return 1;
    }
    key += 19;
    int i = 0;
    while (*key != 0 && *key != ' ' && *key != '\\r')
    {
    input[i++] = *key++;
    }
    strcpy((char *)&input[i], "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
    encode_key(input, strlen((char *)input), output);

    struct stat filestat = {0};
    int sended = snprintf(conn->wbuffer, BUFFER_LENGTH,
    "HTTP/1.1 101 Switching Protocols\\r\\n"
    "Upgrade: websocket\\r\\n"
    "Connection: Upgrade\\r\\n"
    "Sec-WebSocket-Accept: %s\\r\\n\\r\\n", (char *)output);

    printf("%s|||\\n", output);

    conn->wlength = sended;
    return 0;
    }

    int ws_request(struct Conn *conn)
    {
    printf("<<<<<input<<<<<\\n %s\\n", conn->rbuffer);
    if (conn->status == 0)
    {
    handshake(conn);
    conn->status = 1;
    }
    else if (conn->status == 1)
    {
    int ret = 0;
    conn->payload = decode_packet((unsigned char *)conn->rbuffer, conn->mask, conn->rlength, &ret);
    printf("data: %s, length: %d\\n", conn->payload, ret);
    conn->wlength = ret;
    conn->status = 2;
    }
    return 0;
    }

    int ws_response(struct Conn *conn)
    {
    if (conn->status == 2)
    {
    conn->wlength = encode_packet(conn->wbuffer, conn->mask, conn->payload, conn->wlength);
    conn->status = 1;
    }

    conn->wbuffer[conn->wlength] = 0;
    printf(">>>>output>>>>\\n%s\\n", conn->wbuffer);
    return 0;
    }

    5. websocket传输协议实现

    5.1 websocket帧格式

    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 … |
    +—————————————————————+

    rfc6455 5.2介绍了各个域的作用,对应的如果要实现该协议就要定义相关的结构体

    struct _nty_ophdr {

    unsigned char opcode:4,
    rsv3:1,
    rsv2:1,
    rsv1:1,
    fin:1;
    unsigned char payload_length:7,
    mask:1;

    } __attribute__ ((packed));

    struct _nty_websocket_head_126 {
    unsigned short payload_length;
    char mask_key[4];
    unsigned char data[8];
    } __attribute__ ((packed));

    struct _nty_websocket_head_127 {

    unsigned long long payload_length;
    char mask_key[4];

    unsigned char data[8];

    } __attribute__ ((packed));

    typedef struct _nty_websocket_head_127 nty_websocket_head_127;
    typedef struct _nty_websocket_head_126 nty_websocket_head_126;
    typedef struct _nty_ophdr nty_ophdr;

    __attribute__ ((packed)) 是 GNU C 编译器(GCC)的一个扩展,用于告诉编译器不对结构体的成员进行内存对齐。通常,编译器为了提高访问效率,会按照特定的字节对齐规则来放置结构体成员。使用 packed 属性后,编译器不会对齐字段,而是按照定义的顺序紧凑地存储它们,节省内存空间。

    5.2 解包客户端数据

    rfc6455 5.3规定了客户端向服务端发送的数据必须经过掩码加密,其原理是用8位的mask-key对原数据一次逐字节进行异或操作,这样加密和解密的过程是完全一样的。

    在协议帧中masking-key有4字节,协议规定,对payload[i]对应的maskkey为masking-key[i mod 4],这样就可以写出其加解密算法了:

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

    这样对于服务端的解包操作,就是解密payload,拿到原数据。

    char* decode_packet(unsigned char *stream, char *mask, int length, int *ret) {

    nty_ophdr *hdr = (nty_ophdr*)stream;
    unsigned char *data = stream + sizeof(nty_ophdr);
    int size = 0;
    int start = 0;
    //char mask[4] = {0};
    int i = 0;

    if ((hdr->payload_length & 0x7F) == 126) {

    nty_websocket_head_126 *hdr126 = (nty_websocket_head_126*)data;
    size = hdr126->payload_length;

    for (i = 0;i < 4;i ++) {
    mask[i] = hdr126->mask_key[i];
    }

    start = 8;

    } else if ((hdr->payload_length & 0x7F) == 127) {

    nty_websocket_head_127 *hdr127 = (nty_websocket_head_127*)data;
    size = hdr127->payload_length;

    for (i = 0;i < 4;i ++) {
    mask[i] = hdr127->mask_key[i];
    }

    start = 14;

    } else {
    size = hdr->payload_length;

    memcpy(mask, data, 4);
    start = 6;
    }

    *ret = size;
    demask(stream+start, size, mask);

    return stream + start;
    }

    5.3 服务端发包

    对于服务端的发包操作,只需要填充相应的协议字段即可,不需要掩码加密。

    int encode_packet(char *buffer,char *mask, char *stream, int length) {

    nty_ophdr head = {0};
    head.fin = 1;
    head.opcode = 1;
    int size = 0;

    if (length < 126) {
    head.payload_length = length;
    memcpy(buffer, &head, sizeof(nty_ophdr));
    size = 2;
    } else if (length < 0xffff) {
    nty_websocket_head_126 hdr = {0};
    hdr.payload_length = length;
    memcpy(hdr.mask_key, mask, 4);

    memcpy(buffer, &head, sizeof(nty_ophdr));
    memcpy(buffer+sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_126));
    size = sizeof(nty_websocket_head_126);

    } else {

    nty_websocket_head_127 hdr = {0};
    hdr.payload_length = length;
    memcpy(hdr.mask_key, mask, 4);

    memcpy(buffer, &head, sizeof(nty_ophdr));
    memcpy(buffer+sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_127));

    size = sizeof(nty_websocket_head_127);

    }

    memcpy(buffer+2, stream, length);

    return length + 2;
    }

    学习参考

    学习更多相关知识请参考零声 github。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » linux网络编程4——WebSocket协议及服务器的简易实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!