【C/C++】C 语言实现 WebSocket:握手、帧解析、掩码和回显
2026/6/26 4:12:58 网站建设 项目流程

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

1. WebSocket 为什么要先握手

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

GET / HTTP/1.1 Host: 127.0.0.1:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13

服务端要读取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:

#defineGUID"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

然后做 SHA1,再 Base64:

charcombined[512];snprintf(combined,sizeof(combined),"%s%s",client_key,GUID);unsignedcharhash[SHA_DIGEST_LENGTH];SHA1((unsignedchar*)combined,strlen(combined),hash);characcept_key[256];base64_encode(hash,SHA_DIGEST_LENGTH,accept_key);

最后拼出 HTTP 101 响应:

intresponse_length=snprintf(conn->wbuffer,sizeof(conn->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",accept_key);conn->wlength=response_length;

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

3. 从 HTTP 文本切换到 WebSocket 帧

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

intwebsocket_request(structconnection*conn){if(conn->state==0){if(handshake(conn)<0){return-1;}conn->state=1;}elseif(conn->state==1){ws_frame_tframe;ws_parse_result_tresult=ws_parse_frame((constuint8_t*)conn->rbuffer,conn->rlength,&frame);if(result==WS_PARSE_OK){conn->state=2;for(uint64_tindex=0;index<frame.payload_len;++index){conn->payload[index]=frame.payload[index]^frame.masking_key[index%4];}conn->payload_length=(int)frame.payload_len;}}return0;}

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

4. WebSocket 帧头结构

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

typedefstruct{uint8_tfin;uint8_trsv1;uint8_trsv2;uint8_trsv3;uint8_topcode;uint8_tmasked;uint64_tpayload_len;uint8_tmasking_key[4];size_theader_len;constuint8_t*payload;}ws_frame_t;

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

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

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

uint8_tb0=buf[0];uint8_tb1=buf[1];frame->fin=(b0>>7)&0x01;frame->rsv1=(b0>>6)&0x01;frame->rsv2=(b0>>5)&0x01;frame->rsv3=(b0>>4)&0x01;frame->opcode=b0&0x0F;frame->masked=(b1>>7)&0x01;uint8_tlen_code=b1&0x7F;

5. 长度解析和协议校验

长度字段有三种情况:

if(len_code<=125){frame->payload_len=len_code;}elseif(len_code==126){if(len<pos+2){returnWS_PARSE_NEED_MORE;}frame->payload_len=read_be16(buf+pos);pos+=2;}else{if(len<pos+8){returnWS_PARSE_NEED_MORE;}frame->payload_len=read_be64(buf+pos);pos+=8;}

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

if(frame->rsv1||frame->rsv2||frame->rsv3){returnWS_PARSE_PROTOCOL_ERROR;}if(!is_valid_opcode(frame->opcode)){returnWS_PARSE_PROTOCOL_ERROR;}if(!frame->masked){returnWS_PARSE_PROTOCOL_ERROR;}

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

6. 解除 mask 得到真实 payload

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

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

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

7. 服务端打包响应帧

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

out[pos++]=0x80|(opcode&0x0f);if(payload_len<=125){out[pos++]=0x00|(uint8_t)payload_len;}elseif(payload_len<=0xffff){out[pos++]=0x00|126;write_be16(out+pos,(uint16_t)payload_len);pos+=2;}else{out[pos++]=0x00|127;write_be64(out+pos,payload_len);pos+=8;}memcpy(out+pos,payload,(size_t)payload_len);

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

ws_pack_result_tresult=ws_pack_frame((uint8_t*)out_buf,sizeof(out_buf),&out_len,WS_OPCODE_TEXT,conn->payload,conn->payload_length);if(result==WS_PACK_OK){memcpy(conn->wbuffer,out_buf,out_len);conn->wlength=out_len;}conn->state=1;

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

8. 前端测试页面

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

<script>letws;functiondoConnect(addr){ws=newWebSocket("ws://"+addr);ws.onopen=()=>{document.getElementById("log").value+=(" Connection opened\n");};ws.onmessage=(event)=>{document.getElementById("log").value+=(" Receive: "+event.data+"\n\n");};}</script>

启动服务端:

gcc reactor.c websocket.c-owebsocket-lssl-lcrypto./websocket

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

127.0.0.1:8080

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询