SSE と WebSocket はそれぞれサーバー送信メッセージ通知を実装します (Golang、Gin)

序文

サーバー プッシュ (メッセージ プッシュまたは通知プッシュとも呼ばれます) は、アプリケーション サーバーがクライアントに情報をプロアクティブに送信できるようにする機能で、クライアントにリアルタイムの情報更新と通知を提供し、ユーザー エクスペリエンスを向上させます。

サーバー プッシュの背景と要件は、主に次の要求に基づいています。

  • リアルタイム通知: 多くの場合、ユーザーは、新しいメッセージのリマインダー、製品アクティビティのリマインダーなど、アプリケーションからリアルタイムで通知を受信することを期待しています。
  • リソースの節約: サーバー プッシュがない場合、クライアントはポーリングを通じて新しい情報を取得する必要があります。これにより、クライアントとサーバーでリソースが失われます。サーバー プッシュにより、クライアントは通知を受信したときにのみ応答する必要があるため、リソースの消費が大幅に削減されます。
  • ユーザー エクスペリエンスの強化: サーバー プッシュを通じて、アプリケーションは、プロモーション、パーソナライズされた推奨事項などのターゲットを絞ったコンテンツを特定のユーザーまたはユーザー グループに送信できます。これにより、ユーザーの満足度とアプリへの定着率が向上します。

1. 解決策:

