フロントエンドにspringbootを追加し、Web Socket接続の通信とテスト処理を実装(ハートビート検出のバックエンド実装を含む)

序文

このプロジェクトは主に、バックエンドからのデータをフロントエンドに返す必要があるプロジェクトがあったために書かれたもので、最初はフロントエンドのポーリング方式を使用していましたが、後で適時性が十分ではないと感じ、その後、 WebSocket を使用するように変更され、具体的な実装デモとテスト プロセスが送信されました。

1. Web Socket の簡単な紹介

WebSocket は、単一の TCP 接続を介した全二重通信用のプロトコルです。WebSocket 通信プロトコルは、2011 年に IETF によって標準 RFC 6455 として指定され、RFC7936 によって補足されました。WebSocket API も W3C によって標準として指定されています。
WebSocket を使用すると、クライアントとサーバー間のデータ交換が簡単になり、サーバーがデータをクライアントにアクティブにプッシュできるようになります。WebSocket API では、ブラウザとサーバーはハンドシェイクを完了するだけでよく、双方向データ送信のために両者の間に永続的な接続を直接作成できます。

1 WebSocket を使用する理由

言い換えれば、WebSocket はどのような問題を解決するのでしょうか? その答えは、次の 2 つの主な問題を解決するということです。

  • クライアントのみがリクエストを送信できる
  • 一定期間にわたって頻繁にメッセージを送信する

リアルタイム警報システムの通知モジュールを設計する必要がある場合、エンジニアは通知機能をどのように設計すればよいでしょうか? これらのシステムのデータ ソースは通常、ハードウェア デバイスを介してバックグラウンドで収集されるためです。現時点で http プロトコルしかない場合、クライアントがサーバーを継続的にポーリングできるようにすることしかできません。ポーリング間隔が短いほど、リアルタイムに近づきます。効果はあるかもしれません。ただし、ポーリングは非効率的であり、リソースを無駄に消費します。このようなシナリオのために、WebSocket が登場しました。

ここに画像の説明を挿入します
特徴:
(1) TCP プロトコルに基づいて構築されているため、サーバー側の実装が比較的簡単で、信頼性の高い伝送プロトコルです。
(2) HTTPプロトコルとの互換性が良好です。デフォルトのポートも 80 と 443 で、ハンドシェイク フェーズでは HTTP プロトコルが使用されるため、ハンドシェイク中にブロックされにくく、さまざまな HTTP プロキシ サーバーを通過できます。
(3) データ形式は比較的軽量で、パフォーマンスのオーバーヘッドが低く、通信が効率的です。
(4) テキストまたはバイナリデータを送信できます。
(5) 送信元の制限はなく、クライアントは任意のサーバーと通信できます。
(6) プロトコル識別子は ws (暗号化されている場合は wss)、サーバー アドレスは URL です。

2. コードの実装

1. フロントエンド (html)

1.1. フロントエンドを使用せずにバックエンドにメッセージを送信する

ここに画像の説明を挿入します

実際の開発では、uid の一意の値を現在の会話のキーとして使用する必要があります。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
消息展示区:<br/>
<div id="textArea"></div>
 
</body>
<script>
    var textArea = document.getElementById('textArea');
 
 
    var websocket = null;
    //如果浏览器支持websocket就建立一个websocket,否则提示浏览器不支持websocket 
                //uid应该要用唯一标识,为了测试方便看
    if('websocket' in window){
      
      
        websocketPage = new WebSocket('ws://localhost:8080/websocket/' + 99);
    }else{
      
      
        alert('浏览器不支持websocket!');
    }
    //建立websocket时自动调用
    websocketPage.onopen = function (event) {
      
      
        console.log('建立连接');
    }
    //关闭webscoket时自动调用
    websocketPage.oncolse = function (event){
      
      
        console.log('关闭连接');
    }
    //websocket接收到消息时调用
    websocketPage.onmessage = function (event){
      
      
        //将接收到的消息展示在消息展示区  (心跳响应回来的消息不显示)
        if (event.data !== "conn_success"){
      
      
            textArea.innerText += event.data;
            textArea.innerHTML += "<br/>";
        }
    }
    //websocket出错自动调用
    websocketPage.onerror = function () {
      
      
        alert('websocket出错');
    }
    //关闭窗口前关闭websocket连接
    window.onbeforeunload = function (){
      
      
        websocketPage.close();
    }
 
