基于事件驱动的websocket简单实现

websocket的实现

什么是websocket?

WebSocket 是一种网络通信协议,旨在为客户端和服务器之间提供全双工、实时的通信通道。它是在 HTML5 规范中引入的,可以让浏览器与服务器进行持久化连接,以便实现低延迟的数据交换。

WebSocket 的特点:

  1. 全双工通信:客户端和服务器可以同时发送和接收消息,而不必等待对方完成操作。
  2. 轻量级:相较于传统的 HTTP 协议,WebSocket 头部信息更小,这减少了网络开销。
  3. 持久连接:一旦建立连接,双方可以一直保持这个连接,直到主动关闭。这样避免了频繁建立和关闭连接带来的性能损耗。
  4. 实时性:适合需要即时数据更新的应用,如在线聊天、游戏、股票行情等
通信过程

websocket通信协议是基于http的,客户端首先发送连接请求request,在该request中包含了基本的HTTP头信息:

html 复制代码
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

以上这些信息以字符串的形式发送至服务端的rbuffer里,当服务端识别到这些字符串信息后,需要发送相应response进行确认后才能建立websocket连接。确认信息的response应该如下:

接收到客户端的key->

key与"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"进行拼接,得到新的key-->

使用SHA1算法加密-->

再使用base64加密-->

加上http头信息,以字符串形式的发送至客户端。当客户端收到后,websocket建立。

cpp 复制代码
int response_websock(struct conn *c){
        char* key_head = "Sec-WebSocket-Key";
        char* start = strstr(c->rbuffer, key_head);
        start += 19;

        char key[1024] = {0};
        int set = 0;
        while (*start != '='){
            key[set] = *start;
            start++;
            set++;
        }
        key[set] = '\0';
        char* result = strcat(key, GUID);

        unsigned char hash[SHA_DIGEST_LENGTH] = {0};
        SHA1((unsigned char*)result, strlen(result), hash);

        char* base = base64_encode(hash, SHA_DIGEST_LENGTH);

        //strcpy(c->wbuffer, base) ;
        snprintf(c->wbuffer, sizeof(c->wbuffer), "HTTP/1.1 101 Switching Protocols\r\n"
                                                 "Upgrade: websocket\r\n"
                                                 "Connection: Upgrade\r\n"
                                                 "Sec-WebSocket-Accept: %s\r\n\r\n", base);
        c->wlength = strlen(c->wbuffer);
        free(base);
}

建立websocket连接后,可以互发信息,但是信息是以websocket帧,字节流的形式传送的,所以需要进行编码和解码。

websocket帧结构如下:

发送信息(编码):

cpp 复制代码
int encoding(struct conn *c){
        //写入rbuffer
        int message_len = strlen(payload);
        int frame_len = 0;

        // 设置 FIN 位和 Opcode(文本帧)
        c->wbuffer[0] = 0x81; // FIN=1, Opcode=0x1(文本帧)

        // 设置 Payload Length
        if (message_len <= 125) {
            c->wbuffer[1] = message_len; // 不需要额外长度字段
            memcpy(&c->wbuffer[2], payload, message_len);
            frame_len = 2 + message_len;
        } 
        else if(message_len <= 65535){
            c->wbuffer[1] = 126; // 16 位扩展长度
            c->wbuffer[2] = (message_len >> 8) & 0xFF; // 高字节
            c->wbuffer[3] = message_len & 0xFF;        // 低字节
            memcpy(&c->wbuffer[4], payload, message_len);
            frame_len = 4 + message_len;  
        }
        else{
            c->wbuffer[1] = 127; // 64 位扩展长度
            // 这里假设消息长度小于 2^32,因此高 4 字节为 0
            memset(&c->wbuffer[2], 0, 4);
            c->wbuffer[6] = (message_len >> 24) & 0xFF;
            c->wbuffer[7] = (message_len >> 16) & 0xFF;
            c->wbuffer[8] = (message_len >> 8) & 0xFF;
            c->wbuffer[9] = message_len & 0xFF;
            memcpy(&c->wbuffer[10], payload, message_len);
            frame_len = 10 + message_len;
        }
}

接收信息(解码):

