springboot實現websocket客戶端,含重連機制

一、簡介

  • 因爲用前端實現的客戶端,比方說小程序,網絡不穩定,會經常斷,所以考慮用java實現客戶端,穩定。
  • java版的重連機制確實花費了好多時間才正好。
  • 重連的時候剛開始沒有加同步,導致定時器發心跳頻繁的時候上次還沒有完全創建完就又創建了一個客戶端,加同步避免了。
  • sendMsg的時候之前沒有加超時,可能有同時存在多個建立連接佔用資源的隱患,加了超時。額 此處限制被我在生產環境去掉了,因爲這個時間不好控制,短了的話會一直連不上。。。暫時再考慮這塊有沒有必要處理。
  • 還有websocket比較噁心的是 每次error的時候都開一個新線程去通知,如果你一直失敗,就等着cpu爆炸吧。
  • WebSocket有五種狀態:NOT_YET_CONNECTED、CONNECTING、OPEN、CLOSING、CLOSED, 只有not_yet_connected的時候纔可以connect, 一旦connect之後,狀態改變了,就無法再connect了。

  • 代碼的git地址爲:https://github.com/1956025812/websocketdemo

  • 找時間要自己寫個服務端,含踢人和一段時間沒收到響應就踢人的效果;

二、實現

2.1 pom依賴

        <!-- websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.java-websocket</groupId>
            <artifactId>Java-WebSocket</artifactId>
            <version>1.3.5</version>
        </dependency>
        <!-- websocket -->

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.2</version>
        </dependency>

2.2 啓動類

package com.example.websocketdemo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@Slf4j
@SpringBootApplication
@EnableScheduling
public class WebsocketdemoApplication implements ApplicationRunner {

    @Autowired
    private WebSocketClientFactory webSocketClientFactory;

    @Override
    public void run(ApplicationArguments args) {
        // 項目啓動的時候打開websocket連接
        webSocketClientFactory.retryOutCallWebSocketClient();
    }

    public static void main(String[] args) {
        SpringApplication.run(WebsocketdemoApplication.class, args);
    }


}

2.3 WebSocketClientFactory

 

package com.example.websocketdemo;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.URISyntaxException;

@Component
@Slf4j
@Data
public class WebSocketClientFactory {

    public static final String outCallWebSockertUrl = "ws://IP:端口";

    private WebSocketClient outCallWebSocketClientHolder;


    /**
     * 創建websocket對象
     *
     * @return WebSocketClient
     * @throws URISyntaxException
     */
    private WebSocketClient createNewWebSocketClient() throws URISyntaxException {
        WebSocketClient webSocketClient = new WebSocketClient(new URI(outCallWebSockertUrl)) {
            @Override
            public void onOpen(ServerHandshake serverHandshake) {
            }

            @Override
            public void onMessage(String msg) {
                log.info("接收信息爲:{}", msg);
            }

            @Override
            public void onClose(int i, String s, boolean b) {
                log.info("關閉連接");
                retryOutCallWebSocketClient();
            }

            @Override
            public void onError(Exception e) {
                log.error("連接異常");
                retryOutCallWebSocketClient();
            }
        };
        webSocketClient.connect();
        return webSocketClient;
    }


