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