再谈 websocket 论架构设计
导语
本篇文章以websocket的原理和落地为核心,来叙述websocket的使用,以及相关应用场景。
websocket概述
http与websocket
如我们所了解,http连接为一次请求一次响应(request->response),必须为同步调用方式。
而websocket为一次连接以后,会建立tcp连接,后续客户端与服务器交互为全双工方式的交互方式,客户端可以发送消息到服务端,服务端也可将消息发送给客户端。
此图来源于Websocket协议的学习、调研和实现,如有侵权问题,告知后,删除。
根据上图,我们大致可以了解到http与websocket之间的区别和不同。
为什么要使用websocket
那么了解http与websocket之间的不同以后,我们为什么要使用websocket呢? 他的应用场景是什么呢?
我找到了一个比较符合websocket使用场景的描述
“The best fit for WebSocket is in web applications where the client and server need to exchange events at high frequency and with low latency.”
翻译: 在客户端与服务器端交互的web应用中,websocket最适合在高频率低延迟的场景下,进行事件的交换和处理
了解以上知识后,我举出几个比较常见的场景:
游戏中的数据传输
股票K线图数据
客服系统
根据如上所述,各个系统都来使用websocket不是更好吗?
其实并不是,websocket建立连接之后,后边交互都由tcp协议进行交互,故开发的复杂度会较高。当然websocket通讯,本身要考虑的事情要比HTTP协议的通讯考虑的更多.
所以如果不是有特殊要求(即 应用不是”高频率低延迟”的要求),需要优先考虑HTTP协议是否可以满足。
比如新闻系统,新闻的数据晚上10分钟-30分钟,是可以接受的,那么就可以采用HTTP的方式进行轮询(polling)操作调用REST接口。
当然有时我们建立了websocket通讯,并且希望通过HTTP提供的REST接口推送给某客户端,此时需要考虑REST接口接受数据传送给websocket中,进行广播式的通讯方式。
至此,我已经讲述了三种交互方式的使用场景:
websocket独立使用场景
HTTP独立使用场景
HTTP中转websocket使用场景
相关技术概念
websocket
websocket为一次HTTP握手后,后续通讯为tcp协议的通讯方式。
当然,和HTTP一样,websocket也有一些约定的通讯方式,http通讯方式为http开头的方式,e.g. http://xxx.com/path ,websocket通讯方式则为ws开头的方式,e.g. ws://xxx.com/path
SSL:
HTTP: https://xxx.com/path
WEBSOCKET: wss://xxx.com/path
此图来源于WebSocket 教程,如有侵权问题,告知后,删除。
SockJS
正如我们所知,websocket协议虽然已经被制定,当时还有很多版本的浏览器或浏览器厂商还没有支持的很好。
所以,SockJS,可以理解为是websocket的一个备选方案。
那它如何规定备选方案的呢?
它大概支持这样几个方案:
Websockets
Streaming
Polling
当然,开启并使用SockJS后,它会优先选用websocket协议作为传输协议,如果浏览器不支持websocket协议,则会在其他方案中,选择一个较好的协议进行通讯。
看一下目前浏览器的支持情况:
所以,如果使用SockJS进行通讯,它将在使用上保持一致,底层由它自己去选择相应的协议。
可以认为SockJS是websocket通讯层上的上层协议。
底层对于开发者来说是透明的。
STOMP
STOMP 中文为: 面向消息的简单文本协议
websocket定义了两种传输信息类型: 文本信息 和 二进制信息 ( text and binary ).
类型虽然被确定,但是他们的传输体是没有规定的。
当然你可以自己来写传输体,来规定传输内容。(当然,这样的复杂度是很高的)
所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议,即交互中的高级协议来定义交互信息。
STOMP本身可以支持流类型的网络传输协议: websocket协议和tcp协议
它的格式为:
| 1234567891011121314151617181920212223 | COMMANDheader1:value1header2:value2 Body^@ SUBSCRIBEid:sub-1destination:/topic/price.stock.* ^@ SENDdestination:/queue/tradecontent-type:application/jsoncontent-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@ |
当然STOMP已经应用于很多消息代理中,作为一个传输协议的规定,如:RabbitMQ, ActiveMQ
我们皆可以用STOMP和这类MQ进行消息交互.
除了STOMP相关的代理外,实际上还提供了一个stomp.js,用于浏览器客户端使用STOMP消息协议传输的js库。
让我们很方便的使用stomp.js进行与STOMP协议相关的代理进行交互.
正如我们所知,如果websocket内容传输信息使用STOMP来进行交互,websocket也很好的于消息代理器进行交互(如:RabbitMQ, ActiveMQ)
这样就很好的提供了消息代理的集成方案。
总结,使用STOMP的优点如下:
不需要自建一套自定义的消息格式
现有stomp.js客户端(浏览器中使用)可以直接使用
能路由信息到指定消息地点
可以直接使用成熟的STOMP代理进行广播 如:RabbitMQ, ActiveMQ
技术落地
后端技术方案选型
websocket服务端选型:spring websocket
支持SockJS,开启SockJS后,可应对不同浏览器的通讯支持
支持STOMP传输协议,可无缝对接STOMP协议下的消息代理器(如:RabbitMQ, ActiveMQ)
前端技术方案选型
前端选型: stomp.js,sockjs.js
后端开启SOMP和SockJS支持后,前对应有对应的js库进行支持.
所以选用此两个库.
总结
上述所用技术,是这样的逻辑:
开启socktJS:
如果有浏览器不支持websocket协议,可以在其他两种协议中进行选择,但是对于应用层来讲,使用起来是一样的。
这是为了支持浏览器不支持websocket协议的一种备选方案使用STOMP:
使用STOMP进行交互,前端可以使用stomp.js类库进行交互,消息一STOMP协议格式进行传输,这样就规定了消息传输格式。
消息进入后端以后,可以将消息与实现STOMP格式的代理器进行整合。
这是为了消息统一管理,进行机器扩容时,可进行负载均衡部署使用spring websocket:
使用spring websocket,是因为他提供了STOMP的传输自协议的同时,还提供了StockJS的支持。
当然,除此之外,spring websocket还提供了权限整合的功能,还有自带天生与spring家族等相关框架进行无缝整合。
应用场景
应用背景
2016年,在公司与同事一起讨论和开发了公司内部的客服系统,由于前端技能的不足,很多通讯方面的问题,无法亲自调试前端来解决问题。
因为公司技术架构体系以前后端分离为主,故前端无法协助后端调试,后端无法协助前端调试
在加上websocket为公司刚启用的协议,了解的人不多,导致前后端调试问题重重。
一年后的今天,我打算将前端重温,自己来调试一下前后端,来发掘一下之前联调的问题.
当然,前端,我只是考虑stomp.js和sockt.js的使用。
代码阶段设计
角色
客服
客户
登录用户状态
上线
下线
分配策略
用户登陆后,应该根据用户角色进行分配
关系保存策略
应该提供关系型保存策略: 考虑内存式策略(可用于测试),redis式策略
备注:优先应该考虑实现Inmemory策略,用于测试,让关系保存策略与存储平台无关
通讯层设计
归类topic的广播设计(通讯方式:1-n)
归类queue的单点设计(通讯方式:1-1)
代码实现
角色
| 123456789101112131415161718192021 | import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.User;import java.util.Collection;public enum Role { CUSTOMER_SERVICE, CUSTOMER; public static boolean isCustomer(User user) { Collection<GrantedAuthority> authorities = user.getAuthorities(); SimpleGrantedAuthority customerGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + Role.CUSTOMER.name()); return authorities.contains(customerGrantedAuthority); } public static boolean isCustomerService(User user) { Collection<GrantedAuthority> authorities = user.getAuthorities(); SimpleGrantedAuthority customerServiceGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + Role.CUSTOMER_SERVICE.name()); return authorities.contains(customerServiceGrantedAuthority); }} |
代码中User对象,为安全对象,即 spring中org.springframework.security.core.userdetails.User,为UserDetails的实现类。
User对象中,保存了用户授权后的很多基础权限信息,和用户信息。
如下:
| 123456789101112131415 | public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled();} |
方法 #isCustomer 和 #isCustomerService 用来判断用户当前是否是顾客或者是客服。
登录用户状态
| 123456789101112 | public interface StatesManager { enum StatesManagerEnum{ ON_ |


