虽然网上的都是用sse实现将实时消息流不间断的推给前端,但是sse也可以模拟websocket进行突发的消息通知,而不是一直读取数据并返回数据。即服务端保存所有的连接对象,前端管理界面发送正常的http请求,在后端遍历所有的连接对象,将消息广播。就可以实现一种类似双向通讯的形式了。
代码参考了Server-side Events (SSE) : A deep dive into client-server architecture | Implementation in Golang,在这基础上实现了房间机制,房间ID由前端生成并传递,鉴权机制请自行通过token+中间件等形式实现。
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// 房间号为key,client数组为value
var clients = make(map[string][]chan string)
// 广播房间内的所有用户
func broadcast(roomID string, data string) {
for _, client := range clients[roomID] {
client <- data
}
}
//配置跨域
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) {
roomID := c.Param("id")
// Set the response header to indicate SSE content type
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
// Create a channel to send events to the client
println("Client connected")
eventChan := make(chan string)
if clients[roomID] == nil {
clients[roomID] = []chan string{}
}
clients[roomID] = append(clients[roomID], eventChan) // Add the client to the clients map
defer func() {
// 删除该房间的该用户,按值删除数组元素
for _, v := range clients[roomID] {
if v != eventChan {
clients[roomID] = append(clients[roomID], v)
}
}
close(eventChan)
}()
// Listen for client close and remove the client from the list
notify := c.Writer.CloseNotify()
go func() {
<-notify
fmt.Println("Client disconnected")
}()
// Continuously send data to the client
for {
data := <-eventChan
println("Sending data to client", data)
fmt.Fprintf(c.Writer, "data: %s\n\n", data)
c.Writer.Flush()
}
}
// 发送消息接口
func sendMsg(c *gin.Context) {
// data := c.PostForm("data")
roomID := c.Param("id")
data := c.DefaultQuery("name", "urlyy")
// print data to console
println("Data received from client :", data)
broadcast(roomID, data)
c.JSON(http.StatusOK, gin.H{"message": "Data sent to clients"})
}
func main() {
router := gin.Default()
router.Use(configCors())
// SSE endpoint that the clients will be listening to
router.GET("/sse/:id", connect)
// Handle POST request
router.GET("/send/:id", sendMsg)
// Start the server
err := router.Run(":6666")
if err != nil {
fmt.Println(err)
}
}
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Client</title>
</head>
<body>
<h1>SSE Client</h1>
<div id="sse-data"></div>
<script>
const sseDataElement = document.getElementById("sse-data");
// Create an EventSource to listen to the /sse endpoint
// 注意这里多加了一个路径属性,就是房间ID
// 测试时可以另建一个html文件,将它的房间ID更改成不一样的
const eventSource = new EventSource("http://localhost:6666/sse/1");
// Event listener for messages received from the server
eventSource.onmessage = function (event) {
const data = event.data;
appendDataToDiv(data);
};
// Event listener for SSE errors
eventSource.onerror = function (event) {
console.error("SSE Error:", event);
};
// Function to append data to the SSE data div
function appendDataToDiv(data) {
const p = document.createElement("p");
p.textContent = data;
sseDataElement.appendChild(p);
}
</script>
</body>
</html>
发送消息的接口
http://127.0.0.1:8587/send/1?name=1234
,name不传则默认为urlyy。调用该接口会将消息1234发给1号房间的所有用户