💬

【入門】Goでwebsocketを使ったリアルタイムなチャットを実装してみた

2024/01/27に公開

どうも👋

僕は普段webサーバーのエンジニアをやっています。
皆さんは普段リアルタイム通信を実装する機会ってありますか? 僕はないです。

意外とリアルタイムじゃなきゃいけない要件ってあんまりない気がするんですよね。あったとしてもFirebaseとかで簡単に済ませてしまうみたいな。
なので勉強も兼ねてwebsocketを用いたリアルタイム通信を今回は実装してみたいと思います。

作るものとしては簡易的なチャットアプリになります。

Websocketってなに?

Websocketとはサーバーとクライアントで双方向に通信を行うプロトコルで、通常のHTTP通信のようにリクエスト→レスポンスの順番にとらわれず、クライアントとサーバーでお互いに情報を送り合うことができます。
Websocketでは一度サーバーとクライアントでコネクションを作成したあと相互通信が可能になり、どちらかが切断されると通信が終了します。

技術構成

  • Go v1.21.5
  • Gin v1.9.1
  • gorilla/websocket v1.5.1

websocketに関するライブラリとして、簡単に調べた限り一番スター数が多かったgorilla/websocketを使用します。他候補としてはmelodyなどがあります。

実装

ファイル構成は以下のようになっています。go mod initなどで事前に作っておきましょう。

root/
 ├ go.mod
 ├ go.sum
 ├ index.html
 └ main.go

早速ファイルの中身を見ていきましょう。
go.mod, go.sumに関しては省略させていただきます。

以下htmlのテンプレートになります。
htmlの内容としてはgoのwebsocketライブラリであるmelodyのサンプルをそのまま使わせていただきました。
まぁ今回使っているのはmelodyでなくgorilla/websocketですが...😅

index.html
<html>
  <head>
    <title>Chat powered by Melody</title>
  </head>

  <style>
    #chat {
      text-align: left;
      color:#ffffff;
      background: #113131;
      width: 400px;
      min-height: 300px;
      padding: 10px;
      font-family: 'Lucida Grande', 'Hiragino Kaku Gothic ProN', 'ヒラギノ角ゴ ProN W3', 'Meiryo', 'メイリオ', sans-serif;
      font-size: small;
    }
  </style>

  <body>

    <center>
      <h3>Sample Chat</h3>
      <pre id="chat"></pre>
      <label id="title"></label>
      <input placeholder="say something" id="text" type="text">
    </center>

    <script>
      // websocketコネクション作成
      var ws = new WebSocket("ws://" + window.location.host + "/ws");

      // 適当なユーザー名を定義
      var name = "Guest-" + Math.floor(Math.random() * 1000);
      var chat = document.getElementById("chat");
      document.getElementById("title").innerText = name + ": ";

      // 現在時間を取得
      var now = function () {
        return new Date().toLocaleString();
      };

      // サーバーから他ユーザーのメッセージが送られてきた際に、チャット画面へ表示させる。
      ws.onmessage = function (msg) {
        console.log(msg);
        var line =  now() + " : " + msg.data + "\n";
        chat.innerText += line;
      };

      // 入力フォームにテキストを入力してEnterキーが押された時、入力内容をサーバーへ送信する。
      var text = document.getElementById("text");
      text.onkeydown = function (e) {
        if (e.keyCode === 13 && text.value !== "") {
          ws.send("[" + name + "] > " + text.value);
          text.value = "";
        }
      };
    </script>

  </body>
</html>

続いてmain.goの内容です。

main.go
package main

import (
  "log"
  "net/http"

  "github.com/gin-gonic/gin"
  "github.com/gorilla/websocket"
)

var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan Message)

type Message struct {
  Type    int
  Message []byte
}

func main() {
  r := gin.Default()

  // websocketのupgraderを定期
  wsupgrader := websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
  }

  // TOPページ
  r.GET("/", func(ctx *gin.Context) {
    http.ServeFile(ctx.Writer, ctx.Request, "index.html")
  })

  r.GET("/ws", func(ctx *gin.Context) {
    // upgraderを呼び出すことで通常のhttp通信からwebsocketへupgrade
    // コネクションを作成する
    conn, err := wsupgrader.Upgrade(ctx.Writer, ctx.Request, nil)
    if err != nil {
      log.Printf("Failed to set websocket upgrade: %+v\n", err)
      return
    }

    // コネクションをclientsマップへ追加
    clients[conn] = true

    // 無限ループさせることでクライアントからのメッセージを受け付けられる状態にする
    // クライアントとのコネクションが切れた場合はReadMessage()関数からエラーが返る
    for {
      t, msg, err := conn.ReadMessage()
      if err != nil {
        log.Printf("ReadMessage Error. ERROR: %+v\n", err)
        break
      }
      // 受け取ったメッセージをbroadcastを通じてhandleMessages()関数へ渡す
      broadcast <- Message{Type: t, Message: msg}
    }
  })

  // 非同期でhandleMessagesを実行
  go handleMessages()

  r.Run(":4001")
}

// broadcastにメッセージがあれば、clientsに格納されている全てのコネクションへ送信する
func handleMessages() {
  for {
    message := <-broadcast
    for client := range clients {
      err := client.WriteMessage(message.Type, message.Message)
      if err != nil {
        log.Printf("error: %v", err)
        client.Close()
        delete(clients, client)
      }
    }
  }
}

ファイルが用意できたら以下のコマンドで実行できます。

go mod tidy # 初回のみ
go run main.go

試しに2つのウィンドウでlocalhostを立ち上げると以下のような形になるかと思います!

以上で簡易的なチャットアプリが完成しました🎉

あとがき

普段websocketどころかgoroutineもあまり使わなかったので、今回調べて実装してみてとても勉強になりました。
次回は最近話題のHTMX化にも挑戦してみたいと思います!💪

Discussion