🌐

React + GoでWebsocketを使ってみた

2025/02/03に公開

Gorilla WebSocketなるものを使う

Go言語の学習を最近しているのですが何を作れるのかまだよくわかっていない?
CLIツール、Web APIの開発で作れるのは知ってるけどDBに保存する機能を作ってみたり、Gemini APIを使用してAIに質問する機能を作ってみたぐらい。

Webアプリでも作ってみようと思い単純なものですが、Websocketを使用して、ReactとGoで作ってみました。

最初別のものを作っていたのでプロジェクト名に違和感ありますがお気になさらず😅
git cloneして遊んでみてください。

GoのAPI

Chat画面のReact

フロントエンドの方は標準機能を使えばライブラリを使用しなくもWebSocket使えるの見つけました。知らなかった☎️

https://developer.mozilla.org/ja/docs/Web/API/WebSocket

Goの方はライブラリが必要なのでGorilla WebSocketを追加します。
https://pkg.go.dev/github.com/gorilla/websocket@v1.5.3#section-readme

Go WebSocket実装ガイド

今回はGorilla WebSocketにフォーカスするのでフロントエンドのCSSとかコードについて説明は割愛します。完品のロジックやCSSを参考までにみてください。

概要

このリポジトリは、GoでWebSocketを実装する方法を解説したサンプルコードです。WebSocketを使用することで、サーバーとクライアント間でリアルタイムな双方向通信が可能になります。

使用技術

インストール

# Gorilla WebSocketのインストール
go get github.com/gorilla/websocket

主要コンポーネント

Hub

クライアント接続を管理し、メッセージのブロードキャストを行う中央ハブです。

type Hub struct {
    clients    map[*Client]bool    // 接続クライアントの管理
    broadcast  chan []byte         // ブロードキャストメッセージ
    register   chan *Client        // クライアント登録
    unregister chan *Client        // クライアント登録解除
}

Client

個々のWebSocket接続を表します。

type Client struct {
    hub  *Hub               // 所属するHub
    conn *websocket.Conn    // WebSocket接続
    send chan []byte        // メッセージ送信用バッファ
}

主要な機能

1. コネクション管理

  • クライアントの登録/登録解除
  • 接続状態の監視
  • 切断検知

2. メッセージング

  • ブロードキャスト配信
  • ping/pongによる接続維持
  • エラーハンドリング

設定パラメータ

const (
    writeWait      = 10 * time.Second    // 書き込みタイムアウト
    pongWait       = 60 * time.Second    // pong待機時間
    pingPeriod     = (pongWait * 9) / 10 // ping送信間隔
    maxMessageSize = 512                  // 最大メッセージサイズ
)

使用方法

  1. サーバーの起動:
func main() {
    hub := newHub()
    go hub.run()
    
    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        serveWs(hub, w, r)
    })
    
    log.Fatal(http.ListenAndServe(":8080", nil))
}
  1. WebSocket接続のエンドポイント: ws://localhost:8080/ws

全体のコード

package main

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

	"github.com/gorilla/websocket"
)

type Client struct {
	hub  *Hub
	conn *websocket.Conn
	send chan []byte
}

type Hub struct {
	clients    map[*Client]bool
	broadcast  chan []byte
	register   chan *Client
	unregister chan *Client
}

const (
	writeWait      = 10 * time.Second
	pongWait       = 60 * time.Second
	pingPeriod     = (pongWait * 9) / 10
	maxMessageSize = 512
)

func newHub() *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
	}
}

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true // 開発環境用の設定
	},
}

func (h *Hub) run() {
	for {
		select {
		case client := <-h.register:
			h.clients[client] = true
		case client := <-h.unregister:
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
		case message := <-h.broadcast:
			for client := range h.clients {
				select {
				case client.send <- message:
				default:
					close(client.send)
					delete(h.clients, client)
				}
			}
		}
	}
}

func (c *Client) readPump() {
	defer func() {
		c.hub.unregister <- c
		c.conn.Close()
	}()
	c.conn.SetReadLimit(maxMessageSize)
	c.conn.SetReadDeadline(time.Now().Add(pongWait))
	c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
	for {
		_, message, err := c.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				log.Printf("error: %v", err)
			}
			break
		}
		c.hub.broadcast <- message
	}
}

func (c *Client) writePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop()
		c.conn.Close()
	}()
	for {
		select {
		case message, ok := <-c.send:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				c.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}

			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)
			if err := w.Close(); err != nil {
				return
			}
		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
	client.hub.register <- client

	go client.writePump()
	go client.readPump()
}

func main() {
	hub := newHub()
	go hub.run()

	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		serveWs(hub, w, r)
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}

まとめ

以前もWebsocketの記事を書いたのですが今回は、goroutineとチャネルの使ったアプリケーション作るの体験してみたいと思いデモアプリを作ってみました。本だとprintln書いて終わりなので物足りなくて。

Discussion