websocket初识

为什么需要 WebSocket?

我们已经有HTTP协议了,为什么还需要另一个协议?它能给我们带来什么好处呢?

原因是:HTTP协议有一个缺陷:通信只能由客户端发起,做不到服务器主动向客户端推送信息。这种单向请求的特点意味着如果服务器有连续的状态变化,客户端获取就会变的非常麻烦,只能使用轮询的方法获取,其中最典型的就是聊天场景了。而websocket是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。

短轮询(Polling)

短轮询的实现思路是浏览器端每隔几秒钟向服务器端发送HTTP请求,服务端在收到请求后,不论是否有数据更新,都直接进行响应。服务端响应完成,关闭这个TCP连接,代码实现也简单,如下所示:

setInterval(function() {
  // ajax请求
}, 3000);

优点:实现简单。
缺点:会导致数据在一小段时间内不同步和大量无效的请求,安全性差、浪费资源。

长轮询(Long-Polling)

长轮询的实现思路是客户端发送请求后服务器端不会立即返回数据,服务器端会阻塞请求连接不会立即断开,直到服务器端有数据更新或者是连接超时才返回,客户端才再次发出请求新建连接、如此反复获取最新数据。如下图所示:

在这里插入图片描述

  • 优点:在短轮询基础上做了优化,有较好的时效性。
  • 缺点:保持连接挂起会消耗资源,服务器没有返回有效数据,程序超时。

短轮询和长轮询, 都是先由客户端发起Ajax请求,才能进行通信,走的是HTTP协议,服务器端无法主动向客户端推送信息。

websocket是什么

websocket是一种网络通信协议,我们知道http协议只能从客户端主动发起,不能从服务端推送数据到客户端,websocket是一种不仅能从客户端发送数据到服务端,也可主动从服务的推送数据给客户端的一种协议。我们先来看一张经典图:
在这里插入图片描述

从上述图中我们可知:

  1. http请求是客户端发起请求,服务端响应,然后断开连接,客户端发起,服务端响应的一种循环。
  2. websocket协议是客户端发起连接后,就会一直保持连接,期间客户端和服务端都可以向对方发送数据,直到连接关闭。
websocket特点
  1. 建立在TCP协议之上,服务器端的实现比较容易
  2. 与HTTP协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用HTTP协议,因此握手时不容易屏蔽,能通过各种HTTP代理服务器
  3. 数据格式比较轻量,性能开销小,通信高效
  4. 可以发送文本,也可以发送二进制数据
  5. 没有同源限制,客户端可以与任意服务器通信
  6. 协议标识符是ws(如果加密,是wss),服务器网址就是URL

在这里插入图片描述

websocket通信原理

当客户端和服务端建立WebSocket连接时,在客户端和服务器的握手过程中,客户端首先会向服务端发送一个 HTTP 请求,包含一个Upgrade请求头来告知服务端客户端想要建立一个WebSocket连接

// 请求头
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: Upgrade	// 表示该连接要升级协议
Cookie: _hjMinimizedPolls=358479; ts_uid=7852621249; CNZZDATA1259303436=1218855313-1548914234-%7C1564625892; csrfToken=DPb4RhmGQfPCZnYzUCCOOade; JSESSIONID=67376239124B4355F75F1FC87C059F8D; _hjid=3f7157b6-1aa0-4d5c-ab9a-45eab1e6941e; acw_tc=76b20ff415689655672128006e178b964c640d5a7952f7cb3c18ddf0064264
Host: localhost:9000
Origin: http://localhost:9000
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: 5fTJ1LTuh3RKjSJxydyifQ==		// 与响应头 Sec-WebSocket-Accept 相对应
Sec-WebSocket-Version: 13	// 表示 websocket 协议的版本
Upgrade: websocket	// 表示要升级到 websocket 协议
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36

// 响应头

Connection: Upgrade
Sec-WebSocket-Accept: ZUip34t+bCjhkvxxwhmdEOyx9hE=
Upgrade: websocket

// General中
Request URL: ws://localhost:9000
Request Method: GET
Status Code: 101 Switching Protocols

//  status code是101 Switching Protocols ,表示该连接已经从HTTP协议转换为 WebSocket通信协议。 转换成功之后,该连接并没有中断,而是建立了一个全双工通信,后续发送和接收消息都会走这个连接通道。

:请求头中Sec-WebSocket-Key字段,和响应头中的Sec-WebSocket-Accept是配套对应的,作用是提供基本的防护,比如恶意的连接或无效的连接。Sec-WebSocket-Key是客户端随机生成的一个base64编码,服务器会使用这个编码,并根据一个固定的算法。

GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";    //  一个固定的字符串
accept = base64(sha1(key + GUID));	// key是Sec-WebSocket-Key,accept是 Sec-WebSocket-Accept

// 其中GUID字符串是RFC6455 官方定义的一个固定字符串,不得修改。

客户端拿到服务端响应的Sec-WebSocket-Accept后,会拿自己之前生成的Sec-WebSocket-Key用相同算法算一次,如果匹配,则握手成功。然后判断 HTTP Response 状态码是否为 101(切换协议),如果是,则建立连接。

应用场景

我们通过一个例子来想下,比如我们平常生活中买东西,在支付时,支付成功后,需要给用户反馈一个支付成功的提示,那么在websocket应用之前,我们一般使用轮询的方法处理,即客户端定时向服务端发送请求,看有没有收到支付金额,没有就一直发送,收到了再停止。

