WebSocket 實現伺服器訊息推送客戶端

ckxllf發表於2019-12-25

  一、背景

  專案需要做一個訊息能夠實時獲取的功能,系統日活躍量達到10000,產生的訊息是活躍量的數倍,如果採用 Http 的方式輪詢後端服務,會使得後端服務壓力過大而奔潰,因此需要一種新的技術方式來改變 “拉” 的方式。

  二、解決方案

  經過各種 Google、百度 後發現可以使用 html5 的新技術 WebSocket ,將現有 “拉”訊息的方式改變成 “推” 的模式,大大的減少伺服器壓力。

  三、具體實現

  例項採用 Spring Boot 框架,

  引入 pom 依賴

  org.springframework.boot

  spring-boot-starter-websocket

  org.springframework.boot

  spring-boot-starter-undertow

  org.springframework.boot

  spring-boot-starter-web

  org.springframework.boot

  spring-boot-starter-tomcat

  WebSocket 服務可採用 websocket-api 或 spring-websocket 開發,我們採用 websocket-api 的註解開發方式:

  package com.gridsum.techpub.systemhistory.api.server;

  import org.slf4j.Logger;

  import org.slf4j.LoggerFactory;

  import org.springframework.stereotype.Service;

  import javax.websocket.*;

  import javax.websocket.server.PathParam;

  import javax.websocket.server.ServerEndpoint;

  import java.io.IOException;

  import java.util.Objects;

  import java.util.Set;

  import java.util.concurrent.CopyOnWriteArraySet;

  /**

  * @author ouyangrongtao

  * @version 1.0

  * @description WebSocketServer

  * @date 2019/12/23 10:16

  **/

  @ServerEndpoint("/websocket/{sid}")

  @Service

  public class WebSocketServer {

  private static final Logger logger = LoggerFactory.getLogger(WebSocketServer.class);

  private ClientInfo clientInfo;

  /**

  * 存放每個客戶端對應的 ClientInfo 物件。

  */

  private static final Set WEB_SOCKET_SET = new CopyOnWriteArraySet<>();

  /**

  * 連線建立成功呼叫的方法

  *

  * @param session 會話

  * @param sid 客戶端

  */

  @OnOpen

  public void onOpen(Session session, @PathParam("sid") String sid) {

  //加入set中

  this.clientInfo = new ClientInfo(sid, session);

  WEB_SOCKET_SET.add(clientInfo);

  logger.info("有新視窗開始監聽:[{}],當前線上人數為[{}]", sid, WEB_SOCKET_SET.size());

  try {

  this.sendMessage(session, "連線成功");

  } catch (IOException e) {

  logger.error("websocket IO異常");

  }

  }

  /**

  * 連線關閉呼叫的方法

  */

  @OnClose

  public void onClose() {

  //從set中刪除

  WEB_SOCKET_SET.remove(this.clientInfo);

  logger.info("有一連線關閉!當前線上人數為:[{}]", WEB_SOCKET_SET.size());

  }

  /**

  * 

  * @param message 客戶端傳送過來的訊息

  */

  @OnMessage

  public void onMessage(String message) {

  logger.info("收到來自視窗[{}]的資訊:[{}]", this.clientInfo.getSid(), message);

  //發訊息

  for (ClientInfo item : WEB_SOCKET_SET) {

  try {

  this.sendMessage(item.getSession(), message);

  } catch (IOException ignored) {

  }

  }

  }

  /**

  * 錯誤時呼叫

  * @param session 會話

  * @param error 錯誤資訊

  */

  @OnError

  public void onError(Session session, Throwable error) {

  logger.error("發生錯誤", error);

  }

  /**

  * 給 sid 傳送訊息

  * @param message 訊息

  * @param sid sid

  */

  public void sendMessage(String message, String sid) {

  logger.info("推送訊息到視窗[{}],推送內容:[{}]", sid, message);

  ClientInfo client = WEB_SOCKET_SET.parallelStream()

  .filter(item -> item.getSid().equals(sid)).findFirst().orElse(null);

  if (client != null) {

  try {

  this.sendMessage(client.getSession(), message);

  } catch (IOException ignored) {

  }

  }

  }

  /**

  * 實現伺服器主動推送

  * @param session session

  * @param message message

  * @throws IOException IOException

  */ 鄭州哪個婦科醫院好

  private void sendMessage(Session session, String message) throws IOException {

  session.getBasicRemote().sendText(message);

  }

  class ClientInfo {

  /**

  * 接收sid

  */

  private String sid = "";

  /**

  * 客戶端

  */

  private Session session;

  public ClientInfo() { }

  private ClientInfo(String sid, Session session) {

  this.sid = sid;

  this.session = session;

  }

  private String getSid() {

  return sid;

  }

  private Session getSession() {

  return session;

  }

  @Override

  public boolean equals(Object o) {

  if (this == o) {

  return true;

  }

  if (o == null || getClass() != o.getClass()) {

  return false;

  }

  ClientInfo that = (ClientInfo) o;

  return Objects.equals(sid, that.sid);

  }

  @Override

  public int hashCode() {

  return Objects.hash(sid);

  }

  }

  }

  前端程式碼

  執行 WebSocketClient1000001

  來一個發訊息的介面

  /**

  * 傳送訊息給客戶端

  * @author ouyangrongtao

  */

  @RestController

  public class WebSocketController {

  private WebSocketServer webSocketServer;

  @Autowired

  public WebSocketController(WebSocketServer webSocketServer) {

  this.webSocketServer = webSocketServer;

  }

  @PostMapping("/socket/push")

  public boolean pushToWeb(@RequestBody Map content) {

  webSocketServer.sendMessage(content.get("message"), content.get("cid"));

  return true;

  }

  }

  到此已經基本寫完。使用 Postman 呼叫發訊息的介面,發現客戶端可以收到傳送的訊息。

  四、問題記錄

  在做的時候,因為專案用的 Tomcat 容器,導致 Tomcat 相關包與 WebSocket 依賴有衝突,最終專案不能啟動,解決方式只需要將 Tomcat 容器改為 Undertow 。

  org.springframework.boot

  spring-boot-starter-undertow

  org.springframework.boot

  spring-boot-starter-web

  org.springframework.boot

  spring-boot-starter-tomcat

  異常資訊:

  Caused by: java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available

  at org.springframework.util.Assert.state(Assert.java:73)

  at org.springframework.web.socket.server.standard.ServerEndpointExporter.afterPropertiesSet(ServerEndpointExporter.java:106)

  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1753)

  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1690)

  ... 16 common frames omitted


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69945560/viewspace-2670447/,如需轉載,請註明出處,否則將追究法律責任。

相關文章