异步推送使用的是长链接实现,重要的缺点是长链接的时间不知道如何设置
无配置,直接可以使用
控制层
控制层一个长连接请求,一个写入数据请求
@Controller
public class AysncController {
@Autowired
private PushService pushService;
/**
* 此处虽然将DeferredResult返回了,但是其实spring mvc是将请求卡的等值的地方,
* 在等待PushService的refresh方法给DeferredResult中赋值, 只有赋值后的结果才可以通过http请求返回响应
* 此请求在未赋值的情况下等待了42.09s后报错(具体报错时间和具体情况有关)
*
* 此异步天然支持点对点,每一次情况都会产生一个DeferredResult,然后在另外一个线程更新这个DeferredResult的结果即可
*
* @return
*/
@RequestMapping(value = "/defer", produces = { MediaType.APPLICATION_JSON_VALUE })
@ResponseBody
public DeferredResult<String> deferedCall(String user) {
return pushService.getAsyncUpdate(user);
}
@RequestMapping(value = "/value", produces = { MediaType.APPLICATION_JSON_VALUE })
@ResponseBody
public void value(String user, String value) {
pushService.pushValue(user, value);
}
}
@Service
public class PushService {
private Map<String, DeferredResult<String>> deferredResults = new HashMap<String, DeferredResult<String>>();
/**
* 通过返回DeferredResult来进行长链接请求
*
* @param user
* @return
*/
public DeferredResult<String> getAsyncUpdate(String user) {
DeferredResult<String> deferredResult = new DeferredResult<String>();
deferredResults.put(user, deferredResult);
return deferredResult;//此处返回后,连接会卡住不会直接响应,但是卡住的时间无法评估
}
public void pushValue(String user, String value){
DeferredResult<String> deferredResult = deferredResults.get(user);
deferredResult.setResult(value);//在此处设置值
}
}
<!-- WebSocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry handlerRegistry) {
handlerRegistry.addHandler(new MyWebSocketHandler(), "/myhandler");
}
}
集成AbstractWebSocketHandler可以重写handleTextMessage,handleBinaryMessage,handlePongMessage方法,分别处理文本消息,二进制消息和pong消息,也可以直接继承TextWebSocketHandler,BinaryWebSocketHandler,这两个类也是AbstractWebSocketHandler的子类
public class MyWebSocketHandler extends AbstractWebSocketHandler {
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("Connection closed");
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("Connection create");
}
// 处理文本消息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
if(!payload.equals("stop")){
System.out.println("接收到:" + payload);
session.sendMessage(new TextMessage("hello websocket"));
} else {
session.close();
}
}
}
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>websocket.jsp</title>
</head>
<body>
<script>
var url = 'ws://' + window.location.host + '/springmvc/myhandler';
/* 打开websocke */
var sock = new WebSocket(url);
/* 处理连接连接开启事件 */
sock.onopen = function() {
console.log("opending");
sendMessage("hello server");
};
/* 处理连接连接关闭事件 */
sock.onclose = function() {
console.log("closing");
};
/* 处理接收到的消息 */
sock.onmessage = function(e) {
console.log("receive message:", e.data);
sendMessage("stop");//发送关闭连接消息
}
function sendMessage(message){
sock.send(message);
}
</script>
</body>
</html>
有一些浏览器不支持websocket,所以可以使用sockJs模拟websocket,springmvc对sockJs的支持和websocket的配置差不多
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry handlerRegistry) {
handlerRegistry.addHandler(new MyWebSocketHandler(), "/myhandler").withSockJS();//多了withSockJS方法
}
}
public class MyWebSocketHandler extends AbstractWebSocketHandler {
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("Connection closed");
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("Connection create");
}
// 处理文本消息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
if(!payload.equals("stop")){
System.out.println("接收到:" + payload);
session.sendMessage(new TextMessage("hello websocket"));
} else {
session.close();
}
}
// 处理二进制消息
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
super.handleBinaryMessage(session, message);
}
}
除了初始化之外和上边一样
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>websocket.jsp</title>
</head>
<body>
<!-- socket js的代码,需要引入sockjs包 -->
<script
src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript">
<!-- 初始化使用SockJS -->
var sock = new SockJS('http://localhost:8080/springmvc/myhandler');
sock.onopen = function() {
console.log('open');
sock.send('test');
};
sock.onmessage = function(e) {
console.log('message', e.data);
sock.close();
};
sock.onclose = function() {
console.log('close');
};
</script>
</body>
</html>
STOMP 的消息根据前缀的不同分为三种。如下,以 /app 开头的消息都会被路由到带有@MessageMapping 或 @SubscribeMapping 注解的方法中;以/topic 或 /queue,这里的/topic 或 /queue其实都是发布订阅模式的不同话题而已(类似于jsm的topic),stomp没有真正的队列消息,都是发布订阅消息 开头的消息都会发送到STOMP代理中,根据你所选择的STOMP代理不同,目的地的可选前缀也会有所限制;以/user开头的消息会将消息重路由到某个用户独有的目的地上,最后还是会发到代理中。
@Configuration
@EnableWebSocketMessageBroker // 开启stomp websocket消息代理
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
// 配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 永远是发布订阅模式,也就是一个发送源多个接收者,这里默认使用内存消息代理
registry.enableSimpleBroker("/aaa", "/bbb");
// 不加前缀也行,但是一般为了和控制层url保持一致建议加自己工程名为前缀
registry.setApplicationDestinationPrefixes("/springSecurity_springMvc");
}
// 注册STOMP端点,有点类似于activemq的连接url
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/mystomp").withSockJS();// 此处可以选择启用socketjs功能
}
}
就是接收消息的地方
@Controller
public class MessageController {
@MessageMapping("/handleMessage")
@SendToUser("/aaa/response")//@SendToUser可以将消息只发送给请求此消息的用户,需要经过消息代理
// @SendTo("/aaa/response")//@SendTo将消息返回,但会发送给所有订阅的用户,需要经过消息代理
// 此处也可以用对象来接受消息,可以使用json格式如果不需要返回消息,则可以返回void
public String handleMessage(Principal principal, String message) {
System.out.println(principal.getName());
System.out.println(message);
return "server message";
}
//如果另外一端请求此消息,则会得到一个响应,但这个响应消息不会通过消息代理,而是直接返回客户端
@SubscribeMapping("/subscribeMessage")
public String subscribeMessage() {
return "subscribe message";
}
}
作为除@MessageMapping和@SubscribeMapping之外的另一种消息发送器
@RestController // 声明一个控制器
public class MessageTemplateController {
@Autowired
private SimpMessageSendingOperations messageSend;
@RequestMapping(value = "/messageSend")
public void messageSend(Principal principal, String username) {
System.out.println(principal.getName());
// messageSend.convertAndSend("/aaa/response1", "aaa");//一般发送消息
messageSend.convertAndSendToUser(username, "/aaa/response", "aaa");//发送给用户
}
}
@Configuration
@EnableWebSocketMessageBroker // 开启websocket消息代理
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
// 配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/aaa", "/bbb")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("admin")
.setClientPasscode("admin");
registry.setApplicationDestinationPrefixes("/springSecurity_springMvc");// 不加前缀也行,但是一般为了和控制层url保持一致建议加自己工程名为前缀
}
// 注册STOMP端点,有点类似于activemq的连接url
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/mystomp").withSockJS();// 此处可以选择启用socketjs功能
}
}
前端可参考stomp.js官方文档
<body>
<div>
<button id="connect" onclick="connect();">Connect</button>
<button id="disconnect" onclick="disconnect();">Disconnect</button>
<button id="sendmessage" onclick="sendmessage();">Sendmessage</button>
<button id="subscribeMessage" onclick="subscribeMessage();">SubscribeMessage</button>
</div>
<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 type="text/javascript">
var client = null;
function connect() {//初始化连接
var sock = new SockJS('http://localhost:8080/springSecurity_springMvc/mystomp');
client = Stomp.over(sock);
client.connect('guest', 'guest', function(frame) {
console.log("connect success");
}, function(frame) {
console.log("connect fail");
});
};
function disconnect() {//断开连接
client.disconnect(function() {
console.log("disconnect");
});
};
function sendmessage() {//发送消息
client.send("/springSecurity_springMvc/handleMessage", {}, "hello server");
//订阅用户消息,有些类似于get请求,就是发也给url获取一条消息
client.subscribe("/user/aaa/response", function(
message) {
console.log("receive:" + message.body);
});
};
function subscribeMessage() {//订阅消息
client.subscribe("/springSecurity_springMvc/subscribeMessage", function(
message) {
console.log("subscribeMessage receive:" + message.body);
});
};
</script>
</body>
var client = null;
function connect() {//初始化连接
var sock = new SockJS('http://localhost:8080/springSecurity_springMvc/mystomp');
client = Stomp.over(sock);
//这里的guest,guest是后端配置的用户名密码,此处样例后端未配置默认为guest,guest
client.connect('guest', 'guest', function(frame) {
console.log("connect success");
}, function(frame) {
console.log("connect fail");
});
};
//发送一般消息,对应@MessageMapping注解处理
client.send("/springSecurity_springMvc/handleMessage", {}, "hello server");
subscribe也可以发送消息
//问后端要订阅消息,对应@SubscribeMapping处理
client.subscribe("/springSecurity_springMvc/subscribeMessage", function(
message) {
console.log("subscribeMessage receive:" + message.body);
});
client.send("/springSecurity_springMvc/handleMessage", {}, "hello server");
//订阅用户消息消息,对应@SendToUser("/aaa/response"),此注解会自动添加/user
client.subscribe("/user/aaa/response", function(
message) {
console.log("receive:" + message.body);
});
client.send("/springSecurity_springMvc/handleMessage", {}, "hello server");
//订阅非用户消息消息,对应@SendTo("/aaa/response")
client.subscribe("/aaa/response", function(
message) {
console.log("receive:" + message.body);
});
配置:
@Configuration
@EnableWebSocketMessageBroker // 这里使用AbstractSecurityWebSocketMessageBrokerConfigurer
public class WebSocketStompConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
// 配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/aaa", "/bbb").setRelayHost("localhost").setRelayPort(61613)
.setClientLogin("admin").setClientPasscode("admin");
registry.setApplicationDestinationPrefixes("/springSecurity_springMvc");// 不加前缀也行,但是一般为了和控制层url保持一致建议加自己工程名为前缀
}
// 注册STOMP端点,有点类似于activemq的连接url
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/mystomp").withSockJS();// 此处可以选择启用socketjs功能
}
//在此配置发送到服务器的请求权限
//这个和springsecurity一样从上往下匹配,匹配到就算成功,所以最后一般要拒绝所有消息,以防没有匹配到的消息被非法访问
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.nullDestMatcher().authenticated()//任何没有目的地的消息(即除了消息类型或订阅之外的任何消息,比如连接和断连消息等)都需要用户进行身份验证
.simpDestMatchers("/springSecurity_springMvc/handleMessage").hasRole("USER")//一般消息/handleMessage需要有USER角色,这里的目的地址注意加前缀
.simpSubscribeDestMatchers("/springSecurity_springMvc/subscribeMessage").hasRole("ADMIN")//订阅消息/subscribeMessage需要有ADMIN权限
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll()//拒绝一切消息和订阅消息,有后边一行一般这个不用
.anyMessage().denyAll();//拒绝所有消息
}
}
如果使用csrf的话需要提供获取csrf token的url
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
前端:
<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 src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script type="text/javascript">
var headers = null;
//这里获取csrf token
$.get("csrf", function(data, status) {
var headerName = data.headerName;
var token = data.token;
headers = {
login : 'guest',
passcode : 'guest'
};
headers[headerName] = token;
});
var client = null;
function connect() {//初始化连接
var sock = new SockJS(
'http://localhost:8080/springSecurity_springMvc/mystomp');
client = Stomp.over(sock);
//这里需要使用headers来进行连接
client.connect(headers, function(frame) {
console.log("connect success");
}, function(frame) {
console.log("connect fail");
});
};
function disconnect() {//断开连接
client.disconnect(function() {
console.log("disconnect");
});
};
</script>
一般activemq没有连接会导致初始化SockJS失败,报错:
<<<ERROR
message:Broker not avalibale.
content-length:0
跨域问题目前主要依靠ngnix进行代理解决,但websocket也需要进行以下配置
// 注册STOMP端点,有点类似于activemq的连接url
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/mystomp").setAllowedOrigins("*")//解决跨域问题
.withSockJS();// 此处可以选择启用socketjs功能
}