golang语言开发sse(server-send event)

 服务端发布消息,推送给在线用户

1、先定义通知管道配置和消息写入管道接口

package sse


// 消息管道
type SseChannel struct {
	ChannelId   string        // 消息管道id
	ChannelChan chan string   // 消息发送的chan
}

// 存储用户与该用户的消息通道,(key->value userId->消息管道列表)
// 管道列表之所以是多个,是因为一个用户可能打开多个页面,可以建议多个管道,
// 当关闭其中一个页面时,只闭关当前页面关联消息管道,未闭关页面关联的消息管道不受影响
var Clients = make(map[string][]*SseChannel)
package sseServ

import (
	"bootpkg/cmd/api/model/vo"
	"bootpkg/common/sse"
	"encoding/json"
)

// 发送消息接口
func Broadcast(userId string, data vo.SseNotifyVo) {
	if len(sse.Clients[userId]) == 0 {
		return
	}
    
	dataBytes, _ := json.Marshal(data)
    // 遍历用户所有打开的消息管道,发送消息
	for _, client := range sse.Clients[userId] {
		client.ChannelChan <- string(dataBytes)
	}
}

2、定义服务端接口(在main方法中调用即可)

// router定义
func sseRouterConfig() {
	router := gin.Default()
	router.Use(configCors())
	router.GET("/api/sse/notify/subscribe/:channel_id", connect)
	router.POST("/api/sse/notify/send", sendMsg)
	err := router.Run(":9669")
	if err != nil {
		fmt.Println(err)
	}
}

// 配置跨域
func configCors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
		c.Header("Access-Control-Allow-Headers", "*")
		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
		c.Header("Access-Control-Allow-Credentials", "true")
		//放行所有OPTIONS方法
		if method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
		}
		// 处理请求
		c.Next()
	}
}


// 前端初始化时连接该接口
func connect(c *gin.Context) {
    // 建立连接,记录哪个用户建立了连接,并配置该用户接受消息的通道id(uuid即可)
	userId := c.DefaultQuery("user_id", "")
	channelId := c.Param("channel_id")

	c.Header("Content-Type", "text/event-stream")
	c.Header("Cache-Control", "no-cache")
	c.Header("Connection", "keep-alive")

	println("Client connected user_id=" + userId + ";channel_id=" + channelId)
	eventSseChannel := &sse.SseChannel{
		ChannelId:   channelId,
		ChannelChan: make(chan string),
	}

    // 如果用户消息管道尚未初始化,则初始化用户消息管道
	if sse.Clients[userId] == nil {
		sse.Clients[userId] = []*sse.SseChannel{}
	}

    // 将新建立的消息管道添加到用户管道列表中
	sse.Clients[userId] = append(sse.Clients[userId], eventSseChannel) // Add the client to the clients map
	defer func() {
		for _, v := range sse.Clients[userId] {
			if v != eventSseChannel {
				sse.Clients[userId] = append(sse.Clients[userId], v)
			}
		}
		close(eventSseChannel.ChannelChan)
	}()

	// Listen for client close and remove the client from the list
	notify := c.Writer.CloseNotify()
	go func() {
		<-notify
		fmt.Println("Client disconnected, user_id=" + userId + ";channel_id=" + channelId)
        
		r := make([]*sse.SseChannel)
		for _, v := range sse.Clients[userId] {
			if v.ChannelId != channelId {
				r = append(r, v)
			}else {
				// 关闭管道
				close(v.ChannelChan)
            }
		}
		sse.Clients[userId] = r
	}()

	// Continuously send data to the client
	for {
        // 从用户管道读取消息
		data := <-eventSseChannel.ChannelChan
		println("Sending data to user_id=" + userId + ";channel_id=" + channelId + "; data=" + data)
        // 将消息输出到客户端
		fmt.Fprintf(c.Writer, "data: %s\n", data)
		c.Writer.Flush()
	}
}

// 发送消息接口,
func sendMsg(c *gin.Context) {
    // 指定用户发送消息
	userId := c.DefaultQuery("user_id", "")
    
	if len(sse.Clients[userId]) == 0 {
		response.FailErrJSON(c, response.ERROR_PARAMETER, "用户未连接。")
		return
	}
	var jsonp vo.SseNotifyVo
	err := c.ShouldBind(&jsonp)
	if err != nil {
		response.FailErrJSON(c, response.ERROR_PARAMETER, err.Error())
		return
	}
    // 消息写入消息管道
	sseServ.Broadcast(userId, jsonp)
	response.SuccessJSON(c, "Success")
}

测试:

建立连接,打开1~3个terminal,建立消息管道
curl 'http://localhost:9669/api/sse/notify/subscribe/87761706050125845?user_id=111'
curl 'http://localhost:9669/api/sse/notify/subscribe/87761706050125847?user_id=111'
curl 'http://localhost:9669/api/sse/notify/subscribe/87761706050125848?user_id=111'

消息发送测试curl
curl --location 'http://localhost:9669/api/sse/notify/send?user_id=111' \
--header 'Accept: application/json, text/plain, */*' \
--header 'Cache-Control: no-cache' \
--header 'ClientType: h5' \
--header 'Connection: keep-alive' \
--header 'GP-TM: 1718083388' \
--header 'Pragma: no-cache' \
--header 'Sec-Fetch-Dest: empty' \
--header 'Sec-Fetch-Mode: cors' \
--header 'Sec-Fetch-Site: same-origin' \
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' \
--header 'language: zh' \
--header 'merchantCode: XYYL' \
--header 'sec-ch-ua: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'Content-Type: application/json' \
--data '{
    "id": "123",
    "title": "title123",
    "content": "content123",
    "create_time": "2024-06-12 09:26:34"
}'

可以看到两个terminal都会有输出
关闭一个terminal,并没有将用户的所有管道给关闭,仅关闭terminal关联的chennal。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值