    /**
     * 項目啓動或連接失敗的時候打開新鏈接,進行連接認證
     * 需要加同步,不然會創建多個連接
     */
    public synchronized WebSocketClient retryOutCallWebSocketClient() {
        try {
            // 關閉舊的websocket連接, 避免佔用資源
            WebSocketClient oldOutCallWebSocketClientHolder = this.getOutCallWebSocketClientHolder();
            if (null != oldOutCallWebSocketClientHolder) {
                log.info("關閉舊的websocket連接");
                oldOutCallWebSocketClientHolder.close();
            }

            log.info("打開新的websocket連接,並進行認證");
            WebSocketClient webSocketClient = this.createNewWebSocketClient();
            String sendOpenJsonStr = "{\"event\":\"connect\",\"sid\":\"1ae4e3167b3b49c7bfc6b79awww691562914214595\",\"token\":\"df59eba89\"}";
            this.sendMsg(webSocketClient, sendOpenJsonStr);

            // 每次創建新的就放進去
            this.setOutCallWebSocketClientHolder(webSocketClient);
            return webSocketClient;
        } catch (URISyntaxException e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
        return null;
    }


    /**
     * 發送消息
     * 注意: 要加超時設置,避免很多個都在同時超時佔用資源
     *
     * @param webSocketClient 指定的webSocketClient
     * @param message         消息
     */
    public void sendMsg(WebSocketClient webSocketClient, String message) {
        log.info("websocket向服務端發送消息,消息爲:{}", message);
        long startOpenTimeMillis = System.currentTimeMillis();
        while (!webSocketClient.getReadyState().equals(WebSocket.READYSTATE.OPEN)) {
            log.debug("正在建立通道,請稍等");
            long currentTimeMillis = System.currentTimeMillis();
            if(currentTimeMillis - startOpenTimeMillis >= 5000) {
                log.error("超過5秒鐘還未打開連接,超時,不再等待");
                return;
            }
        }
        webSocketClient.send(message);
    }



    @Async
    @Scheduled(fixedRate = 10000)
    public void sendHeartBeat() {
        log.info("定時發送websocket心跳");
        try {
            WebSocketClient outCallWebSocketClientHolder = this.getOutCallWebSocketClientHolder();

            if (null == outCallWebSocketClientHolder) {
                log.info("當前連接還未建立,暫不發送心跳消息");
                return;
            }

            // 心跳的請求串,根據服務端來定
            String heartBeatMsg = "{\"event\":\"heartbeat\",\"sid\":\"1ae4e3167b3b49c7bfc6b79a74f2296915222214595\"}";
            this.sendMsg(outCallWebSocketClientHolder, heartBeatMsg);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("發送心跳異常");
            retryOutCallWebSocketClient();
        }
    }

}

 

三、測試

3.0 控制類

package com.example.websocketdemo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class OutCallWebSocketController {

    @Autowired
    private WebSocketClientFactory webSocketClientFactory;

    @GetMapping("/sendCall")
    public void sendCall() {
        String heartBeatMsg = "{\"event\":\"heartbeat\",\"sid\":\"1ae4e3167b3b49c7bfc6b79a74f229691562914214595\"}";
        webSocketClientFactory.sendMsg(webSocketClientFactory.getOutCallWebSocketClientHolder(), heartBeatMsg);
    }

}

3.1 日誌

3.1.1 啓動的日誌

2020-01-02 21:56:43.365  INFO 14800 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'
2020-01-02 21:56:43.377  INFO 14800 --- [   scheduling-1] c.e.w.WebSocketClientFactory             : 定時發送websocket心跳
2020-01-02 21:56:43.377  INFO 14800 --- [   scheduling-1] c.e.w.WebSocketClientFactory             : 當前連接還未建立,暫不發送心跳消息
2020-01-02 21:56:43.391  INFO 14800 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8000 (http) with context path ''
2020-01-02 21:56:43.393  INFO 14800 --- [           main] c.e.w.WebsocketdemoApplication           : Started WebsocketdemoApplication in 1.208 seconds (JVM running for 1.617)
2020-01-02 21:56:43.394  INFO 14800 --- [           main] c.e.w.WebSocketClientFactory             : 打開新的websocket連接,並進行認證
2020-01-02 21:56:43.398  INFO 14800 --- [           main] c.e.w.WebSocketClientFactory             : websocket向服務端發送消息,消息爲:{"event":"connect","sid":"1ae4e3167b3b49c7bfc6b79awww691562914214595","token":"df59eba89"}
2020-01-02 21:56:43.410  INFO 14800 --- [       Thread-5] c.e.w.WebSocketClientFactory             : 接收信息爲:{"result":2,"reqBody":{"event":"connect","sid":"1ae4e3167b3b49c7bfc6b79awww691562914214595","token":"df59eba89"},"event":"connect","resultMessage":"企業用戶不存在","sid":"1ae4e3167b3b49c7bfc6b79awww691562914214595"}
2020-01-02 21:56:53.377  INFO 14800 --- [   scheduling-1] c.e.w.WebSocketClientFactory             : 定時發送websocket心跳
2020-01-02 21:56:53.377  INFO 14800 --- [   scheduling-1] c.e.w.WebSocketClientFactory             : websocket向服務端發送消息,消息爲:{"event":"heartbeat","sid":"1ae4e3167b3b49c7bfc6b79a74f2296915222214595"}
Disconnected from the target VM, address: '127.0.0.1:55203', transport: 'socket'
2020-01-02 21:56:55.993  INFO 14800 --- [extShutdownHook] o.s.s.c.ThreadPoolTaskScheduler          : Shutting down ExecutorService 'taskScheduler'
2020-01-02 21:56:55.993  INFO 14800 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

3.1.2 測試日誌 TODO

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章