在发送请求的过程中,浪费了大量的资源,而且响应也不是及时的,比如我是每隔1秒请求一次,但是并不能立刻得到支付成功的状态。这时如果使用websocket的方式,就会及时响应支付状态,websocket一般用在一些能及时响应的场景中。主要应用在如下几种场景中:

  1. 社交订阅

有时我们需要及时收到订阅消息,比如开奖通知、在线邀请,支付结果等

  1. 多玩家游戏

很多游戏都是协同作战的,玩家的操作和状态需要及时同步给所有玩家

  1. 协同编辑文档

同一份文档,编辑状态需要同步到所有参与的用户界面上

  1. 数据流状态

比如上传下载文件,文件进度,文件是否上传成功等

  1. 多人聊天

很多场景下都需要多人参与讨论聊天,用户发送的消息得第一时间同步到所有用户

  1. 股票虚拟货币价格

股票和虚拟货币的价格都是实时波动的,价格跟用户的操作息息相关,及时推送对用户跟盘有很大的帮助

websocket客户端的API

  1. WebSocket 构造函数
// WebSocket对象作为一个构造函数,用于新建WebSocket实例。
var ws = new WebSocket('ws://localhost:8080');
// 执行完上面语句之后,客户端就会与服务器进行连接
  1. webSocket.readyState

readyState属性返回实例对象的当前状态,共有四种:

  • CONNECTING:值为0,表示正在连接。
  • OPEN:值为1,表示连接成功,可以通信了。
  • CLOSING:值为2,表示连接正在关闭。
  • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // TODO
    break;
  case WebSocket.OPEN:
   // TODO
    break;
  case WebSocket.CLOSING:
    // TODO
    break;
  case WebSocket.CLOSED:
   // TODO
    break;
  default:
    // this never happens
    break;
}
  1. webSocket.onopen

实例对象的onopen属性,用于指定连接成功后的回调函数

ws.onopen = function () {
  ws.send('Hello Server!');
}

如果要指定多个回调函数,可以使用addEventListener方法

ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});
  1. webSocket.onclose

实例对象的onclose属性,用于指定连接关闭后的回调函数。

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // TODO
};

// 如果要指定多个回调函数,同上面的写法
  1. webSocket.onmessage

实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。

ws.onmessage = function(event) {
  var data = event.data;
  // TODO
};
注:服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)
ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }
  
  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

除了动态判断收到的数据类型,也可使用binaryType属性,指定收到的二进制数据类型。

// 收到blob数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到ArrayBuffer数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};
  1. webSocket.send()

实例对象的send()方法用于向服务器发送数据。

// 发送文本
ws.send('hello javascript');

// 发送Blob对象
var file = document.querySelector('input[type="file"]').files[0];
ws.send(file);

// 发送ArrayBuffer对象
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);
  1. webSocket.bufferedAmount

实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束

var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送没结束
}
  1. webSocket.onerror

实例对象的onerror属性,用于指定报错时的回调函数。

socket.onerror = function(event) {
  // TODO
};

实现一个简单聊天

实现一个一对一的单聊天功能:

客户端

function connectWebsocket() {
    ws = new WebSocket('ws://localhost:9000');
    // 监听连接成功
    ws.onopen = () => {
        ws.send(JSON.stringify(msgData));	// 给服务端发送消息
    };

    // 监听服务端消息(接收消息)
    ws.onmessage = (msg) => {
        let message = JSON.parse(msg.data);
        console.log('收到的消息:', message);
    };

    // 监听连接失败
    ws.onerror = () => {
    	// 连接失败,正在重连...
        connectWebsocket();
    };

    // 监听连接关闭
    ws.onclose = () => {
    	console.log('连接关闭');
    };
};
connectWebsocket();

服务端

const path = require('path');
const express = require('express');
const app = express();
const server = require('http').Server(app);
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server: server });

wss.on('connection', (ws) => { 
  // 监听客户端发来的消息
  ws.on('message', (message) => {
    console.log(wss.clients.size);
    let msgData = JSON.parse(message);   
    if (msgData.type === 'open') {
      // 初始连接时标识会话
      ws.sessionId = `${msgData.fromUserId}-${msgData.toUserId}`;
    } else {
      let sessionId = `${msgData.toUserId}-${msgData.fromUserId}`;
      wss.clients.forEach(client => {
        if (client.sessionId === sessionId) {
          client.send(message);	 // 给对应的客户端连接发送消息
        }
      })  
    }
  })

  // 连接关闭
  ws.on('close', () => {
    console.log('连接关闭');  
  });

 server.listen(9000, function () {
   console.log('http://localhost:9000');
 });
});

最终效果图如下:

在这里插入图片描述

总结:

  1. websocket是一种类似http的一种通讯协议
  2. websocket最大特点是客户端和服务端能相互给对方发送消息
  3. websocket广泛应用在需要实时通讯的一些场景上面
  4. websocket没有同源限制,而且性能开销小,通信高效
  5. websocket几乎所有浏览器都支持
  6. websocket通过send()方法发送消息,onmessage事件接收消息,然后对消息进行处理显示在页面上。 onerror 事件(监听连接失败)触发时,最好进行执行重连,以保持连接不中断。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值