SpringBoot はリアルタイム通知フロントエンドに SSE を使用します

説明する

プロジェクトの要件の 1 つは、タスクが読み込まれたことをリアルタイムでフロントエンドに通知することです。そこで私は 2 つの解決策を考えました。1 つは長時間接続するために WebSocket を使用することであり、もう 1 つは HTTP プロトコルの 1 つである SSE (Sever Send Event) を使用することです。Content-Type は text/event-stream です。長い接続を維持できます。
Websocket はバックエンドにメッセージを送信できるフロントエンドであり、バックエンドもフロントエンドにメッセージを送信できます。
SSE はバックエンドからフロントエンドにメッセージのみを送信できます。
バックエンド通知のみが必要なため、ここでは SSE を使用することにしました。
後で使用方法を忘れないように、最初にここにメモしておきます。

Mavenの依存関係

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.project</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test</name>
    <description>test</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--web依赖,内嵌入tomcat,SSE依赖于该jar包,只要有该依赖就能使用SSE-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok依赖,用来对象省略写set、get方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

SSEツールコード

package com.etone.project.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

@Slf4j
public class SseEmitterServer {
    
    

    /**
     * 当前连接数
     */
    private static AtomicInteger count = new AtomicInteger(0);

    private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    public static SseEmitter connect(String userId){
    
    
        //设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常
        SseEmitter sseemitter = new SseEmitter(0L);
        //注册回调
        sseemitter.onCompletion(completionCallBack(userId));
        //这个onError在springbooot低版本没有这个方法,公司springboot1.4.2版本,没有这个方法,可以进行注释。
        sseemitter.onError(errorCallBack(userId));
        sseemitter.onTimeout(timeoutCallBack(userId));
        sseEmitterMap.put(userId,sseemitter);
        //数量+1
        count.getAndIncrement();
        log.info("create new sse connect ,current user:{}",userId);
        return sseemitter;
    }

