1, 项目介绍

本项目集成websocket在springboot上, 用的是STOMP协议, Stomp可不是是暴风雨(Storm). Stomp 的 全称叫 Simple Text Orientated Messaging Protocol,就是一个简单的文本定向消息协议,除了设计为简单易用之外,它的支持者也非常多。就比如目前主流的消息队列服务器如RabbitMQ、ActiveMQ都支持Stomp 协议。

项目是一个STOMP应用简单例子, 实现了广播, 单播(发给自己和发给别人)的功能, 还实现了一个定时广播功能.

websocket.drawio.png
上图是项目文件的主要功能的伪代码形式.

2, 项目代码

本项目源文件结构如下图:
project-structure.PNG

1, 引入依赖文件

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.webjars</groupId>
  7. <artifactId>webjars-locator-core</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.webjars</groupId>
  11. <artifactId>sockjs-client</artifactId>
  12. <version>1.0.2</version>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.webjars</groupId>
  16. <artifactId>stomp-websocket</artifactId>
  17. <version>2.3.3</version>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.webjars</groupId>
  21. <artifactId>bootstrap</artifactId>
  22. <version>3.3.7</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.webjars</groupId>
  26. <artifactId>jquery</artifactId>
  27. <version>3.1.1-1</version>
  28. </dependency>

2, 编写配置类

  1. package com.junwei.config;
  2. import com.junwei.controller.UserHandshakeHandler;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.messaging.simp.config.MessageBrokerRegistry;
  5. import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
  6. import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
  7. import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
  8. @Configuration
  9. @EnableWebSocketMessageBroker
  10. public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
  11. @Override
  12. public void configureMessageBroker(final MessageBrokerRegistry registry) {
  13. registry.enableSimpleBroker("/topic");
  14. registry.setApplicationDestinationPrefixes("/ws");
  15. }
  16. @Override
  17. public void registerStompEndpoints(final StompEndpointRegistry registry) {
  18. registry.addEndpoint("/our-websocket")
  19. .setHandshakeHandler(new UserHandshakeHandler())
  20. .withSockJS();
  21. }
  22. }

配置类中实现 WebSocketMessageBrokerConfigurer 接口, 重写configureMessageBrokerregisterStompEndpoints两个方法, 其实就是注册各种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

  1. import com.sun.security.auth.UserPrincipal;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.http.server.ServerHttpRequest;
  5. import org.springframework.web.socket.WebSocketHandler;
  6. import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
  7. import java.security.Principal;
  8. import java.util.Map;
  9. import java.util.UUID;
  10. public class UserHandshakeHandler extends DefaultHandshakeHandler {
  11. private final Logger LOG = LoggerFactory.getLogger(UserHandshakeHandler.class);
  12. @Override
  13. protected Principal determineUser(ServerHttpRequest request,
  14. WebSocketHandler wsHandler,
  15. Map<String, Object> attributes) {
  16. final String randomId = UUID.randomUUID().toString();
  17. LOG.info("User with ID '{}' opened the page", randomId);
  18. return new UserPrincipal(randomId);
  19. }
  20. }

3, 编写controller

  1. import com.junwei.entity.Message;
  2. import com.junwei.entity.ResponseMessage;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.messaging.handler.annotation.MessageMapping;
  5. import org.springframework.messaging.handler.annotation.SendTo;
  6. import org.springframework.messaging.simp.SimpMessagingTemplate;
  7. import org.springframework.messaging.simp.annotation.SendToUser;
  8. import org.springframework.stereotype.Controller;
  9. import org.springframework.web.util.HtmlUtils;
  10. import java.security.Principal;
  11. @Controller
  12. public class MessageController{
  13. private final SimpMessagingTemplate messagingTemplate;
  14. @Autowired
  15. public MessageController(SimpMessagingTemplate messagingTemplate) {
  16. this.messagingTemplate = messagingTemplate;
  17. }
  18. @MessageMapping("/message")
  19. @SendTo("/topic/messages")
  20. public ResponseMessage getMessage(final Message message) throws InterruptedException {
  21. Thread.sleep(1000);
  22. return new ResponseMessage(HtmlUtils.htmlEscape(message.getMessageContent()));
  23. }
  24. @MessageMapping("/private-message")
  25. @SendToUser("/topic/private-messages")
  26. public ResponseMessage getPrivateMessage(final Message message,
  27. final Principal principal) throws InterruptedException {
  28. Thread.sleep(1000);
  29. return new ResponseMessage(HtmlUtils.htmlEscape(
  30. "Sending private message to user " + principal.getName() + ": "
  31. + message.getMessageContent())
  32. );
  33. }
  34. }

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类中添加

  1. @Override
  2. public void configureMessageBroker(final MessageBrokerRegistry registry) {
  3. registry.enableSimpleBroker("/broadcast"); // broadcast to user if user sebscribe the /topic/xxx
  4. registry.setApplicationDestinationPrefixes("/ws"); // SockJS service url prefix
  5. //定义一对一推送的时候前缀,默认就是user
  6. //registry.setUserDestinationPrefix("/user");
  7. }

