WebSocket實現服務端推送訊息和聊天會話

我叫yeeking啊發表於2020-10-24

今天在做後臺管理系統,需要實現客服會話聊天 和 服務端推送訊息等功能。我們平常的HTTP無法滿足長連線,如果一直請求伺服器會造成資源的浪費,所以http協議就行不通了,這時候就需要使用WebSocket技術,是獨立與http協議的。
廢話不多說,我整理了使用的操作步驟,如下。

首先,需要引入maven依賴,因為我的工程是springboot+mabatis,所以匯入的依賴如下:

   <!-- 熱啟動依賴包 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <optional>true</optional>
    </dependency>
    <!-- jdbc、mybatis、springboot配合
         事物、oracle、pagehelper、fastjson
         log4j、aop、日誌  -->
    <dependency>
      <groupId>com.oracle</groupId>
      <artifactId>ojdbc6</artifactId>
      <version>11.2.0.4.0-atlassian-hosted</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.5</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.1.1</version>
    </dependency>
    <dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper-spring-boot-starter</artifactId>
      <version>1.2.13</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.73</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
    </dependency>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.2</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
   
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.10</version>
    </dependency>
    
    <!--  websocket-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- lang3   -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.4</version>
    </dependency>

新建service包,其下建一個類WebSocketServer用於連線通訊( 注意:不要使用Aop掃描這個類,會報錯!)

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import  org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
import org.slf4j.LoggerFactory;



@ServerEndpoint("/imserver/{userId}")
@Component
public class WebSocketServer {
    static Logger log =LoggerFactory.getLogger(WebSocketServer.class);

    /**靜態變數,用來記錄當前線上連線數。應該把它設計成執行緒安全的。*/
    private static int onlineCount = 0;
    /**concurrent包的執行緒安全Set,用來存放每個客戶端對應的MyWebSocket物件。*/
    private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    /**與某個客戶端的連線會話,需要通過它來給客戶端傳送資料*/
    private Session session;
    /**接收userId*/
    private String userId="";

    /**
     * 連線建立成功呼叫的方法*/
    @OnOpen
    public void onOpen(Session session,@PathParam("userId") String userId) {
        this.session = session;
        this.userId=userId;
        if(webSocketMap.containsKey(userId)){
            webSocketMap.remove(userId);
            webSocketMap.put(userId,this);
            //加入set中
        }else{
            webSocketMap.put(userId,this);
            //加入set中
            addOnlineCount();
            //線上數加1
        }

        log.info("使用者連線:"+userId+",當前線上人數為:" + getOnlineCount());

        try {
            sendMessage("連線成功");
        } catch (IOException e) {
            log.error("使用者:"+userId+",網路異常!!!!!!");
        }
    }

    /**
     * 連線關閉呼叫的方法
     */
    @OnClose
    public void onClose() {
        if(webSocketMap.containsKey(userId)){
            webSocketMap.remove(userId);
            //從set中刪除
            subOnlineCount();
        }
        log.info("使用者退出:"+userId+",當前線上人數為:" + getOnlineCount());
    }

    /**
     * 收到客戶端訊息後呼叫的方法
     *
     * @param message 客戶端傳送過來的訊息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("使用者訊息:"+userId+",報文:"+message);
        //可以群發訊息
        //訊息儲存到資料庫、redis
        if(StringUtils.isNotBlank(message)){
            try {
                //解析傳送的報文
                JSONObject jsonObject = JSON.parseObject(message);
                //追加傳送人(防止串改)
                jsonObject.put("fromUserId",this.userId);
                String toUserId=jsonObject.getString("toUserId");
                //傳送給對應toUserId使用者的websocket
                if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){
                    webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());
                }else{
                    log.error("請求的userId:"+toUserId+"不在該伺服器上");
                    //否則不在這個伺服器上,傳送到mysql或者redis
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("使用者錯誤:"+this.userId+",原因:"+error.getMessage());
        error.printStackTrace();
    }
    /**
     * 實現伺服器主動推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


    /**
     * 傳送自定義訊息
     * */
    public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {
        log.info("傳送訊息到:"+userId+",報文:"+message);
        if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){
            webSocketMap.get(userId).sendMessage(message);
        }else{
            log.error("使用者"+userId+",不線上!");
        }
    }

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

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

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

@ServerEndpoint("/imserver/{userId}")
@Component 例項化spring容器中,實現@OnOpen開啟連線,@onClose關閉連線,@onMessage接收訊息等方法

新建訊息傳送的Control包,建DemoController類

@RestController
public class DemoController {

    @GetMapping("index")
    public ResponseEntity<String> index(){
        return ResponseEntity.ok("請求成功");
    }

    @GetMapping("page")
    public ModelAndView page(){
        return new ModelAndView("websocket");
    }

    @RequestMapping("/push/{toUserId}")
    public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws IOException {
        WebSocketServer.sendInfo(message,toUserId);
        return ResponseEntity.ok("MSG SEND SUCCESS");
    }
}

頁面發起請求

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通訊</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的瀏覽器不支援WebSocket");
        }else{
            console.log("您的瀏覽器支援WebSocket");
            //實現化WebSocket物件,指定要連線的伺服器地址與埠  建立連線
            //等同於socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
            //var socketUrl="${request.contextPath}/im/"+$("#userId").val();
            var socketUrl="http://localhost:10086/imserver/"+$("#userId").val();
            socketUrl=socketUrl.replace("https","ws").replace("http","ws");
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //開啟事件
            socket.onopen = function() {
                console.log("websocket已開啟");
                //socket.send("這是來自客戶端的訊息" + location.href + new Date());
            };
            //獲得訊息事件
            socket.onmessage = function(msg) {
                console.log(msg.data);
                //發現訊息進入開始處理前端觸發邏輯
            };
            //關閉事件
            socket.onclose = function() {
                console.log("websocket已關閉");
            };
            //發生了錯誤事件
            socket.onerror = function() {
                console.log("websocket發生了錯誤");
            }
        }
    }
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的瀏覽器不支援WebSocket");
        }else {
            console.log("您的瀏覽器支援WebSocket");
            console.log('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
            socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
        }
    }
</script>
<body>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="10"></div>
<p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="20"></div>
<p>【messsage】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>【操作】:<div><button onclick="openSocket()">開啟socket</button></div>
<p>【操作】:<div><button onclick="sendMessage()">傳送訊息</button></div>
</body>

</html>

最後啟動專案

網址是:http://localhost:埠號/page.do?p=index
也可以開啟另一個新頁面手動自定義傳送訊息,這樣在10埠的控制檯也可以列印我們位址列傳送的訊息

在這裡插入圖片描述

相關文章