我们都知道,websocket协议是基于TCP的一种网络通信协议。比常见的http协议不同,http协议是单工的,只能有客户端发送请求,服务端响应并返回结果,这种协议足以应对通常场景,但是在需要客户端和服务端实时通信时却会大量占用资源,效率低下。为了提高效率,需要让服务端主动发送消息给客户端,这就是websocket协议。
stomp是一个面向文本/流的消息协议,提供了能够协作的报文格式,因此stomp客户端可以与任何的stomp消息代理进行通信,从而可以为多语言、多平台和Brokers集群提供简单普遍的消息协作。
stomp overWebsocket既是通过Websocket建立stomp连接,即在Websocket的连接基础上再建立stomp连接
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
import com.websocket.demo.interceptor.UserInterceptor;
import org.springframework.context.annotation.Bean;
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;
/**
* @Author hezhan
* @Date 2019/9/24 11:31
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注册stomp的端点
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加一个/stomp端点,客户端可以通过这个端点进行连接,withSockJS的作用是添加SockJS支持
registry.addEndpoint("/stomp").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//客户端发送消息的请求前缀
registry.setApplicationDestinationPrefixes("/webSocket");
//客户端订阅消息的请求前缀,topic一般用于广播推送,user用于点对点推送,点对点推送的订阅前缀必须与下面定义的通知客户端的前缀保持一致
registry.enableSimpleBroker("/topic", "/user");
//服务端通知客户端的前缀,可以不设置,默认为user
registry.setUserDestinationPrefix("/user/");
}
/**
* 配置客户端入站通道拦截器
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration){
registration.interceptors(createUserInterceptor());
}
/**
* 将自定义的客户端渠道拦截器加入IOC容器中
* @return
*/
@Bean
public UserInterceptor createUserInterceptor(){
return new UserInterceptor();
}
}
自定义的客户端通道拦截器代码如下:
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;
/**
* @Author hezhan
* @Date 2019/9/25 15:25
* 客户端渠道拦截适配器
*/
public class UserInterceptor implements ChannelInterceptor {
/**
* 获取包含在stomp中的用户信息
*/
public Message<?> preSend(Message<?> message, MessageChannel channel){
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())){
String userName = accessor.getNativeHeader("name").get(0);
accessor.setUser(new FastPrincipal(userName));
}
return message;
}
}
其中有一个FastPrincipal类是自定义的用户保存用户信息的类:
import java.security.Principal;
/**
* @Author hezhan
* @Date 2019/9/24 16:51
* 权限验证类
*/
public class FastPrincipal implements Principal {
private final String name;
public FastPrincipal(String name){
this.name = name;
}
@Override
public String getName() {
return name;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
/**
* @Author hezhan
* @Date 2019/9/24 17:28
*/
@RestController
public class StompController {
@Autowired
SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/message")
public void subscription(Message message, Principal principal) throws Exception {
simpMessagingTemplate.convertAndSendToUser(message.getTo(), "/queue", new Message(message.getMessage(), message.getDatetime(), message.getFrom(), message.getTo()));
System.out.println(principal.getName() + "发送了一条消息给:" + message.getTo());
}
}
SimpMessagingTemplate为springboot封装的操作WebSocket的类,使用convertAndSendToUser(参数一,参数二,参数三)方法,可以向指定的用户发送消息,其中参数一为指定的用户标识,参数二为自定义的客户端订阅信息的URL,参数三为发送的具体消息。
若想不指定用户推送,则可以使用convertAndSend(参数一,参数二)方法,其中参数一为制定的订阅URL,参数二为推送的消息主体,其中指定的订阅URL前缀必须为WebSocketConfig配置类中指定的订阅前缀。
为了方便消息的推送,我们这里可以规范一下消息的格式:
/**
* @Author hezhan
* @Date 2019/9/25 15:40
* 通信消息规范格式
*/
public class Message {
private String message;//消息内容
private String datetime;//发送时间
private String from;//消息来源ID
private String to;//发送消息给ID
public Message(String message, String datetime, String from, String to) {
this.message = message;
this.datetime = datetime;
this.from = from;
this.to = to;
}
public Message() {
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDatetime() {
return datetime;
}
public void setDatetime(String datetime) {
this.datetime = datetime;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
}
以上,服务端的代码已经编写完毕,挺简单的吧,毕竟springboot已经封装了大部分底层实现。接下来是客户端的代码编写。
在这里具体我们用html+sockjs来实现客户端的构建
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
话不多说,直接上代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>stomp测试</title>
<link href="https://cdn.bootcss.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<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>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="connect">注册用户,请输入你的账号:</label>
<input type="text" id="userName" class="form-control" value="tony" placeholder="请输入账号"/>
<button id="connect" class="btn" type="submit">连接</button>
</div>
</form>
</div>
<div>
<form class="form-inline">
<div class="form-group">
<label for="name">接收人:</label>
<input type="text" id="name" class="form-control"/>
<label for="message">发送消息:</label>
<input type="text" id="message"/>
</div>
<button id="send" class="btn" type="submit">发送</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table">
<thead>
<tr>
<th>接收到的消息</th>
</tr>
</thead>
<tbody id="greetings"></tbody>
</table>
</div>
</div>
</div>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", connected);
if (connected){
$("#conversation").show();
} else {
$("#conversation").hide();
}
$("#greetings").html("");
}
var url = "http://localhost:8080";
function connect() {
var userName = $("#userName").val();
var socket = new SockJS(url + "/stomp?name=" + userName);
stompClient = Stomp.over(socket);
stompClient.connect({
name:$("#userName").val()
}, function (frame) {
setConnected(true);
console.log("connected:" + frame);
stompClient.subscribe("/user/" + $("#userName").val() +"/queue", function (data) {
var mes = data.body;
showGreeting(mes);
});
});
}
function send() {
stompClient.send("/webSocket/message", {}, JSON.stringify({
message:$("#message").val(),
datetime:"2019-09-25",
from:$("#userName").val(),
to:$("#name").val()
}));
}
function showGreeting(message) {
console.log("显示信息:" + message);
var json = JSON.parse(message).message;
$("#greetings").append("" + json + " ");
}
$(function () {
$("form").on("submit", function (e) {
e.preventDefault();
});
$("#connect").click(function () {
connect();
});
$("#send").click(function () {
send();
});
});
</script>
</body>
</html>
代码上完后,接下来分析一波。
先看一下客户端与服务端连接已经订阅客户端消息的方法
var url = "http://localhost:8080";
function connect() {
var userName = $("#userName").val();
var socket = new SockJS(url + "/stomp?name=" + userName);
stompClient = Stomp.over(socket);
stompClient.connect({
name:$("#userName").val()//这里必须要携带用户信息,这样服务端UserInterceptor客户端拦截器才能从getNativeHeader("name")中拿到用户信息
}, function (frame) {
setConnected(true);
console.log("connected:" + frame);
/*这里是客户端订阅服务端的消息,
订阅路径为/user/指定的用户名/queue,
就是对应上面服务端controller层中convertAndSendToUser方法制定的路径,
由于这里是点对点通信,所以制定的路径前面会加上/user/前缀,之后还要加上指定的用户信息,
如果是不指定用户名的推送的订阅,则URL直接为服务端convertAndSend方法中定义的URL*/
stompClient.subscribe("/user/" + $("#userName").val() +"/queue", function (data) {
var mes = data.body;//这里获取服务端推送的消息主体
showGreeting(mes);
});
});
}
接下来说明一下客户端发送消息的代码
function send() {
/*
这里发送的路径为客户端请求前缀加contronller层方法的路由,
再加上遵循上面自定义的消息规范参数,
这样就将这些这些消息发送给指定的用户了
*/
stompClient.send("/webSocket/message", {}, JSON.stringify({
message:$("#message").val(),
datetime:"2019-09-25",
from:$("#userName").val(),
to:$("#name").val()
}));
}