MessageResponse是两个普通的POJO类

  1. public class Message {
  2. private String messageContent;
  3. public String getMessageContent() {
  4. return messageContent;
  5. }
  6. public void setMessageContent(String messageContent) {
  7. this.messageContent = messageContent;
  8. }
  9. }
  10. public class ResponseMessage {
  11. private String content;
  12. public ResponseMessage() {
  13. }
  14. public ResponseMessage(String content) {
  15. this.content = content;
  16. }
  17. public String getContent() {
  18. return content;
  19. }
  20. public void setContent(String content) {
  21. this.content = content;
  22. }
  23. }

4, 前端JS部分

  1. var stompClient = null;
  2. $(document).ready(function() {
  3. console.log("Index page is ready");
  4. connect();
  5. $("#send").click(function() {
  6. sendMessage();
  7. });
  8. $("#send-private").click(function() {
  9. sendPrivateMessage();
  10. });
  11. });
  12. function connect() {
  13. var socket = new SockJS('/our-websocket');
  14. stompClient = Stomp.over(socket);
  15. stompClient.connect({}, function (frame) {
  16. console.log('Connected: ' + frame);
  17. // 订阅广播地址/topic/messages
  18. stompClient.subscribe('/topic/messages', function (message) {
  19. showMessage(JSON.parse(message.body).content);
  20. });
  21. //用户发送的/ws/private-message地址请求,会被/user/topic/private-messages当前客户端订阅,
  22. //相当于单播
  23. stompClient.subscribe('/user/topic/private-messages', function (message) {
  24. showMessage(JSON.parse(message.body).content);
  25. });
  26. });
  27. }
  28. function showMessage(message) {
  29. $("#messages").append("<tr><td>" + message + "</td></tr>");
  30. }
  31. function sendMessage() {
  32. console.log("sending message");
  33. stompClient.send("/ws/message", {}, JSON.stringify({'messageContent': $("#message").val()}));
  34. }
  35. function sendPrivateMessage() {
  36. console.log("sending private message");
  37. stompClient.send("/ws/private-message", {}, JSON.stringify({'messageContent': $("#private-message").val()}));
  38. }

5, html前端显示

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Hello WS</title>
  6. <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
  7. <script src="/webjars/jquery/jquery.min.js"></script>
  8. <script src="/webjars/sockjs-client/sockjs.min.js"></script>
  9. <script src="/webjars/stomp-websocket/stomp.min.js"></script>
  10. <script src="/js/scripts.js"></script>
  11. </head>
  12. <body>
  13. <div class="container" style="margin-top: 50px">
  14. <div class="row">
  15. <div class="col-md-12">
  16. <form class="form-inline">
  17. <div class="form-group">
  18. <label for="message">Message</label>
  19. <input type="text" id="message" class="form-control" placeholder="Enter your message here...">
  20. </div>
  21. <button id="send" class="btn btn-default" type="button">Send</button>
  22. </form>
  23. </div>
  24. </div>
  25. <div class="row" style="margin-top: 10px">
  26. <div class="col-md-12">
  27. <form class="form-inline">
  28. <div class="form-group">
  29. <label for="private-message">Private Message</label>
  30. <input type="text" id="private-message" class="form-control" placeholder="Enter your message here...">
  31. </div>
  32. <button id="send-private" class="btn btn-default" type="button">Send Private Message</button>
  33. </form>
  34. </div>
  35. </div>
  36. <div class="row">
  37. <div class="col-md-12">
  38. <table id="message-history" class="table table-striped">
  39. <thead>
  40. <tr>
  41. <th>Messages</th>
  42. </tr>
  43. </thead>
  44. <tbody id="messages">
  45. </tbody>
  46. </table>
  47. </div>
  48. </div>
  49. </div>
  50. </body>
  51. </html>

3, 项目地址:

https://github.com/junweipan/springboot-stomp