springboot 整合stomp的websocket

原理讲解并不深,主要是过程

  • 第一步:安装配置
    • maven配置
    • websocket基本配置
    • session验证拦截器,需要验证session配置
    • 用户握手拦截器配置
    • 通道拦截器配置
    • webSocketController类-----接收客户端发送消息
    • WebSocket发送消息服务类
    • 前端STOMP客户端vue配置安装
    • Stomp创建连接
    • Vue创建Stomp websocket连接 js代码如下
    • 前后端联调遇到的坑
      • nginx配置
      • 安卓端和IOS端

第一步:安装配置

springboot整合中间件基本上都是maven导包。编写相关配置类,然后根据自己的业务编写相应的service用就完了

maven配置

 		
            org.springframework.boot
            spring-boot-starter-websocket
        

websocket基本配置

package com.jiuhou.gpfe.config;

import com.jiuhou.gpfe.extend.SessionAuthHandshakeInterceptor;
import com.jiuhou.gpfe.extend.UserHandshakeHandler;
import com.jiuhou.gpfe.extend.WebSocketChannelInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler;
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.config.annotation.WebSocketTransportRegistration;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Autowired
    private TaskScheduler taskScheduler;

    /**
     * Register STOMP endpoints mapping each to a specific URL and (optionally)
     * enabling and configuring SockJS fallback options.
     * 注册STOMP协议的端点以供连接websocket,
     * 这里可以开启或者配置SocketJs
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp")		//端点通道地址 ws://ip:port/stomp
               //session拦截器,用于判断session是否存在
                .addInterceptors(new SessionAuthHandshakeInterceptor())
                //用户握手拦截器,判断websocket是否握手成功,并放入Principal用户,用于向指定用户发送消息
                .setHandshakeHandler(new UserHandshakeHandler())
                //配置允许跨域
                .setAllowedOrigins("*");
               	//配置socketJS以使用http协议连接  使用socketJs连接地址为(http://ip:port/stomp)
               	.withSocketJs();
    }

    /**
     * Configure options related to the processing of messages received from and
     * sent to WebSocket clients.
     * //配置消息
     */
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
        registry.setMessageSizeLimit(128 * 1024);
        registry.setSendBufferSizeLimit(512 * 1024);
        registry.setSendTimeLimit(15 * 1000);

    }

    /**
     * Configure the {@link org.springframework.messaging.MessageChannel} used for
     * incoming messages from WebSocket clients. By default the channel is backed
     * by a thread pool of size 1. It is recommended to customize thread pool
     * settings for production use.
     * 消息通道拦截器在发送消息过程中监听
     */
    public void configureClientInboundChannel(ChannelRegistration regist) {
        regist.interceptors(new WebSocketChannelInterceptor());
    }


    /**
     * Configure message broker options.
     * 设置websocket通知的主题或者队列,决定给客户端发送消息是采用队列queue方式或者分发topic方式
     * 设置服务器端心跳,以保证websocket连接活着不会断掉 
     * 10000表示服务端给客户端两次发送心跳的最小时间 大于等于0,0表示不发送心跳
     * 20000表示服务端接收客户端两次心跳的最小间隔  0表示不接受心跳。
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic/", "/queue/").setHeartbeatValue(new long[]{10000, 20000}).setTaskScheduler(taskScheduler);
        //registry.enableStompBrokerRelay().setRelayHost().setRelayPort().setClientLogin().setClientPasscode().setAutoStartup()
        	//设置客户端请求路径的前缀
//        registry.setApplicationDestinationPrefixes("/app");
        //registry.setUserDestinationPrefix("/user"); //默认 /user
    }
}

session验证拦截器,需要验证session配置

package com.jiuhou.gpfe.extend;

import com.jiuhou.gpfe.entity.Customer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import javax.servlet.http.HttpSession;
import java.util.Map;

/**
 * session验证拦截器
 */
public class SessionAuthHandshakeInterceptor implements HandshakeInterceptor {