    /**
     * 给指定用户发消息
     */
    public static void sendMessage(String userId, String message){
    
    
        if(sseEmitterMap.containsKey(userId)){
    
    
            try{
    
    
                sseEmitterMap.get(userId).send(message);
            }catch (IOException e){
    
    
                log.error("user id:{}, send message error:{}",userId,e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * 想多人发送消息,组播
     */
    public static void groupSendMessage(String groupId, String message){
    
    
        if(sseEmitterMap!=null&&!sseEmitterMap.isEmpty()){
    
    
            sseEmitterMap.forEach((k,v) -> {
    
    
                try{
    
    
                    if(k.startsWith(groupId)){
    
    
                        v.send(message, MediaType.APPLICATION_JSON);
                    }
                }catch (IOException e){
    
    
                    log.error("user id:{}, send message error:{}",groupId,message);
                    removeUser(k);
                }
            });
        }
    }

    public static void batchSendMessage(String message) {
    
    
        sseEmitterMap.forEach((k,v)->{
    
    
            try{
    
    
                v.send(message,MediaType.APPLICATION_JSON);
            }catch (IOException e){
    
    
                log.error("user id:{}, send message error:{}",k,e.getMessage());
                removeUser(k);
            }
        });
    }

    /**
     * 群发消息
     */
    public static void batchSendMessage(String message, Set<String> userIds){
    
    
        userIds.forEach(userid->sendMessage(userid,message));
    }

    //移除用户
    public static void removeUser(String userid){
    
    
        sseEmitterMap.remove(userid);
        //数量-1
        count.getAndDecrement();
        log.info("remove user id:{}",userid);
    }

    public static List<String> getIds(){
    
    
        return new ArrayList<>(sseEmitterMap.keySet());
    }

    public static int getUserCount(){
    
    
        return count.intValue();
    }

    private static Runnable completionCallBack(String userId) {
    
    
        return () -> {
    
    
            log.info("结束连接,{}",userId);
            removeUser(userId);
        };
    }
    private static Runnable timeoutCallBack(String userId){
    
    
        return ()->{
    
    
            log.info("连接超时,{}",userId);
            removeUser(userId);
        };
    }
    private static Consumer<Throwable> errorCallBack(String userId){
    
    
        return throwable -> {
    
    
            log.error("连接异常,{}",userId);
            removeUser(userId);
        };
    }
}

コントローラーのテストコード

package com.project.test.controller;

import com.hjl.test.util.SseEmitterServer;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping(value = "/test")
public class TestController {
    
    

    //sse连接接口
    @GetMapping (value = "/sse/connect/{id}")
    public SseEmitter connect(@PathVariable String id){
    
    
        return SseEmitterServer.connect(id);
    }

    //sse向指定用户发送消息接口
    @GetMapping (value = "/sse/send/{id}")
    public Map<String,Object> send(@PathVariable String id,@RequestParam(value = "message", required = false) String message){
    
    
        Map<String,Object> returnMap = new HashMap<>();
        //向指定用户发送信息
        SseEmitterServer.sendMessage(id,message);
        returnMap.put("message","向id为"+id+"的用户发送:"+message+"成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

    //sse向所有已连接用户发送消息接口
    @GetMapping (value = "/sse/batchSend")
    public Map<String,Object> batchSend(@RequestParam(value = "message", required = false) String message){
    
    
        Map<String,Object> returnMap = new HashMap<>();
        //向指定用户发送信息
        SseEmitterServer.batchSendMessage(message);
        returnMap.put("message",message+"消息发送成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

    //sse关闭接口
    @GetMapping (value = "/sse/close/{id}")
    public Map<String,Object> close(@PathVariable String id){
    
    
        Map<String,Object> returnMap = new HashMap<>();
        //移除id
        SseEmitterServer.removeUser(id);
        System.out.println("当前连接用户id:"+SseEmitterServer.getIds());
        returnMap.put("message","连接关闭成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

}

テスト結果は次のとおりです。

ここで SSE 接続をテストするには、通常のインターフェイスと同様にリクエストするだけです。
ローカル呼び出しインターフェイス /sse/connect/1 は次のとおりです。
ここでは、指定されたユーザー ID への情報の送信と、接続されているユーザーへのメッセージのバッチ送信をシミュレートするために 2 人のユーザーを接続します。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
バックエンド サービスは次のように出力します。
ここに画像の説明を挿入します

ローカル呼び出しインターフェイス /sse/send/1 は次のとおりです。
ここに画像の説明を挿入します
ユーザー 1 の結果は次のとおりで、メッセージを受信したことがわかります。
ここに画像の説明を挿入します
ユーザー 2 は次のように結果を受信しませんでした。
ここに画像の説明を挿入します

ローカル呼び出しインターフェイス /sse/batchSend は次のとおりです。
接続されているすべてのユーザーにメッセージをバッチで送信します。
ここに画像の説明を挿入します
ユーザー 1 の結果は次のとおりであり、メッセージを受信して​​いることがわかります。
ここに画像の説明を挿入します
ユーザー 2 の結果は次のとおりで、メッセージを受信して​​いることがわかります。
ここに画像の説明を挿入します
テスト結果はすべて期待どおりです。
郵便配達員の閉じるボタンをクリックして接続を閉じます。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
フロントエンド接続は閉じられていますが、実際にはバックエンドはまだ接続されており、ユーザーを削除するためのプロンプトはまったく表示されないことがわかります。そのため、手動で次のように記述する必要があります
ここに画像の説明を挿入します
。終了インターフェイスは自分でテストしてください。
ローカル呼び出しインターフェイス /sse/close/1 は次のとおりです。
ここに画像の説明を挿入します
ユーザー ID 1 が削除され、ユーザー 2 のみがまだ接続されていることがわかります。
ここに画像の説明を挿入します
ここでのテストはすべて完了しており、結果は予想どおりです。

知らせ

タイムアウトを元の 0 ミリ秒から 30 ミリ秒 (整数の後の L は Long が 8 バイトを占めることを意味します) に変更すると、エラーが報告されます。

ここに画像の説明を挿入します
テスト結果は次のとおりです。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
org.springframework.web.context.request.async.AsyncRequestTimeoutException で直接例外が発生し
、接続も切断されました。

springboot を 1.4.2.RELEASE などの下位バージョンに下げます。

テストに postman を使用すると、次のように、それが常にリクエストに含まれているわけではないことがわかりました:
Springboot を 1.4.2.RELEASE にダウングレードします。
ここに画像の説明を挿入します

springboot の 1.4.2.RELEASE バージョンには onError メソッドがないため、コメントアウトする必要があります。ここに画像の説明を挿入します
postman テストは次のとおりです:
下位バージョンをテストすると、直接参照できる接続があることがわかりましたが、springboot バージョン 2.x を使用すると、常にリクエストを送信していることがわかりました。フロントエンドにメッセージを送信すると、これが表示されます。
Springboot 1.4.2.RELEASE バージョンの結果:
ここに画像の説明を挿入します
springboot 2.7.3 バージョンの結果:
ここに画像の説明を挿入します

まずこの状況を記録して、後で時間があるときに調べてみましょう。上位バージョンが下位バージョンのようにこの接続情報を返せないのはなぜですか? そのため、上位バージョンの SpringBoot が SSE を使用して接続すると、常にリクエスト送信状態になりますが、これは正常ですか? どなたか教えていただけませんか、よろしくお願いします。

おすすめ

転載: blog.csdn.net/weixin_48040732/article/details/131000339