websocket

一、websocket是什么

  • websocket是一种网络通信协议,是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。
  • 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。(维基百科)

二、websocket与http网络协议的联系与区别

联系:

  1. 都是基于TPC的;
  2. 都是可靠性传输协议;
  3. 都是应用层协议;
  4. WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的;

区别:

http是一种无状态的、无连接的单向的应用层协议、采用请求/响应模式。即,通信请求只能由客户端发起,服务端对请求做出应答处理。

  • 无状态:协议对于事务处理没有记忆能力;在传统的方式上(long pull 与 ajax 轮询),要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输 identity info (鉴别信息),来告诉服务端你是谁。降低了效率,消耗了过多的流量/时间。
  • 无连接:每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后,即断开连接。
  • 单向:每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端不能主动向客户端发送数据。

websocket:最大的特点就是服务端可以主动向客户端推送消息,客户端也可以主动向服务端发送消息。应用场景:

  • 即时聊天通信
  • 多玩家游戏
  • 在线协同编辑/编辑
  • 实时数据流的拉取与推送
  • 体育/游戏实况
  • 实时地图位置
  • 游戏应用程序:在游戏应用程序中,你可能会注意到,服务器会持续接收数据,而不会刷新用户界面。屏幕上的用户界面会自动刷新,而且不需要建立新的连接,因此在WebSocket游戏应用程序中非常有帮助

三、long pull 与ajax 轮询

从下面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议非常消耗资源与被动性。

ajax轮询:

ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

情景再现:

客户端:啦啦啦,有没有新信息(Request)

服务端:没有(Response)

客户端:啦啦啦,有没有新信息(Request)

服务端:没有。。(Response)

客户端:啦啦啦,有没有新信息(Request)

服务端:你好烦啊,没有啊。。(Response)

客户端:啦啦啦,有没有新消息(Request)

服务端:好啦好啦,有啦给你。(Response)

客户端:啦啦啦,有没有新消息(Request)

服务端:。。。。。没。。。。没。。。没有(Response) -loop

long pull:

其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

情景再现:

客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)

服务端:额。。 等待到有消息的时候。。来 给你(Response)

客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop

总结:ajax轮询 需要服务器有很快的处理速度和资源。(速度)long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)

websocket

当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。

情景再现:

客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)

服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)

客户端:麻烦你有信息的时候推送给我噢。。

服务端:ok,有的时候会告诉你的。

服务端:balabalabalabala

服务端:balabalabalabala

服务端:哈哈哈哈哈啊哈哈哈哈

服务端:笑死我了哈哈哈哈哈哈哈

总结:websocket只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你 )

四、websocket的其他特点

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。
  2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。
  4. 可以发送文本,也可以发送二进制数据。
  5. 没有同源限制,客户端可以与任意服务器通信。
  6. 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

五、创建websocket对象

1
2
3
// url, 指定连接的 URL
// protocol 是可选的,指定可接受的子协议。
let Socket = new WebSocket(url, [protocol] );

六、websocket属性

websocket state*

七、websocket事件

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

八、websocket方法

方法 描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接

九、websocket实例

  1. 最基本写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var ws = new WebSocket(url);
    //连接发生错误的回调方法
    ws.onerror = function () {
    console.log("socket连接失败")
    }
    // 连接成功建立的回调方法
    ws.onopen = function () {
    console.log("socket连接已打开")
    }
    // 接收到消息的回调方法
    ws.onmessage = function (e) {
    console.log("客户端接收服务端数据时触发");
    }
    // 连接关闭的回调方法
    ws.onclose = function () {
    console.log("socket连接已关闭");
    }
  2. 稍加复杂

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
        var ws = null; //websocket实例
    var wsUrl = "ws://localhost:8888/websocket/name";
    //1.第一步页面初始化,先调用createWebSocket函数,目的是创建一个websocket的方法:new WebSocket(wsUrl);因此封装成函数内如下代码:
    function createWebsocket(url) {
    //判断当前浏览器是否支持WebSocket
    if (window.WebSocket) {
    ws = new WebSocket(url);
    init();
    } else {
    console.log("浏览器不支持WebSocket!")
    }
    }
    //2.第二步调用init方法,该方法内把一些监听事件封装如下:
    function init() {
    //连接发生错误的回调方法
    ws.onerror = function () {
    console.log("socket连接失败")
    }
    // 连接成功建立的回调方法
    ws.onopen = function () {
    console.log("socket连接已打开")
    }
    // 接收到消息的回调方法
    ws.onmessage = function (e) {
    console.log("客户端接收服务端数据时触发");
    }
    // 连接关闭的回调方法
    ws.onclose = function () {
    console.log("socket连接已关闭");
    }
    // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
    websocket.close();
    };
    }
    createWebsocket();