</script>
</html>

1.2. バックエンドにメッセージを送信するフロントエンドがあります。

ここに画像の説明を挿入します

<!DOCTYPE html>
<html>

	<head>
		<meta charset="utf-8">
		<title>Java后端WebSocket的Tomcat实现</title>
		<script type="text/javascript" src="js/jquery.min.js"></script>
	</head>

	<body>
		
		Welcome<br/><input id="text" type="text" />
		<button onclick="send()">发送消息</button>
		<hr/>
		<button onclick="closeWebSocket()">关闭WebSocket连接</button>
		<hr/>
		<div id="message"></div>
	</body>
	<script type="text/javascript">
		var websocket = null;
		//判断当前浏览器是否支持WebSocket
		if('WebSocket' in window) {
      
      
			//改成你的地址
			websocket = new WebSocket("ws://localhost:8080/websocket/100");
		} else {
      
      
			alert('当前浏览器 Not support websocket')
		}

		//连接发生错误的回调方法
		websocket.onerror = function() {
      
      
			setMessageInnerHTML("WebSocket连接发生错误");
		};

		//连接成功建立的回调方法
		websocket.onopen = function() {
      
      
			setMessageInnerHTML("WebSocket连接成功");
		}
		var U01data, Uidata, Usdata
		//接收到消息的回调方法
		websocket.onmessage = function(event) {
      
      
			console.log(event);
			if (event.data !== "conn_success"){
      
      
				setMessageInnerHTML("接收消息:"+event.data);
				// setMessageInnerHTML(event);
				setechart()
			}
		}

		//连接关闭的回调方法
		websocket.onclose = function() {
      
      
			setMessageInnerHTML("WebSocket连接关闭");
		}

		// //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
		window.onbeforeunload = function() {
      
      
			closeWebSocket();
		}

		//将消息显示在网页上
		function setMessageInnerHTML(innerHTML) {
      
      
			document.getElementById('message').innerHTML += innerHTML + '<br/>';
		}

		//关闭WebSocket连接
		function closeWebSocket() {
      
      
			websocket.close();
		}

		//发送消息
		function send() {
      
      
			var message = document.getElementById('text').value;
			websocket.send('{"msg":"' + message + '"}');
			setMessageInnerHTML("--------------发送消息:"+message + "");
		}
	</script>
</html>

2. バックエンド固有のコード (スプリング ブート)

2.1. Maven の依存関係

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

– yml には何もなく、デフォルトのポートのみが存在します

2.2. 設定クラス

WebSocket エンドポイントによって公開される Bean とタイマー アノテーションを追加する必要があります

@EnableScheduling  //定时器
@SpringBootApplication
public class WebSocketApplication {
    
    

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



    /** 
     * 服务器端点导出 
     * @author zhengfuping
     * @date 2023/8/22 
     * @return ServerEndpointExporter 
     */
    @Bean
    public ServerEndpointExporter getServerEndpointExporter(){
    
    
        return new ServerEndpointExporter();
    }
}

2.3. Web Socket接続ツールクラス

@Slf4j
@Service
@ServerEndpoint("/websocket/{uid}")
public class WebSocketServer2 {
    
    

    //连接建立时长
    private static final long sessionTimeout = 60000;

    // 用来存放每个客户端对应的WebSocketServer对象
    private static Map<String, WebSocketServer2> webSocketMap = new ConcurrentHashMap<>();

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

    // 接收id
    private String uid;

    /**
     * 连接建立成功调用的方法
     * @author zhengfuping
     * @date 2023/8/22
     * @param session
     * @param uid
     */
    @OnOpen
    public void onOpen(Session session , @PathParam("uid") String uid){
    
    
        session.setMaxIdleTimeout(sessionTimeout);
        this.session = session;
        this.uid = uid;
        if (webSocketMap.containsKey(uid)){
    
    
            webSocketMap.remove(uid);
        }
        webSocketMap.put(uid,this);
        log.info("websocket连接成功编号uid: " + uid + ",当前在线数: " + getOnlineClients());

        try{
    
    
        // 响应客户端实际业务数据!
            sendMessage("conn_success");
        }catch (Exception e){
    
    
            log.error("websocket发送连接成功错误编号uid: " + uid + ",网络异常!!!");
        }
    }

