【C/C++】C 语言实现 WebSocket:握手、帧解析、掩码和回显

作者:SilentSlot日期:2026/6/27

【C/C++】C 语言实现 WebSocket:握手、帧解析、掩码和回显

1. WebSocket 为什么要先握手

WebSocket 不是一开始就直接发送二进制帧,它先通过 HTTP 发起升级请求。浏览器会发送类似这样的请求头:

1GET / HTTP/1.1
2Host: 127.0.0.1:8080
3Upgrade: websocket
4Connection: Upgrade
5Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
6Sec-WebSocket-Version: 13
7

服务端要读取 Sec-WebSocket-Key,按 RFC 6455 的规则生成 Sec-WebSocket-Accept,再返回 101 Switching Protocols。之后这条 TCP 连接才进入 WebSocket 帧通信阶段。

项目中的 WebSocket 状态用 conn->state 表示:

  • state == 0:还没有完成握手。
  • state == 1:握手完成,等待 WebSocket 数据帧。
  • state == 2:已经解析出一帧 payload,准备回包。

2. 握手:Key + GUID + SHA1 + Base64

WebSocket 协议规定服务端要把客户端的 Sec-WebSocket-Key 拼上固定 GUID:

1#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
2

然后做 SHA1,再 Base64:

1char combined[512];
2snprintf(combined, sizeof(combined), "%s%s", client_key, GUID);
3unsigned char hash[SHA_DIGEST_LENGTH];
4SHA1((unsigned char *)combined, strlen(combined), hash);
5
6char accept_key[256];
7base64_encode(hash, SHA_DIGEST_LENGTH, accept_key);
8

最后拼出 HTTP 101 响应:

1int response_length = snprintf(conn->wbuffer, sizeof(conn->wbuffer),
2                               "HTTP/1.1 101 Switching Protocols\r\n"
3                               "Upgrade: websocket\r\n"
4                               "Connection: Upgrade\r\n"
5                               "Sec-WebSocket-Accept: %s\r\n\r\n",
6                               accept_key);
7
8conn->wlength = response_length;
9

这段响应通过 Reactor 的 send_cb() 写回浏览器。浏览器收到合法的 101 响应后,ws.onopen 才会触发。

3. 从 HTTP 文本切换到 WebSocket 帧

websocket_request() 根据状态分两种处理:

1int websocket_request(struct connection *conn) {
2    if (conn->state == 0) {
3        if (handshake(conn) < 0) {
4            return -1;
5        }
6        conn->state = 1;
7    } else if (conn->state == 1) {
8        ws_frame_t frame;
9        ws_parse_result_t result = ws_parse_frame((const uint8_t *)conn->rbuffer,
10                                                  conn->rlength,
11                                                  &frame);
12        if (result == WS_PARSE_OK) {
13            conn->state = 2;
14            for (uint64_t index = 0; index < frame.payload_len; ++index) {
15                conn->payload[index] = frame.payload[index] ^ frame.masking_key[index % 4];
16            }
17            conn->payload_length = (int)frame.payload_len;
18        }
19    }
20    return 0;
21}
22

握手之前,收到的是 HTTP 文本;握手之后,收到的是 WebSocket 二进制帧。代码里最重要的转折点就是 conn->state = 1

4. WebSocket 帧头结构

项目中定义了 ws_frame_t 保存解析结果:

1typedef struct
2{
3    uint8_t fin;
4    uint8_t rsv1;
5    uint8_t rsv2;
6    uint8_t rsv3;
7    uint8_t opcode;
8
9    uint8_t masked;
10    uint64_t payload_len;
11    uint8_t masking_key[4];
12
13    size_t header_len;
14    const uint8_t *payload;
15} ws_frame_t;
16

WebSocket 帧的前两个字节非常关键:

  • 第 1 字节:FINRSV1/2/3opcode
  • 第 2 字节:MASK 和 payload length code。
  • 如果 length code 是 126,后面还有 2 字节长度。
  • 如果 length code 是 127,后面还有 8 字节长度。
  • 浏览器发给服务端的数据必须带 masking key。

