写点什么

WebSocket 原理浅析与实现简单聊天

  • 2021-03-06
  • 本文字数:3692 字

    阅读完需:约 12 分钟

WebSocket 原理浅析与实现简单聊天

随着 Web 的发展,用户对于 Web 的实时推送要求也越来越高,在 WebSocket 出现之前,大多数情况下是通过客户端发起轮询来拿到服务端实时更新的数据,因为 HTTP1.x 协议有一个缺陷就是通信只能由客户端发起,服务端没法主动给客户端推送。这种方式在对实时性要求比较高的场景下,比如即时通讯、即时报价等,显然会十分低效,体验也不好。为了解决这个问题,便出现了 WebSocket 协议,实现了客户端和服务端双向通信的能力。介绍 WebSocket 之前,还是让我们先了解下轮询实现推送的方式。

短轮询(Polling)


短轮询的实现思路就是浏览器端每隔几秒钟向服务器端发送 HTTP 请求,服务端在收到请求后,不论是否有数据更新,都直接进行响应。在服务端响应完成,就会关闭这个 TCP 连接,代码实现也最简单,就是利用 XHR, 通过 setInterval 定时向后端发送请求,以获取最新的数据。


setInterval(function() {  fetch(url).then((res) => {      // success code  })}, 3000);
复制代码

优点:实现简单。

缺点:会造成数据在一小段时间内不同步和大量无效的请求,安全性差、浪费资源。


长轮询(Long-Polling)


客户端发送请求后服务器端不会立即返回数据,服务器端会阻塞请求连接不会立即断开,直到服务器端有数据更新或者是连接超时才返回,客户端才再次发出请求新建连接、如此反复从而获取最新数据。大致效果如下:



客户端代码如下:

function async() {    fetch(url).then((res) => {    	async();    	// success code	}).catch(() => {		// 超时        async();	})}
复制代码

优点:比 Polling 做了优化,有较好的时效性。

缺点:保持连接挂起会消耗资源,服务器没有返回有效数据,程序超时。


WebSocket


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


当出现类似体育赛事、聊天室、实时位置之类的场景时,轮询就显得十分低效和浪费资源,因为要不断发送请求,连接服务器。WebSocket 的出现,让服务器端可以主动向客户端发送信息,使得浏览器具备了实时双向通信的能力。


没用过 WebSocket 的人,可能会以为它是个什么高深的技术。其实不然,WebSocket 常用的 API 不多也很容易掌握,不过在介绍如何使用之前,让我们先看看它的通信原理。


通信原理


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


在客户端建立一个 WebSocket 连接非常简单:


let ws = new WebSocket('ws://localhost:9000');
复制代码


类似于 HTTP 和 HTTPS,ws 相对应的也有 wss 用以建立安全连接,本地以 ws 为例。这时的请求头如下:


Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Cache-Control: no-cacheConnection: Upgrade	// 表示该连接要升级协议Cookie: _hjMinimizedPolls=358479; ts_uid=7852621249; CNZZDATA1259303436=1218855313-1548914234-%7C1564625892; csrfToken=DPb4RhmGQfPCZnYzUCCOOade; JSESSIONID=67376239124B4355F75F1FC87C059F8D; _hjid=3f7157b6-1aa0-4d5c-ab9a-45eab1e6941e; acw_tc=76b20ff415689655672128006e178b964c640d5a7952f7cb3c18ddf0064264Host: localhost:9000Origin: http://localhost:9000Pragma: no-cacheSec-WebSocket-Extensions: permessage-deflate; client_max_window_bitsSec-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: UpgradeSec-WebSocket-Accept: ZUip34t+bCjhkvxxwhmdEOyx9hE=Upgrade: websocket
复制代码



此时响应行(General)中可以看到状态码 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(https://tools.ietf.org/html/rfc6455#section-5.5.2)官方定义的一个固定字符串,不得修改。


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


实现简单单聊


下面来实现一个纯文字消息类型的一对一聊天(单聊)功能,废话不多说,直接上代码,注意看注释。


客户端:


function connectWebsocket() {    ws = new WebSocket('ws://localhost:9000');    // 监听连接成功    ws.onopen = () => {        console.log('连接服务端WebSocket成功');        ws.send(JSON.stringify(msgData));	// send 方法给服务端发送消息    };
// 监听服务端消息(接收消息) ws.onmessage = (msg) => { let message = JSON.parse(msg.data); console.log('收到的消息:', message) elUl.innerHTML += `<li class="b">小秋:${message.content}</li>`; };
// 监听连接失败 ws.onerror = () => { console.log('连接失败,正在重连...'); connectWebsocket(); };
// 监听连接关闭 ws.onclose = () => { console.log('连接关闭'); };};connectWebsocket();
复制代码


从上面可以看到 WebSocket 实例的 API 很容易理解,简单好用,通过 send() 方法可以发送消息,onmessage 事件用来接收消息,然后对消息进行处理显示在页面上。当 onerror 事件(监听连接失败)触发时,最好进行执行重连,以保持连接不中断。


服务端 Node: (这里使用 ws 库)


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('连接关闭'); });});
复制代码


同理,服务端也有对应的发送和接收的方法。完整示例代码见 这里


这样浏览器和服务端就可以愉快的发送消息了,效果如下:



其中绿色箭头表示发出的消息,红色箭头表示收到的消息。


心跳保活


在实际使用 WebSocket 中,长时间不通消息可能会出现一些连接不稳定的情况,这些未知情况导致的连接中断会影响客户端与服务端之前的通信,


为了防止这种的情况的出现,有一种心跳保活的方法:客户端就像心跳一样每隔固定的时间发送一次 ping,来告诉服务器,我还活着,而服务器也会返回 pong,来告诉客户端,服务器还活着。

ping/pong 其实是一条与业务无关的假消息,也称为心跳包。


可以在连接成功之后,每隔一个固定时间发送心跳包,比如 60s:


setInterval(() => {    ws.send('这是一条心跳包消息');}, 60000)
复制代码

总结


通过上面的介绍,大家应该对 WebSocket 有了一定认识,其实并不神秘,这里对文章内容简单总结一下。当创建 WebSocket 实例的时候,会发一个 HTTP 请求,请求报文中有个特殊的字段 Upgrade,然后这个连接会由 HTTP 协议转换为 WebSocket 协议,这样客户端和服务端建立了全双工通信,通过 WebSocket 的 send 方法和 onmessage 事件就可以通过这条通信连接交换信息。



头图:Unsplash

作者:叶秋

原文:https://mp.weixin.qq.com/s/yherdrW435BRXOXIoITryw

原文:WebSocket 原理浅析与实现简单聊天

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-03-06 23:485046

评论

发布
暂无评论
发现更多内容

【LeetCode】形成两个异或相等数组的三元组数目Java题解

Albert

算法 LeetCode 5月日更

为什么TCP 建连接要3次,断连接却要4次呢?

架构精进之路

TCP 网络层 5月日更

打破思维定式(十二)

Changing Lin

Sentinel在docker中获取CPU利用率的一个BUG

捉虫大师

Java Docker sentinel

奇亚chia算力挖矿系统开发搭建

薇電13242772558

区块链 算力

Apache Flink在 bilibili 的多元化探索与实践

Apache Flink

大数据 flink 流计算 实时计算

在 Mac 上玩网游的简单方式

懒得勤快

实践解析 | 如何用 OpenGL 实现跨平台应用高效渲染

拍乐云Pano

Android开发

Golang Array and Slice

escray

学习 极客时间 Go 语言 5月日更

2021年5月墨天轮国产数据库排行榜:十强榜单固若金汤

墨天轮

数据库 腾讯云 阿里云 国产化 dba

快时代的知识形态

Ryan Zheng

数字化转型助推,200亿元数据治理市场空间充满想象

DT极客

千万级学生管理系统考试试卷存储方案设计

eoeoeo

多吃点花生米吧!

Nydia

学习

百度 Serverless 函数计算引擎 EasyFaaS 正式开源

百度开发者中心

百度 开源

云时代的数据之约

BinTools图尔兹

数据库 云计算 运维 云服务 dba

去年创建的个人网站,我又给它加多了一些新功能。

彭宏豪95

写作 网站 博客 5月日更

iOS 面试策略之系统框架-网络、推送与数据处理

iOSer

ios

MySQL慢日志全解析

Simon

MySQL 慢查询

还不会使用分布式锁?教你三种分布式锁实现的方式

华为云开发者联盟

数据库 zookeeper 分布式锁 etcd 数据库表

华为发布HarmonyOS Connect品牌升级计划 帮伙伴做好产品、卖好产品、运营好产品

科技汇

ShardingSphere 源码

云淡风轻

ShardingSphere

Kafka万亿级消息实战

vivo互联网技术

kafka 集群

云小课 | 大数据融合分析:GaussDW(DWS)轻松导入MRS-Hive数据源

华为云开发者联盟

华为云 GaussDB(DWS) MRS数据源 融合分析 数据仓库服务

Python 加密解密信息 - DAY9

Qien Z.

Python 加密解密 5月日更

Elasticsearch数据库优化实战:让你的ES飞起来

华为云开发者联盟

数据库 大数据 elasticsearch 日志 ES

牛!马士兵亲自教授坦克大战+精通23种设计模式,视频+笔记+源码

Java架构追梦

Java 架构 面试 23种设计模式 坦克大战

快乐是一种选择

BY林子

快乐

鸿蒙轻内核M核源码分析:数据结构之任务就绪队列

华为云开发者联盟

鸿蒙 数据结构 数组 双向循环链表 任务就绪队列

面试官:啥是请求重放呀?

why技术

Java

话题讨论|做程序员五年后是什么样子?

饭饭

程序员 职业规划 发展现状 内卷 IT行业

WebSocket 原理浅析与实现简单聊天_文化 & 方法_政采云前端团队_InfoQ精选文章