1, 项目介绍
本项目集成websocket在springboot上, 用的是STOMP协议, Stomp可不是是暴风雨(Storm). Stomp 的 全称叫 Simple Text Orientated Messaging Protocol,就是一个简单的文本定向消息协议,除了设计为简单易用之外,它的支持者也非常多。就比如目前主流的消息队列服务器如RabbitMQ、ActiveMQ都支持Stomp 协议。
项目是一个STOMP应用简单例子, 实现了广播, 单播(发给自己和发给别人)的功能, 还实现了一个定时广播功能.

上图是项目文件的主要功能的伪代码形式.
2, 项目代码
1, 引入依赖文件
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency><groupId>org.webjars</groupId><artifactId>webjars-locator-core</artifactId></dependency><dependency><groupId>org.webjars</groupId><artifactId>sockjs-client</artifactId><version>1.0.2</version></dependency><dependency><groupId>org.webjars</groupId><artifactId>stomp-websocket</artifactId><version>2.3.3</version></dependency><dependency><groupId>org.webjars</groupId><artifactId>bootstrap</artifactId><version>3.3.7</version></dependency><dependency><groupId>org.webjars</groupId><artifactId>jquery</artifactId><version>3.1.1-1</version></dependency>
2, 编写配置类
package com.junwei.config;import com.junwei.controller.UserHandshakeHandler;import org.springframework.context.annotation.Configuration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;import org.springframework.web.socket.config.annotation.StompEndpointRegistry;import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(final MessageBrokerRegistry registry) {registry.enableSimpleBroker("/topic");registry.setApplicationDestinationPrefixes("/ws");}@Overridepublic void registerStompEndpoints(final StompEndpointRegistry registry) {registry.addEndpoint("/our-websocket").setHandshakeHandler(new UserHandshakeHandler()).withSockJS();}}
配置类中实现 WebSocketMessageBrokerConfigurer 接口, 重写configureMessageBroker和registerStompEndpoints两个方法, 其实就是注册各种url地址.
| registry.enableSimpleBroker(“/topic”) | 连接后前端接受的广播url地址都要以’/topic’开头 |
|---|---|
| registry.setApplicationDestinationPrefixes(“/ws”) | 前端发送的请求地址url以’/ws’开头 |
| registry.addEndpoint(“/our-websocket”) | 建立前端和后端的连接 |
| .setHandshakeHandler(new UserHandshakeHandler()) | 在建立连接前设置一个UserHandshakeHandler, 用于处理建立连接前处理(比如用户身份获取,验证) |
| .withSockJS() | 把普通的Websocket接口转成SockJS |
UserHandshakeHandler类, 当前只是生成一个随机UUID, 存入UserPrincipal
import com.sun.security.auth.UserPrincipal;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.server.ServerHttpRequest;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.support.DefaultHandshakeHandler;import java.security.Principal;import java.util.Map;import java.util.UUID;public class UserHandshakeHandler extends DefaultHandshakeHandler {private final Logger LOG = LoggerFactory.getLogger(UserHandshakeHandler.class);@Overrideprotected Principal determineUser(ServerHttpRequest request,WebSocketHandler wsHandler,Map<String, Object> attributes) {final String randomId = UUID.randomUUID().toString();LOG.info("User with ID '{}' opened the page", randomId);return new UserPrincipal(randomId);}}
3, 编写controller
import com.junwei.entity.Message;import com.junwei.entity.ResponseMessage;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.messaging.simp.SimpMessagingTemplate;import org.springframework.messaging.simp.annotation.SendToUser;import org.springframework.stereotype.Controller;import org.springframework.web.util.HtmlUtils;import java.security.Principal;@Controllerpublic class MessageController{private final SimpMessagingTemplate messagingTemplate;@Autowiredpublic MessageController(SimpMessagingTemplate messagingTemplate) {this.messagingTemplate = messagingTemplate;}@MessageMapping("/message")@SendTo("/topic/messages")public ResponseMessage getMessage(final Message message) throws InterruptedException {Thread.sleep(1000);return new ResponseMessage(HtmlUtils.htmlEscape(message.getMessageContent()));}@MessageMapping("/private-message")@SendToUser("/topic/private-messages")public ResponseMessage getPrivateMessage(final Message message,final Principal principal) throws InterruptedException {Thread.sleep(1000);return new ResponseMessage(HtmlUtils.htmlEscape("Sending private message to user " + principal.getName() + ": "+ message.getMessageContent()));}}
JS前端会发送请求stompClient.send(“/ws/message”, {}), 后端接受处理的controller会根据注释的url地址去寻找对应的method,@MessageMapping("/message") 这里的/message的前缀/ws是在config中的registry.setApplicationDestinationPrefixes("/ws")配置的.
JS前端的订阅地址是stompClient.subscribe(‘/topic/messages’, function (message),对应后端controller中的 @SendTo("/topic/messages"), 我们发现这里用的就已经是完整url了, 跟上面的注意区别. 尽管我们在config中配置过registry.setApplicationDestinationPrefixes("/ws")
注意, 在单播中方法上注释的是@SendToUser("/topic/private-messages"), 但是在JS前端, 如果我们要订阅该url, 需要加一个默认前缀/user: stompClient.subscribe(‘/user/topic/private-messages’, function (message)
这个/user是默认值, 如果需要修改, 需要在config类中添加
@Overridepublic void configureMessageBroker(final MessageBrokerRegistry registry) {registry.enableSimpleBroker("/broadcast"); // broadcast to user if user sebscribe the /topic/xxxregistry.setApplicationDestinationPrefixes("/ws"); // SockJS service url prefix//定义一对一推送的时候前缀,默认就是user//registry.setUserDestinationPrefix("/user");}
Message和 Response是两个普通的POJO类
public class Message {private String messageContent;public String getMessageContent() {return messageContent;}public void setMessageContent(String messageContent) {this.messageContent = messageContent;}}public class ResponseMessage {private String content;public ResponseMessage() {}public ResponseMessage(String content) {this.content = content;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}}
4, 前端JS部分
var stompClient = null;$(document).ready(function() {console.log("Index page is ready");connect();$("#send").click(function() {sendMessage();});$("#send-private").click(function() {sendPrivateMessage();});});function connect() {var socket = new SockJS('/our-websocket');stompClient = Stomp.over(socket);stompClient.connect({}, function (frame) {console.log('Connected: ' + frame);// 订阅广播地址/topic/messagesstompClient.subscribe('/topic/messages', function (message) {showMessage(JSON.parse(message.body).content);});//用户发送的/ws/private-message地址请求,会被/user/topic/private-messages当前客户端订阅,//相当于单播stompClient.subscribe('/user/topic/private-messages', function (message) {showMessage(JSON.parse(message.body).content);});});}function showMessage(message) {$("#messages").append("<tr><td>" + message + "</td></tr>");}function sendMessage() {console.log("sending message");stompClient.send("/ws/message", {}, JSON.stringify({'messageContent': $("#message").val()}));}function sendPrivateMessage() {console.log("sending private message");stompClient.send("/ws/private-message", {}, JSON.stringify({'messageContent': $("#private-message").val()}));}
5, html前端显示
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Hello WS</title><link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"><script src="/webjars/jquery/jquery.min.js"></script><script src="/webjars/sockjs-client/sockjs.min.js"></script><script src="/webjars/stomp-websocket/stomp.min.js"></script><script src="/js/scripts.js"></script></head><body><div class="container" style="margin-top: 50px"><div class="row"><div class="col-md-12"><form class="form-inline"><div class="form-group"><label for="message">Message</label><input type="text" id="message" class="form-control" placeholder="Enter your message here..."></div><button id="send" class="btn btn-default" type="button">Send</button></form></div></div><div class="row" style="margin-top: 10px"><div class="col-md-12"><form class="form-inline"><div class="form-group"><label for="private-message">Private Message</label><input type="text" id="private-message" class="form-control" placeholder="Enter your message here..."></div><button id="send-private" class="btn btn-default" type="button">Send Private Message</button></form></div></div><div class="row"><div class="col-md-12"><table id="message-history" class="table table-striped"><thead><tr><th>Messages</th></tr></thead><tbody id="messages"></tbody></table></div></div></div></body></html>