项目里的解析代码先读前两个字节:

1uint8_t b0 = buf[0];
2uint8_t b1 = buf[1];
3
4frame->fin = (b0 >> 7) & 0x01;
5frame->rsv1 = (b0 >> 6) & 0x01;
6frame->rsv2 = (b0 >> 5) & 0x01;
7frame->rsv3 = (b0 >> 4) & 0x01;
8frame->opcode = b0 & 0x0F;
9
10frame->masked = (b1 >> 7) & 0x01;
11uint8_t len_code = b1 & 0x7F;
12

5. 长度解析和协议校验

长度字段有三种情况:

1if (len_code <= 125) {
2    frame->payload_len = len_code;
3} else if (len_code == 126) {
4    if (len < pos + 2) {
5        return WS_PARSE_NEED_MORE;
6    }
7    frame->payload_len = read_be16(buf + pos);
8    pos += 2;
9} else {
10    if (len < pos + 8) {
11        return WS_PARSE_NEED_MORE;
12    }
13    frame->payload_len = read_be64(buf + pos);
14    pos += 8;
15}
16

项目也做了基础协议校验:

1if (frame->rsv1 || frame->rsv2 || frame->rsv3) {
2    return WS_PARSE_PROTOCOL_ERROR;
3}
4
5if (!is_valid_opcode(frame->opcode)) {
6    return WS_PARSE_PROTOCOL_ERROR;
7}
8
9if (!frame->masked) {
10    return WS_PARSE_PROTOCOL_ERROR;
11}
12

这里的 !frame->masked 判断非常关键。浏览器作为客户端发给服务端的 WebSocket 帧必须 mask;如果没有 mask,服务端应该认为协议错误。

6. 解除 mask 得到真实 payload

客户端 payload 并不是明文直接放在帧里,而是用 4 字节 masking key 做异或。项目里的还原逻辑很简洁:

1for (uint64_t index = 0; index < frame.payload_len; ++index) {
2    conn->payload[index] = frame.payload[index] ^ frame.masking_key[index % 4];
3}
4conn->payload_length = (int)frame.payload_len;
5

如果浏览器发送文本 hello,服务端最终在 conn->payload 里拿到的才是真正的 hello

7. 服务端打包响应帧

服务端回给浏览器的帧通常不需要 mask。项目里的 ws_pack_frame() 默认构造 FIN=1 的完整帧:

1out[pos++] = 0x80 | (opcode & 0x0f);
2
3if (payload_len <= 125) {
4    out[pos++] = 0x00 | (uint8_t)payload_len;
5} else if (payload_len <= 0xffff) {
6    out[pos++] = 0x00 | 126;
7    write_be16(out + pos, (uint16_t)payload_len);
8    pos += 2;
9} else {
10    out[pos++] = 0x00 | 127;
11    write_be64(out + pos, payload_len);
12    pos += 8;
13}
14
15memcpy(out + pos, payload, (size_t)payload_len);
16

然后在 websocket_response() 中把刚才解析出来的 payload 打包成文本帧返回:

1ws_pack_result_t result = ws_pack_frame((uint8_t *)out_buf,
2                                        sizeof(out_buf),
3                                        &out_len,
4                                        WS_OPCODE_TEXT,
5                                        conn->payload,
6                                        conn->payload_length);
7if (result == WS_PACK_OK) {
8    memcpy(conn->wbuffer, out_buf, out_len);
9    conn->wlength = out_len;
10}
11conn->state = 1;
12

这就形成了一个 WebSocket Echo Server:浏览器发什么文本,服务端解析后再封装成 WebSocket 文本帧回给浏览器。

8. 前端测试页面

项目里的 websocket.html 是一个非常简单的浏览器客户端:

1<script>
2    let ws;
3
4    function doConnect(addr) {
5        ws = new WebSocket("ws://" + addr);
6        ws.onopen = () => {
7            document.getElementById("log").value += (" Connection opened\n");
8        };
9        ws.onmessage = (event) => {
10            document.getElementById("log").value += (" Receive: " + event.data + "\n\n");
11        };
12    }
13</script>
14

