springboot implements webSocket server and client demo

1: pom import dependencies

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>

2: myWebSocketClient custom webSocket client

package com.example.poi.utils;

import org.springframework.stereotype.Component;
import javax.websocket.*;
import java.io.IOException;

/**
 * @Author xu
 * @create 2023/9/11 18
 */
@ClientEndpoint
@Component
public class MyWebSocketClient {
    
    

    public Session session;

    @OnOpen
    public void onOpen(Session session) {
    
    
        this.session = session;
        System.out.println("WebSocket2连接已打开");
    }

    @OnMessage
    public void onMessage(String message) {
    
    
        System.out.println("收到消息2:" + message);
    }

    @OnClose
    public void onClose() {
    
    
        System.out.println("客户端关闭2");
    }
    
    @OnError
    public void onError(Throwable throwable) {
    
    
        System.err.println("发生错误2:" + throwable.getMessage());
    }
    
    public void sendMessage(String message) throws IOException {
    
    
        session.getBasicRemote().sendText(message);
    }
}

3: WebSocketServer custom webSocket server

package com.example.poi.utils;

import cn.hutool.json.JSONObject;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ObjectUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

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

/**
 * @Author xu
 * @create 2023/7/21 19
 */

@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {
    
    

    static Log log = LogFactory.get(WebSocketServer.class);

    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;

    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    public Session session;


    //接收sid
    private String sid = "";

    /**
     * 连接建立成功调用的方法
     *
     * @param session
     * @param sid
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
    
    
        this.session = session;
        webSocketSet.add(this); //加入set中
        addOnlineCount(); //在线数加1
        log.info("有新窗口开始监听:" + sid + ",当前在线人数为" + getOnlineCount());
        this.sid = sid;
        /*try {
            sendMessage("连接成功");
        } catch (IOException e) {
            log.error("websocket IO异常");
        }*/
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
    
    
        webSocketSet.remove(this); //从set中删除
        subOnlineCount(); //在线数减1
        log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     * 客户端发送过来的消息
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
    
    
        log.info("收到来自窗口" + sid + "的信息:" + message);
        //群发消息
        for (WebSocketServer item : webSocketSet) {
    
    
            if (ObjectUtils.equals(item.sid, sid)) {
    
    
                try {
    
    
                    JSONObject jsonObject = new JSONObject();
                    jsonObject.put("name", sid);
                    item.sendMessage(jsonObject.toString());
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }

        }
    }

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

    /**
     * 实现服务器主动推送
     *
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException {
    
    
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 获取存在的webSocket
     */
    public CopyOnWriteArraySet<WebSocketServer> getWebSocketServer() {
    
    
        return webSocketSet;
    }

    public String getSid(){
    
    
        return sid;
    }

    public  void close2(String ss){
    
    
        for (WebSocketServer webSocketServer : webSocketSet) {
    
    
            if (webSocketServer.sid.equals(ss)) {
    
    
                webSocketSet.remove(webSocketServer);
                log.info("删除了:"+ss);
            }
        }
        subOnlineCount(); //在线数减1
        log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }


    /**
     * 发送消息
     *
     * @param message
     * @param sid
     * @throws IOException
     */
    public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
    
    
        log.info("推送消息到窗口" + sid + ",推送内容:" + message);
        for (WebSocketServer item : webSocketSet) {
    
    
            try {
    
    
                //这里可以设定只推送给这个sid的,为null则全部推送
                if (sid == null) {
    
    
                    item.sendMessage(message);
                } else if (item.sid.equals(sid)) {
    
    
                    item.sendMessage(message);
                }
            } catch (IOException e) {
    
    
                continue;
            }
        }
    }

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

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

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


    /**
     * 必须要有这个bean才能生效使用webSocketServer
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    
    
        return new ServerEndpointExporter();
    }

}

4: controller control layer

    @SneakyThrows
    @GetMapping("/testSql")
    public List<EntityDemo> testSql(String id) {
    
    
        /** WebSocket服务器的地址*/
        try {
    
    
            Random random = new Random();
            Integer i = random.nextInt(5) + 1;
            CopyOnWriteArraySet<WebSocketServer> webSocketServerSet = webSocketServer.getWebSocketServer();
            for (WebSocketServer socketServer : webSocketServerSet) {
    
    
                if (socketServer.getSid().equals(i.toString())) {
    
    
                    webSocketServer.close2(i.toString());
                    return null;
                }
            }
            URI uri = new URI("ws://127.0.0.1:9088/test/websocket/" + i);
            WebSocketContainer container = ContainerProvider.getWebSocketContainer();
            container.connectToServer(myWebSocketClient, uri);
            myWebSocketClient.sendMessage("你好" + i);
        } catch (Exception e) {
    
    
            throw new RuntimeException(e);
        }
        log.info("++++++");
        return null;
    }

5: Note: The connection is automatically disconnected

  • After webSocket connection, I found a problem: if no data is transmitted every once in a while, the connection with the front end will be automatically disconnected. This problem can be solved by using heartbeat messages. For example, the client automatically sends a ping message to the server every 30 seconds, and the server returns pong.

5-1: Note: Objects cannot be automatically injected

  • Injecting objects using @Resource or @Autowired in classes annotated with @ServerEndpoint will fail and a null pointer exception will be reported. The solution is as follows:
  • The class annotated with @ServerEndpoint uses a static object and exposes the set method externally, so that it can be injected into WebSocketServer when the object is initialized.
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {
    
    

    private static EntityDemoServiceImpl entityDemoService;

    public static void setEntityDemoService(EntityDemoServiceImpl entityDemoService) {
    
    
        WebSocketServer.entityDemoService = entityDemoService;
    }
  • In the class to be injected into the @ServerEndpoint annotation, use @PostConstruct post-injection
@Service
public class EntityDemoServiceImpl extends ServiceImpl<EntityDemoMapper, EntityDemo> implements IEntityDemoService {
    
    
    @PostConstruct
    public void init() {
    
    
        WebSocketServer.setEntityDemoService(this);
    }
}

5-2: Note: Session cannot be serialized

  • There is a problem in distributed scenarios. When a request is loaded to the first server, the session is on the first server thread. The second request is loaded to the second server. At this time, the userId is used to find the current user's session, it cannot be found.
    I originally thought that by storing the session in redis, I can get the user's session from redis. I hope to use this method to solve the problem of message sending in distributed scenarios. This scenario can be solved by sending message middleware. The specific solution is as follows: every time you connect, the userId and the corresponding session are stored in the local machine. When sending a message, it is sent directly to MQ-Broker. Then each application load consumes the message. After getting the message, it determines On this machine, the session can be found based on the userId. If found, it will be pushed to the client.

Guess you like

Origin blog.csdn.net/qq_19891197/article/details/132840054