🦔
【Go】echoを使ってPOSTで受け取ったデータをSSEで別クライアントに返す
はじめに
Webブラウザではないクライアントが画像を受け取ってその画像をブラウザに表示するという実装が必要になり、SSEを使って設計/実装のイメージを備忘録に残していきます(gRPCなどこれより良いアプローチもあるとは思いますが...)
具体的にはどういうことをやるかというと
- SSEでブラウザに画像を返却するエンドポイントを用意する
- 画像データのbase64文字列を受け取るエンドポイントを用意する
- 2のエンドポイントで画像データの受信されたら1のエンドポイントが検知してブラウザに返す
これをゴールにして実装していきます
前提
- Go 1.24
- echo 4.13.4
データ受け渡し用Channelの実装
エンドポイントの実装前に受信した画像データをSSEなエンドポイントに渡すHubとなる実装が必要です
これを実現するために今回はchannelを採用します
image_hub.go
package main
import (
"log"
"sync"
)
type ImageHub struct {
client chan string
mutex sync.Mutex
}
func NewImageHub() *ImageHub {
return &ImageHub{
client: nil,
}
}
func (h *ImageHub) AddClient() chan string {
h.mutex.Lock()
defer h.mutex.Unlock()
h.client = make(chan string, 1)
return h.client
}
func (h *ImageHub) RemoveClient(client chan string) {
h.mutex.Lock()
defer h.mutex.Unlock()
if h.client != nil {
h.client = nil
close(client)
}
}
func (h *ImageHub) AcceptImage(imageSrc string) {
h.mutex.Lock()
defer h.mutex.Unlock()
select {
case h.client <- imageSrc:
default:
log.Println("受信可能なchannelがないため画像は受信されません")
}
}
画像データを受け取るエンドポイントの実装
それではPOSTで画像データを受け取ってImageHubに渡すエンドポイントを実装します
bodyのbase64文字列を受け取ってImageHubのchannelに送信する処理を行います
main.go
package main
func main() {
e := echo.New()
hub := NewImageHub()
e.Use(middleware.CORS())
// health check
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "status ok!")
})
e.POST("/receive-image", func(c echo.Context) error { return receiveImage(c, hub) })
log.Fatal(e.Start(":8081"))
}
type SendImageBody struct {
ImageSrc string `json:"imageSrc"`
}
func receiveImage(c echo.Context, hub *ImageHub) (err error) {
body := new(SendImageBody)
if err = c.Bind(body); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
_ = loggerOs.Info("Received image!")
hub.AcceptImage(body.ImageSrc)
return c.JSON(http.StatusOK, map[string]string{
"message": "Images received and accept successfully",
})
}
画像データをSSEでクライアントに返却するエンドポイントの実装
最後にImageHubから画像データを受信しSSEでクライアントに画像を返却するエンドポイントを実装します
echoにおけるSSEの実装は公式ドキュメントがとても参考になりました!
main.go
package main
func main() {
// omit
e.POST("/receive-image", func(c echo.Context) error { return receiveImage(c, hub) })
e.GET("/sse-image", func(c echo.Context) error { return monitorImage(c, hub) })
log.Fatal(e.Start(":8081"))
}
func monitorImage(c echo.Context, hub *ImageHub) (err error) {
w := c.Response()
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
client := hub.AddClient()
defer hub.RemoveClient(client)
for {
select {
case <-c.Request().Context().Done():
_ = loggerOs.Infof("SSE client disconnected, ip: %v", c.RealIP())
return nil
case imageSrc, ok := <-client:
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "client channel closed")
}
log.Println("find accepted image")
event := Event{
Data: []byte(imageSrc),
}
if err := event.MarshalTo(w); err != nil {
return err
}
w.Flush()
}
}
}
Event
という構造体は公式ドキュメントのそのままを持ってきています
クライアント側の実装
あとはブラウザ側でSSEを受け取る実装をReactで行います(動作確認だけのためなので雑です...)
import { useEffect, useState } from "react"
export default function SseImagePage() {
const [imageSrc, setImageSrc] = useState<string | null>(null)
useEffect(() => {
const es = new EventSource("http://localhost:8081/sse-image")
es.onmessage = (event) => {
console.log(event)
setImageSrc(event.data)
}
es.onerror = (error) => {
console.error(error)
}
return () => {
es.close()
}
}, [])
return (
<img src={imageSrc} alt="sample_image" width={360} />
)
}
動作確認
curlなどで/receive-image
に対してHTTPリクエストを送ります
curl -X POST http://localhost:8081/receive-image -H 'Content-type: application/json' -d '{"imageSrc": "data:image/png;base64,...."}'
するとブラウザにその画像が表示されるようになります
まとめ
別同士のクライアントでデータのやりとり&SSEも使いたいという多少複雑な条件の実装が達成できたのは少し感動しました
ニッチな要件かもしれませんが、誰かの役に立てば嬉しいです!
参考資料
Discussion