本文使用stomp
STOMP(面向简单文本的消息传递协议)最初是为脚本语言(例如Ruby,Python和Perl)创建的,以连接到企业消息代理。它旨在解决常用消息传递模式的最小子集。STOMP可以在任何可靠的双向流网络协议上使用,例如TCP和WebSocket。尽管STOMP是面向文本的协议,但是消息有效负载可以是文本或二进制。
客户端可以使用SEND或SUBSCRIBE命令来发送或订阅消息,以及destination描述消息的内容和应由谁接收的标头。这启用了一种简单的发布-订阅机制,您可以使用该机制通过代理将消息发送到其他连接的客户端,或者将消息发送到服务器以请求执行某些工作。
当您使用Spring的STOMP支持时,Spring WebSocket应用程序将充当客户端的STOMP代理。消息被路由到@Controller消息处理方法或简单的内存中代理,该代理跟踪订阅并向订阅的用户广播消息。您还可以将Spring配置为与专用的STOMP代理(例如RabbitMQ,ActiveMQ等)一起使用,以实际广播消息。在那种情况下,Spring维护与代理的TCP连接,将消息中继到该代理,并将消息从该代理向下传递到已连接的WebSocket客户端。因此,Spring Web应用程序可以依靠基于HTTP的统一安全性,通用验证以及用于消息处理的熟悉的编程模型。
--摘自官网
- 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
- 定义一个连接校验,用于记录用户的信息
/**
* 连接时校验用户信息,并返回重写的Principal
*/
@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);
}
}
- 定义一个记录用户登陆退出,方便观察
/**
* 用户登录退出操作
*/
@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);
}
};
}
}
- websocket配置
@Configuration
//开启消息代理,默认使用内置消息代理,也可以选择配置RabbitMQ等
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private MyWebSocketHandler myWebSocketHandler;
@Autowired
private MyHandshakeHandler myHandshakeHandler;
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//启用/user /topic两个消息前缀,消息发送的前缀,也是前端订阅的前缀
registry.enableSimpleBroker("/user", "/topic");
//当使用convertAndSendToUser发送消息时,前端订阅用/user开头。即一对一发送消息,使用/user为前缀订阅
registry.setUserDestinationPrefix("/user");
//前端向服务端发送消息的前缀
registry.setApplicationDestinationPrefixes("/im/");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
//客户端和服务端进行连接的endpoint
//如果使用移动端开发app,需要/im/conn/websocket连接
stompEndpointRegistry.addEndpoint("/im/conn")
.setHandshakeHandler(myHandshakeHandler)//设置连接校验
.setAllowedOrigins("*")//跨域
.withSockJS();//开启sockjs
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
//注册登陆退出
registry.addDecoratorFactory(myWebSocketHandler);
}
}
- 创建一个消息的实体类
/**
* 一对一发送的消息
*/
@Data
public class SendMsg implements Serializable {
//发送消息的用户id
private String uid;
//接收消息的用户id
@NotNull(message = "未选择用户")
private String toUid;
//发送的文本消息
@NotNull(message = "消息不能为空")
private String content;
}
- 创建controller
@RestController
public class ImController {
//发送消息的模板
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 发送消息,一对一
* Principal为连接websocket校验时返回的,可以直接在参数中使用
* 也可以使用@Validated校验参数的合法性
*
* @param msg
* @param principal
* @return
*/
@MessageMapping("/send2user")
public String send2user(@Validated SendMsg msg, Principal principal) {
//获取用户的uid
String uid = principal.getName();
//设置发送信息的uid
msg.setUid(uid);
System.out.println(uid + ":" + msg);
//发送给订阅/user/{toUid}/msg的用户
//这里的toUid是接收消息用户的uid
simpMessagingTemplate.convertAndSendToUser(msg.getToUid(), "msg", msg);
return "success";
}
/**
* 发送消息,发送给所有订阅/topic/sys的用户
* 也可以使用@SendTo注解,返回值为发送的消息即可
*
* @param msg
* @return
*/
@GetMapping("/sendAll")
// @SendTo("/topic/sys")
public String sendAll(String msg){
System.out.println("广播消息:" + msg);
//如果是群聊,根据传递参数的群聊房间号,动态拼接/topic/{房间号},前端订阅/topic/{房间号}即可
simpMessagingTemplate.convertAndSend("/topic/sys", msg);
return "success";
// return msg;
}
}
- html页面
<!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>
- 测试
浏览器访问http://localhost:8080/index.html
开启两个页面,分别输入两个不同的uid,这里使用111,222
控制台:
111登陆
222登陆
从111的页面发送消息给222,并从222的页面发送消息给111
控制台:
111:SendMsg(uid=111, toUid=222, content=在吗?)
222:SendMsg(uid=222, toUid=111, content=在的)
从浏览器访问sendAll接口,发送消息给所有订阅/topic/sys的用户
http://localhost:8080/sendAll?msg=大家晚上好
控制台:
广播消息:大家晚上好
示例图:
作者公众号