1.websocket协议诞生于HTTP协议之后。在websocket协议没出现之前,当时人们发现创建需要客户端和服务器之间双向通信的web应用程序(例如,即时消息和游戏应用程序)需要滥用HTTP来轮询服务器更新,这将导致以下几个问题:
2.同时HTTP协议的问题也体现在数据刷新方式上,以前实现方式是以下三种:
面对以上问题,websocket也因此而出现。
协议有两个部分:handshake(握手)和 data transfer(数据传输)。
客户端握手报文是在HTTP的基础上发送一次HTTP协议升级请求。
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
Sec-WebSocket-Key 是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。
Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 协议太多,不同厂商都有自己的协议版本,不过现在已经定下来了。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
服务端响应握手也是在HTTP协议基础上回应一个Switching Protocols。
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
Linux下对应实现代码,注释在代码中
int websocket_handshake(struct qsevent *ev) { char linebuf[128]; int index = 0; char sec_data[128] = {0}; char sec_accept[32] = {0}; do { memset(linebuf, 0, sizeof(linebuf));//清空以暂存一行报文 index = readline(ev->buffer, index, linebuf);//获取一行报文 if(strstr(linebuf, "Sec-WebSocket-Key"))//如果一行报文里面包括了Sec-WebSocket-Key { strcat(linebuf, GUID);//和GUID连接起来 SHA1(linebuf+WEBSOCK_KEY_LENGTH, strlen(linebuf+WEBSOCK_KEY_LENGTH), sec_data);//SHA1 base64_encode(sec_data, strlen(sec_data), sec_accept);//base64编码 memset(ev->buffer, 0, MAX_BUFLEN);//清空服务端数据缓冲区 ev->length = sprintf(ev->buffer,//组装握手响应报文到数据缓冲区,下一步有进行下发 "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-websocket-Accept: %s\r\n\r\n", sec_accept); break; } }while(index != -1 && (ev->buffer[index] != '\r') || (ev->buffer[index] != '\n'));//遇到空行之前 return 0; }
先看数据包格式
%x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片; %x1:表示这是一个文本帧(frame); %x2:表示这是一个二进制帧(frame); %x3-7:保留的操作代码,用于后续定义的非控制帧; %x8:表示连接断开; %x9:表示这是一个 ping 操作; %xA:表示这是一个 pong 操作; %xB-F:保留的操作代码,用于后续定义的控制帧。
表示数据载荷的长度 x 为 0~126:数据的长度为 x 字节; x 为 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度; x 为 127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。
当 Mask 为 1,则携带了 4 字节的 Masking-key; 当 Mask 为 0,则没有 Masking-key。 PS:掩码的作用并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" enum { WS_HANDSHAKE = 0, //握手 WS_TANSMISSION = 1, //通信 WS_END = 2, //end }; typedef struct _ws_ophdr{ unsigned char opcode:4, rsv3:1, rsv2:1, rsv1:1, fin:1; unsigned char pl_len:7, mask:1; }ws_ophdr;//协议前两个字节 typedef struct _ws_head_126{ unsigned short payload_lenght; char mask_key[4]; }ws_head_126;//协议mask和消息体长度 /*解码*/ void websocket_umask(char *payload, int length, char *mask_key) { int i = 0; for( ; i<length; i++) payload[i] ^= mask_key[i%4];//异或 } int websocket_transmission(struct qsevent *ev) { ws_ophdr *ophdr = (ws_ophdr*)ev->buffer;//协议前两个自己 printf("ws_recv_data length=%d\n", ophdr->pl_len); if(ophdr->pl_len <126)//如果消息体长度小于126 { char * payload = ev->buffer + sizeof(ws_ophdr) + 4;//获取消息地址 if(ophdr->mask)//如果消息是掩码 { websocket_umask(payload, ophdr->pl_len, ev->buffer+2);//解码,异或 printf("payload:%s\n", payload); } printf("payload : %s\n", payload);//消息回显 } else if (hdr->pl_len == 126) { ws_head_126 *hdr126 = ev->buffer + sizeof(ws_ophdr); } else { ws_head_127 *hdr127 = ev->buffer + sizeof(ws_ophdr); } return 0; } int websocket_request(struct qsevent *ev) { if(ev->status_machine == WS_HANDSHAKE) { websocket_handshake(ev);//握手 ev->status_machine = WS_TANSMISSION;//设置标志位 }else if(ev->status_machine == WS_TANSMISSION){ websocket_transmission(ev);//通信 } return 0; }
代码是基于reactor百万并发服务器框架实现的,代码在我的github上,更多关于websocket的内容可以查看websocket-rfc6455.
git clone https://github.com/qiushii/reactor.git