websocket背景
1.websocket协议诞生于HTTP协议之后。在websocket协议没出现之前,当时人们发现创建需要客户端和服务器之间双向通信的web应用程序(例如,即时消息和游戏应用程序)需要滥用HTTP来轮询服务器更新,这将导致以下几个问题:
- 服务器*为每个客户机使用许多不同的底层TCP连接:一个用于向客户机发送信息,一个用于每个传入消息。
- 协议的开销很大,因为每个客户机到服务器的消息都有一个HTTP报头。
- 客户端脚本*维护从传出连接到传入连接的映射,以跟踪响应。
2.同时HTTP协议的问题也体现在数据刷新方式上,以前实现方式是以下三种:
- 客户端定时查询:比如10s产生一次,但这样势必会产生大量无效的请求,如果服务器数据没有更新的话,会产生大量的带宽浪费。
- 长轮询机制:客户端依旧发送请求给服务端,当数据更新时候,服务端再下发数据给客户端。但实际上,服务器并不是没有数据更新才响应客户端,而是等待一个超时时间才结束此次长轮询请求。当遇到数据更新比较频繁的场景,长轮询就没有优势。
-
HTTP Streaming:客户端发送获取数据更新请求到服务器,服务器保持该请求的响应数据流一直打开,只有数据更新就实时发送给客户端。
设想很美好,但却带来新的问题:
1.违背了HTTP协议本身的语义,客户端和服务端不再是请求-响应的方式,而是二者之前直接建立的单向通信通道。
2.服务端只要的到数据更新就发送数据给客户端,所以需要协商数据更新的开始和结尾,数据容易出现错误。
3.客户端和服务端之前的网络中介可能会缓存响应数据,客户端就无法获得真正的更新数据。
面对以上问题,websocket也因此而出现。
websocket概念
- WebSocket协议允许在受控环境中运行不受信任代码的客户端与远程主机之间进行双向通信,而远程主机已经选择从该代码进行通信。为此使用的安全模型是web浏览器常用的基于起源的安全模型。该协议包括一个开放握手,然后是基本的消息帧,在TCP之上分层。该技术的目标是为基于浏览器的应用程序提供一种机制,这种应用程序需要与服务器进行双向通信,而不依赖于打开多个HTTP连接。
websocket特点
1. 优点
- 保持连接状态:websocket需要先创建连接,使其成为有状态的协议。
- 更好支持二进制:定义了二进制帧,增加安全性。
- 支持扩展:定义了扩展,可以自己实现部分部分自定义。
- 压缩效果好:可以沿用上下文的内容,有更好的压缩效果。
2. 缺点
- 开发要求高, 前端后端都增加了一定的难度。
- 推送消息相对复杂。
- HTTP协议已经很成熟,现今websocket则太新了一点。
websocket协议通信过程
协议有两个部分:handshake(握手)和 data transfer(数据传输)。
handshake
客户端
客户端握手报文是在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;
}
data transfer
先看数据包格式
- FIN:指示这是消息中的最后一个片段。第一个片段也可能是最后的片段。
- RSV1, RSV2, RSV3:一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接出错。
- opcode:操作代码。
%x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;
%x1:表示这是一个文本帧(frame);
%x2:表示这是一个二进制帧(frame);
%x3-7:保留的操作代码,用于后续定义的非控制帧;
%x8:表示连接断开;
%x9:表示这是一个 ping 操作;
%xA:表示这是一个 pong 操作;
%xB-F:保留的操作代码,用于后续定义的控制帧。
- mask:是否需要掩码。
- Payload length: 7bit or 7 + 16bit or 7 + 64bit
表示数据载荷的长度
x 为 0~126:数据的长度为 x 字节;
x 为 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度;
x 为 127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。
- Masking-key:0 or 4bytes
当 Mask 为 1,则携带了 4 字节的 Masking-key;
当 Mask 为 0,则没有 Masking-key。
PS:掩码的作用并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
-
payload data:消息体。
下面是服务端的代码实现
#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
git clone https://github.com/qiushii/reactor.git