はじめに
使わない日はないくらい仕事でもプライベートでも、チャットサービスが日常的に使われていますが、チャット機能を実装したことがなかったので、
今回はGo(1.17)でWebSocketを使いチャットサーバーを構築していきます。
なぜGoを使うかと言うと、最近仕事で使っていて、私の中でブームなのと、
そして何よりマスコットキャラクターのGopherがかわいいからです(笑)
The Go gopher was designed by Renée French.
余談ですが、2022/2リリース予定のバージョン1.18ではgo get
のバイナリインストールの機能が削除されるため、
バージョンアップの際は手順書やスクリプトを修正する必要があります。皆さんお気をつけください。
システムイメージ
クライアントがチャットサーバーにWebSocketで接続し、そのコネクションをgoroutineでハンドリングし、メッセージの送信を受け付けます。
送信されたメッセージはchannel型の変数に送り、別のgoroutineでメッセージを拾い、それをすべてのクライアントに送信します。
goroutineは、Goのランタイムに管理される軽量なスレッドで、並行処理を可能にします。
channelは、送受信する値の型を定義することで、goroutine間で値を送受信できるようにします。
以下のリンクからそれぞれ動きを確認できます。
Goroutines
Channels
実装
さっそくチャットサーバーの実装を進めていきます。
WebSocketを利用するために、今回はWebツールキットのgorilla
を使います。🦍
まずはWebSocket、ブロードキャストするためのchannel宣言と、チャットメッセージ用に構造体を宣言します。
// 接続されるクライアント
var clients = make(map[*websocket.Conn]bool)
// メッセージブロードキャストチャネル
var broadcast = make(chan Message)
// アップグレーダ
var upgrader = websocket.Upgrader{}
// メッセージ用構造体
type Message struct {
Username string `json:"username"`
Message string `json:"message"`
}
main関数では、静的ファイルを参照するファイルサーバーとWebSocketのルーティングを行います。
※今回は割愛しますが、ファイルサーバー(public配下)にフロントで動かすHTML,JS,CSSを配置します。
func main() {
// ファイルサーバー
fs := http.FileServer(http.Dir("./public"))
http.Handle("/", fs)
// WebSocket
http.HandleFunc("/ws", handleConnections)
go handleMessages()
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
次にコネクションハンドラとして動くhandleConnections
の中身を実装します。
処理としては
- GETリクエストをWebSocketにアップグレード
- 受け取ったリクエストをクライアントとして登録し、コネクションを確立
- メッセージを待つ
- メッセージを受け取ったらチャネルに送りブロードキャスト
といった感じです。
このhandleConnections
をgo
ステートメントで呼び出して並行実行しています。
そしてchannel
を使用し、並行実行されるgoroutine
間で値(チャットメッセージ)を送受信します。
func handleConnections(w http.ResponseWriter, r *http.Request) {
// 送られてきたGETリクエストをWebSocketにアップグレード
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatal(err)
}
defer ws.Close()
// クライアントを登録
clients[ws] = true
for {
var message Message
// 新しいメッセージをJSONとして読み込み、Message構造体にマッピング
err := ws.ReadJSON(&message)
if err != nil {
log.Printf("error: %v", err)
delete(clients, ws)
break
}
// 受け取ったメッセージをbroadcastチャネルに送る
broadcast <- message
}
}
最後にbroadcast
チャネルにメッセージが送信された時、
接続している全てのクライアントメッセージを送信します。
func handleMessages() {
for {
// broadcastチャネルからメッセージを受け取る
message := <-broadcast
// 接続中の全クライアントにメッセージを送る
for client := range clients {
err := client.WriteJSON(message)
if err != nil {
log.Printf("error: %v", err)
client.Close()
delete(clients, client)
}
}
}
}
まとめ
これでWebSocketを使ったチャットサーバーの完成です!
簡単ですね!フロントの実装は割愛しますが、こんな感じでチャットができます。
今回Goでチャットサーバーを作ることで、
WebSocket、goroutine、channelの理解を少し深められたと思います。
普段あまり触れていなかった部分ですが、実際に書いて動かしてみると理解しやすかったです。
皆さんも是非トライしてみてください!