spring mvc websocket和异步推送

异步推送

异步推送使用的是长链接实现,重要的缺点是长链接的时间不知道如何设置

配置:

无配置,直接可以使用
控制层
控制层一个长连接请求,一个写入数据请求

@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

依赖的jar包:
		<!-- 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");
	}
}
SocketHandler实现:

集成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>

使用sockJs实现websocket

有一些浏览器不支持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方法
	}
}
SocketHandler和上边一样
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(simple text oriented messaging protocol )

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";
	}
}
SimpMessageSendingOperations消息发送器

作为除@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");//发送给用户
	}
}
配置activemq消息代理
@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);
	});
添加springsecurity特性

配置:

@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功能
	}
发布了36 篇原创文章 · 获赞 5 · 访问量 5348

猜你喜欢

转载自blog.csdn.net/weixin_43060721/article/details/88132325