go 实现 sse推送

482 阅读2分钟

关于服务端和客户端即时消息我所知道的方式有三种:轮询、websocket和SSE,去百度很多文章都是或多或少借用一些框架实现并且有些模凌两可,这篇文章主要记录一下用go不借助第三方库实现一个简单的SSE服务端推送。

SSE通常来说,一个网页获取新的数据通常需要发送一个请求到服务器,也就是向服务器请求的页面。使用服务器发送事件,服务器可以随时向我们的 Web 页面推送数据和信息。这些被推送进来的信息可以在这个页面上以 事件 + 数据 的形式来处理。

具体查看SSE介绍可以查看这篇文章,这里就不重复赘述了,文章地址

  • 服务端代码,中间想完善一些功能,代码还没添加,可以当demo
import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
)
type event struct {
    Timestamp int64  `json:"timestamp"`
    Message   string `json:"message"`
}

func main() {
    broker := NewBroker()
    go func() {
       for {
          time.Sleep(time.Second * 2)
          now := time.Now()
          evt := event{
             Timestamp: now.Unix(),
             Message:   fmt.Sprintf("Hello at %s", now.Format("2006/01/02 15:04:05")),
          }
          evtBytes, err := json.Marshal(evt)
          if err != nil {
             log.Println(err)
             continue
          }
          broker.Notifier <- evtBytes
       }
    }()

    time.LoadLocation("Asia/Shanghai")
    http.HandleFunc("/", broker.ServeHTTP)
    http.ListenAndServe(":8080", nil)
}
type Broker struct {
 Notifier chan []byte
 newClients chan chan []byte
 closingClients chan chan []byte
 clients map[chan []byte]bool
}

func (b *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request){
w.Header().Add("Content-Type", "text/event-stream")
w.Header().Add("Cache-Control", "no-cache")
w.Header().Add("Connection", "keep-alive")
w.Header().Add("Access-Control-Allow-Origin", "*")

messageChan := make(chan []byte)
messageChan <- []byte(r.RemoteAddr)
b.newClients <- messageChan
ctx := r.Context()

/// 这是之前使用 notifier来监听连接断开,发现该接口已经弃用了(未删除,还可以正常使用,但不建议),新文档建议使用context
// notify := w.(http.CloseNotifier).CloseNotify()
//go func(req chan []byte) {
    // if res := <-notify; res {
    //     // req断开连接
    //     b.closingClients <- req
    // }
//}(messageChan)

for {
    select {
    case <-ctx.Done():
       b.closingClients <- messageChan
       fmt.Println("客户端断开连接")
    default:
       fmt.Fprintf(w, "data: %s\n\n", <-messageChan)
       w.(http.Flusher).Flush()
    }
}
}

func NewBroker() *Broker {
    b := &Broker{
       Notifier:       make(chan []byte, 1),
       newClients:     make(chan chan []byte),
       closingClients: make(chan chan []byte),
       clients:        make(map[chan []byte]bool),
    }
    go b.listen()
    return b
}

func (b *Broker) listen() {
    for {
       select {
       case s := <-b.newClients:
          b.clients[s] = true
          log.Printf("新客户端连接+1,当前客户端数量:%d \n", len(b.clients))
       case s := <-b.closingClients:
          delete(b.clients, s)
          log.Printf("客户端离线,当前客户端数量:%d \n", len(b.clients))
       case event := <-b.Notifier:
          for clientMessageChan := range b.clients {
             clientMessageChan <- event
          }
       }
    }
}
  • 客户端html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SSe</title>
</head>
<body>
<div id="message"></div>
  <script>
const messageElement = document.getElementById('message')
    const eventSource = new EventSource("http://localhost:8080/")
eventSource.onopen = () => {
        messageElement.innerHTML += `SSE 连接成功,状态${eventSource.readyState}<br />`
      }

      eventSource.onerror = () => {
        messageElement.innerHTML += `SSE 连接错误,状态${eventSource.readyState}<br />`
      }

eventSource.onmessage = msg => {
  messageElement.innerHTML += `${msg.data} <br />`
}

  </script>
</body>
</html>

参考文档