    private static final Logger logger = LogManager.getLogger(SessionAuthHandshakeInterceptor.class);

    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        //握手之前从session中获取用户
        HttpSession session = getSession(serverHttpRequest);
        if (session == null || session.getAttribute("customer") == null) {
            logger.error("用户session为null,握手失败");
            return false;
        }
        //业务中的用户类 需要实现Principal接口,并重写getName方法,发送指定用户的name
        Customer customer = (Customer) session.getAttribute("customer");
        logger.info("websocket用户{}握手成功", customer.getName());
        map.put("customer", customer);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
    }

    private HttpSession getSession(ServerHttpRequest request) {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
            return serverRequest.getServletRequest().getSession();
        }
        return null;
    }

}

用户握手拦截器配置

	将业务用户和websocket的用户进行绑定 
package com.jiuhou.gpfe.extend;

import com.jiuhou.gpfe.entity.Customer;
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;

/**
 * 用户握手处理器
 */
public class UserHandshakeHandler extends DefaultHandshakeHandler {
    /**
     * 确定用户
     * @param request
     * @param wsHandler
     * @param attributes
     * @return
     */
    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
    //这里的customer是业务用户类,实现了Principal 接口 ,重写了getName方法为返回用户ID
    //也可以重新创建一个Principal类 把session拦截器中放入的用户ID取出来创建新的Principal如注释部分
        Customer customer = (Customer) attributes.get("customer");
//        Principal principal = new Principal(){//确定websocket中用户名称为业务用户ID
//            @Override
//            public String getName() {
//                return customer.getId()+"";
//            }
//        };
        return customer;
    }
}

通道拦截器配置

配置消息通道拦截器可以拦截在wobSocket消息中发送的每一条消息的不同时期进行拦截,获取消息内容并修改消息内容
package com.jiuhou.gpfe.extend;

import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;

public class WebSocketChannelInterceptor implements ChannelInterceptor {

	/**
	 * Invoked before the Message is actually sent to the channel.
	 * This allows for modification of the Message if necessary.
	 * If this method returns {@code null} then the actual
	 * send invocation will not occur.
	 */
	public Message<?> preSend(Message<?> message, MessageChannel channel) {
//			StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//		if(StompCommand.CONNECT.equals(accessor.getCommand())){
//			Customer customer=(Customer) accessor.getSessionAttributes().get("customer");
//			accessor.setUser(customer);
//		}
		return message;
	}

	@Override
	public void postSend(Message<?> message, MessageChannel messageChannel, boolean b) {
	}

	@Override
	public void afterSendCompletion(Message<?> message, MessageChannel messageChannel, boolean b, Exception e) {
	}

	@Override
	public boolean preReceive(MessageChannel messageChannel) {
		return true;
	}

	@Override
	public Message<?> postReceive(Message<?> message, MessageChannel messageChannel) {
		return message;
	}

	@Override
	public void afterReceiveCompletion(Message<?> message, MessageChannel messageChannel, Exception e) {

	}
}

webSocketController类-----接收客户端发送消息

ResponseResult为业务返回值结构体,根据自己业务框架的结构体进行修改
@RestController("ApiWebSocket")和 @MessageMapping("/notice")注解的地址拼接成客户端发送消息的路径。
@SendToUser("/topic/ws/notice")注解作用为将消息内容返回给发送消息的用户
package com.jiuhou.gpfe.controller.common;

import com.jiuhou.common.bean.ResponseResult;
import com.jiuhou.gpfe.entity.Customer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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.messaging.simp.user.SimpUserRegistry;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

@RestController("ApiWebSocket")
@MessageMapping("/api/ws")
public class WebSocketController {
    private static final Logger logger= LogManager.getLogger(WebSocketController.class);

    @MessageMapping("/notice")
    @SendToUser("/topic/ws/notice")
    public ResponseResult queneNotice(Map<String,String> param, Principal principal){
        logger.info("当前用户{},发送信息{}",principal.getName(),param);
        return ResponseResult.oK("成功订阅排队成功主题!"+param);
    }
}

WebSocket发送消息服务类

这里只是多余的配置一个类 方便管理订阅地址类,如果不需要的话可以直接注入调用消息模板类SimpMessagingTemplate进行发送消息
package com.jiuhou.gpfe.component;

import com.jiuhou.common.bean.ResponseResult;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

@Component
public class WebSocketService {
    public static  final Logger logger= LogManager.getLogger(WebSocketService.class);

    private static final String QUEUE_NOTICE="/topic/ws/notice";


    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;