1. 従来のリアルタイム処理ソリューション:

  • ポーリング: これはより伝統的な方法で、クライアントは定期的にサーバーにリクエストを送信し、新しいデータがあるかどうかを尋ねます。サーバーはデータのステータスをチェックし、結果をクライアントに返すだけで済みます。ポーリングの利点は実装が簡単で互換性が高いことですが、欠点は大幅な遅延が発生する可能性があり、サーバー リソースを大量に消費することです。

  • ロングポーリング: ポーリングの改良版。クライアントはサーバーにリクエストを送信し、サーバーはリクエストを受信した後、新しいデータがあればすぐにクライアントに返しますが、新しいデータがない場合は一定時間待機します(たとえば、この期間中、新しいデータがある場合はクライアントに返され、そうでない場合は空のデータが返されます。クライアントは、サーバーから返された応答を処理した後、再度新しいリクエストを開始します。ロング ポーリングでは、従来のポーリングと比較してリクエストの数が減りますが、それでも一定の遅延が発生します。

2. HTML5標準により導入されたリアルタイム処理ソリューション:

  • WebSocket: サーバーとクライアント間のリアルタイム対話をサポートする双方向通信プロトコル。WebSocket は、TCP に基づいた長い接続です。HTTP プロトコルと比較して、軽量かつ低遅延のデータ送信を実現できます。リアルタイム通信シナリオに非常に適しており、主に高度なインタラクティブな双方向通信に使用されます。

  • SSE: SSE (Server-Sent Events) は、HTTP プロトコルに基づくプッシュ テクノロジです。サーバーは SSE を使用してクライアントにデータをプッシュできますが、クライアントは SSE を通じてサーバーにデータを送信できません。SSE は WebSocket に比べてシンプルで軽量ですが、一方向の通信しか実現できません。

2 つの主な違いは次のとおりです。

SSE ウェブソケット
通信 一方通行のコミュニケーション 双方向コミュニケーション
プロトコル HTTP ウェブソケット
自動的に再接続する サポート サポートされていません。クライアントが独自にサポートする必要があります
データ形式 テキスト形式 バイナリデータ、テキスト形式
ブラウザのサポート ほとんどサポートされています 主流のブラウザは優れたサポートを備えています
クロスドメイン サポートされません (クロスドメインの場合、指定された Access-Control-Allow-Origin を構成する必要があります) サポート

3. サードパーティのプッシュ:

一般的なオペレーティング システムは、Apple の APN (Apple Push Notification サービス)、Google の FCM (Firebase Cloud Messaging) など、対応するプッシュ サービスを提供します。同時に、開発者がさまざまなプラットフォームで統合プッシュ機能を実現できるように、Personal Push、Jiguang Push、Umeng Push などのクロスプラットフォーム プッシュ サービスもいくつかあります。

2.SSE

HTTP プロトコルに基づいているため、実装と展開が簡単で、サーバーが積極的に情報をプッシュする必要があり、クライアントはデータを受信することだけが必要なシナリオに特に適しています。
ここに画像の説明を挿入します

1. クライアント:

Server-Sent Events (SSE) は HTML5 の一部であり、サーバーからリアルタイムで更新を受信するために使用されます。現在、ほとんどの主要なブラウザーがサポートを提供しています。

<!DOCTYPE html>
<html>
<head>
    <title>SSE test</title>
    <script type="text/javascript">
        const es = new EventSource("http://localhost:8080/notification/socket-connection");
        // const es = new EventSource("http://localhost:8080/notification/socket-connection", { withCredentials: true });
        es.onmessage = function (e) {
      
      
            document.getElementById("test")
                .insertAdjacentHTML("beforeend", "<li>" + e.data + "</li>");
            console.log(e);
        }
        // es.addEventListener("test-event", (e) => {
      
      
        //     document.getElementById("test")
        //         .insertAdjacentHTML("beforeend", "<li>" + e.data + "</li>");
        // });
        es.onerror = function (e) {
      
      
            // readyState说明
            // 0:浏览器与服务端尚未建立连接或连接已被关闭
            // 1:浏览器与服务端已成功连接,浏览器正在处理接收到的事件及数据
            // 2:浏览器与服务端建立连接失败,客户端不再继续建立与服务端之间的连接
            console.log("readyState = " + e.currentTarget.readyState);
        }
    </script>
</head>
<body>
<h1>SSE test</h1>
<div>
    <ul id="test">
    </ul>
</div>
</body>
</html>

2. サーバー:

サーバー側では 2 つのインターフェイスを作成しました。最初のインターフェイスは SSE 接続の確立に使用され、2 番目のインターフェイスは通知のトリガーとメッセージの送信に使用されます。

	r.GET("/notification/socket-connection", handler.SocketConnection)
	r.GET("/notification/export-excel", handler.ExportExcel)
(1) sse接続を確立する

マップ コレクションが確立されます。接続が確立された後、一意のキーが生成され、マップ コレクションに保存されます。その後、現在のキーの値が継続的に読み取られ、メッセージがあるとすぐにクライアントに送信されます。
closeNotify を使用して、クライアントのステータスを監視します。これは、ページが更新されるか閉じられたときにのみトリガーされます。メモリ オーバーフローを防ぐために、一意のキーを削除してください。

var ifChannelsMapInit = false

var channelsMap = map[string]chan string{
    
    }

func initChannelsMap() {
    
    
	channelsMap = make(map[string]chan string)
}

func AddChannel(userEmail string, traceId string) {
    
    
	if !ifChannelsMapInit {
    
    
		initChannelsMap()
		ifChannelsMapInit = true
	}
	var newChannel = make(chan string)
	channelsMap[userEmail+traceId] = newChannel
	log.Infof("Build SSE connection for user = " + userEmail + ", trace id = " + traceId)
}

func BuildNotificationChannel(userEmail string, traceId string, c *gin.Context) {
    
    
	AddChannel(userEmail, traceId)
	c.Writer.Header().Set("Content-Type", "text/event-stream")
	c.Writer.Header().Set("Cache-Control", "no-cache")
	c.Writer.Header().Set("Connection", "keep-alive")

	w := c.Writer
	flusher, _ := w.(http.Flusher)
	closeNotify := c.Request.Context().Done()
	go func() {
    
    
		<-closeNotify
		delete(channelsMap, userEmail+traceId)
		log.Infof("SSE close for user = " + userEmail + ", trace id = " + traceId)
		return
	}()
	//for {
    
    
	//	fmt.Fprintf(w, "data: %s\n\n", "--ping--")
	//	time.Sleep(2 * time.Second)
	//	flusher.Flush()
	//}
	for msg := range channelsMap[userEmail+traceId] {
    
    
		fmt.Fprintf(w, "data: %s\n\n", msg)
		flusher.Flush()
	}
}
(2) 通知を送信する

メッセージを NoticeLog オブジェクトにカプセル化し、JSON 形式に変換します (コンテンツは必要に応じて変更できます)。 userEmail キーワードを使用してマップ内でメッセージを検索し、メッセージをマップに保存して、メッセージの送信を完了します。

type NotificationLog struct {
    
    
	MessageBody string    `json:"messageBody"`
	UserEmail   string    `json:"userEmail"`
	Type        string    `json:"type"`
	Status      string    `json:"status"`
	CreateTime  time.Time `json:"createTime"`
}

func SendNotification(userEmail string, messageBody string, actionType string) {
    
    
	log.Infof("Send notification to user = " + userEmail)
	var msg = NotificationLog{
    
    }
	msg.MessageBody = messageBody
	msg.UserEmail = userEmail
	msg.Type = actionType
	msg.Status = "UNREAD"
	msg.CreateTime = time.Now()
	//msg.Create()
	msgBytes, _ := json.Marshal(msg)
	for key := range channelsMap {
    
    
		if strings.Contains(key, userEmail) {
    
    
			channel := channelsMap[key]
			channel <- string(msgBytes)
		}
	}
}
(3)エフェクト表示

HTML ページを入力します。
ここに画像の説明を挿入します
通知送信インターフェイスを呼び出してhttp://localhost:8080/notification/export-excel通知を送信すると、ページがリアルタイムで表示されます。
ここに画像の説明を挿入します

(4) クロスドメインはサポートされていません

テスト中に、sse がクロスドメインをサポートしていないことがわかりました。これは、Access-Control-Allow-Origin を指定することで実現できます。
クロスドメイン アクセス:
クロスドメインアクセス
クロスドメイン要求を構成しても、エラーが報告されます:
Access to resource at 'http://localhost:8080/notification/socket-connection' from origin 'http://localhost:63342' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
ここに画像の説明を挿入します
Access-Control-Allow-Origin
を指定してクロスドメインを構成します。

func CORS() gin.HandlerFunc {
    
    
	return func(context *gin.Context) {
    
    
		// 允许 Origin 字段中的域发送请求
		context.Writer.Header().Add("Access-Control-Allow-Origin", "http://localhost:63342")
		// 设置预验请求有效期为 86400 秒
		context.Writer.Header().Set("Access-Control-Max-Age", "86400")
		// 设置允许请求的方法
		context.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE, PATCH")
		// 设置允许请求的 Header
		context.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Apitoken")
		// 设置拿到除基本字段外的其他字段,如上面的Apitoken, 这里通过引用Access-Control-Expose-Headers,进行配置,效果是一样的。
		context.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Headers")
		// 配置是否可以带认证信息
		context.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		// OPTIONS请求返回200
		if context.Request.Method == "OPTIONS" {
    
    
			fmt.Println(context.Request.Header)
			context.AbortWithStatus(200)
		} else {
    
    
			context.Next()
		}
	}
}
	r.Use(middlewares.CORS())

アクセス成功:
ここに画像の説明を挿入します

一般的にフロントエンドとバックエンドが分離されている現状において、クロスドメインにアクセスできないことは非常に大きなデメリットだと思います。

3.Webソケット

WebSocket は HTML5 の新しいプロトコルです。TCP に基づくアプリケーション層プロトコルです。全二重通信を実現するために必要な接続は 1 つだけです。クライアントとサーバーは相互にメッセージをアクティブに送信できます。クライアントは応答メッセージを監視および処理し、表示します。

1.サーバー

サーバーは、関連する WebSocket の使用法を実装するだけです。

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"log"
	"net/http"
)

