示例:(基于stomp协议)
用的<<深入浅出SpringBoot 2.x>>书中的例子:
不是所有浏览器都支持WebSocket协议,为了兼容不支持的浏览器,可以使用STOMP(Simple or Streaming Text Orientated Messaging Protocol)协议进行处理。
1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 配置STOMP服务器端点和请求订阅前缀
添加@Enable WebSocketMessageBroker注解启动WebSocket下的子协议STOMP,可以通过实现WebSocketMessageBrokerConfigurer接口或继承这个接口的空实现的抽象类AbstractWebSocketMessageBrokerConfigurer,覆盖相关方法接口。
registerStompEndpoints 方法是用于注册端点的方法,这里定义了'/socket'和'wsuser'两个服务端点,而在定义端点时还加入了withSockJS 方法, 这个方法的声明代表着可以支持SockJS 。SockJS 是一个第二方关于支持WebSocket 请求的JavaScript 框架。再看到configureMessageBroker方法,这个方法可以注册请求的前缀和客户端订阅的前缀。
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@EnableWebSocketMessageBroker
@Configuration
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
// 增加一个聊天服务端点
stompEndpointRegistry.addEndpoint("/socket").withSockJS();
// 增加一个用户服务端点
stompEndpointRegistry.addEndpoint("/wsuser").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) {
// 客户端订阅路径前缀
messageBrokerRegistry.enableSimpleBroker("/sub", "/queue");
// 服务端点请求前缀
messageBrokerRegistry.setApplicationDestinationPrefixes("/request");
}
}
3. 接收与发送消息
通过步骤2的配置,Spring Boot 就会自动创建SimpMessagingTemplate 对象,它是一个可以进行转发消息的模板,通过这个模板可以发送消息到特定的地址,甚至是限制给特定的用户发送消息。
sendMsg 和sendToUser 方法,这两个方法标注了@MessageMapping注解,这个注解与Spring MVC 的@RequestMapping类似,它是定义WebSocket 请求的路径,需要与WebSocketConfig 所定义的前缀('/request')连用。
sendMsg方法还标注了@SendTo注解,配置为'/sub/chat',说明在执行完成这个方法后,会将返回结果发送到订阅的这个目的地中,这样客户端就可以得到消息。
sendToUser 方法,这个方法存在Principal 参数,如果使用了Spring Security,这个参数可以获取当前用户的消息,通过SimpMessagingTemplate的convertAndSendToUser 方法,就可以设置发送给对应的目的地并且限定特定的用户消息。因为这里涉及用户,所以需要使用Spring Security 。添加对应的starter后,添加Spring Security的配置。
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.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.security.Principal;
@Controller
@RequestMapping("/static/websocket")
public class WebSocketController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
// 定义消息请求路径
@MessageMapping("/send")
// 定义结果发送到特定路径
@SendTo("/sub/chat")
public String sendMsg(String value) {
return value;
}
// 将消息发送给特定用户
@MessageMapping("/sendUser")
public void sendToUser(Principal principal, String body) {
String srcUser = principal.getName();
// 解析用户和消息
String[] args = body.split(",");
String desUser = args[0];
String message = "【" + srcUser + "】给你发来消息:" + args[1];
// 发送到用户和监听地址
simpMessagingTemplate.convertAndSendToUser(desUser, "/queue/customer", message);
}
}
添加SpringSecurity用户:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 定义3个可以登录的内存用户
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码加密器
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 加入3个内存用户,
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder)
.withUser("user1")
.password(passwordEncoder.encode("user1"))
.roles("USER")
.and()
.withUser("user2")
.password(passwordEncoder.encode("user2"))
.roles("ADMIN")
.and()
.withUser("user3")
.password(passwordEncoder.encode("user3"))
.roles("USER");
}
}
4. 客户端页面
加入了socket.min.js 和stomp.min.js 两个JavaScript 脚本,这样就可以通过对应JavaScript API 进行请求服务器端点。首先是建立连接connect函数,其次是发送消息sendMsg 函数, 最后是关闭连接disconnect 函数。这里的sendMsg 函数请求的是WebSocketController的sendMsg 方法,这样这个方法就将消息发送到'/sub/chat'中, 所以需要一个客户端去订阅这个地址。
(1)发送消息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket测试</title>
</head>
<body>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<p><label>发送的内容</label></p>
<p><textarea id="message" rows="5"></textarea></p>
<button id="sendMsg" onclick="sendMsg();">Send</button>
<p id="response"></p>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.3.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient = null;
// 设置连接
function setConnected(conncted) {
$('#connect').attr({
'disabled': conncted
});
$('#disconnect').attr({
'disabled': !conncted
});
if (conncted) {
$('#conversationDiv').show();
} else {
$('#conversationDiv').hide();
}
$('#response').html('');
}
// 开启socket连接
function connect() {
// 定义请求服务器的端点
var socket = new SockJS('/socket');
// stomp客户端
stompClient = Stomp.over(socket);
// 连接服务器端点
stompClient.connect({}, function(frame) {
setConnected(true);
});
}
// 断开socket连接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 向request/send服务端发送消息
function sendMsg() {
var value = $('#message').val();
// 发送消息到/request/send,其中/request是服务器定义的前缀,/send是@MessageMapping所配置的路径
stompClient.send('/request/send', {}, value);
}
</script>
</body>
</html>
(2)订阅消息
客户端stompC!ient 在服务器端点之后,加入了订阅消息的地址,这样就能够获取WebSocketController的sendMsg方法发送到'/sub/chat'的消息。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket测试</title>
</head>
<body>
<h1><span id="receive">等待接收消息</span></h1>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.3.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var noticeSocket = function() {
var s = new SockJS('/socket');
var stompClient = Stomp.over(s);
stompClient.connect({}, function() {
console.log('notice socket connected!');
// 订阅消息地址
stompClient.subscribe('/sub/chat', function (data) {
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
</body>
</html>
5. 发送给指定用户
(1)发送消息
连接服务器端点'/wsuser',请求'/request/sendUser',这样就对应了WebSocketController 的sendToUser 方法。把用户名称和消息都发给了服务端点,所以在sendToUser 方法里,分离了用户名和消息,然后就通过消息模板simpMessagingTemplate的convertAndSendToUser 方法,指定了用户参数,发送到地l止'/queue/customer',这样对应的用户登录后,通过订阅这个地址就能够得到服务器发送的消息。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket测试</title>
</head>
<body>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<p><label>发送给用户</label></p>
<p><input type="text" id="user"/></p>
<p><label>发送的内容</label></p>
<p>
<textarea id="message" rows="5"></textarea>
</p>
<button id="sendMsg" onclick="sendMsg();">Send</button>
<p id="response"></p>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.3.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient = null;
// 设置连接
function setConnected(conncted) {
$('#connect').attr({
'disabled': conncted
});
$('#disconnect').attr({
'disabled': !conncted
});
if (conncted) {
$('#conversationDiv').show();
} else {
$('#conversationDiv').hide();
}
$('#response').html('');
}
// 开启socket连接
function connect() {
// 定义请求服务器的端点
var socket = new SockJS('/wsuser');
// stomp客户端
stompClient = Stomp.over(socket);
// 连接服务器端点
stompClient.connect({}, function(frame) {
setConnected(true);
});
}
// 断开socket连接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 向request/send服务端发送消息
function sendMsg() {
var value = $('#message').val();
var user = $('#user').val();
var text = user + ',' + value;
// 发送消息到/request/send,其中/request是服务器定义的前缀,/send是@MessageMapping所配置的路径
stompClient.send('/request/sendUser', {}, text);
}
</script>
</body>
</html>
(2)接收消息
订阅了'/user/queue/customer',这里的'/user'前缀是不能缺少的, 它代表着订阅指定用户的消息。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket测试</title>
</head>
<body>
<h1><span id="receive">等待接收消息</span></h1>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.3.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var noticeSocket = function() {
var s = new SockJS('/wsuser');
var stompClient = Stomp.over(s);
stompClient.connect({}, function() {
console.log('notice socket connected!');
// 订阅消息地址
stompClient.subscribe('/user/queue/customer', function(data) {
console.log(data);
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
</body>
</html>
(3)测试
使用user1和user2账号登录,user1用户打开发送页面,指定接收用户是user2;user2用户打开接收页面进行订阅。