1, 项目介绍
本项目集成websocket在springboot上, 用的是STOMP协议, Stomp可不是是暴风雨(Storm). Stomp 的 全称叫 Simple Text Orientated Messaging Protocol,就是一个简单的文本定向消息协议,除了设计为简单易用之外,它的支持者也非常多。就比如目前主流的消息队列服务器如RabbitMQ、ActiveMQ都支持Stomp 协议。
项目是一个STOMP应用简单例子, 实现了广播, 单播(发给自己和发给别人)的功能, 还实现了一个定时广播功能.
上图是项目文件的主要功能的伪代码形式.
2, 项目代码
1, 引入依赖文件
<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.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.1.1-1</version>
</dependency>
2, 编写配置类
package com.junwei.config;
import com.junwei.controller.UserHandshakeHandler;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(final MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/ws");
}
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("/our-websocket")
.setHandshakeHandler(new UserHandshakeHandler())
.withSockJS();
}
}
配置类中实现 WebSocketMessageBrokerConfigurer
接口, 重写configureMessageBroker
和registerStompEndpoints
两个方法, 其实就是注册各种url地址.
registry.enableSimpleBroker(“/topic”) | 连接后前端接受的广播url地址都要以’/topic’开头 |
---|---|
registry.setApplicationDestinationPrefixes(“/ws”) | 前端发送的请求地址url以’/ws’开头 |
registry.addEndpoint(“/our-websocket”) | 建立前端和后端的连接 |
.setHandshakeHandler(new UserHandshakeHandler()) | 在建立连接前设置一个UserHandshakeHandler, 用于处理建立连接前处理(比如用户身份获取,验证) |
.withSockJS() | 把普通的Websocket接口转成SockJS |
UserHandshakeHandler
类, 当前只是生成一个随机UUID, 存入UserPrincipal
import com.sun.security.auth.UserPrincipal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import java.security.Principal;
import java.util.Map;
import java.util.UUID;
public class UserHandshakeHandler extends DefaultHandshakeHandler {
private final Logger LOG = LoggerFactory.getLogger(UserHandshakeHandler.class);
@Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
final String randomId = UUID.randomUUID().toString();
LOG.info("User with ID '{}' opened the page", randomId);
return new UserPrincipal(randomId);
}
}
3, 编写controller
import com.junwei.entity.Message;
import com.junwei.entity.ResponseMessage;
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.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;
import java.security.Principal;
@Controller
public class MessageController{
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public MessageController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
@MessageMapping("/message")
@SendTo("/topic/messages")
public ResponseMessage getMessage(final Message message) throws InterruptedException {
Thread.sleep(1000);
return new ResponseMessage(HtmlUtils.htmlEscape(message.getMessageContent()));
}
@MessageMapping("/private-message")
@SendToUser("/topic/private-messages")
public ResponseMessage getPrivateMessage(final Message message,
final Principal principal) throws InterruptedException {
Thread.sleep(1000);
return new ResponseMessage(HtmlUtils.htmlEscape(
"Sending private message to user " + principal.getName() + ": "
+ message.getMessageContent())
);
}
}
JS前端会发送请求stompClient.send(“/ws/message”, {}), 后端接受处理的controller会根据注释的url地址去寻找对应的method,@MessageMapping("/message")
这里的/message的前缀/ws是在config中的registry.setApplicationDestinationPrefixes("/ws")
配置的.
JS前端的订阅地址是stompClient.subscribe(‘/topic/messages’, function (message),对应后端controller中的 @SendTo("/topic/messages")
, 我们发现这里用的就已经是完整url了, 跟上面的注意区别. 尽管我们在config中配置过registry.setApplicationDestinationPrefixes("/ws")
注意, 在单播中方法上注释的是@SendToUser("/topic/private-messages")
, 但是在JS前端, 如果我们要订阅该url, 需要加一个默认前缀/user: stompClient.subscribe(‘/user/topic/private-messages’, function (message)
这个/user是默认值, 如果需要修改, 需要在config类中添加
@Override
public void configureMessageBroker(final MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/broadcast"); // broadcast to user if user sebscribe the /topic/xxx
registry.setApplicationDestinationPrefixes("/ws"); // SockJS service url prefix
//定义一对一推送的时候前缀,默认就是user
//registry.setUserDestinationPrefix("/user");
}
Message
和 Response
是两个普通的POJO类
public class Message {
private String messageContent;
public String getMessageContent() {
return messageContent;
}
public void setMessageContent(String messageContent) {
this.messageContent = messageContent;
}
}
public class ResponseMessage {
private String content;
public ResponseMessage() {
}
public ResponseMessage(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
4, 前端JS部分
var stompClient = null;
$(document).ready(function() {
console.log("Index page is ready");
connect();
$("#send").click(function() {
sendMessage();
});
$("#send-private").click(function() {
sendPrivateMessage();
});
});
function connect() {
var socket = new SockJS('/our-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
// 订阅广播地址/topic/messages
stompClient.subscribe('/topic/messages', function (message) {
showMessage(JSON.parse(message.body).content);
});
//用户发送的/ws/private-message地址请求,会被/user/topic/private-messages当前客户端订阅,
//相当于单播
stompClient.subscribe('/user/topic/private-messages', function (message) {
showMessage(JSON.parse(message.body).content);
});
});
}
function showMessage(message) {
$("#messages").append("<tr><td>" + message + "</td></tr>");
}
function sendMessage() {
console.log("sending message");
stompClient.send("/ws/message", {}, JSON.stringify({'messageContent': $("#message").val()}));
}
function sendPrivateMessage() {
console.log("sending private message");
stompClient.send("/ws/private-message", {}, JSON.stringify({'messageContent': $("#private-message").val()}));
}
5, html前端显示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello WS</title>
<link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/js/scripts.js"></script>
</head>
<body>
<div class="container" style="margin-top: 50px">
<div class="row">
<div class="col-md-12">
<form class="form-inline">
<div class="form-group">
<label for="message">Message</label>
<input type="text" id="message" class="form-control" placeholder="Enter your message here...">
</div>
<button id="send" class="btn btn-default" type="button">Send</button>
</form>
</div>
</div>
<div class="row" style="margin-top: 10px">
<div class="col-md-12">
<form class="form-inline">
<div class="form-group">
<label for="private-message">Private Message</label>
<input type="text" id="private-message" class="form-control" placeholder="Enter your message here...">
</div>
<button id="send-private" class="btn btn-default" type="button">Send Private Message</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="message-history" class="table table-striped">
<thead>
<tr>
<th>Messages</th>
</tr>
</thead>
<tbody id="messages">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>