【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 Base64WebSocket 协议规定服务端要把客户端的Sec-WebSocket-Key拼上固定 GUID#defineGUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11然后做 SHA1再 Base64charcombined[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_lengthsnprintf(conn-wbuffer,sizeof(conn-wbuffer),HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n,accept_key);conn-wlengthresponse_length;这段响应通过 Reactor 的send_cb()写回浏览器。浏览器收到合法的 101 响应后ws.onopen才会触发。3. 从 HTTP 文本切换到 WebSocket 帧websocket_request()根据状态分两种处理intwebsocket_request(structconnection*conn){if(conn-state0){if(handshake(conn)0){return-1;}conn-state1;}elseif(conn-state1){ws_frame_tframe;ws_parse_result_tresultws_parse_frame((constuint8_t*)conn-rbuffer,conn-rlength,frame);if(resultWS_PARSE_OK){conn-state2;for(uint64_tindex0;indexframe.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 字节FIN、RSV1/2/3、opcode。第 2 字节MASK和 payload length code。如果 length code 是 126后面还有 2 字节长度。如果 length code 是 127后面还有 8 字节长度。浏览器发给服务端的数据必须带 masking key。项目里的解析代码先读前两个字节uint8_tb0buf[0];uint8_tb1buf[1];frame-fin(b07)0x01;frame-rsv1(b06)0x01;frame-rsv2(b05)0x01;frame-rsv3(b04)0x01;frame-opcodeb00x0F;frame-masked(b17)0x01;uint8_tlen_codeb10x7F;5. 长度解析和协议校验长度字段有三种情况if(len_code125){frame-payload_lenlen_code;}elseif(len_code126){if(lenpos2){returnWS_PARSE_NEED_MORE;}frame-payload_lenread_be16(bufpos);pos2;}else{if(lenpos8){returnWS_PARSE_NEED_MORE;}frame-payload_lenread_be64(bufpos);pos8;}项目也做了基础协议校验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_tindex0;indexframe.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()默认构造 FIN1 的完整帧out[pos]0x80|(opcode0x0f);if(payload_len125){out[pos]0x00|(uint8_t)payload_len;}elseif(payload_len0xffff){out[pos]0x00|126;write_be16(outpos,(uint16_t)payload_len);pos2;}else{out[pos]0x00|127;write_be64(outpos,payload_len);pos8;}memcpy(outpos,payload,(size_t)payload_len);然后在websocket_response()中把刚才解析出来的 payload 打包成文本帧返回ws_pack_result_tresultws_pack_frame((uint8_t*)out_buf,sizeof(out_buf),out_len,WS_OPCODE_TEXT,conn-payload,conn-payload_length);if(resultWS_PACK_OK){memcpy(conn-wbuffer,out_buf,out_len);conn-wlengthout_len;}conn-state1;这就形成了一个 WebSocket Echo Server浏览器发什么文本服务端解析后再封装成 WebSocket 文本帧回给浏览器。8. 前端测试页面项目里的websocket.html是一个非常简单的浏览器客户端scriptletws;functiondoConnect(addr){wsnewWebSocket(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 的核心流程可以概括为HTTP Upgrade 请求进入服务端。服务端根据Sec-WebSocket-Key生成Sec-WebSocket-Accept。服务端返回 101连接升级完成。后续数据不再是 HTTP 文本而是 WebSocket 帧。客户端到服务端的帧必须 mask服务端要先解析再异或还原 payload。服务端响应时重新打包帧写回 TCP 连接。项目把 WebSocket 接在 Reactor 上代码层次比较清楚reactor.c管 fd 和事件websocket.c管协议状态和帧格式。学习链接: https://github.com/0voice