0 前言
0.1websocket由来
学习websocket首先需要关注websocket的由来以及一些使用场景。首先是因为http在通信的时候存在缺陷,只能由客户端发起请求,服务器响应,造成服务器没办法主动推送消息,客户端需要轮询带来效率上的问题。websocket诞生于2008年,2011年成为国际标准目前所有浏览器都支持websocket,用于建立服务器与客户端的对等通信,通信的发起方可以是客户端也可以是服务器,使得服务器可以主动推送相关信息。
0.2websocket特点
- 建立在TCP协议之上
- 对http具有很好的兼容性
- 支持数据文本和二进制等数据形式
- 协议标识符:ws;加密则为wss
1 websocket协议格式
1.1 协议头定义
标准协议的学习绕不开的就是RFC文档【RFC6455】的学习,首先对于websocket的协议格式进行解读,包括协议头和协议数据两部分。具体如下图所示:
**1 FIN:**结束标识符,当该位置1则会话结束;
**2 RSV1-3:**预留位必须为0;除非双方规定特殊含义;
**3 opcode:**数据类型定义;
* %x0:连续帧
* %x1:文本帧
* %x2:二进制帧
* %x3-7:为非控制帧保留
* %x8:连接关闭
* %x9:ping
* %xA:pong
* %xB-F:为制帧保留
WebSocket控制帧有3种:Close(关闭帧)、Ping以及Pong。控制帧的操作码定义了0x08(关闭帧)、0x09(Ping帧)、0x0A(Pong帧)。Close关闭帧很容易理解,客户端如果接受到了就关闭连接,客户端也可以发送关闭帧给服务端。Ping和Pong是websocket里的心跳,用来保证客户端是在线的,一般来说只有服务端给客户端发送Ping,然后客户端发送Pong来回应。
**4 MASK:**掩码位,当这一位置1表示当前的协议是加密传输,协议头中的masking-key需要设置。
5 Payload len: 如果如果为 0-125,则为有效载荷长度。如果为 126,则以下 2 个字节解释为16 位无符号整数是有效载荷长度。如果是 127,则以下 8 个字节被解释为 64 位无符号整数(最高有效位必须为 0)是有效载荷长度。
pyload_len = 7bit (0-125)
= 7+16bit(126)
= 7+64bit(127)
**6 Masking-key:**当mask为0时这个字段不存在,当mask为1时这个字段为4字节,所有由客户端发往服务端的 frame 都必须使用掩码覆盖, 即对于所有由客户端发往服务端的 frame, 该字段都必须存在, 该字段的值是由客户端使用熵值足够大的随机数发生器生成。
**7 payload data:**数据段,存放实际传输数据。
1.2 协议头数据结构
根据协议定义进行数据结构定义,将协议头分为两部分一部分是公共部分,解析前两个公共字段。第二部分是扩展长度部分,分为两种情况一种是payload_len长度为126情况;另一种是payload_len长度为127情况。
typedef struct WS_COM_HDR_T {
unsigned char opcode:4,
resv3:1,
resv2:1,
resv1:1,
fin:1;
unsigned char payload_len:7,
mask:1;
}WS_COM_HDR;
typedef struct WS_HDR_EXT126_T {
unsigned short extended_payload_len;
char mask_key[4];
}WS_HDR_EXT126 ;
typedef struct WS_HDR_EXT127_T {
long long extended_payload_len;
char mask_key[4];
}WS_HDR_EXT127 ;
2 websoket交互流程
websocket状态包括握手(Handshake)状态,数据传输(Data Transfer)状态和关闭(close)状态。
2.1 websocket握手
接收来自客户端的请求第一个请求连接的包基本格式如下,主要目的是为了兼容以http协议为基础的服务器或者是服务中继。Sec-WebSocket-Key,用于证明服务器收到的是一个websocket服务请求。经过与全局的唯一GUID拼接,经过SHA1哈希和base64加密最终服务器通过Sec-WebSocket-Accept字段返回给客户端验证websocket连接。
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
服务器的回应连接的基本格式如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Key字段生成和Sec-WebSocket-Accept字段生成的伪代码如下:
const char GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
char str_key[16] = random();
Sec_WebSocket_Key = base64(str_key);
Sec_WebSocket_Accept = Sec_WebSocket_Key + GUID;
Sec_WebSocket_Accept = SHA1(Sec_WebSocket_Accept);
Sec_WebSocket_Accept = base64(Sec_WebSocket_Accept);
2.2 数据传输流程
当握手建立完成后,websoket即可进行数据传输,数据传输方式分为两种形式一种是明文传输,一种是密文传输一般情况下采用密文传输,主要是设置是在mask置为1。解析websocket数据包首先需要解析公共头部,获取数据大小根据大小进行数据分类,数据分类包括0-125,126和127三种情况。之后是数据解密部分解密完成基本接收流程解析工作已经完成,下一步是数据处理和数据返回给客户端。
websocket解密过程整体比较简单,主要包括两部分,第一部分是取出Masking-key进行循环对4取余操作;第二步是将取余数据与接收数据进行异或操作得到解码后的真实数据
poyload[i] = payload[i] ^ masking_key[i % 4];
2.3 websocket断开
关闭握手旨在补充 TCP 关闭握手 (FIN/ACK),基于 TCP 关闭握手并不总是端到端可靠,尤其是在存在拦截代理和其他中介。websocket通过发送关闭帧并等待关闭帧作为响应,可以避免某些可能不必要地丢失数据的情况。opcode设置为0x8,可以带上状态码和关闭原因。
3 实战websocket服务器
本文主要实现websocket回显服务器,将接收数据回发给客户端。具体源码见:my_websocket_server.c
3.1 连接处理
连接处理分为几个步骤:
- 1 获取接收数据中的Sec-WebSocket-Keykey值
- 2 将key值拼接GUID字段
- 3 进行SHA1哈希加密操作;
- 4 经过base64加密;
- 5 最后按照固定格式回复给客户端
int websocket_handshake(event_item *event)
{
char line_buf[MAX_LINE_BUF] = {0};
int index = 0;
char sec_data[128] = {0};
char sec_accept[32] = {0};
do {
memset(line_buf, 0, sizeof(line_buf));
index = readline(event->buffer, index, line_buf);
printf("line:%s\n", line_buf);
if(strstr(line_buf, "Sec-WebSocket-Key"))
{
strcat(line_buf, GUID);
//Sec-WebSocket-Key:
//linebuf:
//Sec-WebSocket-Key: c1WhjNBGlWE8hIh5IFaoyQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
int skip_len = strlen("Sec-WebSocket-Key: ");
SHA1(line_buf+skip_len, strlen(line_buf+skip_len), sec_data);
base64_encode(sec_data, strlen(sec_data), sec_accept);
memset(event->buffer, 0, MAX_BUFFER_LENGTH);
event->length = sprintf(event->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);
printf("websocket_handshake response : %s\n", event->buffer);
break;
}
}while((line_buf[index] != '\r' || line_buf[index+1] != '\n') && (index != -1));
return 0;
}
3.2 数据解析
数据解析重点如下:
- 1 websocket头部解析,获取掩码值;
- 2 利用掩码masking_key 解析;
- 3 需要注意不同长度数据头有所区分。
int websocket_transfer(event_item *event)
{
WS_COM_HDR *hdr = (WS_COM_HDR*)event->buffer;
unsigned char *payload = NULL;
unsigned char *masking_key = NULL;
int head_len = 0;
int payload_len = 0;
printf("length: %d mask:%d\n", hdr->payload_len, hdr->mask);
if (hdr->payload_len < 126) { //
payload_len = hdr->payload_len;
if (hdr->mask) { // mask set 1
head_len = sizeof(WS_COM_HDR) + MASKING_KEY_LEN;
payload = event->buffer + head_len;
masking_key = event->buffer + head_len - MASKING_KEY_LEN;
umask(payload, hdr->payload_len, masking_key);
}
else
{
head_len = sizeof(WS_COM_HDR);
payload = event->buffer + head_len;
}
} else if (hdr->payload_len == 126) {
WS_HDR_EXT126 *hdr126 = (WS_HDR_EXT126*)(event->buffer + sizeof(WS_COM_HDR));
payload_len = ntoh16(hdr126->extended_payload_len);
if (hdr->mask) { // mask set 1
head_len = sizeof(WS_COM_HDR) + sizeof(WS_HDR_EXT126) + MASKING_KEY_LEN;
payload = event->buffer + head_len;
masking_key = event->buffer + head_len - MASKING_KEY_LEN;
umask(payload, ntoh16(hdr126->extended_payload_len), masking_key);
}
else
{
head_len = sizeof(WS_COM_HDR) + sizeof(WS_HDR_EXT126);
payload = event->buffer + head_len;
}
} else {
WS_HDR_EXT127 *hdr127 = (WS_HDR_EXT127*)(event->buffer + sizeof(WS_COM_HDR));
payload_len = ntoh64(hdr127->extended_payload_len);
if (hdr->mask) { // mask set 1
head_len = sizeof(WS_COM_HDR) + sizeof(WS_HDR_EXT127) + MASKING_KEY_LEN;
payload = event->buffer + head_len;
masking_key = event->buffer + head_len - MASKING_KEY_LEN;
umask(payload, ntoh64(hdr127->extended_payload_len), masking_key);
}
else
{
head_len = sizeof(WS_COM_HDR) + sizeof(WS_HDR_EXT127);
payload = event->buffer + head_len;
}
}
printf("payload : %s\n", payload);
int mk = ntoh32(*(int *)masking_key);
printf("masking_key : %08x\n", mk);
//build send buffer
int buffer_len = payload_len + sizeof(WS_COM_HDR) + sizeof(WS_HDR_EXT127) + 4;
char *buffer = (char *)malloc(buffer_len);
int send_len = 0;
if(!buffer)
printf("malloc err(%d).\n", errno);
memset(buffer, 0, buffer_len);
if(hdr->opcode == WS_OPCODE_TEXT_FRAME)
{
send_len = encode_packet(buffer, payload, payload_len);
}
else if(hdr->opcode == WS_OPCODE_CLOSE_FRAME)
{
send_len = wetsocket_close(buffer);
}
memset(event->buffer, 0, event->length);
memcpy(event->buffer, buffer, send_len);
event->length = send_len;
printf("send_len:%d\n", send_len);
free(buffer);
return 0;
}
3.3 回复数据封装
回复数据封装重点:
- 1 需要封装数据websocket头;
- 2 注意服务器回复mask需要设置为0;masking_key为空。
int encode_packet(char *buffer, char *stream, int length) {
printf("length:%d stream:%s\n", length, stream);
WS_COM_HDR head = {0};
head.fin = 1;
head.opcode = WS_OPCODE_TEXT_FRAME;
int size = 0;
if (length < 126) {
head.payload_len = length;
size = 2;
memcpy(buffer, &head, sizeof(WS_COM_HDR));
} else if (length < 0xffff) {
WS_HDR_EXT126 hdr = {0};
hdr.extended_payload_len = length;
size = sizeof(WS_COM_HDR) + sizeof(WS_HDR_EXT126);
memcpy(buffer, &head, sizeof(WS_COM_HDR));
memcpy(buffer+sizeof(WS_COM_HDR), &hdr, sizeof(WS_HDR_EXT126));
} else {
WS_HDR_EXT127 hdr = {0};
hdr.extended_payload_len = length;
size = sizeof(WS_COM_HDR)+sizeof(WS_HDR_EXT127);
memcpy(buffer, &head, sizeof(WS_COM_HDR));
memcpy(buffer+sizeof(WS_COM_HDR), &hdr, sizeof(WS_HDR_EXT127));
}
memcpy(buffer+size, stream, length);
return length + size;
}
3.4 关闭连接
关闭连接重点:
- 1 设置opcode为8;
- 2 加上状态码,可以更为清晰的了解错误原因。
int wetsocket_close(char *buffer) {
WS_COM_HDR head = {0};
head.fin = 1;
head.opcode = WS_OPCODE_CLOSE_FRAME;
int size = sizeof(WS_COM_HDR);
head.payload_len = WS_STATUS_CODES_LEN;
memcpy(buffer, &head, sizeof(WS_COM_HDR));
short nStatusCode = hton16(WS_STATUS_CODES_NORMAL);
memcpy(buffer+size, &nStatusCode, WS_STATUS_CODES_LEN);
size += WS_STATUS_CODES_LEN;
return size;
}