启动服务端:

1gcc reactor.c websocket.c -o websocket -lssl -lcrypto
2./websocket
3

浏览器打开 websocket.html,把地址改成:

1127.0.0.1:8080
2

点击连接后发送文本,页面日志里应该能看到服务端回显。

9. 小结

WebSocket 的核心流程可以概括为:

  1. HTTP Upgrade 请求进入服务端。
  2. 服务端根据 Sec-WebSocket-Key 生成 Sec-WebSocket-Accept
  3. 服务端返回 101,连接升级完成。
  4. 后续数据不再是 HTTP 文本,而是 WebSocket 帧。
  5. 客户端到服务端的帧必须 mask,服务端要先解析再异或还原 payload。
  6. 服务端响应时重新打包帧,写回 TCP 连接。

项目把 WebSocket 接在 Reactor 上,代码层次比较清楚:reactor.c 管 fd 和事件,websocket.c 管协议状态和帧格式。

学习链接: https://github.com/0voice


【C/C++】C 语言实现 WebSocket:握手、帧解析、掩码和回显》 是转载文章,点击查看原文


相关推荐


Makefile自动化编译实战项目
唐 城2026/6/18

有人说:一个人从1岁活到80岁很平凡,但如果从80岁倒着活,那么一半以上的人都可能不凡。 生活没有捷径,我们踩过的坑都成为了生活的经验,这些经验越早知道,你要走的弯路就会越少。  这是一份 Makefile 自动化编译实战项目资源包。这份指南从核心语法到企业级多目录架构,再到自动化依赖生成,带你彻底掌握 C/C++ 项目的构建自动化,告别手动敲 gcc 的低效时代。 📦 一、 项目目录结构规划 一个标准的工程化项目应具备清晰的目录划分,这是编写高级 Makefile 的基


https连接传输流程
Aphasia2026/6/10

引言:为什么需要 HTTPS? 在传统的 HTTP 协议中,数据是以明文形式在网络中传输的,这带来了三大安全风险:窃听(隐私泄露) 、篡改(数据被劫持修改)和冒充(钓鱼网站) 。 为了解决这些问题,HTTPS 应运而生。HTTPS 的本质是在 HTTP 与 TCP 之间引入了一个安全层——TLS/SSL 协议。它通过混合加密体系,完美兼顾了安全与效率: 非对称加密:在握手阶段使用,用于验证服务器身份并安全地协商出“会话密钥”。 对称加密:在握手完成后使用,双方用协商出的“会话密钥”进行高性能的


Flutter 屏幕旋转适配
Bowen_Jin2026/6/3

mindmap root((Flutter 屏幕旋转适配)) 原理 旋转手机 = 窗口尺寸变了 Flutter 检测到 → 自动重新布局 怎么监听 OrientationBuilder 根据横竖屏切布局 MediaQuery.sizeOf 根据宽度切布局 更推荐 怎么锁定屏幕 SystemChrome.setPreferredOrientations 锁定竖屏 portraitUp 锁


HTML应用指南:利用GET请求获取智己汽车门店位置信息
图说交通2026/5/26

智己汽车作为高端智能电动汽车品牌,深度融合先锋设计美学、纯电驱动技术、高阶智能驾驶与全场景出行服务,依托L7、LS7、LS6、L6等产品矩阵,打造兼具科技感与驾控乐趣的高端出行体验。在营销推广层面,智己摒弃传统4S店模式,创新采用“体验中心+用户中心”的新零售策略,系统构建以用户旅程为核心的全域触点网络。 目前,品牌已在北京、上海、广州、深圳、杭州、成都、武汉、西安、南京、苏州、重庆等一线及新一线城市核心商圈布局直营体验中心与交付中心,并战略性入驻上海BFC外滩金融中心、北京侨福芳草地、深圳万


Scrapy 分布式爬虫:大规模采集汽车之家电车评论
小白学大数据2026/5/5

