🦔

【Go】echoを使ってPOSTで受け取ったデータをSSEで別クライアントに返す

に公開

はじめに

Webブラウザではないクライアントが画像を受け取ってその画像をブラウザに表示するという実装が必要になり、SSEを使って設計/実装のイメージを備忘録に残していきます(gRPCなどこれより良いアプローチもあるとは思いますが...)

具体的にはどういうことをやるかというと

  1. SSEでブラウザに画像を返却するエンドポイントを用意する
  2. 画像データのbase64文字列を受け取るエンドポイントを用意する
  3. 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の実装は公式ドキュメントがとても参考になりました!
https://echo.labstack.com/docs/cookbook/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も使いたいという多少複雑な条件の実装が達成できたのは少し感動しました
ニッチな要件かもしれませんが、誰かの役に立てば嬉しいです!

参考資料

https://echo.labstack.com/docs/cookbook/sse

GitHubで編集を提案

Discussion