SSE与WebSocket分别实现服务器发送消息通知(Golang、Gin)

前言

服务端推送,也称为消息推送或通知推送,是一种允许应用服务器主动将信息发送到客户端的能力,为客户端提供了实时的信息更新和通知,增强了用户体验。

服务端推送的背景与需求主要基于以下几个诉求:

  • 实时通知:在很多情况下,用户期望实时接收到应用的通知,如新消息提醒、商品活动提醒等。
  • 节省资源:如果没有服务端推送,客户端需要通过轮询的方式来获取新信息,会造成客户端、服务端的资源损耗。通过服务端推送,客户端只需要在收到通知时做出响应,大大减少了资源的消耗。
  • 增强用户体验:通过服务端推送,应用可以针对特定用户或用户群发送有针对性的内容,如优惠活动、个性化推荐等。这有助于提高用户对应用的满意度和黏性。

一、解决方案:

1、传统实时处理方案:

  • 轮询:这是一种较为传统的方式,客户端会定时地向服务端发送请求,询问是否有新数据。服务端只需要检查数据状态,然后将结果返回给客户端。轮询的优点是实现简单,兼容性好;缺点是可能产生较大的延迟,且对服务端资源消耗较高。

  • 长轮询(Long Polling):轮询的改进版。客户端向服务器发送请求,服务器收到请求后,如果有新的数据,立即返回给客户端;如果没有新数据,服务器会等待一定时间(比如30秒超时时间),在这段时间内,如果有新数据,就返回给客户端,否则返回空数据。客户端处理完服务器返回的响应后,再次发起新的请求,如此反复。长轮询相较于传统的轮询方式减少了请求次数,但仍然存在一定的延迟。

2、HTML5 标准引入的实时处理方案:

  • WebSocket:一种双向通信协议,同时支持服务端和客户端之间的实时交互。WebSocket 是基于 TCP 的长连接,和HTTP 协议相比,它能实现轻量级的、低延迟的数据传输,非常适合实时通信场景,主要用于交互性强的双向通信。

  • SSE:SSE(Server-Sent Events)是一种基于 HTTP 协议的推送技术。服务端可以使用 SSE 来向客户端推送数据,但客户端不能通过SSE向服务端发送数据。相较于 WebSocket,SSE 更简单、更轻量级,但只能实现单向通信。

两者的主要区别:

SSE WebSocket
通信 单向通信 双向通信
协议 HTTP WebSocket
自动重连 支持 不支持,需要客服端自行支持
数据格式 文本格式 二进制数据、文本格式
浏览器支持 大部分支持 主流浏览器支持较好
跨域 不支持(若跨域需配置指定的Access-Control-Allow-Origin) 支持

3、第三方推送:

常见的有操作系统提供相应的推送服务,如苹果的APNs(Apple Push Notification service)、谷歌的FCM(Firebase Cloud Messaging)等。同时,也有一些跨平台的推送服务,如个推、极光推送、友盟推送等,帮助开发者在不同平台上实现统一的推送功能。

二、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、服务端:

服务端这里我们编写了两个接口,第一个接口用于建立sse连接,第二个接口用于触发通知,发送消息。

	r.GET("/notification/socket-connection", handler.SocketConnection)
	r.GET("/notification/export-excel", handler.ExportExcel)
(1)建立sse连接

建立了一个map集合,建立连接后生成一个唯一key存入map集合中,然后持续读取当前key的值,一有消息就发送给客户端。
用closeNotify 监听客户端状态,刷新或者关闭页面才会触发,删除唯一key,防止内存溢出。

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()
	}
}
(二)发送通知

将消息封装在NotificationLog 对象中转为JSON格式(内容可以根据自己需要修改),通过userEmail 关键字在map找到,将消息存入map中,完成消息发送。

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)
		}
	}
}
(三)效果展示

进入html页面:
在这里插入图片描述
调用发送通知接口http://localhost:8080/notification/export-excel发送通知,页面实时展示:
在这里插入图片描述

(四)不支持跨域

我在测试过程中发现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())

访问成功:
在这里插入图片描述

我认为不能跨域访问在现在普遍前后端分离的情况下算是一个非常大的缺点。

三、WebSocket

WebSocket是HTML5下一种新的协议,是基于TCP的应用层协议,只需要一次连接,便可以实现全双工通信,客户端和服务端可以相互主动发送消息。客户端进行监听,并对响应的消息处理显示。

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网站进行测试http://www.jsons.cn/websocket/
在这里插入图片描述
这里简单的测试了一下,基本功能没问题,是全双工通信,客户端与服务端可以相互发送消息。

四、总结

SSE 技术是一种轻量级的实时推送技术,具有使用简单、支持自动重连等特点,使得其在实时消息推送、股票交易等场景下广泛使用。另外,SSE 相对于 WebSocket 更加轻量级,如果需求场景不需要交互式动作、不需要跨域,那么 SSE 是一个不错的选择。
而WebSocket功能更强大,但相对资源消耗会更多。

猜你喜欢

转载自blog.csdn.net/qq_51110841/article/details/132522757