    /**
     * 连接关闭调用的方法
     * @author zhengfuping
     * @date 2023/8/22
     */
    @OnClose
    public void onClose(){
    
    
        try {
    
    
            if (webSocketMap.containsKey(uid)){
    
    
                webSocketMap.remove(uid);
            }
            log.info("websocket退出编号uid: " + uid + ",当前在线数为: " + getOnlineClients());
        } catch (Exception e) {
    
    
            log.error("websocket编号uid连接关闭错误: " + uid + ",原因: " + e.getMessage());
        }
    }


    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
    
    
        try {
    
    
            WebSocketServer2.sendInfo(message);
            log.info("websocket收到客户端编号uid消息: " + uid + ", 报文: " + message);
        } catch (Exception e) {
    
    
            log.error("websocket发送消息失败编号uid为: " + uid + ",报文: " + message);
        }

    }

    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
    
    
        log.error("websocket编号uid错误: " + this.uid + "原因: " + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     * @author yingfeng
     * @date 2023/8/22 10:11
     * @Param * @param null
     * @return
     */

    public void sendMessage(String message) throws IOException {
    
    
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 获取客户端在线数
     * @author zhengfuping
     * @date 2023/8/22 10:11
     * @param
     */
    public static synchronized int getOnlineClients() {
    
    
        if (Objects.isNull(webSocketMap)) {
    
    
            return 0;
        } else {
    
    
            return webSocketMap.size();
        }
    }




    /**
     * 单机使用,外部接口通过指定的客户id向该客户推送消息
     * @param key
     * @param message
     * @return boolean
     */
    public static boolean sendMessageByWayBillId(@NotNull String key, String message) {
    
    
        WebSocketServer2 webSocketServer = webSocketMap.get(key);
        if (Objects.nonNull(webSocketServer)) {
    
    
            try {
    
    
                webSocketServer.sendMessage(message);
                log.info("websocket发送消息编号uid为: " + key + "发送消息: " + message);
                return true;
            } catch (Exception e) {
    
    
                log.error("websocket发送消息失败编号uid为: " + key + "消息: " + message);
                return false;
            }
        } else {
    
    
            log.error("websocket未连接编号uid号为: " + key + "消息: " + message);
            return false;
        }
    }

    /**
     * 群发自定义消息
     * @author zhengfuping
     * @date 2023/8/22 9:52
     * @param message
     */
    public static void sendInfo(String message) {
    
    
        webSocketMap.forEach((k, v) -> {
    
    
            WebSocketServer2 webSocketServer = webSocketMap.get(k);
            try {
    
    
                webSocketServer.sendMessage(message);
                log.info("websocket群发消息编号uid为: " + k + ",消息: " + message);
            } catch (IOException e) {
    
    
                log.error("群发自定义消息失败: " + k + ",message: " + message);
            }
        });
    }
    /**
     * 服务端群发消息-心跳包
     * @author zhengfuping
     * @date 2023/8/22 10:09
     * @param message 推送数据
     * @return int 连接数
     */
    public static synchronized int sendPing(String message){
    
    
        if (webSocketMap.size() == 0)
            return 0;
        StringBuffer uids = new StringBuffer();
        AtomicInteger count = new AtomicInteger();
        webSocketMap.forEach((uid,server)->{
    
    
            count.getAndIncrement();

            if (webSocketMap.containsKey(uid)){
    
    
                WebSocketServer2 webSocketServer = webSocketMap.get(uid);
                try {
    
    
                    if (Integer.valueOf(uid) ==101){
    
    
                        Integer i=1/0;
                    }

                    webSocketServer.sendMessage(message);
                    if (count.equals(webSocketMap.size() - 1)){
    
    
                        uids.append("uid");
                        return;

                    }
                    uids.append(uid).append(",");
                } catch (Exception e) {
    
    
                    webSocketMap.remove(uid);
                    log.info("客户端心跳检测异常移除: " + uid + ",心跳发送失败,已移除!");

                }
            }else {
    
    
                log.info("客户端心跳检测异常不存在: " + uid + ",不存在!");

            }
        });
        log.info("客户端心跳检测结果: " + uids + "连接正在运行");
        return webSocketMap.size();
    }
    /**
     * 连接是否存在
     * @param uid
     * @return boolean
     */
    public static boolean isConnected(String uid) {
    
    
        if (Objects.nonNull(webSocketMap) && webSocketMap.containsKey(uid)) {
    
    
            return true;
        } else {
    
    
            return false;
        }
    }
}

2.4. コントローラーはアクティブなメッセージ送信をテストするために使用されます

@RestController
@RequestMapping("/test")
public class WebSocketController{
    
    
    /**
     * 检验连接
     * @date 2023/8/22
     * @Param * @param webSocketId
     * @return * @return String
     */
    @GetMapping("/webSocketIsConnect/{webSocketId}")
    public String webSocketIsConnect(@PathVariable("webSocketId") String webSocketId){
    
    
        if (WebSocketServer2.isConnected(webSocketId)) {
    
    
            return webSocketId+"正在连接";
        }
        return webSocketId+"连接断开!";
    }

    /**
     * 单发 消息
     * @author zhengfuping
     * @date 2023/8/22 10:25
     * @param webSocketId  指定 连接
     * @param message  数据
     * @param pwd 验证密码
     * @return String
     */
    @GetMapping("/sendMessageByWayBillId")
    public String sendMessageByWayBillId(String webSocketId, String message, String pwd) {
    
    
        boolean flag = false;

            flag = WebSocketServer2.sendMessageByWayBillId(webSocketId, message);

        if (flag) {
    
    
            return "发送成功!";
        }
        return "发送失败!";
    }

    /**
     * 群发
     * @author zhengfuping
     * @date 2023/8/22 10:26
     * @param message
     * @param pwd
     */
    @GetMapping("/broadSendInfo")
    public void sendInfo(String message, String pwd) {
    
    
            WebSocketServer2.sendInfo(message);
    }
}

2.5. クライアントにハートビートをアクティブに送信するようにスケジュールされたタスクを設定します。

10 秒ごとに呼び出され、クライアント接続が異常に切断されているかどうかをアクティブに検出します。異常に切断された場合は、無限のバックログを避けるためにセッションがコレクションから削除されます。

@Component
@Slf4j
public class WebSocketTask {
    
    
    
    @Scheduled(cron = "0/10 * * * * ?")
    public void clearOrders(){
    
    
        int num = 0;
        try {
    
    
            num = WebSocketServer2.sendPing("conn_success");
        } finally {
    
    
            log.info("websocket心跳检测结果,共【" + num + "】个连接");
        }
    }
}

3. テスト

1. メッセージ送信のテスト

1.1. フロントエンドログ

ここに画像の説明を挿入します

1.2. バックエンドログ

ここに画像の説明を挿入します

2. テスト クライアントが異常に切断された場合、サーバーはハートビート検出によって異常なダイアログを自動的に削除します。

テストが不便なため、ブレークポイントを介してのみ効果を達成できます

  1. フロントエンドは、アクティブな終了を防ぐために、セッションのアクティブな終了をコメントアウトする必要があります。
    ここに画像の説明を挿入します

  2. まず、接続終了ポイントとハートビート検出ポイントにブレークポイントを置きます。ブレークポイントはスレッドに設定する必要があります。そうしないと非同期になりません。
    ここに画像の説明を挿入します

  3. 次に、フロントエンド ページを閉じてセッションを閉じるように要求すると、ブレークポイントの位置に入り、ブレークポイントを介して停止して、正常に閉じることができなくなります。

ここに画像の説明を挿入します4. 次に、ハートビート検出を実行するブレークポイント コードを選択します
ここに画像の説明を挿入します
5. ハートビート ループに入り、ハートビート検出を各セッションに送信します この時点で、フロント エンドは異常切断されています 6. フロント エンドが
ここに画像の説明を挿入します
セッションを閉じているため、ハートビートの送信は失敗します。catch ブロックに直接移動し、セットからセッションを削除します。
ここに画像の説明を挿入します
最終ログ
ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/weixin_52315708/article/details/132428774