springboot之websocket,STOMP协议
一、WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
二、STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
三、首先,我们先理解一下为什么需要STOMP。
1)常规的websocket连接和普通的TCP基本上没有什么差别的。
2)那我们如果像http一样加入一些响应和请求层。
3)所以STOMP在websocket上提供了一中基于帧线路格式(frame-based wire format)。
4)简单一点,就是在我们的websocket(TCP)上面加了一层协议,使双方遵循这种协议来发送消息。
四、STOMP
1)Frame
例如:
command:CONNECT
其他部分都是headers的一部分。
2)command类别
CONNECT
SEND
SUBSCRIBE
UNSUBSCRIBE
BEGIN
COMMIT
ABORT
ACK
NACK
DISCONNECT
3)客户端常用连接方式
a、ws
var url = "ws://localhost:8080/websocket"; var client = Stomp.client(url);
b、sockJs
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script> <script> // use SockJS implementation instead of the browser's native implementation var ws = new SockJS(url); var client = Stomp.over(ws); [...] </script>
说明:使用ws协议需要浏览器的支持,但是一些老版本的浏览器不一定支持。Stomp.over(ws)的凡是就是用来定义服务websocket的协议。
4)服务端的实现过程
a、服务端:/app,这里访问服务端,前缀通过设定的方式访问。
b、用户:/user,这里针对的是用户消息的传递,针对于当前用户进行传递。
c、其他消息:/topic、/queue,这两种方式。都是定义出来用于订阅。并且消息只能从这里通过并处理
五、springboot的简单例子
1)目录结构
2)依赖包(pom.xml)
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
3)websocket配置(WebSocketConfiguration、SecurityConfiguration)
/** * webSocket配置 */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { /** * 注册stomp端点,主要是起到连接作用 * @param stompEndpointRegistry */ @Override public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { stompEndpointRegistry .addEndpoint("/webSocket") //端点名称 //.setHandshakeHandler() 握手处理,主要是连接的时候认证获取其他数据验证等 //.addInterceptors() 拦截处理,和http拦截类似 .setAllowedOrigins("*") //跨域 .withSockJS(); //使用sockJS } /** * 注册相关服务 * @param registry */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //这里使用的是内存模式,生产环境可以使用rabbitmq或者其他mq。 //这里注册两个,主要是目的是将广播和队列分开。 //registry.enableStompBrokerRelay().setRelayHost().setRelayPort() 其他方式 registry.enableSimpleBroker("/topic", "/queue"); //客户端名称前缀 registry.setApplicationDestinationPrefixes("/app"); //用户名称前 registry.setUserDestinationPrefix("/user"); } }
认证配置:
/** * 配置基本登录 */ @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter{ /** * 加密方式 */ @Autowired private BCryptPasswordEncoder passwordEncoder; /** * 所有请求过滤,包含webSocket * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().anyRequest().authenticated() .and() .httpBasic(); } /** * 加入两个用户测试不同用的接受情况 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password(passwordEncoder.encode("admin")).roles("ADMIN") .and() .withUser("user").password(passwordEncoder.encode("user")).roles("USER"); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }
4)服务端
/** * 消息接收处理 */ @RestController public class MessageResource { //spring提供的推送方式 @Autowired private SimpMessagingTemplate messagingTemplate; /** * 广播模式 * @param requestMsg * @return */ @MessageMapping("/broadcast") @SendTo("/topic/broadcast") public String broadcast(RequestMsg requestMsg) { //这里是有return,如果不写@SendTo默认和/topic/broadcast一样 return "server:" + requestMsg.getBody().toString(); } /** * 订阅模式,只是在订阅的时候触发,可以理解为:访问——>返回数据 * @param id * @return */ @SubscribeMapping("/subscribe/{id}") public String subscribe(@DestinationVariable Long id) { return "success"; } /** * 用户模式 * @param requestMsg * @param principal */ @MessageMapping("/one") //@SendToUser("/queue/one") 如果存在return,可以使用这种方式 public void one(RequestMsg requestMsg, Principal principal) { //这里使用的是spring的security的认证体系,所以直接使用Principal获取用户信息即可。 //注意为什么使用queue,主要目的是为了区分广播和队列的方式。实际采用topic,也没有关系。但是为了好理解 messagingTemplate.convertAndSendToUser(principal.getName(), "/queue/one", requestMsg.getBody()); } }
客户端(JavaScript):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>webSocket</title> <script src="js/jquery.js"></script> <script src="js/sockjs.min.js"></script> <script src="js/stomp.js"></script> </head> <body> <div> <button id="connect">连接</button> <button id="disconnect" disabled="disabled">断开</button> </div> <div> <h3>广播形式</h3> <button id="broadcastButton">发送</button><input id="broadcastText" type="text"> <label>广播消息:</label><input id="broadcastMsg" type="text" disabled="disabled"> </div> <div> <h3>订阅形式</h3> <label>订阅消息:</label><input id="subscribeMsg" type="text" disabled="disabled"> </div> <div> <h3>角色形式</h3> <button id="userButton">发送</button><input id="userText" type="text"> <label>用户消息:</label><input id="userMsg" type="text" disabled="disabled"> </div> <div> <h3>无APP</h3> <button id="appButton">发送</button><input id="appText" type="text"> <label>前端消息:</label><input id="appMsg" type="text" disabled="disabled"> </div> </body> <script> var stomp = null; $("#connect").click(function () { var url = "http://localhost:8080/webSocket" var socket = new SockJS(url); stomp = Stomp.over(socket); //连接 stomp.connect({}, function (frame) { //订阅广播 stomp.subscribe("/topic/broadcast", function (res) { $("#broadcastMsg").val(res.body); }); //订阅,一般只有订阅的时候在返回 stomp.subscribe("/app/subscribe/1", function (res) { $("#subscribeMsg").val(res.body); }); //用户模式 stomp.subscribe("/user/queue/one", function (res) { $("#userMsg").val(res.body); }); //无APP stomp.subscribe("/topic/app", function (res) { $("#appMsg").val(res.body); }); setConnect(true); }); }); $("#disconnect").click(function () { if (stomp != null) { stomp.disconnect(); } setConnect(false); }); //设置按钮 function setConnect(connectStatus) { $("#connect").attr("disabled", connectStatus); $("#disconnect").attr("disabled", !connectStatus); } //发送广播消息 $("#broadcastButton").click(function () { stomp.send("/app/broadcast", {}, JSON.stringify({"body":$("#broadcastText").val()})) }); //发送用户消息 $("#userButton").click(function () { stomp.send("/app/one", {}, JSON.stringify({"body":$("#userText").val()})) }); //发送web消息 $("#appButton").click(function () { stomp.send("/topic/app", {}, JSON.stringify({"body":$("#appText").val()})) }); </script> </html>
5)普通测试
角色测试:
六、相关资料