一、WebSocket概述
1.1 诞生背景
早期的时候,网站与服务器进行通信与信息获取大都采用轮询方式。只有网站发起请求,服务器才能给与响应。服务器不能主动发起消息。
通过HTTP请求来轮询获取信息的这种形式叫做半双工通信,在获取静态资源时还可以,但是当获取动态信息时,客户端需要不断地轮询以获取结果。比如客户端发起一个请求:“请服务器做一件事”,并且客户端需要知道服务器这件事做完了,客户端才能按顺序去做下一件事。于是客户端需要不停的询问服务端:
Client:“你弄完了吗?”,Server:“没呢!”。
(隔几秒)Client:“你弄完了?”,Server:“没呢!”
(隔几秒)……(不知多少个请求后)
Client:“你弄完了吗?”,Server:“弄完了”
这样频繁的请求无疑占用了很多硬件资源和网络资源,同时由于HTTP请求每次都会携带大量的重复的header信息,也造成了资源的浪费。
可以发现,在这种通信方式下,某些形式时使用起来并不是很方便。为了节省服务器的资源和带宽,WebSocket应运而生。
1.2 简介
Websocket是基于TCP的,使用全双工通信模式的应用层协议。在客户端与服务器连接之后,任何一方都可以主动发出信息。
他与HTTP使用了相同的端口,即80端口和使用TLS时的443端口
使用WebSocket通信,前后端的沟通就变成了:
Client:“hello,服务端你去帮我做一件事,做完跟我说一声哈,我还有别的事情先忙着”
Server:“行,你忙吧,一会我弄完通知你”
……(一段时间过后)
Server:“老客,那事我处理完了哈,给你说一声”
Client:“OKOK”
1.3协议
引用一张图片:
图片上方的数字代表内存占用,从左到右一共32bit
简单整理一个表格:
字段 | 含义 |
---|---|
FIN | 用于指示当前的 frame 是否结束 |
RSV | 在WebSocket扩展时才会使用,不启用时置1 |
Opcode | 指示 frame 的类型(文本、二进制、pingpong等) |
Mask | 指示 frame 的数据是否使用掩码掩盖 |
Payload Len | 标识数据的长度,其长度也会随着数据长度变化 |
Masking-key | Mask为1时该字段才存在,为32位长度用于覆盖frame |
Payload | frame 的数据部分 |
简单了解一下数据格式后,来看下面这一张图片:
介绍一下图中的字段:
1.首先可以看到常规信息里的内容:
ws://127.0.0.1:29999/
websocket连接时,关键字为ws,如果使用SSL则为wss。后面跟的是服务器的地址。这里是一个本机ip,29999端口的服务。
请求方法:GET
WebSocket连接时会使用 HTTP GET方法
连接成功时固定返回
2.再来看一下请求头
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: tEsrNpPFpJMOcMUxs/a9sA==
Sec-WebSocket-Version: 13
这些都是websocket连接需要增加和注意的字段。
Upgrade: websocket和Connection: Upgrade标识为WebSocket连接,并表示连接要升级
Sec-WebSocket-Extensions 字段是可选的,表示为拓展
Sec-WebSocket-Version: 13标识Websocket的版本
Sec-WebSocket-Key:客户端随机生成的值,转为base64后发送
3.响应头
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Version: 13
Sec-WebSocket-Accept 字段是使用固定的算法将Sec-WebSocket-Key计算后得出的。他不是用来保护数据的,是用来提供基础的防护,减少恶意连接、意外连接的。
二、javascript创建WebSocket客户端
WebSocket.onopen:用于指定连接成功后的回调函数,当WebSocket的连接状态readyState变为“OPEN”时调用
WebSocket.onclose:用于指定连接关闭后的回调函数,当WebSocket的连接状态readyState变为“CLOSED”时被调用
WebSocket.onmessage:用于指定当从服务器接受到信息时的回调函数,当从服务器收到一条消息时,该回调函数将被调用
WebSocket.onerror:用于指定连接失败后的回调函数,定义一个发生错误时执行的回调函数
websocket对象的创建:
var ws;
function buttonConnnect() {
if ("WebSocket" in window)
{
alert("您的浏览器支持 WebSocket!");
ws = new WebSocket("ws://127.0.0.1:10010");
}
else
{
alert("您的浏览器不支持 WebSocket!");
}
}
websocket对象的回调函数赋值:
ws.onerror=function (event) {
alert("error"+event);
}
也可以以这种格式赋给web的回调函数:
ws.onmessage = OnMessage;
function OnMessage(msg) {
try{
var obj = JSON.parse(msg.data);
var message = obj.Message;
alert(message);
}
catch(e){
alert(msg.data);
}
}
示例代码:
var obj = JSON.parse(msg.data);
var msgID = obj.msgID;
var HWPenSign = obj.HWPenSign;
if (msgID=="0") {
if (HWPenSign == "HWGetStatus")
{
var DeviceStatus = obj.DeviceStatus;
if(DeviceStatus==1)
{
document.getElementById("Init").removeAttribute("disabled");
alert("设备已连接");
}
else
alert("设备未连接");
}
else if(HWPenSign == "HWInit")
{
var message = obj.message;
alert(message);
alert("初始化成功,可以开始签字");
document.getElementById("StartSign").removeAttribute("disabled");
document.getElementById("GetSign").removeAttribute("disabled");
document.getElementById("ClearSign").removeAttribute("disabled");
document.getElementById("EndSign").removeAttribute("disabled");
}
else if (HWPenSign=="HWGetSign") {
document.getElementById("signimg").src=obj.message;
}
else if (HWPenSign=="HWClearSign") {
clearCanvas();
}
else if (HWPenSign=="HWGetPoint") {
var x = obj.pX;
var y = obj.pY;
var s = obj.pStatus;
addClick(x,y,s);
draw();
}
else
{
alert(obj.message);
}
}
else
{
alert(obj.message);
}
}catch(e){
alert(msg.data);
}
}
三、使用mongoose创建服务端
1.概述
Mongoose是C语言网络库,为TCP、UDP、HTTP、WebSocket、CoAP、MQTT实现了事件驱动型的非阻塞api。
这是一个非常轻量的网络库,直接在网上下载mongoose.cpp和mongoose.h,包含到项目里就能用了
2.代码示例
#include "stdafx.h"
#include "mongoose.h"
static sig_atomic_t s_signal_received = 0;
static const char *s_http_port = "10086";
static struct mg_serve_http_opts s_http_server_opts;
static void ev_handler(struct mg_connection *nc,
int ev,
void *ev_data)
{
struct mg_connection *c;
char buf[500];
char addr[32];
mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr),MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
switch (ev) {
case MG_EV_WEBSOCKET_HANDSHAKE_DONE: { //websocket握手事件完成
char addr[32];
std::string res_msg="Connect Success!";
mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT,res_msg.c_str() , strlen(res_msg.c_str()));
break;
}
case MG_EV_WEBSOCKET_FRAME: { //收到websocket数据
struct websocket_message *wm = (struct websocket_message *) ev_data;
struct mg_str d = {(char *) wm->data, wm->size};
mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT,"hello client",strlen("hello client"));
break;
}
case MG_EV_HTTP_REQUEST: { //http请求
//http处理
break;
}
case MG_EV_CLOSE: { /* Disconnect. Tell everybody. */
//关闭处理
break;
}
}
}
int main(void) {
struct mg_mgr mgr;
struct mg_connection *nc;
mg_mgr_init(&mgr, NULL); //创建并初始化事件管理器
nc = mg_bind(&mgr, s_http_port, ev_handler);//绑定端口号,设置回调函数
mg_set_protocol_http_websocket(nc);
s_http_server_opts.document_root = "."; /
s_http_server_opts.enable_directory_listing = "yes";
while (s_signal_received == 0) {
mg_mgr_poll(&mgr, 500);
}
mg_mgr_free(&mgr);
return 0;
}
代码中有几个结构体和函数需要介绍一下:
struct mg_mgr;///事件管理器,保存所有的活动链接
struct mg_connection;///描述一个链接
struct mbuf;///接收和发送的数据
每次回调调用的函数:
static void ev_handler(struct mg_connection *nc,
int ev,
void *ev_data)
对于HTTP请求和WebSocket收到的信息都在这个函数中处理。
其中struct mg_connection *nc,是发送消息用的指针,int ev标识请求类型,void *ev_data里存放的是客户端发来的数据。
mongoose定义了一些宏用于标识区分通信协议和状态。
mg_set_protocol_http_websocket(nc); 这个函数用于支持HTTP与WebSocket