https://blog.csdn.net/qq_21019419/article/details/82804921
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_21019419/article/details/82804921
第二篇,主要是使用socketjs,stomp模式的websocket简单实现。
第一篇的地址:springboot2.0+websocket集成【群发消息+单对单】
参考:
http://tech.lede.com/2017/03/08/qa/websocket+spring/
https://blog.csdn.net/mr_zhuqiang/article/details/46618197
继续上次的项目。如果对下面的代码有部分看不明白的,请到上一篇看看流程,或者到文末贴出项目的git地址。
1. 先从配置开始,WebStompConfig
代码中的注释基本能够解释清楚每行的意思了,这里就不再细说
完整代码
package com.example.websocketdemo1.stomp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
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;
/**
* EnableWebSocketMessageBroker 注解表明: 这个配置类不仅配置了 WebSocket,还配置了基于代理的 STOMP 消息;
* registerStompEndpoints() 方法:添加一个服务端点,来接收客户端的连接。将 “/chat” 路径注册为 STOMP 端点。这个路径与之前发送和接收消息的目的路径有所不同, 这是一个端点,客户端在订阅或发布消息到目的地址前,要连接该端点,即用户发送请求 :url=’/127.0.0.1:8080/chat’ 与 STOMP server 进行连接,之后再转发到订阅url;
* configureMessageBroker() 方法:配置了一个 简单的消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
*
* @author linyun
* @date 2018/9/13 下午5:15
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebStompConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketHandleInterceptor interceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加一个/chat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
registry.addEndpoint("/chat").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
registry.enableSimpleBroker("/message", "/notice");
//定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
//注册了一个接受客户端消息通道拦截器
registration.interceptors(interceptor);
}
}
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
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
2. 用户信息注册,WebSocketHandleInterceptor
上一篇里面,用户信息我们是直接存储到session中,然后再通过握手的时候,将用户信息存入WebSocketSession。
这次使用stomp的模式也存在一个单对单的发送消息,就需要知道对方是谁,所以也要注册一下用户信息。
完整的代码
package com.example.websocketdemo1.stomp;
import com.sun.security.auth.UserPrincipal;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Principal;
/**
* @author linyun
* @date 2018/9/13 下午5:57
*/
@Component
public class WebSocketHandleInterceptor implements ChannelInterceptor {
/**
* 绑定user到websocket conn上
* @param message
* @param channel
* @return
*/
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String username = accessor.getFirstNativeHeader("username");
if (StringUtils.isEmpty(username)) {
return null;
}
// 绑定user
Principal principal = new UserPrincipal(username);
accessor.setUser(principal);
}
return message;
}
}
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
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
注意这里的username信息
String username = accessor.getFirstNativeHeader("username");
1
1
username是在页面中传递来的,具体的传递方式在后面的页面中,具体的参数名称可以随意自定义。
另外一种获取用户信息的方式:
Object raw = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
System.out.println(raw);
// 打印raw之后,可以查看头部的参数,包含了username。
}
1
2
3
4
5
1
2
3
4
5
3.处理消息的类,GreetingController
用来接收和发送消息。
先来一个消息的model,用来包装消息,使用lombok插件,省去了getset了。
代码:
package com.example.websocketdemo1.stomp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author linyun
* @date 2018/9/13 下午5:44
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String to;
private Long date;
private String from;
private String content;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
controller的完整代码
package com.example.websocketdemo1.stomp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.*;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.security.Principal;
import java.util.Map;
/**
* @author linyun
* @date 2018/9/13 下午5:42
*/
@Slf4j
@Controller
public class GreetingController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 测试页面
* @return
*/
@RequestMapping("/chat4")
public String chat4() {
return "chat4";
}
/**
* 测试页面2
* @return
*/
@RequestMapping("/chat5")
public String chat5() {
return "chat5";
}
/**
* 测试订阅
* @param message
* @param messageHeaders
* @param destination
* @param headers
* @param id
* @param body
*/
@MessageMapping("/hello/{id}")
public void hello(Message message,
MessageHeaders messageHeaders,
@Header("destination") String destination,
@Headers Map<String, Object> headers,
@DestinationVariable long id,
@Payload String body) {
log.info("message:{}", message);
log.info("messageHeaders:{}", messageHeaders);
log.info("destination:{}", destination);
log.info("headers:{}", headers);
log.info("id:{}", id);
log.info("body:{}", body);
}
/*** 群消息 ***/
/**
* 主动返回消息。
* @param message
*/
@MessageMapping("/hello")
public void hello(@Payload com.example.websocketdemo1.stomp.Message message) {
System.out.println(message);
com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
returnMessage.setContent("转发," + message.getContent());
simpMessagingTemplate.convertAndSend("/message/public", returnMessage);
}
/**
* 使用注解的方式返回消息
* @param message
* @return
*/
@MessageMapping("/hello1")
@SendTo("/message/public")
public com.example.websocketdemo1.stomp.Message hello1(@Payload com.example.websocketdemo1.stomp.Message message) {
System.out.println(message);
com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
returnMessage.setContent("转发2," + message.getContent());
return returnMessage;
}
/*** 点对点 ***/
/**
* 点对点发送消息。接收消息的人是从消息中获取的。
* @param message
* @param principal
*/
@MessageMapping("/hello2")
public void hello2(@Payload com.example.websocketdemo1.stomp.Message message, Principal principal) {
System.out.println(message);
System.out.println(principal);
com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
returnMessage.setContent("转发3," + message.getContent());
simpMessagingTemplate.convertAndSendToUser(message.getTo(), "/notice/msg", returnMessage);
}
}
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
稍微解释一下代码中的几个方法
第一个方法,/hello/{id},主要是用来测试在一次发送消息的请求中能够获取到那些参数,合理的利用这些参数于自己的业务中。
@MessageMapping("/hello/{id}")
public void hello(Message message,
MessageHeaders messageHeaders,
@Header("destination") String destination,
@Headers Map<String, Object> headers,
@DestinationVariable long id,
@Payload String body) {
log.info("message:{}", message);
log.info("messageHeaders:{}", messageHeaders);
log.info("destination:{}", destination);
log.info("headers:{}", headers);
log.info("id:{}", id);
log.info("body:{}", body);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
4.结合页面测试
新建2个页面,页面中设置用户的信息
http://127.0.0.1:8080/chat4
username='tom';
http://127.0.0.1:8080/chat5
username='jerry';
1
2
3
4
主要是引入 stomp.js和socketjs这2个js。
页面完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<title>测试websocket</title>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.css">
</head>
<body>
<div class="container">
<button type="button" class="btn btn-primary" onclick="connect()">链接</button>
<button type="button" class="btn btn-primary" onclick="disconnect()">断开</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js"></script>
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script language=javascript>
var username = 'tom';
var sendMessage = null;
var disConnect = null;
function connect() {
var socket = new SockJS("http://127.0.0.1:8080/chat");
var client = Stomp.over(socket);
client.connect({
username: username
}, function (succ) {
console.log('client connect success:', succ);
client.subscribe("/message/public", function (res) {
console.log('收到消息---/message/public:',res);
});
client.subscribe("/user/notice/msg", function (res) {
console.log('个人消息:',res)
});
}, function (error) {
console.log('client connect error:', error);
});
sendMessage = function (destination, headers, body) {
client.send(destination, headers, body)
};
disConnect = function () {
client.disconnect();
console.log('client connect break')
}
}
function disconnect() {
disConnect();
}
//发送聊天信息
function send(roomId, ct) {
var messageModel = {};
messageModel.type = 1;
messageModel.content = ct;
messageModel.from = username;
sendMessage("/app/hello/" + roomId, {}, JSON.stringify(messageModel));
}
/**
* 测试发送一个消息,如果订阅了/sub/public的用户都会收到消息。
*/
function send1() {
var messageModel = {};
messageModel.type = 1;
messageModel.content = '你好,' + new Date().getTime();
messageModel.from = username;
sendMessage("/app/hello", {}, JSON.stringify(messageModel));
}
function send2() {
var messageModel = {};
messageModel.type = 1;
messageModel.content = 'hello1,' + new Date().getTime();
messageModel.from = username;
sendMessage("/app/hello1", {}, JSON.stringify(messageModel));
}
/** 发送消息给个人,接收者 to **/
function send3() {
var messageModel = {};
messageModel.to = 'jerry';
messageModel.type = 1;
messageModel.content = 'hello1,' + new Date().getTime();
messageModel.from = username;
sendMessage("/app/hello2", {}, JSON.stringify(messageModel));
}
}
</script>
</body>
</html>
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
点击链接按钮,主要是做了几个操作,
1. 链接到websocket
2. 订阅/message/public
3. 订阅/user/notice/msg
5. 跑起来测试
进入页面后,打开控制台,直接输入命令。如果觉得不方便,可以在页面加几个按钮,美观点=)
send(‘123456’,‘hello’);
此方法会发起一个消息,推送到/app/hello/123456,这个地址,并带上参数messageModel。
监控到后台 @MessageMapping("/hello/{id}") 接收到的消息。
send1();
发送一条消息给/app/hello,后台接收到之后通过 simpMessagingTemplate.convertAndSend("/message/public", returnMessage); 广播一条消息给所有订阅了/message/public的用户。
所以为了测试,最好多开几个浏览器。观察一下console的打印信息。
send3();
发送一条消息给 @MessageMapping("/hello2") ,注意这里的消息messageModel中加入了to=jerry。后台接收到参数之后,使用 simpMessagingTemplate.convertAndSendToUser(message.getTo(), “/notice/msg”, returnMessage); 将消息发送给jerry,从而实现了单对单的消息推送。
6.总结一下
stomp底层实现都是广播,单对单只是表面看起来特殊一点,本质其实也是生成的一个唯一的广播地址。测试也简单,打开2个页面登录tom,一个登录jerry。用jerry发送消息给tom。2个tom都会收到消息。
页面中注册订阅地址,后台通过注册的地址发送广播。所以/message和/notice后面的参数其实可以随便写。
完整项目 git:https://gitlab.com/tulongx/websocketdemo1
以上。
---------------------
版权声明:本文为CSDN博主「暴躁兔子」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_21019419/article/details/82804921