汽车之家电车评论包含车型体验、续航表现等关键信息,是产品分析与市场调研的核心数据源。单台机器运行Scrapy爬虫易触发反爬、效率低下,分布式爬虫通过多机器协同,可有效解决这一问题。本文将精简讲解Scrapy分布式爬虫的搭建、配置、开发及部署,附带完整可运行代码,助力开发者快速实现大规模评论采集。 一、核心技术栈与环境准备 搭建Scrapy分布式爬虫需多组件协同,核心配置如下: 1.1 核心技术选型 Scrapy:核心爬虫框架,负责请求、解析与调度,支持中间件扩展。Scrapy-Redis


散户如何使用手机T0算法?
韭菜修养2026/4/25

1、什么是T0算法?T0算法如何运作? 据记者了解,当前多家券商已在其APP中推出T0算法服务,旨在通过智能化交易工具帮助投资者捕捉日内波动收益,执行“低买高卖”策略,帮助投资者降低持仓成本或增厚收益。 T0算法,全称“日内交易算法”,是一种基于量化模型的自动化交易工具。其核心逻辑是通过实时分析市场数据(如价格、成交量、盘口信息等),在极短时间内捕捉股票日内波动产生的价差,执行“低买高卖”操作,从而帮助投资者降低持仓成本或增厚收益。   具体来说,算法会设定一系列的规则和指标,当市场价


深度解密 Rollup 插件开发:核心钩子函数全生命周期图鉴
发现一只大呆瓜2026/4/17

前言 Rollup 的强大在于其精简的插件系统。一个 Rollup 插件本质上就是一个包含各种“钩子函数”的对象。理解这些钩子的执行时序,是编写高性能插件、优化构建流程的关键。本文将带你深度复盘 Rollup 的两大核心阶段:构建 (Build) 与 输出 (Output) 。 一、构建阶段钩子函数(核心阶段) 构建阶段主要负责模块的解析、加载和转换,最终完成模块依赖图的构建,是Rollup打包的基础。该阶段可细分为5个小阶段,钩子执行顺序固定为: 初始化阶段(options、buildSta


记一次 OKE 集群上的 TCP 流量黑洞排查与解决全过程
小猿姐2026/4/9

作为 Apecloud 团队,我们致力于通过开源的 KubeBlocks 项目,在 Kubernetes (K8s) 上为用户提供企业级的数据库高可用方案[1]。其中,SQL Server on K8s with Always On 是我们支持的关键能力之一,相比Microsoft 为在容器中运行 SQL Server 提供的基础StatefulSet方案, KubeBlocks 的 MSSQL Addon 提供了一整套生产级的生命周期管理能力,包括:多节点高可用配置、动态扩缩容、数据库/账户管


Spring Boot 牵手Spring AI,玩转DeepSeek大模型
小码哥_常2026/4/1

Spring Boot 牵手Spring AI,玩转DeepSeek大模型 引言:开启 AI 集成之旅 在当今这个 AI 技术迅猛发展的时代,你是否想过如何将强大的大模型融入到我们日常的 Java 开发中,为应用赋予智能交互的能力呢?今天,我们就来探索如何通过 Spring Boot 集成 Spring AI,进而调用 DeepSeek 大模型,为你的项目注入全新的 AI 活力。这不仅能让你深入了解 AI 与后端开发融合的前沿技术,还能为你在实际项目中运用 AI 技术提供宝贵的经验 。接下来,就


OpenClaw实战|从识图到公众号内容自动化,我跑通了完整链路
后端小肥肠2026/3/23

大家好,我是小肥肠。今天这篇文章,想跟大家分享一下我最近刚跑通两个skill:让 OpenClaw图像理解和 飞书 文章一键转存至公众号草稿箱。 1. 前言 最近我在慢慢做一件事:把之前在 Coze 里折腾过的插件和工作流,陆续迁到 OpenClaw + Skill 这一套上。上周末我主要搞定了两件事,都已经不是“纸上谈兵”,是真的跑起来了。 第一件:把图像理解插件迁移成 OpenClaw Skill 我把之前做的图像理解Coze插件,迁成了一个独立的 xfc-img-understand sk

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读