一. 为什么需使用webSocket ?
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。
二. webSocket简介 :
WebSocket 协议在2008年诞生,2011年成为国际标准。WebSocket 能够在建立连接之后,在服务器端推送数据到客户端,解决HTTP协议的弊端。
WebSocket 是在单个TCP连接上进行全双工通信的协议,允许Server主动向Client推送数据。
客户端和服务器只需要完成一次握手,就可以创建持久性的连接,进行双向数据传输。
WebSocket 是独立的,作用在TCP上的协议。
为了向前兼容, WebSocket 协议使用 HTTP Upgrade 协议升级机制来进行 WebSocket 握手, 当握手完成之后, 客户端和服务端就可以依据WebSocket 协议的规范格式进行数据传输。
三. webscoket 特点 :
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
四. websocket相对HTTP协议的优点 :
- 支持双向通信,数据的实时性更新更强。
- 开销小;客户端和服务端进行数据通信时,websocket的header(数据头)较小。服务端到客户端的header只有2~10 Bytes,客户端到服务端的需要加上额外的4 Bytes的masking-key。而HTTP协议每次通信都需要携带完整的数据头。
- 扩展性。
- 二进制数据支持更好。
WebSocket 协议主要为了解决 HTTP/1.x 缺少双向通信机制的问题, 它使用 TCP 作为传输层协议, 使用 HTTP Upgrade 机制来握手,它与 HTTP 是相互独立的协议, 二者没有上下的分层关系。
五. websocket的应用场景 :
从websocket的优点可以知道,主要应用场景有:
- 视频弹幕。
- 媒体即时通讯。
- 需要实时位置/数据的应用。
- 金融行业的股票基金价格实时更新等。
六. websocket握手
1. 客户端:Upgrade(申请升级到websocket协议)
协议包含两个部分:握手和数据传输。WebSocket复用了HTTP的握手通道。
客户端通过HTTP请求与WebSocket服务端协商升级到websocket协议。协议升级完成后,后续的数据传输按照WebSocket的data frame进行。
WebSocket 握手采用 HTTP Upgrade 机制,使用标准的HTTP报文格式,只支持使用HTTP的GET方法,客户端发送如下所示的结构发起握手:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://fly.example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
说明:
参数 | 值 | 含义 |
---|---|---|
Upgrade: | websocket | 升级到websocket协议 |
Connection: | Upgrade | 升级协议 |
Sec-WebSocket-Key: | (key value) | 与服务端响应的sec-websocket-accept对应,提供安全防护 |
Sec-WebSocket-Version: | 13 | 指示websocket的版本 |
2. 服务器:响应协议升级
服务端如果支持 WebSocket 协议,则返回 101 的 HTTP 状态码。返回如下所示的结构:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat Sec-WebSocket-Version: 13
参数说明:
参数 | 说明 |
---|---|
Sec-WebSocket-Accept | 必须有,与客户端的Sec-WebSocket-Key对应 |
Sec-WebSocket-Version | 必须有, 返回服务端和客户端都支持的 WebSocket 协议版本。如果服务端不支持客户端的协议版本则立即终止握手, 并返回 HTTP 426 状态码,同时设置 Sec-WebSocket-Version 说明服务端支持的 WebSocket 协议版本列表 |
Sec-WebSocket-Protocol | 可选, 是否支持 WebSocket 子协议 |
Sec-WebSocket-Extensions | 可选, 是否支持拓展列表 |
注意:每个HTTP的header都以\r\n结尾,并且最后一行要加上一个额外的\r\n。这是由于http协议制定的时候,就是用分隔符进行分包。
3. Sec-WebSocket-Accept值的计算
客户端发起握手时通过 Sec-WebSocket-Key 传递了一个安全防护字符串,服务端将该值与 WebSocket 魔数 "258EAFA5-E914-47DA- 95CA-C5AB0DC85B11" 进行字符串拼接,将得到的字符串做 SHA-1 哈希, 将得到的哈希值再做 base64 编码,最后得到的值就是Sec-WebSocket-Accept值。
计算公式为:
(1)将Sec-WebSocket-Key的值与258EAFA5-E914-47DA-95CA-C5AB0DC85B11魔数进行字符串拼接;
(2)使用SHA1对拼接的字符串做哈希,得到一个哈希值;
(3)将哈希值做base64编码得到Sec-WebSocket-Accept值。
伪代码:
...... // 字符串拼接 char *str=Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 计算sha1哈希 char sec_data[64]; SHA1(str,strlen(str),sec_data); // 编码成base64 char sec_accept[64]; base64_encode(sec_data,strlen(sec_data),sec_accept); ......
base64_encode函数实现:
#include <openssl/sha.h> #include <openssl/pem.h> #include <openssl/bio.h> #include <openssl/evp.h> int base64_encode(char *in_str,int in_len,char *out_str) { BIO *b64, *bio; BUF_MEM *bptr = NULL; size_t size = 0; if (in_str == NULL || out_str == NULL) return -1; b64 = BIO_new(BIO_f_base64()); bio = BIO_new(BIO_s_mem()); bio = BIO_push(b64, bio); BIO_write(bio, in_str, in_len); BIO_flush(bio); BIO_get_mem_ptr(bio, &bptr); memcpy(out_str, bptr->data, bptr->length); out_str[bptr->length - 1] = '\0'; size = bptr->length; BIO_free_all(bio); return size; }
七. 心跳包--保持连接
有些场景,客户端、服务端虽然长时间没有数据交互,但仍需要保持连接。这个时候,可以采用心跳来实现。
逻辑:
- 发送方 --> 接收方:ping,探测,实现 WebSocket 的 Keep-Alive,可以有Payload。
- 接收方 --> 发送方:pong,Ping 的响应,Payload 的内容需要和 Ping frame 相同
ping、pong的操作对应opcode分别是0x9、0xA。
八. Sec-WebSocket-Key/Sec-WebSocket-Accept的作用
Sec-WebSocket-Key主要目的不是确保数据的安全性,最主要作用是提供基本的安全防护,减少恶意连接。连接是否安全、数据是否安全、客户端/服务端是否合法,并没有实际性的保证。
九. 数据掩码(Masking-key)的作用
WebSocket协议中,数据掩码的作用是增强websocket协议的安全性,并不是为了保护数据本身。
数据掩码并不是为了防止数据泄密,而是为了防止代理缓存污染攻击(proxy cache poisoning attacks)问题。
十. websocket服务器实现
处理流程:
- 接收到client发送的请求升级协议包。
- 解析请求包,获取Sec-WebSocket-Key字符串,转换到数据解析状态。
- 解析升级协议包,获取相关信息,转换到数据交互状态。
- 打包websocket协议头,发送frame。
代码简单示例:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/epoll.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <openssl/sha.h> #include <openssl/pem.h> #include <openssl/bio.h> #include <openssl/evp.h> #include <assert.h> #define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" #define BUFFER_LENGTH 1024 enum { WS_HANDSHARK = 0, WS_TRANMISSION=1, WS_END=2, WS_COUNT }; struct ws_ophdr { unsigned char opencode : 4, rsv3 : 1, rsv2 : 1, rsv1 : 1, fin : 1; unsigned char pl_len : 7, mask : 1; }; struct ntyevent { int fd; int events; void *arg; int (*callback)(int fd, int events, void *arg); int status; char buffer[BUFFER_LENGTH]; int length; //long last_active; char wbuffer[BUFFER_LENGTH]; //response int wlength; char sec_accept[ACCEPT_KEY_LENGTH]; int wsstatus; //0, 1, 2, 3 char mask_key[4]; }; /* ...... */ // 按行读取数据 int readline(char *buffer,int idx,char *linebuffer) { int len = strlen(buffer); for (; idx < len; ++idx) { if (buffer[idx] == '\r' && buffer[idx + 1] == '\n') return idx + 2; *(linebuffer++) = buffer[idx]; } return -1; } // base64 encode int base64_encode(char *in_str,int in_len,char *out_str) { BIO *b64, *bio; BUF_MEM *bptr = NULL; size_t size = 0; if (in_str == NULL || out_str == NULL) return -1; b64 = BIO_new(BIO_f_base64()); bio = BIO_new(BIO_s_mem()); bio = BIO_push(b64, bio); BIO_write(bio, in_str, in_len); BIO_flush(bio); BIO_get_mem_ptr(bio, &bptr); memcpy(out_str, bptr->data, bptr->length); out_str[bptr->length - 1] = '\0'; size = bptr->length; BIO_free_all(bio); return size; } // 握手,获取Sec-WebSocket-Key的字符串,并转换为Sec-WebSocket-Accept所需字符串 int ws_open_shark(struct ntyevent *ev) { int idx = 0; char sec_data[128] = { 0 }; char sec_accept[128] = { 0 }; do { char linebuff[BUFFER_LENGTH] = { 0 }; idx = readline(ev->buffer, idx, linebuff); if (strstr(linebuff, "Sec-WebSocket-Key")) { strcat(linebuff, GUID); int keylen = strlen("Sec-WebSocket-Key: "); SHA1(linebuff + keylen, strlen(linebuff + keylen), sec_data); base64_encode(sec_data, strlen(sec_data), sec_accept); printf("index %d, line : %s\n", idx, sec_accept); memcpy(ev->sec_accept, sec_accept, ACCEPT_KEY_LENGTH); } } while ((ev->buffer[idx] != '\r' || ev->buffer[idx + 1] != '\n') && idx != -1); return 0; } // 掩码、反掩码 void umask(char *payload, int len, char *mask) { int i = 0; for (i = 0; i < len; i++) { payload[i] ^= mask[i % 4]; } } // 解析payloade数据 int ws_tranmission(struct ntyevent *ev) { struct ws_ophdr *ophdr = (struct ws_ophdr *)ev->buffer; char *payload = NULL; size_t datalen = 0; int mask_key_offset = 0; if (ophdr->pl_len < 126) { if (ophdr->mask) { payload = ev->buffer + 6; mask_key_offset = 2; umask(payload, ophdr->pl_len, ev->buffer + mask_key_offset); } else payload = ev->buffer + 2; datalen = ophdr->pl_len; } else if(ophdr->pl_len == 126) { printf("%x %x\n", (unsigned char)ev->buffer[2], (unsigned char)ev->buffer[3]); datalen = (((unsigned char)ev->buffer[2]) << 8) | ((unsigned char)ev->buffer[3]); if (ophdr->mask) { payload = ev->buffer + 8; mask_key_offset = 4; umask(payload, datalen, ev->buffer + mask_key_offset); } else payload = ev->buffer + 4; } else if (ophdr->pl_len == 127) { int i = 0; for (i = 2; i <10; i++) { datalen |= ((unsigned char)ev->buffer[i]); if (i + 1 < 10) datalen <<= 8; } if (ophdr->mask) { payload = ev->buffer + 14; mask_key_offset = 10; umask(payload, datalen, ev->buffer + mask_key_offset); } else payload = ev->buffer + 10; } else assert(0); printf("fin : %d\n", ophdr->fin); printf("rsv1: %d,rsv2: %d,rsv3: %d\n", ophdr->rsv1, ophdr->rsv2, ophdr->rsv3); printf("opcode: %d\n", ophdr->opencode); printf("mask : %d\n", ophdr->mask); printf("payload len : %d\n", ophdr->pl_len); if (mask_key_offset) printf("mask-key: %x %x %x %x\n", ev->buffer[mask_key_offset], ev->buffer[mask_key_offset + 1], ev->buffer[mask_key_offset + 2], ev->buffer[mask_key_offset + 3]); printf("data len: %lu\n", datalen); printf("payload data [len = %ld]: %s\n", strlen(payload), payload); strcpy(ev->wbuffer, payload); ev->wlength = datalen; memcpy(ev->mask_key, ev->buffer + mask_key_offset, 4); return datalen; } // 解析获取申请升级协议请求,转换状态 void ws_status(struct ntyevent *ev) { char linebuff[BUFFER_LENGTH] = { 0 }; readline(ev->buffer, 0, linebuff); if (strstr(linebuff, "GET ")) ev->wsstatus = WS_HANDSHARK; else ev->wsstatus = WS_TRANMISSION; } // 响应请求 int ws_request(struct ntyevent *ev) { ev->wlength = ev->length; ws_status(ev); if (ev->wsstatus == WS_HANDSHARK) { ws_open_shark(ev); } else if (ev->wsstatus = WS_TRANMISSION) { ws_tranmission(ev); } } // 处理大小端的函数 void ws_inverted_string(char *str,int len) { int i = 0; char temp; for (i = 0; i < len / 2; ++i) { temp = *(str + i); *(str + i) = *(str + len - i - 1); *(str + len - i - 1) = temp; } } // 发送websocket的header int ws_send_hdr(struct ntyevent *ev) { struct ws_ophdr ophdr; char extend[16] = { 0 }; int extend_length = 0; int ret = 0; ophdr.fin = 1; ophdr.rsv1 = ophdr.rsv2 = ophdr.rsv3 = 0; ophdr.mask = 1; ophdr.opencode = 1; if (ev->wlength<126) ophdr.pl_len = ev->wlength; else if (ev->wlength < 0xFFFF) { ophdr.pl_len = 126; extend_length += 2; extend[2] = (ev->wlength >> 8) & 0xFF; extend[3] = ev->wlength & 0xFF; printf("plelode length: %x%x\n", extend[2], extend[3]); } else { ophdr.pl_len = 127; extend_length += 8; printf("plelode length: "); int i = 0; for (i = 0; i<8; i++) { extend[i+2] = (ev->wlength >> ((7-i)*8)) & 0xFF; printf("%x", extend[i+2]); } // 处理大小端问题 //ws_inverted_string((char *)extend + 2, sizeof(unsigned long long)); printf("\n"); } extend_length += 2;// ophdr length if (ophdr.mask) { printf("mask key start index: %d\n", extend_length); extend[extend_length] = ev->mask_key[0]; extend[extend_length+1] = ev->mask_key[1]; extend[extend_length+2] = ev->mask_key[2]; extend[extend_length+3] = ev->mask_key[3]; printf("mask-key: %x %x %x %x\n", extend[extend_length], extend[extend_length + 1], extend[extend_length + 2], extend[extend_length + 3]); umask(ev->wbuffer, ev->wlength, ev->mask_key); extend_length += 4; printf("mask key end index: %d\n", extend_length); } printf("fin: %d\nmask: %d\nopcode: %d\n", ophdr.fin, ophdr.mask, ophdr.opencode); printf("send hdr[%d],extend_length=%d\n\n", ophdr.pl_len, extend_length); char *tmp = (char*)&ophdr; extend[0] = tmp[0]; extend[1] = tmp[1]; struct ws_ophdr_mask *maskkey=(struct ws_ophdr_mask *)extend; printf("mask key: %s,%d,%s\n",ev->mask_key, maskkey->mask, maskkey->mask_key); int i = 0; printf("\n\nALL: "); for (i = 0; i < extend_length; i++) { printf("%x ", extend[i]); } printf("\n\n"); send(ev->fd, &extend, extend_length, 0); return ret; } // 响应请求 int ws_response(struct ntyevent *ev) { if (ev->wsstatus == WS_HANDSHARK) { ev->wlength = sprintf(ev->wbuffer, "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n\r\n", ev->sec_accept); } else if (ev->wsstatus = WS_TRANMISSION) { ws_send_hdr(ev); } return ev->wlength; } /* ...... */ int main(int argc,char *argv[]) { int listenfd=socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in server; memset(&server,0,sizeof(server)); server.sin_family=AF_INET; server.sin_addr.s_addr=htonl(INADDR_ANY); server.sin_port=htons(8888); bind(listenfd,(struct sockaddr*)&server,sizeof(server)); if(listen(listenfd,10)<0) return -1; while(1) { struct sockaddr_in client; socklen_t len=sizeof(client); int clientfd=accept(listenfd,(struct sockaddr *)&client,&len); struct ntyevent ev; memset(&ev,0,sizeof(ev)); recv(clientfd,ev.buffer,BUFFER_LENGTH,0); // 解析请求 ws_request(&ev); // 响应请求 ws_response(&ev); send(clientfd,ev->wbuffer,ev->wlength,0); } return 0; }
十一. 服务端的实现 :
WebSocket 服务器的实现,可以查看维基百科的列表。
常用的 Node 实现有以下三种。
具体的用法请查看它们的文档,这里不详细介绍了。
十二. WebSocketd :
下面,我要推荐一款非常特别的 WebSocket 服务器:Websocketd。
它的最大特点,就是后台脚本不限语言,标准输入(stdin)就是 WebSocket 的输入,标准输出(stdout)就是 WebSocket 的输出。
十三. GoEasy WebSocket 消息推送服务:
大家看了前面的介绍,可能已经发现了,WebSocket 的使用有一个前提条件,就是要自己搭建一个服务。
但是很多时候,它只是一个前后端消息的中介,没有其他功能。单独搭一个服务似乎有点麻烦,尤其是在你的应用并不大的情况下。
很多开发者都希望,直接使用现成的 WebSocket 服务,免得自己搭建,最好还是免费的。
下面就介绍一个国内这样的 WebSocket 服务商 GoEasy。你不需要自己搭建了,前后端接入他们的服务器就可以了,他们的机器都在国内,速度和可靠性有保证。
示例代码可以参考文档,只要几行,就能立刻使用 WebSocket 了。
服务端使用 PHP、C#、Java、Go、Node、Python 等各种语言,都没有问题。客户端支持 Uniapp、各种小程序、Web 等不同应用和 Vue、React、Taro 等主流框架。
GoEasy 2015年就上线了,有很多大企业客户,做到了百万级用户同时在线,每秒千万级消息的实时送达。他们保证消息数据全程加密传输,高并发、低延时,99.95%的高可用。
只要你的 DAU(日活跃用户数)不超过200,他们的服务是永久免费的,对于个人的小型应用应该够用了。企业的商业项目需要付费,还提供私有部署。
十四. 参考链接 :
WebSocket 教程 - 阮一峰的网络日志 (ruanyifeng.com)
Linux网络编程之websocket服务器实现 - 知乎 (zhihu.com)
WebSockets 教程 (tutorialspoint.com)