为什么有了http,还需要websocket,懂了!
前言
初次接触 websocket 的人,可能都会有这样的疑问:我们已经有了 http 协议,为什么还需要websocket协议?它带来了什么好处?
原因是http每次请求只能由客户发起,而websocket最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息
刚好目前项目中用到了websocket,当然关于websocket的知识无论是前端还是后台开发的同学都得掌握,不会也没关系,关注小许,这次给你讲明白!
📚 全文字数 : 4k
⏳ 阅读时长 : 6min
📢 关键词 : websocket、Upgrade、http、数据帧
使用场景再现
在线教育:
老师进行一对多的在线授课,在客户端内编写的笔记、大纲等信息,需要实时推送至多个学生的客户端,需要通过WebSocket协议来完成。
视频弹幕:
终端用户A在自己的手机端发送了一条弹幕信息,但是您也需要在客户A的手机端上将其他N个客户端发送的弹幕信息一并展示。需要通过WebSocket协议将其他客户端发送的弹幕信息从服务端全部推送至客户A的手机端,从而使客户A可以同时看到自己发送的弹幕和其他用户发送的弹幕。
当然还有体育实况更新、视频会议和聊天等等,这里都不一一列举了
Web端即时通信方式
什么是web端即时通讯技术?
可以理解为实现这样一种功能:服务器端可以即时地将数据的更新或变化反应到客户端,例如消息推送等功能都是通过这种技术实现的。
但是在Web中,由于浏览器的限制,实现即时通讯需要借助一些方法。这种限制出现的主要原因是,一般的Web通信都是浏览器先发送请求到服务器,服务器再进行响应完成数据的现实更新。
Web端实现即时通讯主要有四种方式:轮询、长轮询(comet)、长连接(SSE)、WebSocket。
它们大体可以分为两类,一种是在HTTP基础上实现的,包括短轮询、长轮询(comet)、长连接(SSE);另一种不是在HTTP基础上实现是,即WebSocket。下面分别介绍一下这四种轮询方式。
轮询
基本思路就是客户端每隔一段时间向服务器发送http请求,服务器端在收到请求后,不管是否有所需数据返回,都直接进行响应。
这种方式本质上还是客户端不断发送请求,才形成客户端能实时接收服务端数数据变化的假象。
实现比较简单,缺点是需要不断建立http连接,浪费资源,而且在客户端数量级很大的情况下会导致服务器压力陡增,显然不是好选择!
轮询方式,你会发现在你打开F12调试页面时,你会发现大量的HTTP请求呢 🌐🌐🌐...
长轮询
轮询方式是在服务器接收到请求后迅速做出响应
而长轮询方式是服务器收到客户端发来的请求后,想挂起请求,服务器端不会直接进行响应,在超时时间内(比如20S),接收请求和处理请求进行响应。
有两种情况长轮询会响应:
- 达到http请求超时时间
- 服务器正常处理请求返回响应结果
长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,但是连接挂起也会导致资源的浪费!
长连接 SSE
官方介绍:长连接是指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包
SSE是HTML5新增的功能,全称为Server-Sent Events,它可以允许服务器推送数据到客户端。
SSE在本质上就与之前的长轮询、轮询不同,虽然都是基于http协议的,但是轮询需要客户端先发送请求,服务端才能响应。而SSE最大的特点就是不需要持续客户端发送请求,可以实现只要服务器端数据有更新,就可以马上发送到客户端。
长链接流程:连接->传输数据->保持连接 -> 传输数据-> ....->直到一方关闭连接,客户端关闭连接
SSE的优势在于,它不需要建立或保持大量的客户端发往服务器端的请求,节约了很多资源,提升应用性能,但是可以关闭一些长时间不读写操作的连接,这样可以避免一些恶意连接导致server端压力。
websocket
WebSocket协议是基于TCP的一种新的网络协议,它实现了客户端与服务器全双工(full-duplex)通信(同一时间里,双方都可以主动向对方发送数据)。
在WebSocket中,客户端和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
通信方式总结
✏️兼容性角度:短轮询>长轮询>长连接SSE>WebSocket
✏️性能方面:WebSocket>长连接SSE>长轮询>短轮询
Websocket
我们已经知道了WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。
而通过WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据,只需要完成一次握手,两者之间就直接可以创建持久性的连接。
ok,一起来看看websocket知识!
协议升级
出于兼容性的考虑,websocket 的握手使用 HTTP 来实现,客户端的握手消息就是一个「普通的,带有 Upgrade 头的,HTTP Request 消息」。
📢 想建立websoket连接,就需要在http请求上带一些特殊的header头才行!
我们看下WebSocket协议客户端请求和服务端响应示例,关于http这里就不多介绍了(这里自行回想下Http请求的request和reposone部分)
header头的意思是,浏览器想升级http协议,并且想升级成websocket协议
客户端请求:
arduino
复制代码以下是WebSocket请求头中的一些字段:
Upgrade: websocket // 1
Connection: Upgrade // 2
Sec-WebSocket-Key: xx== // 3
Origin: http: // 4
Sec-WebSocket-Protocol: chat, superchat // 5
Sec-WebSocket-Version: 13 // 6
上述字段说明如下:
- Upgrade:字段必须设置 websocket,表示希望升级到 WebSocket 协议
- Connection:须设置 Upgrade,表示客户端希望连接升级
- Sec-WebSocket-Key:是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要
- Origin:字段是可选的,只包含了协议和主机名称
- Sec-WebSocket-Extensions:用于协商本次连接要使用的 WebSocket 扩展
- Sec-WebSocket-Version:表示支持的 WebSocket 版本,RFC6455 要求使用的版本是 13
服务端响应:
arduino
复制代码HTTP/1.1 101 Web Socket Protocol Handshake // 1
Connection: Upgrade // 2
Upgrade: websocket // 3
Sec-WebSocket-Accept: 2mQFj9iUA/Nz8E6OA4c2/MboVUk= //4
上述字段说明如下:
- 101 响应码确认升级到 WebSocket 协议
- Connection:值为 “Upgrade” 来指示这是一个升级请求
- Upgrade:表示升级为 WebSocket 协议
- Sec-WebSocket-Accept:签名的键值验证协议支持
🚩 1:ws 协议默认使用 80 端口,wss 协议默认使用 443 端口,和 http 一样
🚩 2:WebSocket 没有使用 TCP 的“IP 地址 + 端口号”,开头的协议名不是“http”,引入的是两个新的名字:“ws”和“wss”,分别表示明文和加密的 WebSocket 协议
连接确认
发建立连接是前提,但是只有当请求头参数Sec-WebSocket-Key字段的值经过固定算法加密后的数据和响应头里的Sec-WebSocket-Accept的值保持一致,该连接才会被认可建立。
如下图从浏览器截图的两个关键参数:
服务端返回的响应头字段 Sec-WebSocket-Accept 是根据客户端请求 Header 中的Sec-WebSocket-Key计算出来。
那么时如何进行参数加密验证和比对确认的呢,如下图!
具体流程如下:
- 客户端握手中的 Sec-WebSocket-Key 头字段的值是16字节随机数,并经过base64编码
- 服务端需将该值和固定的 GUID 字符串( 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)拼接后使用 SHA-1 进行哈希,并采用 base64 编码后
- 服务端将编码后的值作为响应作为的Sec-WebSocket-Accept 值返回。
- 客户端也必须按照服务端生成 Sec-WebSocket-Accept 的方式一样生成字符串,与服务端回传的进行对比
- 相同就是协议升级成功,不同就是失败
在协议升级完成后websokcet就建立完成了,接下来就是客户端和服务端使用websocket进行数据传输通信了!
数据帧
一旦升级成功 WebSocket 连接建立后,后续数据都以帧序列的形式传输
📄 协议规定了数据帧的格式,服务端要想给客户端推送数据,必须将要推送的数据组装成一个数据帧,这样客户端才能接收到正确的数据;同样,服务端接收到客户端发送的数据时,必须按照帧的格式来解包,才能真确获取客户端发来的数据
我们来看下对帧的格式定义吧!
看看数据帧字段代表的含义吧:
- FIN 1个bit位,用来标记当前数据帧是不是最后一个数据帧
- RSV1, RSV2, RSV3 这三个,各占用一个bit位用做扩展用途,没有这个需求的话设置位0
- Opcode 的值定义的是数据帧的数据类型
值为1 表示当前数据帧内容是文本
值为2 表示当前数据帧内容是二进制
值为8表示请求关闭连接
- MASK 表示数据有没有使用掩码
服务端发送给客户端的数据帧不能使用掩码,客户端发送给服务端的数据帧必须使用掩码
- Payload len 数据的长度,Payload data的长度,占7bits,7+16bits,7+64bits
- Masking-key 数据掩码 (设置位0,则该部分可以省略,如果设置位1,则用来解码客户端发送给服务端的数据帧)
- Payload data 帧真正要发送的数据,可以是任意长度
上面我们说到Payload len三种长度(最开始的7bit
的值)来标记数据长度,这里具体看下是哪三种:
🚩 情况1:值设置在0-125
那么这个有效载荷长度(Payload len)就是对应的数据的值
🚩 情况2:值设置为126
如果设置为 126,可表示payload的长度范围在 126~65535 之间,那么接下来的 2 个字节(扩展用16bit Payload长度)会包含Payload真实数据长度
🚩 情况3:值设置为127
可表示payload的长度范围在 >=65535 ,那么接下来的 8 个字节(扩展用16bit + 32bit + 16bit Payload长度)会包含Payload真实数据长度,这种情况能表示的数据就很大了,完全够用
socket和websocket
这两者名字上差距不大,虽然都有带个socket,但是完全是两个不同的东西, 大家千万别被名字给带的傻傻分不清楚了!
我们来看下之间的区别
socket:是在应用层和传输层之间的一个中间软件抽象层,是一组接口,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。
websocket:是基于TCP的一种新的网络协议,和http协议一样属于应用层协议。
下图中分别表示了socket和websocket在网络中的位置
常见状态码
下面显示了从服务器到客户端的通信的 WebSocket 状态码和错误提示,WebSocket 状态码遵循 RFC 正常关闭连接标准
- 1000 CLOSE_NORMAL 连接正常关闭
- 1001 CLOSE_GOING_AWAY 终端离开 例如:服务器错误,或者浏览器已经离开此页面
- 1002 CLOSE_PROTOCOL_ERROR 因为协议错误而中断连接
- 1003 CLOSE_UNSUPPORTED 端点因为受到不能接受的数据类型而中断连接
- 1004 保留
- 1005 CLOSE_NO_STATUS 保留, 用于提示应用未收到连接关闭的状态码
- 1006 CLOSE_ABNORMAL 期望收到状态码时连接非正常关闭 (也就是说, 没有发送关闭帧)
- 1007 Unsupported Data 收到的数据帧类型不一致而导致连接关闭
- 1008 Policy Violation 收到不符合约定的数据而断开连接
- 1009 CLOSE_TOO_LARGE 收到的消息数据太大而关闭连接
- 1010 Missing Extension 客户端因为服务器未协商扩展而关闭
- 1011 Internal Error 服务器因为遭遇异常而关闭连接
- 1012 Service Restart 服务器由于重启而断开连接
- 1013 Try Again Later 服务器由于临时原因断开连接, 如服务器过载因此断开一部分客户端连接
- 1015 TLS握手失败关闭连接
总结
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信。
适用于需要客户端(浏览器)和服务端频繁交互的大部分场景,比如网页游戏,体育实况,弹幕,以及一些协同办公软件等
而websocket是利用http协议,然后加上一些特殊的header头进行握手Upgrade升级操作,升级成功后就跟http没有任何关系了,之后就用websocket的数据格式进行收发数据。