Spring WebSocket + SockJs + Stomp详解

Spring WebSocket + SockJs + Stomp详解

一、 WebSocket配置及连接

1. 服务器端

  • 依赖(gradle配置,以:分隔,可以手动转换为maven)
   testCompile('org.springframework.boot:spring-boot-starter-test')
   compile('org.springframework.boot:spring-boot-starter')
   compile('org.springframework.boot:spring-boot-starter-web')
   compile 'org.springframework.boot:spring-boot-starter-websocket'
  • springboot 配置
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfigure implements WebSocketMessageBrokerConfigurer{
    /**
    * @Description: 注册stomp端点并将端点映射到特定的路径
    **/
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册一个断点 用于握手的地址
        registry.addEndpoint("/websocket")
                //允许websocket跨域
                .setAllowedOrigins("*")
                //启用websocket备选方案(浏览器不支持的话就会启动)
                .withSockJS();
    }
 
 
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 启用简单的消息代理,用于客户端订阅,进行广播的发送的前缀,如下面/agent将会推送经理相关的消息
        registry.enableSimpleBroker("/agent","/topic");
        // 点对点时使用 给某个用户发信息时的前缀
        registry.setUserDestinationPrefix("/user");
        // 前端发送信息给后端的前缀
        registry.setApplicationDestinationPrefixes("/app");
    }
}

如果你们的应用式前后端分离的一般都需要配置跨域,否则会无法连接。然后要注意的是configureMessageBroker方法中的相关配置,订阅、点对点、以及前端发送消息的前缀。

2. 前端连接

  • 依赖(使用npm install即可)

    sockjs-client

    stompjs

  • 代码实现

	// 主机地址以及registerStompEndpoints中配置的握手地址
	const sock=new SockJS("http://localhost:9999/websocket")
    //这个stompCLient对象主要是用于前后台交互用的
    const stompClient = Stomp.over(sock)
    // 连接服务器端
    stompClient.connect(
      // 请求头
      {},
      // 连接成功回调
      function connectCallback(frame) {
      	console.log('连接成功')
      },
      // 连接失败回调
      function errorCallBack(error) {
        // 连接失败时(服务器响应 ERROR 帧)的回调方法
        console.error('连接失败');
      }
    );

二、消息收发

websocket的消息机制是基于订阅发布形式传输的,客户端可以直接发消息给服务端,但是服务端要传输消息给客户端只能通过消息的发布,而且客户端需要订阅相应的消息

1. 后端接受代码

// 注册为controller,返回值将自动处理为json
@Controller
@MessageMapping("/test")
public class WebSocketController {

	// MessageMapping注解类似于RequestMapping,这里配置的是发送消息的路径
	// 前端通过/app/test/agentInfo可以访问该方法
	// 目前的话返回值是无效的
    @MessageMapping("/agentInfo")
    public Map getAgentInfo () {
        final HashMap map = new HashMap<>();
        map.put("name","xxc");
        map.put("age",21);
        System.out.println("发送客户经理消息");
        return map;
    }
}

2. 前端发送消息

// 发送消息格式 stompClient.send(destination,headers,body)
stompClient.send('/app/test/agentInfo',{},'')
  • destination 目的地
  • headers 附带的请求头,没有传递{}
  • body 请求体,消息部分,没有则使用’’

由此便能调用WebSocketController的getAgentInfo方法,但是目前还不能返回信息

3. 返回消息给客户端

如果想要getAgentInfo返回的消息发送回客户端,则可以使用以下方式:

  • @SendTo注解
@MessageMapping("/agentInfo")
@SendTo("/agent/updateAgentInfo")
public Map<String,Object> getAgentInfo () 

通过@SendTo注解,方法返回时会将返回值发送给当前订阅了/agent/updateAgentInfo消息的用户

  • SimpleMessagingTemplate.convertAndSend
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;
    
    @MessageMapping("/agentInfo")
    public void getAgentInfo () {
        final HashMap<String, Object> map = new HashMap<>();
        map.put("name","xxc");
        map.put("age",21);
        System.out.println("发送客户经理消息");
        // 使用api进行推送
        simpMessagingTemplate.convertAndSend('/agent/updateAgentInfo', map);
    }

那么前端如何订阅此消息呢?

前端可以通过subscribe方法订阅指定消息

// api stompClient.subscribe(destination, callback)
stompClient.subscribe('/agent/updateAgentInfo', (resp)=> {
	// 这样可以在服务器广播消息的时候接受到并打印消息内容
	console.log(resp.body)
    // 打印: {"name":"xxc","age":21}
    // 收到信息后可以手动ack确认一下,让服务器知道你收到了消息
    resp.ack()
})
  • destination 订阅的消息
  • callback 接受消息后的处理

