3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go × ReactでSSEを実現する(Step1:配信サーバー基盤の実装)

3
Posted at

はじめに

チャットツールみたいに「何かあったらすぐ画面に反映させたい」っていうリアルタイムな仕組み、いざ作ろうとすると意外と悩みますよね。

定番なのはWebSocketですが、実は「サーバーから通知を送るだけ」ならSSE(Server-Sent Events)がめちゃくちゃ手軽で便利です。普通のHTTPの仕組みの上で動くので、インフラ周りでハマることが少ないのも嬉しいポイント。

ということで、今回から数回に分けて、GoとReactを組み合わせてSSEをフル活用する仕組みを自作していこうと思います。

こんなことを学習していきます

  • SSEとWebSocket、どっちをいつ使うべきか?
  • Goで「接続中のユーザー全員に一斉通知」を送るハブの作り方
  • ReactでSSEをスマートに受け取る方法
  • 送信はREST API、受信はSSEっていう「いいとこ取り」な設計

まずはその第一歩として、Step 1では全ての土台になる「バックエンドの配信基盤」をGoでサクッと作っていきます。


SSE実装の全体ロードマップ

  1. 【本記事】Step 1:Goで最小限のSSEサーバーを作る
  2. Step 2:Reactでシグナルの受信を実装する
  3. Step 3:GoでHub(配信所)を設計し、一斉送信を可能にする
  4. Step 4:API送信と連携し、実用的なチャット基盤を完成させる

今回のゴール:配信サーバー基盤の構築(PoC)

成果物

まずは難しいことは抜きにして、「SSEの仕組み」だけを動かしてみます。
ブラウザやターミナルからアクセスすると、接続しっぱなしの状態で、サーバーから2秒おきに「Ping!」というメッセージが降ってくるようなAPIを作ります。

sse_step1.gif

プロジェクトのディレクトリ構成

適当なディレクトリ(例: sse-project)を作って、こんな感じの構成にします。

sse-project/
├── docker-compose.yml
└── backend/
    ├── Dockerfile
    └── main.go
    (※go.mod は後でコマンドで作ります)

1. インフラの準備(Docker環境)

まずは docker-compose.yml を用意します。

# docker-compose.yml
version: "3.9"
services:
  backend:
    build: ./backend
    ports:
      - "8080:8080"
    volumes:
      - ./backend:/app

次に、backend ディレクトリの中に Dockerfile を作ります。
Go 1.26をベースに、保存するたびに自動で再起動してくれる air を入れちゃいます。

# backend/Dockerfile
FROM golang:1.26-alpine

WORKDIR /app

# ホットリロードツール「Air」をインストール
RUN go install [github.com/air-verse/air@latest](https://github.com/air-verse/air@latest)

# 起動時は air を使う
CMD ["air"]

2. GoでSSEハンドラを書く

backend/main.go を書いていきます。
Go標準の http.Flusher を使うのがコツです。これのおかげで、レスポンスを終わらせずにデータを「後出し」し続けることができます。

// backend/main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func main() {
	// SSEのエンドポイント
	http.HandleFunc("/events", sseHandler)

	fmt.Println("SSE Server is running on http://localhost:8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

func sseHandler(w http.ResponseWriter, r *http.Request) {
	// 1. SSEに必要なヘッダーをセット
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	// React(別ポート)から繋げるようにCORSを許可
	w.Header().Set("Access-Control-Allow-Origin", "*")

	// 2. http.Flusherを使えるようにする
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
		return
	}

	fmt.Println("Client connected!")

	// 2秒おきに通知を送るためのタイマー
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	// 3. クライアントが切れるまでループ!
	for {
		select {
		case <-r.Context().Done():
			// ブラウザを閉じたりした時の切断検知
			fmt.Println("Client disconnected")
			return
		case t := <-ticker.C:
			// SSEの決まり文句 "data: <中身>\n\n" で送る
			msg := fmt.Sprintf(`{"time": "%s", "message": "Ping!"}`, t.Format("15:04:05"))
			fmt.Fprintf(w, "data: %s\n\n", msg)

			// 溜め込まずにすぐ送り出す
			flusher.Flush()
		}
	}
}

3. 起動して確認してみる

手順 1: Goモジュールの初期化

cd backend
go mod init sse-project
cd ..

手順 2: コンテナ起動

docker compose up --build

これで air が立ち上がって、コードの監視が始まります。

手順 3: ターミナルで確認
別のターミナルから curl を叩いてみます。-N をつけるのを忘れずに。

curl -N http://localhost:8080/events

こんな感じで、2秒ごとにデータが降ってきたら成功です!


おわりに

今回はサーバー側から一方的に送りつける基盤ができました。
次回のStep 2では、この降ってきたデータをReactでキャッチして、画面にリアルタイムで表示させていきます。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?