WebSocket协议简介
WebSocket 是一种全新的协议,将 TCP 的 Socket 应用在了 web page 上,从而使通信双方建立起一个保持在活动状态的连接通道,并且属于全双工工作模式。WebSocket建立在 TCP 协议之上,与 HTTP 协议有着良好的兼容性。默认端口是80或443,并且握手阶段采用 HTTP 协议,因此握手时不容易被屏蔽,能通过各种 HTTP 代理服务器。它是一种双向通信协议,采用异步回调的方式接受消息,成功建立通信连接后,可以做到持久性的连接,除非客户端或者服务端的一方主动关闭连接。WebSocket服务器和客户端都能主动的向对方发送或接收数据,实质的推送方式是服务器主动推送,只要有数据就推送到请求方。协议标识符是ws(一般采用wss协议对通信内容加密),服务器网址就是URL。(例如:wss://www.example.com/chat)
握手阶段,客户端需发送如下格式的报文:
- Connection:必须设置Upgrade,表示客户端希望连接升级
- Upgrade:必须设置Websocket,表示希望升级到Websocket协议
- Sec-WebSocket-Key:客户端发送的一个 base64 编码的密文,用于简单的认证秘钥。要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept应答,否则客户端会抛出错误,并关闭连接
- Sec-WebSocket-Version :表示支持的Websocket版本
- Sec-WebSocket-Protocol:官方并没有对该字段进行强制要求,开发人员可根据自身需求进行设置。
服务端返回的报文:
- HTTP/1.1 101 Switching Protocols:表示服务端接受 WebSocket 协议的客户端连接
- Sec-WebSocket-Accep:验证客户端请求报文,同样也是为了防止误连接。具体做法是把请求头里“Sec-WebSocket-Key”的值,加上一个专用的 UUID,再计算摘要。
关闭连接:正常关闭 WebSocket 连接就是发送 opcode 为 0x08 的报文,可以包含具体关闭连接的原因,一旦一端接收到了 Close 报文,那么就开始了 Close HandShake ,并进入到 Closing 状态。关闭帧前两个字节为错误码,可选择发送关闭连接的具体原因。当收到 Close 帧之后,如果之前没有向对方发送过 Close 帧,那么就需要发送一个错误码相同的帧,一般来说会立即发送错误帧,当然,也可以将当前正在发送的帧发送完成后再发送。Close 消息发送完之后,服务端会立即关闭 Socket 连接,而客户端一般来说需要等待服务端关闭连接,但也可以主动关闭,例如发送 Close帧之后一直没有收到连接关闭信息。也就是说客户端和服务端均可以主动关闭连接。
WebSocket协议优势
- websocket协议可以实现全双工通信,实时性更强,开启连接之后可以不用每次都携带状态信息,而 HTTP 协议,通信只能由客户端发起,服务端无法主动向客户端推送信息,通过轮询方式需要消耗大量带宽资源。
- 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
- 使用场景更灵活,可以发送文本,也可以发送二进制数据,没有同源限制,客户端可以与任意服务器通信。
- 支持扩展,ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
- 更适配复杂的交互场景,尤其是对上下文信息要求比较严格且频繁进行查询更新的应用中,通过设计前后端通信协议,可以用很小的代价满足复杂的需求。
WebSocket应用场景
- 聊天室
- 协同编辑
- 基于位置的应用
- SQL查询工具
- 实时信息订阅
- 音视频聊天 / 视频会议 / 在线教育
- 多玩家游戏
WebSocket开发实践
在具体的实践中,WebSocket在后端有两种使用方式。基于STOMP协议的方式抽象程度比较高,适合订阅类型的需求。
STOMP是一个用于C/S之间进行异步消息传输的简单文本协议, 全称是Simple Text Oriented Messaging Protocol。其实STOMP协议并不是为WS所设计的, 它其实是消息队列的一种协议, 和AMQP,JMS是平级的。 只不过由于它的简单性恰巧可以用于定义WS的消息体格式。 目前很多服务端消息队列都已经支持了STOMP, 比如RabbitMQ, Apache ActiveMQ等。很多语言也都有STOMP协议的客户端解析库,像JAVA的Gozirra,C的libstomp,Python的pyactivemq,JavaScript的stomp.js等等。STOMP是一种基于帧的协议,一帧由一个命令,一组可选的Header和一个可选的Body组成。 STOMP是基于Text的,但也允许传输二进制数据。 它的默认编码是UTF-8,但它的消息体也支持其他编码方式,比如压缩编码。
STOMP服务端被设计为客户端可以向其发送消息的一组目标地址。STOMP协议并没有规定目标地址的格式,它由使用协议的应用自己来定义。 例如/topic/a,/queue/a,queue-a对于STOMP协议来说都是正确的。应用可以自己规定不同的格式以此来表明不同格式代表的含义。比如应用自己可以定义以/topic打头的为发布订阅模式,消息会被所有消费者客户端收到,以/user开头的为点对点模式,只会被一个消费者客户端收到。
对于STOMP协议来说, 客户端会扮演下列两种角色的任意一种:作为生产者,通过SEND帧发送消息到指定的地址;作为消费者,通过发送SUBSCRIBE帧到已知地址来进行消息订阅,而当生产者发送消息到这个订阅地址后,订阅该地址的其他消费者会受到通过MESSAGE帧收到该消息。
实际上,WebSocket结合STOMP相当于构建了一个消息分发队列,客户端可以在上述两个角色间转换,订阅机制保证了一个客户端消息可以通过服务器广播到多个其他客户端,作为生产者,又可以通过服务器来发送点对点消息。
代码示例如下:
首先引入依赖
然后Enable WebSocket,新增注解处理WebSocket消息,在application上添加该注解
配置主要包含两部分内容,一个是消息代理,另一个是Endpoint,消息代理指定了客户端订阅地址,以及发送消息的路由地址;Endpoint指定了客户端建立连接时的请求地址。
在Controller中创建方法,greeting()方法的作用是,处理所有发到/hello这个destination的信息,并将处理的结果,发送到所有订阅了/topic/greetings这个destination的客户端。其中模拟的延时,其本质是为了演示在WebSocket中,我们无需考虑超时这样的问题,客户端与服务端连接建立后,服务端可以根据实际场景,在“任何有需要”的时候“推送”消息到客户端,直到连接释放。
针对比较灵活的需求,需要使用简单的WebSocket进行开发,示例如下:
引入依赖
新建WebSocketConfig配置类,注入一个ServerEndpointExporterBean,该Bean会自动注册使用@ServerEndpoint注解申请的websocket endpoint。
新建WebSocketServer,实现前后端交互。
WebSocket开发注意事项
- WebSocket接口需要实现单独的拦截器
- Nginx默认不支持WebSocket协议,在配置文件上需要进行适配
- WebSocket服务是多对象的,与spring的单例机制冲突,所以在WebSocket的实现中不能采用自动注入的方式注入对象,可以采用static申明或者上下文信息注入的方式注入对象。
- 当请求的内容比较大时,需要对WebSocket的缓存空间进行设置,否则多并发很容易耗尽应用的内存空间。
更多关于WebSocket在后端实践中的知识,欢迎大家留言讨论。