近期公司做IM,用户与后台专家聊天的需求,顺便小研究下webSocket。
webSocket的目标是在一个单独的持久链接上提供全双工,双向通信。
为了解决什么问题:
- 传统的客户端与服务器通信,请求只能由客户端发起,服务器无法向客户端主动推送消息。
- 为了实时获取最新数据,长链接,轮循方式效率低。
- webSocket,让服务器可以主动向客户端推送数据,使浏览器具备了双向通信的能力。
在js中创建webSocket 之后,会有一个http请求发送到服务器来发起链接。在取得服务器响应后,建立的链接会使用http升级从http协议交换为socket协议。
也就是说,使用标准的http服务无法使用socket,只有支持这种协议的专门服务器才能正常工作。
由于websocket使用了自定义协议,url模式也略有不同,是ws:// 和wss://。
使用自定义协议的好处是,能够在客户端和服务器之间发送少量的数据,而不担心http那样字节级的开销。
使用自定义协议的缺点在于,制定协议的时间比制定jsAPI的时间还要长。webSocket曾几度搁浅,因为不断有人发现新协议存在一致性和安全性的问题,不过现在已经统一了。
socket优点:
- 双向通信。
- 数据格式轻量,性能开销小,通信高效。
- 支持二进制。
- 没有同源策略限制。
- 与http协议良好兼容,握手阶段采用http协议,因此能通过各种http代理服务器。
使用socket需要解决问题有:
webSocket不稳定,如果是有服务端主动关掉,会触发客户端关闭的回调。但是如果出现其他方面的通信异常,客户端或服务端是无法感知到当前socket已经不能正常工作了。
总结有两种办法:
在error和close事件中都触发重连,除非手动关闭。
优点是方便一点,缺点在重连这段事件可能会丢数据,可能有异常不会触发这两个事件。
心跳机制:
每个一段事件发送一次ping到服务端,来告诉服务端,我还在,服务端返回一个pong,告诉客户端,嗯,我也在。
心跳有两种策略,
一种你直接发送一个ping,如果紧接着返回一个pong,那么在过程中发送的消息如何界定成功与失败。如果ping与pong返回错乱,无法保证互相的对应,除非为每一个ping添加标示ID,同时返回pong也携带标识ID。
一种你没发送两次ping,去检测一次pong,如果检测到有一测pong,就认为当前socket的状态是正常的,如果两次都没有返回pong,则认为socket异常。
其实最后没有用心跳来检测当前socket状态,心跳只是为了维持与后台的socket连接,因为后台有超时时间,如果超时时间内没有数据交互,则会断开socket。
问题点:
如何判断当前socket状态,来进而判断socket是否有发送成功。
以及在何时触发重连。
close事件,
eror时间,
内部readyState,
心跳状态。
websocket 的close和error事件很怪。有人说socket 的底层也是用心跳来说实现的,但是断网完全不会触发socket的close事件,所以说断网的逻辑还需要单独添加。
同时也说明,内部的readystate也不靠谱。更为严格的说,socket发消息还需要确认逻辑,但是实现起来更为复杂,给每条消息赋值一个唯一的ID,然后服务器接收到消息后再将次ID返回,用于发送成功的确认。同时服务器给用户推送消息也需要确认。
封装了一个简单的socket类:
包含收发消息,心跳检测,中断重连,限制重连次数。
class SocketClass{
constructor(params){
this.params = params;
const NOOP = () => {};
let {url, name = 'default', socketOpen = NOOP, msgCbk = NOOP} = params;
this.url = url;
this.msgCbk = msgCbk;
this.socketOpen = socketOpen;
this.name = name;
this.ws = null; // websocket对象
this.status = ''; // websocket是否被手动关闭
this.pingPong = '';
this.pingInterval = null;
this.pongInterval = null;
this.reConnectTime = 1; // 重连限制时间 minuit
this.firsReConnectTime = ''; // 内部用 记录第一次重连时间
this.reConnectCount = 0; // 内部用 当前时间段内已经重连次数
this.reConnectMaxCount = 5; // 重连限制时间内的最大重连次数,
this.reConnectFlag = false; // 超过次数限制 正在等待重连的标志位
}
connect(firstData) {
this.ws = new WebSocket(this.url);
this.ws.onopen = e => {
this.status = 'open';
this.heartCheck();
this.socketOpen();
if (firstData) {
this.ws.send(firstData);
}
};
this.ws.onmessage = e => {
if (e.data === 'ping') {
this.pingPong = 'pong';
}
return this.msgCbk(e.data);
};
this.ws.onclose = e => {
console.warn('close');
this.closeHandle(e);
};
this.ws.onerror = e => {
console.warn('error');
this.closeHandle(e);
}
}
sendMessage(data) {
if(this.ws.readyState === 1){
this.ws.send(data);
return true;
}
return false;
}
heartCheck() {
this.pingPong = 'ping';
this.pingInterval = setInterval(() => {
if (this.ws.readyState === 1) {
this.ws.send('ping');
}
}, 10000);
this.pongInterval = setInterval(() => {
if (this.pingPong === 'pong') {
this.closeHandle('pingPong没有改变为pong');
}
this.pingPong = 'ping'
}, 20000)
}
closeHandle(e = 'err') {
if (this.status === 'close') {
console.log(`${this.name}websocket手动关闭`)
return;
}
if(!this.firsReConnectTime){
this.firsReConnectTime = new Date().getTime();
}
if((new Date().getTime() - this.firsReConnectTime) < this.reConnectTime * 60 * 1000){
if(this.reConnectCount < (this.reConnectMaxCount - 1) ){
this.reConnectCount ++;
}else{
if(!this.reConnectFlag){
console.warn('一分钟内重连超过五次, 取消重连');
this.reConnectFlag = true;
setTimeout(() => {
this.connect();
this.reConnectFlag = false;
}, 60000);
}
return;
}
}else{
this.reConnectCount = 0;
this.firsReConnectTime = new Date().getTime();
}
if (this.pingInterval !== undefined && this.pongInterval !== undefined) {
clearInterval(this.pingInterval);
clearInterval(this.pongInterval);
}
this.connect();
}
closeSocket() {
this.status = 'close';
return this.ws.close();
}
}
msgCbk = (msg) => {
console.warn(msg);
};
const wsValue = new SocketClass({
// url: 'ws://123.207.136.134:9010/ajaxchattest',
url: 'ws://127.0.0.1:8080',
msgCbk,
});
wsValue.connect('立即与服务器通信'); // 连接服务器
let i = 0;
setInterval(() => {
i++;
console.warn(wsValue.sendMessage(`传消息给服务器${i}`));
wsValue.sendMessage(`传消息给服务器${i}`);
}, 1000);
|