cpp 复制代码
int encoding(struct conn *c){
        int fin = (c->rbuffer[0] & 0X80) >> 7;
        int opcode = c->rbuffer[0] & 0x0F;              // 操作码
        int masked = (c->rbuffer[1] & 0x80) >> 7;       // 是否有掩码
        int payload_len = c->rbuffer[1] & 0x7F;

        unsigned char *mask = NULL;                 // 掩码键
        unsigned char *payload = NULL;              // 数据指针
        if (payload_len <= 125) {
            mask = &c->rbuffer[2];
            payload = &c->rbuffer[6];
        } else if (payload_len == 126) {
            payload_len = ntohs(*(uint16_t *)&c->rbuffer[2]);
            mask = &c->rbuffer[4];
            payload = &c->rbuffer[8];
        } else if (payload_len == 127) {
            payload_len = ntohl(*(uint64_t *)&c->rbuffer[2]);
            mask = &c->rbuffer[10];
            payload = &c->rbuffer[14];
        }

        for (int i = 0; i < payload_len; i++) {  //解析数据(去除掩码)
            payload[i] ^= mask[i % 4];
        }

        // 输出解码后的消息
        payload[payload_len] = '\0';
        printf("Message from client: %s\n", payload);
}
流程总结

由于在建立连接阶段和通信阶段发送的数据形式不同,所以需要在结构体中引入状态机,用于记录是哪种请求,根据不同的状态机,做出不同的response。

cpp 复制代码
int ws_request(struct conn *c){
    //判断建立请求连接还是数据帧
    if (strstr(c->rbuffer, "Sec-WebSocket-Key") != NULL) {
        printf("HTTP handshake request detected.\n");
        printf("request: %s", c->rbuffer);
        c->wlength = 0;
        c->status = 0;
    } 
    else {
        printf("WebSocket frame detected.\n");
        c->wlength = 0;
        c->status = 1;
    }
    return 0;
}

客户端发送request -> 服务端读取数据,判断是请求连接还是发送websocket帧 ->根据不同status做出相应反应

cpp 复制代码
int ws_response(struct conn *c){
    //返回建立连接
    if(c->status == 0){
        response_websock(xxx);
    }
    else if (c->status == 1){
        //解码
        encoding(xxx);
        
        //编码
        decoding(xxx);

    }
    return c->wlength;
}

整体流程如下:

conn_list数组相当于一个用户和内核的中介,用来存放内核建立的连接以及用于拷贝内核接收到的数据。在websocket时,还额外引入了status的状态。

这个图可以清晰的显示出reactor的优点,即将业务和网络io管理分开。websocket用来实现业务,reactor用来实现网络io的管理。

课程地址:www.github.com/0voice

相关推荐
碧海银沙音频科技研究院3 小时前
炬芯ATS3089X手表的HFP通话不自动路由到设备BT的问题解决
网络·人工智能·嵌入式硬件·算法
EasyDSS7 小时前
连锁扩张管控难?一套视频会议/直播/点播/集群对讲EasyDSS音视频云平台打通门店全流程数字化
网络·音视频·easydss
比兔代理9 小时前
游戏、直播、远程办公遇到动态IP怎么办?
网络
Lzg_na9 小时前
ARP 地址解析协议详解
网络·网络协议
北京盛世宏博10 小时前
通信异常深度排查:以太网温湿度设备 TCP 粘包、UDP 丢包根治方案
单片机·网络协议·tcp/ip·udp·以太网·传感器·温湿度
上海广测检测科技有限公司10 小时前
消费类智能联网设备安全分级国标征求意见要点解析:三级五维框架与跨境对标逻辑
网络·经验分享
QZ1665609515911 小时前
金融数据流转监测排行:从看见到拦截的距离
网络·安全·网络安全·金融
paopaokaka_luck12 小时前
滑雪场管理系统(AI智能助手流式对话,ECharts 运营数据分析、教练、雪具与场地一体化预约,钱包支付退款、订单核销与服务评价)
java·人工智能·spring boot·网络协议·数据分析·echarts
CCblandy13 小时前
HTTP 长连接 3 类故障实测:[WinError 10053]、99 个 TIME_WAIT、69 次多余握手,4 种对策怎么选
后端·python·网络协议·http·云原生·性能优化·架构
雪雪爱冲浪13 小时前
2026 年网络爬虫领域的Puppeteer vs. Playwright
爬虫·网络协议·测试工具·接口隔离原则