写点什么

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:484944

评论

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

如何访问TDH中Inceptor 底层的元数据库TxSQL

明哥的IT随笔

大数据 hive

用友系列之 YonBuilder 低代码平台概论和基本使用

YonBuilder低代码开发平台

低代码 可视化

活动预告 | 中国数据库联盟(ACDU)中国行第三站定档成都,邀您探讨数据库前沿技术

墨天轮

MySQL 数据库 oracle postgresql zabbix

华为阅读“鲁迅专栏”已上线,读国内名家作品就上华为阅读

最新动态

为什么要使用zookeeper

Jerry Tse

zookeeper 分布式锁 分布式系统 共识算法 数据强一致性

IPQ9574 IPQ9554 QCN9274 QCN6274 WIFI7 SolutionUnlocking the Potential of Wi-Fi 7

wallyslilly

ipq9554 qcn9274 qcn6274 ipq9574

ICCV 2023|小红书 4 篇入选论文亮点解读,「开集视频目标分割」获得 Oral

小红书技术REDtech

算法 ICCV

WorkPlus私有化部署IM即时通讯平台,构建高效安全的局域网办公环境

WorkPlus

如何在低代码平台中应用可视化编程

力软低代码开发平台

火山引擎边缘云:数智化项目管理助力下的业务增长引擎

火山引擎边缘云

数字化 飞书 数智化 #项目管理

IoTDB 在国际数据库性能测试排行榜中位居第一?测试环境复现与流程详解第一弹!

Apache IoTDB

TinyEngine 低代码引擎到底是什么?

英勇无比的消炎药

开源 前端 低代码

OpenHarmony自定义组件介绍

OpenHarmony开发者

OpenHarmony

华为3场重磅主题演讲先睹为快,顶级云原生&开源盛会即刻出发

华为云开源

华为 开源 云原生 KubeCON

彻底告别传统FTP,新的替代FTP产品比你想象的好的多

镭速

传输协议 FTP传输替代方案

rabbitMQ到底是个啥东西?

程序员万金游

Java 开发 #java Rabbit MQ

Redis类型(Type)与编码(Encoding)

Java随想录

redis

Java训练营毕业总结

jjn0703

上新啦!腾讯云云原生数据湖产品DLC 2.2.5版本发布,来看特性详解

腾讯云大数据

数据湖

HarmonyOS使用多线程并发能力开发

HarmonyOS开发者

HarmonyOS

Generative AI 新世界 | 扩散模型原理的代码实践之采样篇

亚马逊云科技 (Amazon Web Services)

机器学习 #人工智能 生成式人工智能 Amazon SageMaker 大语言模型

WorkPlus Meet 视频会议,自主可控,支持私有化部署

WorkPlus

Github上线即遭狂转!上百人通过这份算法手抄本成功上岸字节

程序员万金游

#java java 架构 #算法 #数据结构 #java编程

开启 Kerberos 安全认证的大数据环境中如何正确指定 HS2 的 jdbc url 地址?

明哥的IT随笔

大数据 hive kerberos

从技术创新到应用实践,百度智能云发起大模型平台应用开发挑战赛!

不叫猫先生

百度智能云 千帆大模型平台

企业内部通讯,WorkPlus助您打造高效沟通平台

WorkPlus

Redis内存碎片:深度解析与优化策略

Java随想录

Java redis

华为云API对话机器人CBS的魅力—要是有AI,我要做“李白”- 5分钟开发作诗机器人

华为云PaaS服务小智

云计算 软件开发 华为云

跨网传输文件时,如何通过日志记录来审计追溯?

镭速

跨网文件传输

使用Optional优雅避免空指针异常

Java随想录

Java 异常

福利贴|这是一个程序员不看一定会后悔的问题

Zilliz

非结构化数据 Milvus Zilliz 向量数据库

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