心跳重连机制

一、概念:

在使用websocket的过程中,有时候会遇到网络断开的情况,比如信号不好,或者网络临时性关闭,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。因此就有了websocket的心跳了。还有心跳,说明还活着,没有心跳说明已经挂掉了。

二、心跳包概念:

它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

三、心跳机制概念:

心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~

四、实现心跳检测的思路:

大白话:

每隔一段固定的时间,向服务器端发送一个ping数据,如果在正常的情况下,服务器会返回一个pong给客户端,如果客户端通过
onmessage事件能监听到的话,说明请求正常,这里我们使用了一个定时器,每隔xx秒的情况下,如果是网络断开的情况下,在指定的时间内服务器端并没有返回心跳响应消息,因此服务器端断开了,因此这个时候我们使用ws.close关闭连接,在一段时间后(在不同的浏览器下,时间是不一样的,firefox响应更快),可以通过 onclose事件监听到。因此在onclose事件内,我们可以调用 reconnect事件进行重连操作。

整理一下:

  1. 客户端每隔一段固定的时间发送一个探测包给服务器。
  2. 客户端发包时启动一个超时定时器。
  3. 服务器端接收到检测包,应该回应一个包。
  4. 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器。
  5. 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了。

心跳检测代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//心跳检测
var heartCheck = {
timeout: 60000,//60秒
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
ws.send("HeartBeat");
self.serverTimeoutObj = setTimeout(function(){//如果超过一定时间还没重置,说明后端主动断开了
ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout)
}, this.timeout)
}
}

WebSocket心跳重连机制完整代码:

参考文献:自定义事件——Event和CustomEvent自定义事件对象 CustomEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// ws.js
let socket = null;
let wsUrl = ""; // 重连的url
let lockReconnect = false;
let lockReconnectTimer = null;

function createWebSocket(url) {
if (window.WebSocket) {
socket && socket.close();
wsUrl = url;
socket = new WebSocket(url);
init();
} else {
console.log("您的浏览器不支持 WebSocket!");
}
}

function init() {
socket.onopen = socketOnopen;
socket.onmessage = socketOnmessage;
socket.onerror = socketOnerror;
socket.onclose = socketOnclose;
}

function socketOnopen() {
console.log("连接成功");
heartCheck.reset().start();
}

function socketOnmessage(event) {
// console.log(event);
heartCheck.reset().start();
window.dispatchEvent(
new CustomEvent("onMessage", {
detail: {
data: event.data,
},
})
);
}

function socketOnerror() {
console.log("连接失败");
heartCheck.reset();
reconnet(); // 其实可以通过打印发现,websocket走了onerror,就会自动立即走onclose
}

function socketOnclose() {
console.log("websocket已断开......正在尝试重连!", socket.readyState);
heartCheck.reset();
reconnet();
}

/**
* 手动发送消息
* @param {any} msg 发送的消息
*/
function handleSocketSend(msg) {
if (socket.readyState === WebSocket.OPEN) {
// console.log('WebSocket连接已经打开');
socket.send(msg);
} else {
// console.log('WebSocket连接未打开');
reconnet();
}
}

// 手动关闭
function handleSocketClose() {
socket.close();
socket = null;
}

/**
* 重连
* @param {string} url ws地址
* @param {number} timeout 重连时间
* @returns
*/
function reconnet(url = wsUrl, timeout = 1000) {
if (lockReconnect) {
return;
}
lockReconnect = true;
lockReconnectTimer && clearTimeout(lockReconnectTimer);
lockReconnectTimer = setTimeout(function () {
createWebSocket(url);
console.log(`正在重连,重连时间${new Date().toLocaleString()}`);
lockReconnect = false;
}, timeout);
}

// 心跳检测
const heartCheck = {
timeout: 10000,
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
const that = this;
this.timeoutObj = setTimeout(function () {
// socket.send("ping");
handleSocketSend("ping");
that.serverTimeoutObj = setTimeout(function () {
console.log("ping timeout");
socket.close();
}, that.timeout);
}, this.timeout);
},
};
1
2
3
4
5
6
7
8
9
10
11
// index.html
<script src="./ws.js"></script>
<script>
createWebSocket("ws://192.189.11.22/a"); // 创建ws
handleSocketSend("发送ws消息"); // 发送消息

window.addEventListener("onMessage", getSocketDaata); // 监听ws消息
function getSocketDaata(e) {
console.log(JSON.parse(e.detail.data));
}
</script>

参考文献