実装パターンについて
- Echo:双方向通信の基礎(1対1)
送信したメッセージをそのまま送信元のクライアントに返します。 - Chat:複数接続への配信(多対多)
多対多の通信モデルで複数のクライアント間でメッセージを共有します。
まずはEchoで双方向ストリームの型と挙動を掴みます。
次にChatに進み複数接続へブロードキャストする実装をしてみます。
0. 事前準備
- Goプロジェクトを初期化しておきます。
go mod init goa-test - goa v3系を前提としています。
1. Echoの実装
-
DSLでWebSocket前提の双方向ストリームを定義します。
design.gopackage design var _ = API("goa-test", func() { Title("Goa Test API") }) var _ = Service("ws", func() { Description("WebSocketサービス") HTTP(func() { Path("/ws") }) Method("echo", func() { Description("クライアントから受信したメッセージをそのまま返す") // 受信 StreamingPayload(func() { Attribute("message", String) Required("message") }) // 送信 StreamingResult(func() { Attribute("message", String) Required("message") }) HTTP(func() { // WebSocketは必ずGETでUpgrade GET("/echo") }) }) }) -
go mod tidyで 依存関係を解決してから、goa gen goa-test/design/goa example goa-test/designを実行して、gen/ws/...とcmd/goa_test/...を生成します。 -
生成された
cmd/goa_test/http.go内のupgraderを編集します。cmd/.../http.go- upgrader := &websocket.Upgrader{} + upgrader := &websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + if dbg { // --debug=true なら許可 + return true + } + switch r.Header.Get("Origin") { + // 本番では許可するオリジンを定義します + case "https://your-frontend.example.com": + return true + default: + return false + } + }, + } -
Echoロジックの実装
ポイント-
Recv()で1件受信 → そのままSend()で返す。 - 受信側が切断すれば
Recv()はエラーで落ちるのでループを抜けて終了。
ws.gofunc (s *wssrvc) Echo(ctx context.Context, stream ws.EchoServerStream) error { log.Printf(ctx, "ws.echo") for { in, err := stream.Recv() // client -> server if err != nil { return err } out := &ws.EchoResult{Message: fmt.Sprintf("echo: %s", in.Message)} if err := stream.Send(out); err != nil { // server -> client return err } } } -
-
APIを起動
go run goa-test/cmd/goa_test --http-port=8080 --debug=true -
動作検証用のHTMLを作成し、Live Serverなどで起動
<!DOCTYPE html> <html> <head> <title>Chat</title> </head> <body> <h1>WebSocket Chat</h1> <form action="" onsubmit="sendMessage(event)"> <input type="text" id="senderName" autocomplete="off" placeholder="Your Name" value="USER1" /> <textarea id="messageText" autocomplete="off"></textarea> <button>Send</button> </form> <ul id='messages'> </ul> <script> const ws = new WebSocket("ws://localhost:8080/ws/echo"); ws.onmessage = (event) => { const messages = document.getElementById('messages') const message = document.createElement('li') const content = document.createTextNode(event.data) message.appendChild(content) messages.appendChild(message) }; const sendMessage = (event) => { const input = document.getElementById("messageText") const sender = document.getElementById("senderName") ws.send(JSON.stringify({ message: input.value, sender: sender.value })) input.value = '' event.preventDefault() } </script> </body> </html>今回はAPIの実装に焦点をあてるためシンプルなHTMLを使用します。
https://fastapi.tiangolo.com/ja/advanced/websockets/#_1 から拝借しました。 -
ブラウザを開き動作確認

送信と同時にAPIが応答します。
しかしこのままでは各クライアントは独立したままなので、他クライアントとのメッセージの共有はできません。
次に、Chatメソッドを新たに定義し、多対多でのメッセージの共有ができるようにしたいと思います。
2. chat(複数接続に配信)の実装
-
DSLでChatメソッドを定義します。
design.goMethod("chat", func() { Description("チャットサービス。受信したメッセージをすべてのクライアントに配信する。") StreamingPayload(func() { Attribute("message", String, "client→server") Attribute("sender", String, "送信者名(クライアントが指定)") Required("message", "sender") }) StreamingResult(func() { Attribute("message", String, "server→client") Attribute("sender", String, "送信者名") Attribute("timestamp", String, "送信時刻", func() { Format(FormatDateTime) }) Required("message", "sender", "timestamp") }) HTTP(func() { GET("/chat") }) }) -
goa gen goa-test/designを実行して、コードを生成します。
cmd/goa_test/...は Echo で生成済み。 -
cmd/.../http.goの upgrader は Echo でやった通り。 -
Chatロジックの実装
ws.gotype wssrvc struct { hub *hub } // NewWs returns the ws service implementation. func NewWs() ws.Service { return &wssrvc{hub: newHub()} } // 接続している一人のクライアントを表す type client struct { stream ws.ChatServerStream // サーバ→クライアント送信用ストリーム send chan *ws.ChatResult // サーバ→クライアント送信用チャネル } // 全クライアントを管理する type hub struct { mu sync.RWMutex // clients への同時アクセスを防止するためのロック clients map[*client]struct{} // 今サーバーに接続している全員 } func newHub() *hub { return &hub{clients: make(map[*client]struct{})} } // クライアント追加 func (h *hub) add(c *client) { h.mu.Lock() h.clients[c] = struct{}{} // マップにクライアントを追加 h.mu.Unlock() } // クライアント削除 func (h *hub) remove(c *client) { h.mu.Lock() delete(h.clients, c) // マップからクライアントを削除 h.mu.Unlock() close(c.send) } // 全クライアントにメッセージ配信 func (h *hub) broadcast(msg *ws.ChatResult) { h.mu.RLock() defer h.mu.RUnlock() for c := range h.clients { select { case c.send <- msg: // メッセージを送信チャネルに送る default: // 詰まり対策: 送信バッファが詰まっていたら切断 go h.remove(c) } } } // チャットサービス。受信したメッセージをすべてのクライアントに配信する。 func (s *wssrvc) Chat(ctx context.Context, stream ws.ChatServerStream) (err error) { log.Printf(ctx, "ws.chat") // クライアントを生成してHubに登録 c := &client{ stream: stream, send: make(chan *ws.ChatResult, 16), } s.hub.add(c) defer s.hub.remove(c) // 送信用ゴルーチン go func() { for msg := range c.send { // ここでチャネル(chan)からのメッセージを待つ チャネルは他のゴルーチンから送られてくるデータを順に受け取る通路 // メッセージ送信 if err := c.stream.Send(msg); err != nil { return } } }() // 受信→全員へ配信 for { // Recv はクライアントからの1メッセージを受信する in, err := stream.Recv() if err != nil { // クライアント切断やctx.Done()でここに来る return err } msg := &ws.ChatResult{ Message: in.Message, Sender: in.Sender, Timestamp: time.Now().Format(time.RFC3339), } s.hub.broadcast(msg) } } -
HTMLのwebsocketのURLをchatへ向けます
- const ws = new WebSocket("ws://localhost:8080/ws/echo"); + const ws = new WebSocket("ws://localhost:8080/ws/chat");
参考にしたサイト
https://zenn.dev/takehiro1111/articles/go_web_socket
https://fastapi.tiangolo.com/ja/advanced/websockets/