4. 类似于http请求响应形式(一次性的订阅)

  • 服务器端使用@SubscribeMapping注解替代MessageMapping注解
    // 订阅的路径会自动加上发送消息的前缀,WebsocketConfigure.java中我配置的是/app
    @SubscribeMapping("/agent/updateAgentInfo")
    public String onAgentInfoSubscribe () {
        System.out.println("hahah");
        return "hello";
    }
  • 客户端只需要使用subscribe即可完成类似http请求的功能

    stompClient.subscribe('/app/agent/updateAgentInfo',(resp) => {
    	// 处理响应
    })
    

    这种订阅是一次性的,用法类似于一次http请求

5. 点对点的返回消息

以上方式返回的消息是广播给所有订阅了该消息的客户端,大家都能收到,那如何做到类似于http的效果,发送消息后接受到想要的推送?

这时我们可以使用@SendToUser注解替换掉原来的@SendTo注解

@MessageMapping("/agentInfo")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
@SendToUser("/agent/updateAgentInfo")
public Map getAgentInfo () 

通过@SendToUser注解则可以做到谁发的消息,推送返回值给谁

客户端js需要做的修改就是将订阅目的地修改为/user/agent/updateAgentInfo,也就是加上点对点前缀/user

stompClient.subscribe('/user/agent/updateAgentInfo', (resp)=> {//...})

我的理解是@SendToUser注解在@SendTo注解的基础上,判断了是否为当前用户,但他使用的destination还是需要为广播的destination,只是在这个基础上加上点对点前缀

6. 推送消息给指定用户

当一些用户相关的消息更新了,可能要及时推送给指定的用户,但不是广播给所有人,这怎么做到呢?

首先,在客户端订阅的时候,就要指定自己的用户标识,如用户名,可以根据自己的业务逻辑定义

stompClient.subscribe('/user/xxc/updateAgentInfo', (resp) => {//...})
// destination 格式  点对点前缀/{userid}/真实路径

则,在后端即可使用SimpMessagingTemplate发送消息给xxc

simpMessagingTemplate.convertAndSendToUser("xxc","/updateAgentInfo","推送消息");

在源码中SimpMessagingTemplate使用的仍是convertAndSend方法

	@Override
	public void convertAndSendToUser(String user, String destination, Object payload,
			@Nullable Map headers, @Nullable MessagePostProcessor postProcessor)
			throws MessagingException {

		Assert.notNull(user, "User must not be null");
		user = StringUtils.replace(user, "/", "%2F");
		destination = destination.startsWith("/") ? destination : "/" + destination;
		// 将点对点前缀、用户id和真实的路径进行了拼接
		super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
	}

三、安全验证

有的时候应用需要一定的安全性,而不是谁都可以连接的,这个时候就需要进行身份的验证

  1. 客户端需要发送身份信息

    ​ 可以在connect的时候通过header传递一些自定义头,如token

     stompClient.connect(
          {
            token: token // token是用户登录系统后返回的身份码,这里需要你自己定
          },
          function connectCallback(frame) {
            console.log(frame);
          },
          function errorCallBack(error) {
            // 连接失败时(服务器响应 ERROR 帧)的回调方法
            console.error("连接失败");
          }
        );
    
  2. 服务器拦截验证

    public class UserInterceptor implements ChannelInterceptor {
    
        @Override
        public Message preSend(Message message, MessageChannel channel) {
            final StompHeaderAccessor wrap = StompHeaderAccessor.wrap(message);
            final Object ls = wrap.getHeader("token");
            if (ls == null) {
                return null;
            }
            if (ls instanceof LinkedList) {
                String token = (String) ((LinkedList) ls).get(0);
                // 校验操作 ...
                System.out.println(token);
                // 如果没有token就返回null
                if (token == null) {
                    return null;
                }
            }
            return message;
        }
    }
    
    

    在拦截器返回空则客户端连接成功的回调不会调用,但是也不会调用失败的回调

    配置拦截器 WebsocketConfigure.java

       @Override
        public void configureClientInboundChannel(ChannelRegistration registration) {
            registration.interceptors(userInterceptor());
        }
    
        /**
         * WebSocket的拦截器,做用户认证操作
         * @return
         */
        @Bean
        public UserInterceptor userInterceptor() {
            return new UserInterceptor();
        }
    

参考

讲的比较体系化的文章

讲的比较好的一篇文章

比较完整的websocket、stomp、socketJs的介绍

你可能感兴趣的:(spring)