func WebSocketHandler(context *gin.Context) {
    
    
	upGrader := websocket.Upgrader{
    
    
		CheckOrigin: func(r *http.Request) bool {
    
    
			return true
		},
	}
	// 建立WebSocket连接
	conn, err := upGrader.Upgrade(context.Writer, context.Request, nil)
	if err != nil {
    
    
		log.Println(err)
		http.NotFound(context.Writer, context.Request)
		return
	}
	// 接收并处理消息
	for {
    
    
		msgType, content, err := conn.ReadMessage()
		if err != nil {
    
    
			log.Println(err)
			break
		} else {
    
    
			if msgType == websocket.CloseMessage {
    
    
				log.Println("client closed!")
				break
			} else {
    
    
				fmt.Println(fmt.Sprintf("%+v", string(content)))
				err := conn.WriteMessage(websocket.TextMessage, content)
				if err != nil {
    
    
					return
				}
			}
		}
	}
	_ = conn.WriteMessage(websocket.CloseMessage, []byte("closed"))
	_ = conn.Close()
}

2.WebSocketをテストする

簡単かつ高速に行うために、オンライン テスト用 WebSocket Web サイトを使用してテストしました。http://www.jsons.cn/websocket/
ここに画像の説明を挿入します
ここで簡単なテストを行ったところ、基本的な機能は問題ないことがわかりました。全二重通信です。クライアントとサーバーはそれぞれにメッセージを送信できます他の。

4. まとめ

SSE テクノロジーは、使いやすく自動再接続をサポートする軽量のリアルタイム プッシュ テクノロジーであり、リアルタイム メッセージ プッシュ、株式取引、その他のシナリオで広く使用されています。さらに、SSE は WebSocket よりも軽量であるため、需要シナリオで対話型アクションやクロスドメインが必要ない場合は、SSE が適切な選択となります。
WebSocket はより強力ですが、より多くのリソースを消費します。

おすすめ

転載: blog.csdn.net/qq_51110841/article/details/132522757