Spring Boot搭建稳定的WebSocket

后端搭建WebSocket

1.Maven引用jar包

引用spring boot依赖的websocket

<!-- websocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.配置bean

如果用springboot内置tomcat启动项目的,需要配置这个Bean;
如果是外部的启动容器,则不需要配置

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Component
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3.配置返还DTO

如果返回给前端的信息是实体,就需要配置这个

import com.alibaba.fastjson.JSONObject;
import me.zhengjie.modules.socket.rest.WebSocketResDto;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class ServerEncoder implements Encoder.Text<WebSocketResDto> {

	//WebSocketResDto 就是返回的实体
    @Override
    public String encode(WebSocketResDto webSocketResDto) throws EncodeException {
        return JSONObject.toJSONString(webSocketResDto);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}

4.编写WebSocket

刚刚配置的ServerEncoder 在注解 @ServerEndpoint中使用

import com.alibaba.fastjson.JSONObject;
import me.zhengjie.modules.socket.util.ApplicationContextRegister;
import me.zhengjie.modules.socket.util.ENUM_MESSAGE_TYPE;
import me.zhengjie.modules.socket.util.ServerEncoder;
import me.zhengjie.modules.system.util.ENUM_TOKEN_AES_KEY;
import me.zhengjie.modules.system.util.SymmetricEncoder;
import me.zhengjie.test.domain.BcUser;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint(value = "/ws/{userId}",encoders = {ServerEncoder.class})
public class WebSocketController {
    //静态变量,用来记录当前在线连接数
    private static int onlineCount = 0;
    //使用Map来存放每个用户的连接,其中Key为用户标识
    private static ConcurrentHashMap<String, WebSocketController> webSocketMap = new ConcurrentHashMap<String, WebSocketController>();
    //客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
   
    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(WebSocketController.class);

    /**
     * 连接建立成功调用的方法
     * @param session  客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId){
        logger.info("进入WebSocket--onOPen,userID:"+userId);
        this.session = session;
        //加入map中
        webSocketMap.put(userId,this);   
        //获取业务Service处理类  
        ApplicationContext applicationContext = ApplicationContextRegister.getApplicationContext();
        BcUserService bc = applicationContext.getBean(BcUserService.class);
        addOnlineCount();           //在线数加1
        //第一次进来需要处理的业务
        sendUser(userId,bc);
        logger.info("有新连接加入!当前在线人数为" + getOnlineCount());
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId){
        logger.info("进入onClose,userID:"+userId);
        //从map中删除
        webSocketMap.remove(userId);  
        subOnlineCount();           //在线数减1
        logger.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 当用户余额发生变动时,其他地方的业务处理可以直接调用该方法来发送socket
     * 因为该方法不支持传参实体,所以用json转换
     */
    @OnMessage
    public void onMessage(String jsonStr) {
        try {
            WebSocketResDto resDto = JSONObject.parseObject(jsonStr, WebSocketResDto.class);
            //如果该属性有值就是心跳连接
            if(!StringUtils.isBlank(resDto.getHeartbeat())){
                logger.info("进入WebSocketTest--心跳,token:"+resDto.getUserId());
                String userId = resDto.getUserId();
                //获取业务Service处理类  
                ApplicationContext applicationContext = ApplicationContextRegister.getApplicationContext();
                BcUserService bc = applicationContext.getBean(BcUserService.class);
                String userId="";
                sendUser(userId,bc);
            }else{
                sendMessage(resDto);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        logger.info("发生错误");
        error.printStackTrace();
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketController.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketController.onlineCount--;
    }

    /**
     * 该方法用来处理业务
     */
    public void sendMessage(WebSocketResDto resDto) throws IOException {
        try {
            if(resDto.getMessageType()==ENUM_MESSAGE_TYPE.USER.getType()){
                String userId= resDto.getUserId();
                logger.info("个人用户发送信息,user_id:"+userId);
                //发送最新数据给用户
                WebSocketController webSocketTest = webSocketMap.get(userId);
                if(webSocketTest!=null){
                    webSocketTest.session.getBasicRemote().sendObject(resDto);
                    logger.info("个人用户信息发送完成!");
                }else{
                    logger.info("无法查询到该用户的连接,无法发送数据!ID:"+userId);
                }
            }else if(resDto.getMessageType()==ENUM_MESSAGE_TYPE.SYSTEM.getType()){
                logger.info("-----发送系统信息------");
                //给所有用户推送信息
                for (WebSocketController webSocketController : webSocketMap.values()) {
                    webSocketController.session.getBasicRemote().sendObject(resDto);
                }
                logger.info("-----发送系统信息完成------");
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.info("发送信息异常!");
            logger.info(e.toString());
        }
    }

    //初始连接就发送用户信息到前端
    public void sendUser(String userId,BcUserService bc){
        try {
            logger.info("发送余额至前端 userId"+userId);
            BcUser bcUser = bc.findOneById(Integer.parseInt(userId));
            WebSocketController webSocketTest = webSocketMap.get(userId);
            if(bcUser!=null && webSocketTest!=null){
                logger.info("开始发送");
                WebSocketResDto resDto=new WebSocketResDto();
                resDto.setBalance(bcUser.getBalance());
                webSocketTest.session.getBasicRemote().sendObject(resDto);
            }else{
                logger.info("发送失败! bcUser:"+bcUser+"----webSocketTest:"+webSocketTest);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.浏览器使用webSocket

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>
<script>
	var websocket = null;
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        //建立连接,这里的/websocket ,是Servlet中注解中的那个值
        websocket = new WebSocket("ws://localhost:81/ws/1");
    }else {
        alert('当前浏览器 Not support websocket');
    }
    //连接发生错误的回调方法
    websocket.onerror = function () {
        console.log("WebSocket连接发生错误");
		//如果发生错误,就重连一次
        websocket = new WebSocket("ws://localhost:81/ws/1");
    };
    //连接成功建立的回调方法
    websocket.onopen = function () {
        console.log("WebSocket连接成功");
        //连接成功时,开启心跳
		heartCheck.reset().start();
    }
    //接收到消息的回调方法
    websocket.onmessage = function (event) {
    	//收到消息时,再开启一次心跳
		heartCheck.reset().start();
        console.log(event);
        console.log(event.data);
    }
    //连接关闭的回调方法
    websocket.onclose = function () {
        console.log("WebSocket连接关闭");
    }
    //监听窗口关闭事件,当窗口关闭时,主动去关闭WebSocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }
    //关闭WebSocket连接
    function closeWebSocket() {
        websocket.close();
    }
	
	//心跳检测
	var heartCheck = {
		timeout: 1000 * 60,        //1分钟发一次心跳
		timeoutObj: null,
		reset: function(){
			clearTimeout(this.timeoutObj);
			return this;
		},
		start: function(){
			var self = this;
			console.log("心跳检测");
			this.timeoutObj = setTimeout(function(){
				//这里发送一个心跳,后端收到后,返回一个心跳消息,
				//onmessage拿到返回的心跳就说明连接正常
				console.log("发送心跳检测");
				websocket.send("{'heartbeat':'true','token':'1'}");
			}, this.timeout)
		}
	}
</script>

加入心跳检测,是为了保持连接的稳定。

6.总结

该方法只支持ws协议的WebSocket!!!
因为之前项目加了了HTTPS协议(看上一篇),然后我又找不到wss协议的弄法,于是把后台监控HTTPS协议请求改了下:
在这里插入图片描述
这样 HTTPS就只会监控请求地址为 /api/* 的接口,然后给项目开启了HTTP的端口,这样WebSocket又能走ws协议的,项目接口访问也能走HTTPS!!!*

最后,是后台使用到的ApplicationContext工具类代码
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
@Lazy(false)
public class ApplicationContextRegister implements ApplicationContextAware{

    private static ApplicationContext APPLICATION_CONTEXT;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        APPLICATION_CONTEXT=applicationContext;
    }
    
    public static ApplicationContext getApplicationContext() {
        return APPLICATION_CONTEXT;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值