众所周知,为了应对人们日益强烈的RealTime 交互需求
,模拟 TCP socket 全双工 的特性,产生了 websocket 这一双向通信协议
,它借用 HTTP协议的101 switch protocal
协议转换,所以在一定程度跟HTTP协议传输有很大程度的相似,但是请放心,ws是一个独立的协议。本次我们从 websocket client 的角度实现协议功能
以及对 ws协议进行解析
。
环境说明:
- 完整代码: websocket_client.c
- RFC6455协议
- gcc version 4.4.7 x86_64-redhat-linux
- websocket服务端:Workerman 3.5.3 (
websocket://0.0.0.0:2346
)
websocket client 功能流程
首先我们先对 websocket 客户端的功能实现的流程进行一个将要的说明,后面结合协议进行解析的时候可能仅仅解释一些比较重要的功能模块(有网络编程功底的同学可以先将例子clone下来查看,简单例子,见谅!^-^)
- 配置地址以及服务端口
- 往服务端发送 握手信息的header
- 验证服务端返回的握手信息(验证成功,完成握手)
- 掩码加密 客户端实体数据信息 “aaaaa”
- 掩码解密 服务端返回的数据信息
- close 连接
协议概况
ws是一个基于 TCP 的独立的协议,和HTTP的关系就是握手请求可以作为一个升级请求(Upgrade)经由HTTP服务器进行解释。协议主体主要分为两部分 握手
,数据帧
。
握手
私以为握手部分就是为了兼容现有的 基于HTTP的服务端组件(web服务器软件) 或者 代理服务器软件类型的中间件等。这样一个端口既可以接收普通的web请求,又可以对 websocket 进行横向兼容。所以 websocket client 的握手是一个 HTTP升级版请求(HTTP Upgrade request),ws协议规定,握手中的header字段并没有顺序要求。
可以看一下 vpgame 的 websocket
Request header
GET /null HTTP/1.1
Connection: Upgrade
Host: 127.0.0.1:2346
Sec-WebSocket-Key: xh2LW9mb1ZklWVH2gCUlHw==
Sec-WebSocket-Version: 13
Upgrade: websocket
GET
请求标识符 Request-URI,用于识别 websocket 链接到不同的服务终结点。Connection
告诉服务端对协议进行升级,具体升级内容取决于 Upgrade部分Host
服务器地址以及service端口Sec-WebSocket-Key
为了保证握手一致性,由客户端生成随机字符串并base64编码,发送给服务端Sec-WebSocket-Version
ws协议版本,常用13Upgrade
升级至*协议
Response header
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Version: 13
Connection: Upgrade
Server: workerman/3.5.3
Sec-WebSocket-Accept: 47N20KYf6c6Qe2+UDp2BQfK0hkw=
状态码101
代表协议升级成功后的状态码Connection
和Upgrade
内容代表协议成功升级为wsSec-WebSocket-Version
和Server
内容代表 ws协议版本 以及 服务端服务软件信息Sec-WebSocket-Accept
重点在此,证明服务端接受了客户端的请求,然后客户端对accept值进行验证,任何 为空或者不符合验证规则的都视为服务器拒绝了请求。服务端生成 Sec-WebSocket-Accept 规则
:服务端将客户端传递的key进行去除首尾空白,然后和一段固定的GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)进行连接,连接后的结果使用 SHA-1(160数位)进行哈希操作,对哈希后的字符串进行base64编码,即为 Accept 内容。客户端验证规则
仿上述规则对 Sec-WebSocket-Key 进行加密,跟服务端返回 Sec-WebSocket-Accept 值进行对比即可。
上述只是基础的 header 字段,当然还会根据具体需求添加一些 cookie 或者 其他衍生字段。
握手过程的实现代码片段如下(请着重注意代码注释,注意事项过程
)(代码中使用的函数均会存在于完整代码中
)
/**
* 客户端连接服务端握手处理
* @param struct *socket_info 服务地址等信息
* @return uint32_t 调用握手状态的返回
*/
static int w_client_to_server(socket_info *socket_infos)
{
struct sockaddr_in addr = socket_infos->addr;
int fd, ret, timeout;
char buf[512] = {"0"}, *p;
/*握手协议内容*/
uint8_t shake_buf[512] = {"0"}, shake_key[128] = {"0"};
/*建立TCP socket通道*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
fprintf(stderr, "socket create faild: %s n", strerror(errno));
return fd;
}
/*非阻塞*/
ret = fcntl(fd, F_SETFL, 0);
fcntl(fd, F_SETFL, ret | O_NONBLOCK);
timeout = 0;
/*socket connect*/
while ((ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr))) == -1) {
if (++timeout > 100) {
fprintf(stderr, "connect timeout: %s n", strerror(errno));
close(fd);
return -1;
}
delayms(1);
}
/*握手key*/
memset(shake_key, 0, sizeof(shake_key));
/*生成Sec-WebSocket-Key,函数实现为 随机生成 16位字符串,然后进行 base64加密*/
build_shake_key(shake_key);
/*握手协议包*/
memset(shake_buf, 0, sizeof(shake_buf));
/*组装握手协议包,按照之前讲解的 Request header 格式进行组装 */
build_header(socket_infos->host, socket_infos->port, shake_key, (char *)shake_buf);
printf("Request header:-------------- n%sn", shake_buf);
/*发送协议包*/
ret = send(fd, shake_buf, strlen((const char *)shake_buf), MSG_NOSIGNAL);
if (ret < 0) {
fprintf(stderr, "Send to shake info faild: %s n", strerror(errno));
return ret;
}
/*握手*/
timeout = 0;
while(true) {
memset(buf, 0, sizeof(buf));
/*接收服务端返回数据*/
ret = recv(fd, buf, sizeof(buf), MSG_NOSIGNAL);
printf("Response header:-------------- n%sn", buf);
if (ret > 0) {
/*判断是不是HTTP协议头*/
if (strncmp((const char *)buf, (const char *)"HTTP", strlen((const char *)"HTTP")) == 0) {
/*检验握手信号*/
if ((p = (uint8_t *)strstr((const char*)buf, (const char *)"Sec-WebSocket-Accept: ")) != NULL) {
p += strlen((const char *)"Sec-WebSocket-Accept: ");
sscanf((const char *)p, "%srn", p);
printf("accept key: %sn", p);
/*握手成功,握手规则如之前所述:服务端将客户端传递的key进行去除首尾空白,*/
/*然后和一段固定的 GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)进行连接,*/
/*连接后的结果使用 SHA-1(160数位)进行哈希操作,对哈希后的字符串进行base64编码*/
if (match_shake_key(shake_key, strlen((const char*)shake_key), p,
strlen((const char *)p)) == 0) {
return fd;
} else {
/*握手不对,重新发送协议包*/
ret = send(fd, shake_buf, strlen((const char *)shake_buf), MSG_NOSIGNAL);
}
} else {
ret = send(fd, shake_buf, strlen((const char *)shake_buf), MSG_NOSIGNAL);
}
}
}
if (++timeout > 100) {
fprintf(stderr, "shake to server timeout: %s n", strerror(errno));
close(fd);
return -1;
}
delayms(1);
}
return -1;
}
“数据帧”
“数据帧”
之所以加引号,是因为它包含 控制帧 + 数据帧 两个基本组成部分,基础帧协议如下【或者点此查看高清协议图片】
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
常规套路上我们先解释一下帧协议字段内容,为了更加详细一点儿,我们提前打印demo(发送字符串为 aaaaa)中的控制帧:
// 0x605090 地址为控制帧数据的指针地址
// 这里打印从 0x605090 内存地址开始的20个地址空间 (0x605090/0x605098/0x6050a0 分别开始)
(gdb) x /20tb 0x605090
0x605090: 10000001 10000101 00010011 10110010 11110100 01111000 01110010 11010011
0x605098: 10010101 00011001 01110010 00000000 00000000 00000000 00000000 00000000
0x6050a0: 00000000 00000000 00000000 00000000
FIN
:1 bit
表示这是不是消息的最后一帧。第一帧也有可能是最后一帧。%x0
: 还有后续帧%x1
:最后一帧RSV1
、RSV2
、RSV3
:1 bit
扩展字段,除非一个扩展经过协商赋予了非零值的某种含义,否则必须为0opcode
:4 bit
解释 payload data 的类型,如果收到识别不了的opcode,直接断开。分类值如下:%x0
:连续的帧%x1
:text帧%x2
:binary帧%x3 - 7
:为非控制帧而预留的%x8
:关闭握手帧%x9
:ping帧%xA
:pong帧%xB - F
:为非控制帧而预留的
0x605090:10000001
// 第1部分: 1, FIN数据
// 第2部分: 000, RSV1/RSV2/RSV3数据
// 第3部分: 0001, opcode数据text帧
MASK
:1 bit
标识 Payload data 是否经过掩码处理,如果是 1,Masking-key域的数据即为掩码密钥,用于解码Payload data。协议规定客户端数据需要进行掩码处理,所以此位为1Payload len
:7 bit | 7+16 bit | 7+64 bit
表示了 “有效负荷数据 Payload data”,以字节为单位
: - 如果是 0~125,那么就直接表示了 payload 长度 - 如果是 126,那么 先存储 0x7E(=126)接下来的两个字节表示的 16位无符号整型数的值就是 payload 长度 - 如果是 127,那么 先存储 0x7E(=126)接下来的八个字节表示的 64位无符号整型数的值就是 payload 长度
0x605091:10000101
// 第1部分: 1, MASK
// 第2部分: 0000101, 发送数据“aaaaa”的长度5
Masking-key
:0 | 4 bytes 掩码密钥,所有从客户端发送到服务端的帧都包含一个 32bits 的掩码(如果mask被设置成1),否则为0。一旦掩码被设置,所有接收到的 payload data 都必须与该值以一种算法做异或运算来获取真实值。
ws协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法,那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)
答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题
0x605092:00010011
0x605093:10110010
0x605094:11110100
0x605095:01111000
// 掩码占用 4 bytes = 32 bits,生成的字符为 {19 '023', 178 '262', 244 '364', 120 'x'},转化成二进制即为上述的四个bytes
Payload data
:(x+y) bytes
它是Extension data
和Application data
数据的总和,但是一般扩展数据为空。Extension data
:x bytes
除非扩展被定义,否则就是0Application data
:y bytes
占据Extension data
后面的所有空间
0x605096:01110010
0x605097:11010011
0x605098:10010101
0x605099:00011001
0x60509a:01110010
// 数据前提:mask_key[4] = {19 '023', 178 '262', 244 '364', 120 'x'}; data[4] = 'aaaaa';
// 第1部分: 01110010,计算来源 mask key 的第一位 和 数据的第一位进行计算,((~mask_key[0])&data[0]) | (mask_key[0]&(~data[0])) = 01110010
// 第2部分: 11010011,((~mask_key[1])&data[1]) | (mask_key[1]&(~data[1])) = 11010011
// 第3部分: 10010101,((~mask_key[2])&data[2]) | (mask_key[2]&(~data[2])) = 10010101
// 第4部分: 00011001,((~mask_key[3])&data[3]) | (mask_key[3]&(~data[3])) = 00011001
// 第5部分: 01110010,mask key是循环利用的,((~mask_key[0])&data[0]) | (mask_key[0]&(~data[0])) = 01110010
数据帧数据的生成 实现代码片段如下(请着重注意代码注释,注意事项过程
)(代码中使用的函数均会存在于完整代码中
)
/**
* websocket数据收发阶段的数据打包, 通常client发server的数据
* 都要isMask(掩码)处理, 反之server到client却不用
*
* @param uint8_t *data 准备发出的数据
* @param uint32_t data_len 长度
* @param uint8_t *package 打包后存储地址
* @param uint32_t package_max_len 存储地址可用长度
* @param bool is_mask 是否使用掩码,1要 0不要
* @param w_com_type type 数据类型, 由打包后第一个字节决定, 这里默认是数据传输, 即0x81
*
* @return uint32_t 打包后的长度(会比原数据长2~16个字节不等) <=0 打包失败
*/
static int w_enpackage(uint8_t *data, uint32_t data_len, uint8_t *package, uint32_t package_max_len, bool is_mask, w_com_type type)
{
/*掩码*/
uint8_t mask_key[4] = {0};
uint8_t temp1, temp2;
int count;
uint32_t i, len = 0;
if(package_max_len < 2)
return -1;
if(type == WCT_MINDATA)
*package++ = 0x00;
else if(type == WCT_TXTDATA)
*package++ = 0x81;
else if(type == WCT_BINDATA)
*package++ = 0x82;
else if(type == WCT_DISCONN)
*package++ = 0x88;
else if(type == WCT_PING)
*package++ = 0x89;
else if(type == WCT_PONG)
*package++ = 0x8A;
else
return -1;
if(is_mask)
*package = 0x80;
len += 1;
if(data_len < 126) {
*package++ |= (data_len&0x7F);
len += 1;
} else if(data_len < 65536) {
if(package_max_len < 4)
return -1;
*package++ |= 0x7E;
*package++ = (char)((data_len >> 8) & 0xFF);
*package++ = (uint8_t)((data_len >> 0) & 0xFF);
len += 3;
} else if(data_len < 0xFFFFFFFF) {
if(package_max_len < 10)
return -1;
*package++ |= 0x7F;
/*(char)((data_len >> 56) & 0xFF); 数据长度变量是 uint32_t data_len, 暂时没有那么多数据*/
*package++ = 0;
/*(char)((data_len >> 48) & 0xFF);*/
*package++ = 0;
/*(char)((data_len >> 40) & 0xFF);*/
*package++ = 0;
/*(char)((data_len >> 32) & 0xFF);*/
*package++ = 0;
/*到这里就够传4GB数据了*/
*package++ = (char)((data_len >> 24) & 0xFF);
*package++ = (char)((data_len >> 16) & 0xFF);
*package++ = (char)((data_len >> 8) & 0xFF);
*package++ = (char)((data_len >> 0) & 0xFF);
len += 9;
}
/*数据使用掩码时, 使用异或解码, maskKey[4]依次和数据异或运算, 逻辑如下*/
if(is_mask) {
if(package_max_len < len + data_len + 4)
return -1;
/*随机生成掩码*/
get_random_string(mask_key, sizeof(mask_key));
*package++ = mask_key[0];
*package++ = mask_key[1];
*package++ = mask_key[2];
*package++ = mask_key[3];
len += 4;
for(i = 0, count = 0; i < data_len; i++) {
temp1 = mask_key[count];
temp2 = data[i];
/*异或运算后得到数据*/
*package++ = (char)(((~temp1)&temp2) | (temp1&(~temp2)));
count += 1;
/*maskKey[4]循环使用*/
if(count >= sizeof(mask_key))
count = 0;
}
len += i;
*package = '0';
} else {
/*数据没使用掩码, 直接复制数据段*/
if(package_max_len < len + data_len)
return -1;
memcpy(package, data, data_len);
package[data_len] = '0';
len += data_len;
}
return len;
}
解密数据帧
// 实际服务端返回主体文字内容为 "hello aaaaa", 但是数据帧内容就肯定不止主体文字内容
// 服务端返回数据的指针地址为0x605040, 打印0x605040开始的内存数据可以确定服务端返回数据的二进制状态
(gdb) x /20tb 0x605040
0x605040: 10000001 00001011 01101000 01100101 01101100 01101100 01101111 00100000
0x605048: 01100001 01100001 01100001 01100001 01100001 00000000 00000000 00000000
0x605050: 00000000 00000000 00000000 00000000
0x605040:10000001
// 第1部分: 1, FIN数据
// 第2部分: 000, RSV1/RSV2/RSV3数据
// 第3部分: 0001, opcode数据text帧
0x605041:00001011
// 第1部分: 0, NO MASK
// 第2部分: 00001011, 发送数据“hello aaaaa”的长度11
0x605042:01101000
0x605043:01100101
0x605044:01101100
0x605045:01101100
0x605046:01101111
0x605047:00100000
0x605048: 01100001
0x605049:01100001
0x60504a:01100001
0x60504b:01100001
0x60504c:01100001
// 没有mask, 没有 mask key,所以后全部是数据内容,分别对应:
0x605042:h
0x605043:e
0x605044:l
0x605045:l
0x605046:o
0x605047:空格
0x605048:a
0x605049:a
0x60504a:a
0x60504b:a
0x60504c:a
服务端返回的数据帧的解析 实现代码片段如下(请着重注意代码注释,注意事项过程
)(代码中使用的函数均会存在于完整代码中
)
/**
* websocket数据收发阶段的数据解包, 通常client发server的数据都要isMask(掩码)处理,
* 反之server到client却不用
*
* @param uint8_t *data 解包的数据
* @param uint32_t data_len 长度
* @param uint8_t *package 解包后存储地址
* @param uint32_t package_max_len 存储地址可用长度
* @param uint32_t *package_len 解包所得长度
*
* @return int 解包识别的数据类型 如 : txt数据, bin数据, ping, pong等
*
*/
static int w_depackage(uint8_t *data, uint32_t data_len, uint8_t *package, uint32_t package_max_len,
uint32_t *package_len)
{
/*掩码*/
uint8_t mask_key[4] = {0};
uint8_t temp1, temp2;
char mask = 0, type;
int count, ret;
uint32_t i, len = 0, data_start = 2;
if(data_len < 2)
return -1;
type = data[0]&0x0F;
if((data[0]&0x80) == 0x80) {
if(type == 0x01)
ret = WCT_TXTDATA;
else if(type == 0x02)
ret = WCT_BINDATA;
else if(type == 0x08)
ret = WCT_DISCONN;
else if(type == 0x09)
ret = WCT_PING;
else if(type == 0x0A)
ret = WCT_PONG;
else
return WCT_ERR;
} else if(type == 0x00) {
ret = WCT_MINDATA;
} else {
return WCT_ERR;
}
if((data[1] & 0x80) == 0x80) {
mask = 1;
count = 4;
} else {
mask = 0;
count = 0;
}
len = data[1] & 0x7F;
if(len == 126) {
if(data_len < 4)
return WCT_ERR;
len = data[2];
len = (len << 8) + data[3];
if(data_len < len + 4 + count)
return WCT_ERR;
if(mask) {
mask_key[0] = data[4];
mask_key[1] = data[5];
mask_key[2] = data[6];
mask_key[3] = data[7];
data_start = 8;
} else {
data_start = 4;
}
} else if(len == 127) {
if(data_len < 10)
return WCT_ERR;
/*使用8个字节存储长度时, 前4位必须为0, 装不下那么多数据...*/
if(data[2] != 0 || data[3] != 0 || data[4] != 0 || data[5] != 0)
return WCT_ERR;
len = data[6];
len = (len << 8) + data[7];
len = (len << 8) + data[8];
len = (len << 8) + data[9];
if(data_len < len + 10 + count)
return WCT_ERR;
if(mask) {
mask_key[0] = data[10];
mask_key[1] = data[11];
mask_key[2] = data[12];
mask_key[3] = data[13];
data_start = 14;
} else {
data_start = 10;
}
} else {
if(data_len < len + 2 + count)
return WCT_ERR;
if(mask) {
mask_key[0] = data[2];
mask_key[1] = data[3];
mask_key[2] = data[4];
mask_key[3] = data[5];
data_start = 6;
} else {
data_start = 2;
}
}
if(data_len < len + data_start)
return WCT_ERR;
if(package_max_len < len + 1)
return WCT_ERR;
/*解包数据使用掩码时, 使用异或解码, maskKey[4]依次和数据异或运算, 逻辑如下*/
if(mask) {
for(i = 0, count = 0; i < len; i++) {
temp1 = mask_key[count];
temp2 = data[i + data_start];
/*异或运算后得到数据*/
*package++ = (char)(((~temp1)&temp2) | (temp1&(~temåp2)));
count += 1;
/*mask_key[4]循环使用*/
if(count >= sizeof(mask_key))
count = 0;
}
*package = '0';
} else {
/*解包数据没使用掩码, 直接复制数据段*/
memcpy(package, &data[data_start], len);
package[len] = '0';
}
*package_len = len;
return ret;
}