springboot整合websocket-stomp

摘自官网

STOMP(简单文本导向消息传递协议)最初是为脚本语言(例如 Ruby、Python 和 Perl)创建的,用于连接到企业消息代理。它旨在解决常用消息传递模式的最小子集。STOMP 可用于任何可靠的双向流式网络协议,例如 TCP 和 WebSocket。虽然 STOMP 是一种面向文本的协议,但消息负载可以是文本或二进制。

客户端可以使用SEND或SUBSCRIBE命令发送或订阅消息,以及destination描述消息内容和接收者信息的标头。这样便可以启用一种简单的发布-订阅机制,您可以使用该机制通过代理向其他连接的客户端发送消息,或向服务器发送消息以请求执行某些工作。

引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

配置

  1. 连接时校验用户信息,并返回重写的Principal
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component
    public class MyHandshakeHandler extends DefaultHandshakeHandler {

    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
    if (!(request instanceof ServletServerHttpRequest)) {
    return null;
    }
    ServletServerHttpRequest req = (ServletServerHttpRequest) request;
    // 获取请求参数中携带的uid
    String uid = req.getServletRequest().getParameter("uid");
    if(uid == null){
    throw new RuntimeException("未登录");
    }
    return new MyPrincipal(uid);
    }
    }
  2. 用户连接,退出的操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Component
    public class MyWebSocketHandler implements WebSocketHandlerDecoratorFactory {

    @Override
    public WebSocketHandler decorate(WebSocketHandler handler) {
    return new WebSocketHandlerDecorator(handler) {
    // 用户登录
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    String uid = session.getPrincipal().getName();
    System.out.println(uid + "登陆");
    super.afterConnectionEstablished(session);
    }

    // 用户退出
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
    String uid = session.getPrincipal().getName();
    System.out.println(uid + "退出");

    super.afterConnectionClosed(session, closeStatus);
    }
    };
    }
    }
  3. websocket配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    @Configuration
    //开启消息代理,默认使用内置消息代理,也可以选择配置RabbitMQ等
    @EnableWebSocketMessageBroker
    public class WebSocketConfigurer implements WebSocketMessageBrokerConfigurer {

    @Autowired
    private MyHandshakeHandler myHandshakeHandler;
    @Autowired
    private MyWebSocketHandler myWebSocketHandler;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
    //客户端和服务端进行连接的endpoint
    stompEndpointRegistry.addEndpoint("/im/conn")
    //设置连接校验
    .setHandshakeHandler(myHandshakeHandler)
    //跨域
    .setAllowedOriginPatterns("*")
    //开启sockjs
    .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
    // 启用/user /topic两个消息前缀,消息发送的前缀,也是客户端订阅的前缀
    registry.enableSimpleBroker("/user", "/topic");
    // 当使用SimpMessagingTemplate#convertAndSendToUser发送消息时,客户端订阅用/user开头。
    // 即一对一发送消息,使用/user为前缀订阅
    registry.setUserDestinationPrefix("/user");
    // 客户端端向服务端发送消息的前缀
    registry.setApplicationDestinationPrefixes("/im/");
    }

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
    // 登陆退出的提示
    registry.addDecoratorFactory(myWebSocketHandler);
    }
    }

服务端

  1. 定义一个消息的实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Message implements Serializable {
    //发送消息的用户id
    private String uid;
    //接收消息的用户id
    private String toUid;
    //发送的文本消息
    private String content;
    // getter setter ...
    }
  2. controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    @RestController
    public class ImController {

    // 发送消息的模板
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    /**
    * 发送消息,一对一。
    * @MessageMapping接收客户端/im/send2user发送来的消息。
    * /im开头参考setApplicationDestinationPrefixes("/im/")。
    * Principal为连接websocket校验时返回的,可以直接在参数中使用。
    */
    @MessageMapping("/send2user")
    public String send2user(Message message, Principal principal) {
    // 获取用户的uid
    String uid = principal.getName();
    // 设置发送信息的uid
    message.setUid(uid);
    System.out.println(uid + ":" + message);
    // 发送给订阅/user/{toUid}/msg的用户
    // /user开头参考setUserDestinationPrefix("/user")
    // 这里的toUid是接收消息用户的uid
    simpMessagingTemplate.convertAndSendToUser(message.getToUid(), "msg", message);
    return "success";
    }

    /**
    * 广播消息,发送给所有订阅/topic/sys的用户。
    * 也可以使用@SendTo注解,返回值为发送的消息即可。
    * @SendTo是发送给订阅的用户
    * 使用@GetMapping方便直接使用http请求
    */
    @GetMapping("/sendAll")
    // @SendTo("/topic/sys")
    public String sendAll(String message) {
    System.out.println("广播消息:" + message);
    // 如果是群聊,根据传递参数的群聊房间号,动态拼接/topic/{房间号},前端订阅/topic/{房间号}即可
    simpMessagingTemplate.convertAndSend("/topic/sys", message);
    return "success";
    // return msg;
    }
    }

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<!-- sockjs stomp -->
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script>
let stompClient = null;

//连接websocket
function conn() {
let uid = $("#uidInput").val();
//可以在后面直接拼接参数,在MyHandshakeHandler中校验用户uid
let socket = new SockJS('http://localhost:8080/im/conn?uid=' + uid);
stompClient = Stomp.over(socket);
stompClient.connect({}, function () {
//订阅/user/{uid}/msg这个地址,接收往这个地址发送的消息
stompClient.subscribe('/user/' + uid + '/msg', function (msg) {
//后台返回的信息是实体类,将其转换成json,添加到ul中
let msgBody = JSON.parse(msg.body);
$("#userMsg").append("<li>" + msgBody.uid + ":" + msgBody.content + "</li>")
});
//订阅/topic/sys这个地址,接收往这个地址发送的消息
stompClient.subscribe('/topic/sys', function (msg) {
$("#sysMsg").append("<li>" + msg.body + "</li>")
});
//stompClient.subscribe()....多个订阅地址,也可以在外面定义。前提是stompClient已经连接

//隐藏连接div
$("#connDiv").hide();
//显示消息div
$("#msgDiv").show();
}, function (err) {
console.log(err);
});
}

//发送一对一消息
function send() {
let content = $("#content").val();
if (!content) {
alert("请输入消息");
}
let toUid = $("#toUid").val();
if (!toUid) {
alert("请输入发送给用户的uid");
}
let msg = {"content": content, "toUid": toUid};
if (!stompClient) {
alert("未连接");
}
//前端发送消息以/im开头,往send2user中发送消息,消息为JSON.stringify(msg)
stompClient.send("/im/send2user", {}, JSON.stringify(msg));
}
</script>
</head>
<body>
<div id="connDiv">
<input type="text" id="uidInput" placeholder="请输入uid">
<button onclick="conn()">连接</button>
</div>
<div id="msgDiv" style="display: none">
<input id="content" type="text" placeholder="消息内容"/>
<input id="toUid" type="text" placeholder="发送给用户的uid">
<button onclick="send()">发送</button>
<br/>
<label>用户消息:</label>
<ul id="userMsg"></ul>
<label>系统消息:</label>
<ul id="sysMsg"></ul>
</div>
</body>
</html>