SpringCloud Sockjs 前后端分离 实现WebSocket点对点发送消息

项目后端架构采用SpringCloud+SpringBoot,前段使用Vue + SockJS。

三方框架版本
SpringCloudDalston.SR4
SpringBoot1.5.10
spring-cloud-starter-zuul1.3.5

现在需要将异步任务的结果通过websocket通知前端,希望可以websocket连接穿透zuul网关访问内部的websocket server微服务。找了一圈,目前zuul 1.x还不支持websocket,说是2.x会支持。后来找到github上有针对1.x的解决方法:https://github.com/mthizo247/spring-cloud-netflix-zuul-websocket,作者提供了demo可以跑通hello world。不过demo中是订阅topic广播的例子。

下面围绕demo提供的例子来实现点对点发送消息的功能。

思路是,每个客户端连接后创建一个clientId,并且将clientid存储到数据库,后续可以将用户信息或者公司信息绑定到clientId上,服务器端发送消息到指定client。

f3bbcd9e002f0ba50e65e534de99fe8cc5a.jpg

对于zuul和websocket微服务上的websocket连接必须使用同一个clientId,这样方可实现点对点。具体步骤:在zuul网关部分,通过websocket handshakeinteceptor获取一个clientId,然后clientId作为principal,并且将clientId传递到websocket微服务,在websocket微服务通过websocket handshakeinteceptor获取到clientId,然后clientId作为principal。

连接创建后,后端通过messagingTemplate的convertAndSendToUser发送消息给用户。

@MessageMapping("/greeting")
	public void greeting(HelloMessage message, MessageHeaders messageHeaders) throws Exception {
		String sessionId = this.getSessionId(messageHeaders);
		Map a = new HashMap<>();
		a.put("body", message.getName());
		messagingTemplate.convertAndSendToUser(sessionId, "/queue/notifications", payload)
	}
	
private String getSessionId(MessageHeaders messageHeaders){
		if(messageHeaders.get("simpSessionAttributes")!=null && messageHeaders.get("simpSessionAttributes") instanceof Map){
			Object sessionId = ((Map)messageHeaders.get("simpSessionAttributes")).get("session_id");
			if (sessionId!=null) {
				return sessionId.toString();
			}
		}
		throw new RuntimeException("session id 丢失");
}

hello项目中websocketConfig的配置比较简单,

package com.github.mthizo247.hello;

import java.security.Principal;
import java.util.Map;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/gs-guide-websocket")
				.setHandshakeHandler(myDefaultHandshakeHandler())
				.addInterceptors(handshakeInterceptor())
				// bypasses spring web security
				.setAllowedOrigins("*").withSockJS();
	}
	
	@Override
	public void configureMessageBroker(MessageBrokerRegistry config) {
		// prefix for subscribe
		config.enableSimpleBroker("/queue");
		// prefix for send
		config.setApplicationDestinationPrefixes("/app");
	}
	
	
	@Bean
	public HandshakeInterceptor handshakeInterceptor() {
	    return new HandshakeInterceptor() {
			
			@Override
			public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
					Map<String, Object> attributes) throws Exception {
				if (request instanceof ServletServerHttpRequest) {
             //此处是关键,header中的session_id是通过zuul端创建websocket conenction中传递过来
				    ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
				    attributes.put("session_id", servletRequest.getServletRequest().getHeader("session_id"));
				    return true;
				}
				return true;
			}
			
			@Override
			public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
					Exception exception) {
				
			}
		};
	}
	

	//WebSocket 握手处理器
    private DefaultHandshakeHandler myDefaultHandshakeHandler(){
        return new DefaultHandshakeHandler(){
            @Override
            protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
                //设置认证通过的用户到当前会话中
    			final String sessionid = (String) attributes.get("session_id");
    	    	Principal principal = new Principal() {
					@Override
					public String getName() {
						return sessionid;
					}
				};
    	    	return principal;
            }
        };
    }
	
}

zuul网关部分,需要使用spring-cloud-netflix-zuul-websocket的源代码,直接引用jar包不能解决问题,proxy项目pom.xml中添加源码项目的依赖,接着修改spring-cloud-netflix-zuul-websocket中的ZuulWebSocketConfiguration的addStompEndpoint方法。

private SockJsServiceRegistration addStompEndpoint(StompEndpointRegistry registry, String... endpoint) {
        return registry.addEndpoint(endpoint)
        		// 手动添加handshakehandler和interceptor,用于设置principal clientId 
        		.setHandshakeHandler(myDefaultHandshakeHandler())
        		.addInterceptors(handshakeInterceptor())
                // bypasses spring web security
                .setAllowedOrigins("*").withSockJS();
    }
    

并且添加两个方法,在beforehandshake中获取request中的请求链接,这里有sockjssession的id,我们的clientid其实是这个sessionid

@Bean
	public HandshakeInterceptor handshakeInterceptor() {
	    return new HandshakeInterceptor() {
			
			@Override
			public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
					Map<String, Object> attributes) throws Exception {
				if (request instanceof ServletServerHttpRequest) {
					// 从websocket的请求链接requestURI中获取到sockjssession的id,并用于user
				    ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
				    String uri = servletRequest.getServletRequest().getRequestURI();
				    int lastLashIndex = uri.lastIndexOf("/");
				    uri = uri.substring(0, lastLashIndex);
				    uri = uri.substring(uri.lastIndexOf("/")+1);
				    attributes.put("session_id", uri);
				    return true;
				}
				return true;
			}
			
			@Override
			public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
					Exception exception) {
				
			}
		};
	}
    
    //WebSocket 握手处理器
    private DefaultHandshakeHandler myDefaultHandshakeHandler(){
        return new DefaultHandshakeHandler(){
            @Override
            protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
            	// 利用client_id用于点对点发送
    			final String client_id = (String) attributes.get("session_id");
    	    	Principal principal = new Principal() {
					@Override
					public String getName() {
						return client_id;
					}
				};
    	    	return principal;
            }
        };
    }

目前为止,zuul及websocket server服务各自的配置完成,接着需要将这个sessionid传递,需要修改ProxyWebSocketConnectionManager的buildWebSocketHttpHeaders方法,将sessionid添加到socket connection的header中

private WebSocketHttpHeaders buildWebSocketHttpHeaders() {
        WebSocketHttpHeaders wsHeaders = new WebSocketHttpHeaders();
        if (httpHeadersCallback != null) {
            httpHeadersCallback.applyHeaders(userAgentSession, wsHeaders);
            // 将session_id添加到header中
            wsHeaders.put("session_id", Lists.newArrayList(userAgentSession.getId()));
        }
        return wsHeaders;
    }

服务器端修改完毕,接着在web客户端app.js中添加订阅用户队列通知:

function connect() {
    var socket =
    	new SockJS('http://localhost:7078/gs-guide-websocket', null, {
        'transports': ['websocket']
    });
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        // 订阅用户消息通知
        stompClient.subscribe("/user/queue/notifications",handleNotification);
    });
    function handleNotification(message) {
    	showGreeting(message);
    }
}

 

见证一下效果

f31c6b67ace4d8f3db5f5c3d7b1e1a29ef3.jpg

 

转载于:https://my.oschina.net/u/3706162/blog/1935071

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值