目录
1 Websocket简介
1.1 什么是WebSocket
WebSocket是一种协议,用于在Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接,这使得Web应用程序可以更加实时地传递数据。WebSocket协议最初由W3C开发,并于2011年成为标准。
1.2 WebSocket的优势和劣势
1.2.1 WebSocket的优势
-
实时性: 由于WebSocket的持久化连接,它可以实现实时的数据传输,避免了Web应用程序需要不断地发送请求以获取最新数据的情况。
-
双向通信: WebSocket协议支持双向通信,这意味着服务器可以主动向客户端发送数据,而不需要客户端发送请求。
-
减少网络负载: 由于WebSocket的持久化连接,它可以减少HTTP请求的数量,从而减少了网络负载。
1.2.1 WebSocket的劣势
-
需要浏览器和服务器都支持: WebSocket是一种相对新的技术,需要浏览器和服务器都支持。一些旧的浏览器和服务器可能不支持WebSocket。
-
需要额外的开销: WebSocket需要在服务器上维护长时间的连接,这需要额外的开销,包括内存和CPU。
-
安全问题: 由于WebSocket允许服务器主动向客户端发送数据,可能会存在安全问题。服务器必须保证只向合法的客户端发送数据。
1.3 WebSocket的生命周期
WebSocket 生命周期描述了 WebSocket 连接从创建到关闭的过程。一个 WebSocket 连接包含以下四个主要阶段:
-
连接建立阶段(Connection Establishment): 在这个阶段,客户端和服务器之间的 WebSocket 连接被建立。客户端发送一个 WebSocket 握手请求,服务器响应一个握手响应,然后连接就被建立了。
-
连接开放阶段(Connection Open): 在这个阶段,WebSocket 连接已经建立并开放,客户端和服务器可以在连接上互相发送数据。
-
连接关闭阶段(Connection Closing): 在这个阶段,一个 WebSocket 连接即将被关闭。它可以被客户端或服务器发起,通过发送一个关闭帧来关闭连接。
-
连接关闭完成阶段(Connection Closed): 在这个阶段,WebSocket 连接已经完全关闭。客户端和服务器之间的任何交互都将无效。
需要注意的是,WebSocket 连接在任何时候都可能关闭,例如网络故障、服务器崩溃等情况都可能导致连接关闭。因此,需要及时处理 WebSocket 连接关闭的事件,以确保应用程序的可靠性和稳定性。
下面是一个简单的 WebSocket 生命周期示意图:
2 WebSocket协议解析
WebSocket实现了浏览器与服务器之间的全双工通讯。本质是前端 与服务器端建立一条TCP长连接,服务端可以随时向前端推送数据,前端也可以随时向服务端发送数据,实现了两者间双向数据实时传输。
WebSocket协议的官方文档是RFC6455文件,下面对协议的核心部分做一个讲解,每一部分会列出关键注意点。
协议分为:连接握手和数据传输
2.1 连接握手
2.1.1 客户端握手连接格式
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
客户端连接格式下面几个关键点需要注意:
-
请求行: 请求方法必须是GET, HTTP版本至少是1.1
-
请求必须含有Host
-
如果请求来自浏览器客户端, 必须包含Origin
-
请求必须含有Connection, 其值必须含有"Upgrade"记号
-
请求必须含有Upgrade, 其值必须含有"websocket"关键字
-
请求必须含有Sec-Websocket-Version, 其值必须是13
-
请求必须含有Sec-Websocket-Key, 用于提供基本的防护, 比如无意的连接
2.1.2 服务端收到客户端连接后回复格式
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
-
响应行: HTTP/1.1 101 Switching Protocols
-
响应必须含有Upgrade, 其值为"weboscket"
-
响应必须含有Connection, 其值为"Upgrade"
-
响应必须含有Sec-Websocket-Accept, 根据请求首部的Sec-Websocket-key计算出来
服务端回复中关键点在于Sec-Websocket-Accept值的计算,具体计算方式如下:
-
将客户端送来的Sec-Websocket-Key的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
-
258EAFA5-E914-47DA-95CA-C5AB0DC85B11是一个magic key,是RFC6455 Page24页中定义的一个固定值,直接用即可。
-
通过SHA1计算出摘要, 并转成base64字符串
至此,一来一回,客户端和服务器端已经完成WebSocket 握手,连接建立,下一步就是传输数据了。
2.2 数据传输
RFC6455中定义了数据帧的格式,如下:
数据帧的组成结构和其他协议类似,归纳起来:数据头+载荷
WebSocket的数据头长度是可变的,有两个因素影响:
-
载荷长度的数值大小,Payload length: 占7或7+16或7+64bit,具体看下面详解。
-
是否有maks key,有的话头部多4个字节
数据帧格式如下:
/*-------------------------------------------------------------------
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 ... |
+---------------------------------------------------------------+
--------------------------------------------------------------------*/
-
FIN: 占1bit
-
0表示不是消息的最后一个分片
-
1表示是消息的最后一个分片
-
-
RSV1, RSV2, RSV3: 各占1bit, 一般情况下全为0, 与Websocket拓展有关, 如果出现非零的值且没有采用WebSocket拓展, 连接出错
-
Opcode: 占4bit
-
%x0: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片
-
%x1: 表示这是一个文本帧
-
%x2: 表示这是一个二进制帧
-
%x3-7: 保留的操作代码, 用于后续定义的非控制帧
-
%x8: 表示连接断开
-
%x9: 表示这是一个心跳请求(ping)
-
%xA: 表示这是一个心跳响应(pong)
-
%xB-F: 保留的操作代码, 用于后续定义的非控制帧
-
-
Mask: 占1bit
-
0表示不对数据载荷进行掩码异或操作
-
1表示对数据载荷进行掩码异或操作
-
-
Payload length: 占7或7+16或7+64bit
-
0~125: 数据长度等于该值
-
126: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度
-
127: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度
-
-
Masking-key: 占0或4bytes
-
1: 携带了4字节的Masking-key
-
0: 没有Masking-key
-
-
payload data: 载荷数据
这里有几个关键点需要注意:
-
Fin为0,表示一个完整的消息被分片成多个数据帧中传输的,需要一直等待接到Fin为1的数据帧之后,才算收到一个完整的消息。websocketfiles 中的recv_dataframe已经考虑到这一点,因此为此返回给上层的数据都是一个完整的消息包。
-
只有客户端给服务器端发送数据时才会有masking key,服务器端给客户端发送数据不需要masking key
-
mask掩码计算可以查看文档,或者直接阅读 pack_dataframe代码。
2 Websocket的C++服务端实现
#define SHA_DIGEST_LENGTH 20
#define BUFFER_SIZE 1024
#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
typedef struct _frame_head {
char fin;
char opcode;
char mask;
unsigned long payload_length;
char masking_key[4];
}frame_head;
int _readline(char* allbuf, int level, char* linebuf)
{
int len = strlen(allbuf);
for (;level<len;++level)
{
if(allbuf[level]=='\r' && allbuf[level+1]=='\n')
{
return level+2;
}
else
{
*(linebuf++) = allbuf[level];
}
}
return -1;
}
int shakehands(int cli_fd)
{
int level = 0;
char buffer[BUFFER_SIZE] = {0};
char linebuf[256] = {0};
char sec_accept[32] = {0};
unsigned char sha1_data[SHA_DIGEST_LENGTH]={0};
char head[BUFFER_SIZE] = {0};
if (recv(cli_fd, buffer, sizeof(buffer), 0)<=0)
{
WriteInfoLog("Handshake, accept data error.");
return -1;
}
WriteInfoLog("buffer:%s", buffer);
do {
memset(linebuf, 0, sizeof(linebuf));
level = _readline(buffer, level, linebuf);
if (strstr(linebuf,"Sec-WebSocket-Key")!=NULL)
{
WriteInfoLog("key1:%s", linebuf);
strcat_s(linebuf, GUID);
WriteInfoLog("key2:%s", linebuf);
sha1_hash((unsigned char*)&linebuf+19, strlen(linebuf+19),
(unsigned char*)sha1_data);
B64Encode(sha1_data, 20, (char*)sec_accept, 32);
WriteInfoLog("key3:%s", sec_accept);
sprintf_s(head, BUFFER_SIZE, "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);
WriteInfoLog("rsp head:%s", head);
if (send(cli_fd, head, strlen(head), 0) != strlen(head))
{
WriteInfoLog("Handshake,sending data error.");
return -2;
}
break;
}
}while((buffer[level]!='\r' || buffer[level+1]!='\n') && level!=-1);
return 0;
}
int recv_frame_head(int fd, frame_head* head)
{
char one_char;
/*read fin and op code*/
if (recv(fd,&one_char,1, 0)<=0)
{
WriteInfoLog("Accept frame header,accept fin error.");
return -1;
}
head->fin = (one_char & 0x80) == 0x80;
head->opcode = one_char & 0x0F;
if (recv(fd, &one_char, 1, 0)<=0)
{
WriteInfoLog("Accept frame header,accept mask error.");
return -1;
}
head->mask = (one_char & 0x80) == 0X80;
/*get payload length*/
head->payload_length = one_char & 0x7F;
if (head->payload_length == 126)
{
char extern_len[2];
if (recv(fd, extern_len, 2, 0) != 2)
{
WriteInfoLog("Accept frame header,accept extern_len error.");
return -1;
}
head->payload_length = (extern_len[0]&0xFF) << 8 | (extern_len[1]&0xFF);
}
else if(head->payload_length == 127)
{
char extern_len[8];
if (recv(fd, extern_len, 8, 0) != 8)
{
WriteInfoLog("Accept frame header,accept extern_len error.");
return -1;
}
head->payload_length = (extern_len[4]&0xFF)<<24 | (extern_len[5]&0xFF)<<16
| (extern_len[6]&0xFF)<<8 | (extern_len[7]&0xFF);
}
/*read masking-key*/
if (recv(fd, head->masking_key, 4, 0) != 4)
{
WriteInfoLog("Accept frame header,accept masking_key error.");
return -1;
}
return 0;
}
void umask(char *data, int len, char *mask)
{
int i;
for (i=0; i<len; ++i)
{
*(data+i) ^= *(mask+(i%4));
}
}
int send_frame_head(int fd, char opcode, int payload_length)
{
char response_head[20];
int head_length = 0;
if(payload_length < 126)
{
response_head[0] = 0x80+opcode;
response_head[1] = payload_length;
head_length = 2;
}
else if(payload_length>=126 && payload_length<0xffff)
{
response_head[0] = 0x80+opcode;
response_head[1] = 126;
response_head[2] = (payload_length >> 8 & 0xFF);
response_head[3] = (payload_length & 0xFF);
head_length = 4;
}
else
{
response_head[0] = 0x80+opcode;
response_head[1] = 127;
response_head[2] = 0;
response_head[3] = 0;
response_head[4] = 0;
response_head[5] = 0;
response_head[6] = (payload_length >> 24 & 0xFF);
response_head[7] = (payload_length >> 16 & 0xFF);
response_head[8] = (payload_length >> 8 & 0xFF);
response_head[9] = (payload_length & 0xFF);
head_length = 10;
}
if(send(fd, response_head, head_length, 0)<=0)
{
WriteInfoLog("Send head error.");
return -1;
}
return 0;
}
//栈上空间不宜过大(最大10M),会放不下,改成全局
char g_payload_data[IMAGE_X*IMAGE_Y * 2] = { 0 };
char g_szJsonData[IMAGE_X*IMAGE_Y * 2 + 1024] = { 0 };
int DealCommEvent(SOCKET hCommSock)
{
if (shakehands(hCommSock) != 0)
{
return 0;
}
if(WaitEvent(hCommSock,500) <= 0)
{
WriteInfoLog("Timeout waiting for driving fingerprint data.");
return 0;
}
frame_head head;
int rul = recv_frame_head(hCommSock, &head);
if (rul < 0)
{
return 0;
}
int size = 0;
do {
int rul;
rul = recv(hCommSock, g_payload_data+size, 1024, 0);
if (rul <= 0)
{
WriteInfoLog("Accept Client Data Error.");
return 0;
}
size += rul;
}while(size<head.payload_length);
umask(g_payload_data, size, head.masking_key);
int nstlenl=strlen(g_payload_data);
WriteInfoLog("Receive client data:%d[%s]", head.payload_length, g_payload_data);
// 解析请求数据 & 生成响应应答数据
Requst2Respond(g_payload_data, g_szJsonData);
WriteInfoLog("Respond g_szJsonData=%s", g_szJsonData);
//响应应答
send_frame_head(hCommSock,1, strlen(g_szJsonData));
int len=send(hCommSock, g_szJsonData, strlen(g_szJsonData), 0);
// 接收客户端发起的关闭请求
if(WaitEvent(hCommSock, 200) <= 0)
{
WriteInfoLog("Timeout waiting for client to initiate shutdown request.");
return 0;
}
memset(g_payload_data, 0, 1024);
recv(hCommSock, g_payload_data, 1024, 0);
send_frame_head(hCommSock, 8, 0);
return 0;
}
3 Websocket的Javascript客户端实现
WebSocket API 是用于在 Web 应用程序中创建和管理 WebSocket 连接的接口集合。WebSocket API 由浏览器原生支持,无需使用额外的 JavaScript 库或框架,可以直接在 JavaScript 中使用。
下面是一些常用的 WebSocket API:
3.1 WebSocket 构造函数
WebSocket 构造函数用于创建 WebSocket 对象。它接受一个 URL 作为参数,表示要连接的 WebSocket 服务器的地址。例如:
var ws = new WebSocket('ws://localhost:12345/demo');
3.2 WebSocket.onopen 事件
WebSocket.onopen
事件在 WebSocket 连接成功建立时触发。例如:
ws.onopen = function() {
console.log('WebSocket 连接已经建立。');
};
3.3 WebSocket.send() 方法
WebSocket.send()
方法用于向服务器发送数据。它接受一个参数,表示要发送的数据。数据可以是字符串、Blob 对象或 ArrayBuffer 对象。例如:
ws.send('Hello, server!');
3.4 WebSocket.onmessage 事件
WebSocket.onmessage
事件在接收到服务器发送的消息时触发。它的 event 对象包含一个 data 属性,表示接收到的数据。例如:
ws.onmessage = function(event) {
console.log('收到服务器消息:', event.data);
};
3.5 WebSocket.onerror 事件
WebSocket.onerror
事件在 WebSocket 连接出现错误时触发。例如:
ws.onerror = function(event) {
console.error('WebSocket 连接出现错误:', event);
};
3.6 WebSocket.onclose 事件
WebSocket.onclose
事件在 WebSocket 连接被关闭时触发。例如:
ws.onclose = function() {
console.log('WebSocket 连接已经关闭。');
};
4 演示Demo
4.1 开发环境
-
Visual Studio 2015
-
Javascript
-
Windows 10 Pro x64
4.2 功能介绍
演示Websocket的C++服务端和JS客户端通信。
4.3 下载地址
开发环境:
-
Visual Studio 2015
-
Javascript
-
Visual Studio 2015