HTTP,WebSocket 和 聊天室

4,008 阅读12分钟

前言

统一回复私信

想交个朋友的可以添加 微信号:Mr10212021 ,也欢迎关注同名公众号《熊的猫》,文章会同步更新!

在涉及到网络层面的相关内容时,不免会联系到 HTTP、TCP、WebSocket 等,但相信大部分人都并不是很清楚其中的一些关系和概念,特别是需要你去做语言表述时,网上有不少优秀的资料和文章,但知识仍需要自己去消化和总结,于是有了本文!!!

3DED65F8.gif

本文的核心内容就是 WebSocket,主要从以下几个方面来进行介绍和实践:

  • 是什么 WebSocketwhat
  • 为什么需要 WebSocketwhy
  • WebSocketHTTP 的关系
  • WebSocket 的使用场景 (when / where
  • WebSocket 实现简易聊天室(how

吾乃 WebSocket

为了更好的解释 WebSocket 是什么,就需要一个熟悉且常用的东西来作为对比参考,它就是 HTTP。

HTTP

在大多数项目都会使用 HTTP 协议来实现前后端交互,而 HTTP 协议又是基于 TCP 协议来建立连接的,它们的关系是大致如下:

image.png

全双工 & 半双工

所谓 全双工 指的是发送端和接收端是可以 随时(包含同一时刻) 向对方发送消息进行通信,而 半双工 指的是发送端和接收端是也可以向对方发送消息进行通信,但是 同一时刻 只能有一方进行发送动作。

我们常用 HTTP1.1 协议就属于 半双工,即服务端不具备主动推送数据资源给客户端的能力,当服务端需要推送数据给客户端时,必须要客户端先发起一个请求,这也被称为 请求-响应 模型。

长轮询 & 服务端推送

由于 HTTP1.1 协议并不支持服务端主动向客户端发送数据消息,但实际需求又有需要实现这样的功能,比如扫码登录:

基于 请求-响应 模型如果我们需要服务端的消息数据,就必须先向服务端发送对应的查询请求,因此只要每隔一段时间向服务器发起查询请求,在根据响应结果决定是继续下一步操作,还是继续发起查询。

但这个查询请求是需要设定时间间隔,而时间间隔可结合 HTTP 请求超时时间来得出,毕竟如果一直发送查询请求,就会得到很多无意义的查询请求和响应结果。

上述在用户无感知下实现 服务端推送 功能的方案,其实就是所谓的 长轮询

WebSocket

HTTP & 超文本

前面我们知道了 HTTP1.1 是 半双工,而 HTTP 是基于 TCP 的,然后 TCP 是 全双工 的,也就是说 HTTP1.1 中根本没用到 TCP 的 全双工 的能力,这是为啥?

早期 HTTP(超文本传输协议) 主要目的就是传输超文本,因为当时网络上绝大多数的资源都是纯文本,许多通信协议也都使用纯文本,因此 HTTP 在设计上不可避免地受到了时代的限制。

基于 TCP 的新协议

由于 HTTP 存在早期设计上的限制,因此需要一种新的基于 TCP 实现 全双工通信 的协议,而这个协议就是 WebSocket

WebSocket 其中的 Socket 其实并不是一个协议,它是为了方便使用 TCP 或 UDP 而抽象出来的一层,是位于应用层和传输控制层之间的一组接口.

建立连接

从上述内容可以知道,HTTP 和 WebSocket 都是基于 TCP 的,因此建立连接的过程肯定少不了大家熟知的 TCP 三次握手,关于这部分在早前的 重新认识 TCP 三次握⼿和四次挥⼿ 一文中有过相关的介绍,这里不过多阐述。

接下来我们从下面的几个问题来一窥究竟!!!

服务端如何知道本次的协议是 WebSocket 呢?

答案很简单,就是通过 请求头 来做标识:

  • Connection: Upgrade
    • Connection 特定于连接的标头字段,最常见的指定值包含 "close、keep-alive" 等,它不能与 HTTP/2 一起使用
  • Upgrade: websocket
    • HTTP 1.1 中用 Upgrade 标头可用于将已建立的客户端/服务器连接升级到不同的协议(通过相同的传输协议)
    • 例如,客户端可以使用它来将连接从 HTTP 1.1 升级到 HTTP 2.0,或者将 HTTPHTTPS 连接升级到 WebSocket
    • 而在 HTTP/2 中是被明确禁止使用这种机制/标头
  • Sec-WebSocket-Key
    • 是由浏览器随机生成的一个 Base64 编码字符串值
  • Sec-WebSocket-Version
    • 表明客户端所使用的协议版本
  • Sec-WebSocket-Extensions
    • 表示客户端想要表达的协议级的扩展

服务端响应 101 状态码表示什么?

  • 客户端发送了对应的关于将协议升级的信息 Upgrade: websocket,但并不会立马就会建立连接,因为服务端必须也要支持 websocket 协议,否则就会失败
  • 如果服务端支持 websocket 协议,那么服务端就会返回状态码为 101 且带有响应头 Sec-WebSocket-Accept 的消息给客户端
    • Status Code: 101 表示指服务器将按照请求头信息变为一个不同的协议,是 HTTP 1.1 中新加入的
    • Sec-WebSocket-Accept 中存放的是在请求头 Sec-WebSocket-Key 中经过特定加密算法得到的结果

什么时候连接建立完成?

其实在前两个问题中就已经完成 WebSocket 的连接了,也就是 两次握手

  • 客户端发送携带 Connection: Upgrade、Upgrade: websocket、Sec-WebSocket-Key: VE9X2xPp8teUvHEvmBL8Tw== 等必要信息请求头的请求给服务端
  • 服务端若支持对应的协议升级,则会响应客户端,并返回状态码为 101 且响应头携带 Sec-WebSocket-Accept 值为 加密算法 处理 Sec-WebSocket-Key 后的值
  • 经过两次 HTTP 握手后,后续的通信就由 websocket 协议接手

image.png

WebSocket 使用场景

WebSocket 的特性非常适用于 即时性高 的服务场景,例如:

  • 视频弹幕类
  • 协同编辑类
  • 媒体聊天类
  • 多玩家游戏类
  • 实时监控类(如 运动轨迹)
  • 扫码登录
  • ......

相信这些大家都没有什么疑问,但值得思考的是,在大多数的项目里也未必会真正的使用到 WebSocket,这一点是值得思考的。

为什么大多可以用 WebSocket 的场景,却没有真正使用呢?

打开网页上随处可见的客服聊天窗口,结合上述的介绍,也许你就会认为是通过 WebSocket 实现的,但其实不然,例如:

你会发现这样的场景下,仍然没有使用 WebSocket ,其实多是因为历史项目的兼容性问题,WebSocket 的特性虽好用,但 WebSocket 仍然不是 HTTP

如果之前系统链路是针对 HTTP 协议设计的,那么改造带来的中间风险与特殊处理的地方是否能把控好?这样的改造成本与收益是否合理?

因此,大多数场景下还是会使用:短连接(心跳包测试)+ 长链接(接收消息) 的方式来实现。

服务端推送并非 WebSocket 不可

WebSocket 看起来真香,但并不是所有场景都可以直接使用,在只需要 服务端推送 的场景下,也并非没有其他方式可以选择,如 EventSourceServer Push 就可实现。

EventSource — SSE(Server-Sent-Events)

EventSource 是服务器推送的一个网络事件接口。一个 EventSource 实例会对 HTTP 服务开启一个 持久化 的连接,以 text/event-stream 格式发送事件,会一直保持开启直到被要求关闭。

换句话说,EventSource 基于 HTTP 协议的 单向通信,即数据信息被单向从 服务端客户端 分发,当不需要以消息形式将数据从 客户端 发送到 服务器 时,EventSource 无疑是一个有效方案。

这里不在讲具体用法,具体用法可 参考此处

优点

  • SSE 基于 HTTP 的轻量级协议,改造成本不大
  • SSE 默认支持 断线重连
  • SSE 支持发送 自定义数据类型

缺点

  • SSE 不支持 CORS,参数 url 表示服务器网址,且 url协议、域名、端口 需要与当前页面的网址保持一致
  • 兼容性较差,只适用高级浏览器,不支持 IE 浏览器
  • 只支持 单向通信,即只能服务器向客户端推送数据,客户端无法向服务器端推送数据

HTTP/2 (Server Push)

HTTP 的每个版本都是基于之前的版本来试下优化的,就像 HTTP/1.1 是基于 HTTP/1.0 进行的优化,同样的 HTTP/2 也是基于 HTTP/1.1 进行的优化。

为了解决 HTTP/1.1 链接需要请求以正确的顺序发送,理论上可用一些并行的链接(58 个)所带来的成本和复杂性,在 2010 年早期,谷歌通过实践了一个实验性的 SPDY 协议,随后明确了响应数量的增加和解决复杂的数据传输,SPDY 成为了 HTTP/2 协议的基础。

HTTP/2 相比于 HTTP1.1 的特性

  • HTTP/2二进制协议 而非 HTTP1.1文本协议
  • HTTP/2 支持 多路复用,即并行请求可在 同一个 TCP 连接 中处理
  • HTTP/2 采用 HPACK 算法实现 头部压缩,在客户端和服务器间建立 “字典”,用索引号表示重复的字符串,并采用哈夫曼编码来压缩整数和字符串
  • HTTP/2 提供 Server Push 服务端推送 的能力实现提前请求

而其中 Server Push 服务端推送缺点 就是只能向客户端推送 静态资源,而不能推送 自定义数据

所谓的 Server Push 这里举个例子就很容易理解了:

  • HTTP/2 之前访问一个站点:
    • 服务器返回对应的 xxx.html 文件
    • 客户端预解析 xxx.html 文件
    • 根据预解析的识别到的 link、script 标签等并行加载文件资源
    • ...
  • HTTP/2 后访问一个站点:
    • 服务器返回对应的 xxx.html 文件,同时可以返回相应的 x.css、x.js 等资源,即实现了静态资源的 提前请求,于是就能加快页面的渲染和显示

Chrome 将移除对 Server Push 支持

稍微总结一下,Server Push 会在响应 HTML 文件时,服务器 会同时将所需的资源文件 主动推送 给浏览器,并将资源会缓存到本地,当解析 HTML 时所需加载的资源会直接从本地缓存中读取,不需要再额外等待网络传输。

看起来似乎没什么大问题,但却有着比较大的 缺陷

  • Server Push 难以避免的会推送浏览器已经拥有的 子资源,因为很多资源会在浏览器 首次请求 被响应时就已缓存下来
  • 这样的 过度推动/无意义推送 会导致 网络带宽使用效率降低,因此会明显降低整体的性能优势

总体而言,Chrome 数据显示 HTTP2/Push 实际上对整个网络的性能产生了 负面影响,因此 Chrome 宣布将在下一个主要版本(Chrome 106)中将删除对 Server Push 的支持,点此了解更多

WebSocket 实现聊天室

上面说了那么多,终究还是得落实到代码上!

543B41AA.png 543B41AA.png 543B41AA.png

实现功能

以下实现的都是以极简的方式实现的,很多东西不会考虑的很全面,例如数据存储方面完全没有用到数据库,大家感兴趣可以自己完善:

  • 用户注册
    • 昵称标识
    • 生成用户 uuid
  • 入群欢迎提示
    • 根据 uuid 判断用户是否首次加入群聊,若是则进行欢迎提示
  • 群聊消息收发
    • 当前用户发送消息时,通知服务端将聊天信息存储在服务端的 chatList
    • chatList 列表数据发生变化,通过广播的方式发送给其他连接的用户
  • 群消息同步
    • 用户每次进入聊天室,会同步接收其他用户之前的聊天信息
  • .gif 表情包
    • 表情包数据存储在 emotions.json 文件中
    • 用户选择表情后会作为对应的字符添加到消息中,当该消息同步给其他用户时,会根据 emotions.json 的数据做匹配和生成
  • 头像切换
    • 当前用户可通过点击自己的头像选择对应的图片替换默认头像
    • 因为不涉及图片存储,因此通过 FileReader 将接收到的 File 文件转成 base64 的格式

效果演示

源码

戳此可直达源码位置:源码地址

源码中通过 socket.iosocket.io-client 来实现,主要是因为 socket.io 则已经做了很多基础性工作,能够很好地与一些主流的技术集成,开发者只需要完成一些简单的配置即可。

前端部分

  • 可通过 npm run dev 启动

服务端部分

  • 可通过 npm run server 启动

局域网访问

  • 若要支持局域网访问,则需要在 vite.config.ts 指定 host 配置选项,对应的 IP 地址可通过 ipconfigCMD 终端上查看
  • 若仍不支持局域网访问,可尝试短暂关闭防火墙后再访问

统一回复私信

想交个朋友的可以添加 微信号:Mr10212021 ,也欢迎关注同名公众号《熊的猫》,文章会同步更新!

参考