    //给排队的用户发送通知
    public void sendQueueNotice(Long customerId, ResponseResult responseResult){
        logger.info("websocket给{}用户发送消息:{}",customerId,responseResult);
        simpMessagingTemplate.convertAndSendToUser(customerId.toString(),QUEUE_NOTICE, responseResult);
    }



}

前端STOMP客户端vue配置安装

1.npm 安装 stomp

// npm 安装Stomp
npm install stomp

1.导入Stomp.js

// 导入stomp.js
import Stomp from "stompjs";

Stomp创建连接

创建连接有两种方式,一种是使用stomp客户端直接进行连接,一种是使用Socket.js,创建连接的地址是http形式。

1.使用stomp直接创建连接
1.引入Stomp客户端

import Stomp from "stompjs";

2.创建STOMP子协议的客户端对象
如果是加密的http协议https需要使用wss方式创建对象,http使用ws前缀进行连接
“/stomp”之前是项目地址。“/stomp”在java中WebSocket基本配置中配置。

      let url = "wss://127.0.0.1:8001/gpfe/stomp";
      // 获取STOMP子协议的客户端对象
      this.stompClient = Stomp.client(url);

3.调用connect方法进行连接

      this.stompClient.connect(headers,function());

4.订阅服务器端的主题,也就是发送消息的地址。
这里有个需要注意的坑,在服务器端webSocket配置中有个默认的订阅前缀“/user”,所有这里要加上
,否则接收不到服务器发送的信息。在WebSocket基本配置中可以更改此配置。
“/topic/ws/notice”这是服务器端给客户端发送的消息的地址,和WebSocketService中的地址进行对应。

      this.stompClient.subscribe("/user/topic/ws/notice", response => {
            // 订阅服务端提供的某个topic
            console.log(response); // msg.body存放的是服务端发送给我们的信息
            console.log("广播成功");
          });

5.给服务器端发送消息
“/api/ws/notice”客户端访问服务器端的地址,和WebSocketController中访问地址对应。

      this.stompClient.send(
            "/api/ws/notice",
            headers,
            JSON.stringify({ sender: "123", chatType: "JOIN" })
          ); //用户加入接口

2.Socket.js方式创建连接
使用SocketJs只是在创建stompClient客户端的时候略有不同,需要先创建一个sockJS对象。其他stomp的使用同上。
注意:这里使用的地址为http开头的。调用Stomp的方法为over而不是client。

import SockJS from "sockjs-client";
import Stomp from "stompjs";
<script>
 let socket = new SockJS("http[://127.0.0.1:8001/gpfe/stomp");
  this.stompClient = Stomp.over(socket);
</script>

Vue创建Stomp websocket连接 js代码如下

<script>
	//SockJS使用
// import SockJS from "sockjs-client";
import Stomp from "stompjs";
export default {
  name: "home",
  components: {
    HelloWorld
  },
  data() {
    return {
      contentText: "消息内容",
      stompClient: null
    };
  },
  methods: {
    webSocket() {
    =
      // let socket = new SockJS("http://127.0.0.1:8001/gpfe/stomp");
      // let url = "wss://127.0.0.1:8001/gpfe/stomp";
      // 获取STOMP子协议的客户端对象
      this.stompClient = Stomp.client(url);
      // this.stompClient = Stomp.over(socket);
      // 定义客户端的认证信息,按需求配置
      let headers = {
        Authorization: ""
      };
      console.log(this.stompClient);

      this.stompClient.debugg = false;
      // 向服务器发起websocket连接
      this.stompClient.connect(
        headers,
        frame => {
          console.log("我的websocke连接成功!");
          console.log(frame);
          this.stompClient.subscribe("/user/topic/ws/notice", response => {
            // 订阅服务端提供的某个topic
            console.log(response); // msg.body存放的是服务端发送给我们的信息
            console.log("广播成功");
          });

          this.stompClient.send(
            "/api/ws/notice",
            headers,
            JSON.stringify({ sender: "123", chatType: "JOIN" })
          ); //用户加入接口
        },
        err => {
          // 连接发生错误时的处理函数
          console.log("websocke连接失败");
          console.log(err);
        }
      );
    }
  },
  mounted() {
    this.webSocket();
  }
};
</script>

前后端联调遇到的坑

nginx配置

需要升级http为1.1

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

安卓端和IOS端

访问一定要加上token,否则肯定连不上,在连接是要加,发送消息也要加。

你可能感兴趣的:(Java,WebSocket)