WebSocket是一种在单个TCP连接上进行全双工通信的协议,浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输。
消息群发#
1.添加如下依赖#
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
|
<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.1.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
|
2.配置WebSocket#
Spring框架提供了基于WebSocket的STOMP支持,STOMP是一个简单的可互操作的协议,通常被用于通过中间服务器在客户端之间进行异步消息传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 如果消息的前缀是"/topic",就会将消息转发给消息代理(broker)
registry.enableSimpleBroker("/topic","/queue");
// 前缀为"/app"的destination可以通过@MessageMapping注解的方法处理
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 客户端通过这里配置的URL建立WebSocket连接
registry.addEndpoint("portal/chat").withSockJS();
}
}
|
3.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
|
@Controller
public class GreetingController {
@Autowired
SimpMessagingTemplate messagingTemplate;
// @MessageMapping("/hello")用来接收"/app/hello"发送的消息
// 再将消息转发到@SendTo定义的路径上(前缀为"/topic",消息将会被broker代理,再由broker广播)
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message) throws Exception{
return message;
}
// @MessageMapping("/hello")
// public void greeting(Message message) throws Exception{
// messagingTemplate.convertAndSend("/topic/greetings",message);
// }
}
@Data
public class Message {
private String name;
private String content;
}
|
4.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
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>群聊</title>
<script src="../lib/jquery/3.3.1/jquery.min.js"></script>
<script src="../lib/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="../lib/stomp-websocket/2.3.3/stomp.min.js"></script>
<script src="../js/app.js"></script>
</head>
<body>
<div>
<label for="name">请输入用户名</label>
<input type="text" id="name" placeholder="用户名">
</div>
<div>
<button id="connect" type="button">连接</button>
<button id="disconnect" type="button" disabled="disabled">断开连接</button>
</div>
<div id="chat" style="display: none"></div>
<div>
<label for="content">请输入聊天内容</label>
<input type="text" id="content" placeholder="聊天内容">
</div>
<button id="send" type="button">发送</button>
<div id="greetings"></div>
<div id="conversation" style="display: none">群聊进行中...</div>
</body>
</html>
|
5.JavaScript#
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
|
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled",connected);
$("#disconnect").prop("disabled",!connected);
if (connected){
$("#conversation").show();
$("#chat").show();
}
else {
$("#conversation").hide();
$("#chat").hide();
}
$("#greetings").html("");
}
function connect() {
if (!$("#name").val()){
return;
}
var socket = new SockJS('/portal/chat');
// 创建一个stomp实例发起连接请求
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
// 订阅服务端发回来的消息
stompClient.subscribe('/topic/greetings',function (greeting) {
showGreeting(JSON.parse(greeting.body))
});
});
}
function disconnect() {
if (stompClient !== null){
stompClient.disconnect();
}
setConnected(false);
}
function sendName() {
stompClient.send("/app/hello",{},
JSON.stringify({'name': $("#name").val(),'content':$("#content").val()}));
}
function showGreeting(message) {
$("#greetings")
.append("<div>" + message.name + ":" + message.content + "</div>")
}
$(function () {
$("#connect").click(function () { connect(); });
$("#disconnect").click(function () { disconnect(); });
$("#send").click(function () { sendName(); });
})
|
消息点对点发送#
点对点发送,应该有用户的概念,因此需加入spring security依赖
1.配置spring security#
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
|
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// System.out.println("------------encoder.encode:" + encoder.encode("456"));
// super.configure(auth);
auth.inMemoryAuthentication()
.withUser("admin")
.password(encoder.encode("123"))
.roles("admin")
.and()
.withUser("sang")
.password(encoder.encode("456"))
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
|
2.configuration#
同上,多加了一个/queue
,方便对群发消息和点对点消息进行管理
3.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
|
@Controller
public class GreetingController {
@Autowired
SimpMessagingTemplate messagingTemplate;
// @MessageMapping("/hello")用来接收"/app/hello"发送的消息
// 再将消息转发到@SendTo定义的路径上(前缀为"/topic",消息将会被broker代理,再由broker广播)
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message) throws Exception{
return message;
}
// @MessageMapping("/hello")
// public void greeting(Message message) throws Exception{
// messagingTemplate.convertAndSend("/topic/greetings",message);
// }
@MessageMapping("/chat")
public void chat(Principal principal, Chat chat){
// Principal用来获取当前登录用户的信息,第二个参数是客户端发送来的消息
String from = principal.getName();
chat.setFrom(from);
// System.out.println("------------chat.getTo():" + chat.getTo());
messagingTemplate.convertAndSendToUser(chat.getTo(),"/queue/chat",chat);
}
}
@Data
public class Chat {
private String to;
private String from;
private String content;
}
|
消息发送使用的方法是convertAndSendToUser
,该方法内部调用了convertAndSend
方法,并对消息路径做了处理,部门源码如下:
1
2
3
4
5
6
7
|
public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Assert.notNull(user, "User must not be null");
Assert.isTrue(!user.contains("%2F"), "Invalid sequence \"%2F\" in user name: " + user);
user = StringUtils.replace(user, "/", "%2F");
destination = destination.startsWith("/") ? destination : "/" + destination;
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}
|
这里destinationPrefix
的默认值是"/user/"
,也就是说消息的最终发送路径是 "/user/用户名/queue/chat"
4.html#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单聊</title>
<script src="../lib/jquery/3.3.1/jquery.min.js"></script>
<script src="../lib/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="../lib/stomp-websocket/2.3.3/stomp.min.js"></script>
<script src="../js/chat.js"></script>
</head>
<body>
<div id="chat"></div>
<div id="chatsContent"></div>
<div>
请输入聊天内容:
<input type="text" id="content" placeholder="聊天内容">
目标用户:
<input type="text" id="to" placeholder="目标用户">
<button id="send" type="button">发送</button>
</div>
</body>
</html>
|
5.JavaScript#
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
|
var stompClient = null;
function connect() {
var socket = new SockJS('/portal/chat');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
// 订阅的地址比服务端配置的地址多了"/user"前缀,
// 是因为SimpMessagingTemplate类中自动添加了路径前缀
stompClient.subscribe('/user/queue/chat', function (chat) {
console.log("chat:",chat);
showGreeting(JSON.parse(chat.body));
});
});
}
function sendMsg() {
stompClient.send("/app/chat", {},
JSON.stringify({'content': $("#content").val(), 'to': $("#to").val()}));
}
function showGreeting(message) {
$("#chatsContent")
.append("<div>" + message.from + ":" + message.content + "</div>")
}
$(function () {
connect();
$("#send").click(function () {
sendMsg();
});
})
|