こんにちは。KOUKIです。
Go言語とJavaScriptでチャットアプリの作成方法を記事にしています。
前回は、簡単なWebサーバーとチャットUIを作成し、画面表示するところまで実装しました。
今回からいよいよWebSocketsを使って、チャットアプリケーションを作成していきます!
本記事では、Go言語側でWebSocketsのエンドポイントを、JavaScript側でWebsocketsオブジェクトをそれぞれ作成し、ブラウザとサーバー間で、WebSocketsのコネクションを確立します。
<目次>
前回
WebSocketsのエンドポイントの作成
WebSocketsのエンドポイントを作成しましょう。
connect.go
まずは、WebSockets返却用のデータ構造体をconnect.goに実装します。
1 2 3 4 5 6 7 8 |
// connect.go package domain // WebSocketsからの返却用データの構造体 type WsJsonResponse struct { Action string `json:"action"` Message string `json:"message"` } |
Actionには、WebSocketsへ送る命令を格納します。例えば、「ブロードキャストしろ」、とか「セッションを閉じろ」とかですね。
Messageには、ブラウザから受け取ったメッセージを格納します。
handlers.go
handlers.goには、WebSocketsのエンドポイントを実装します。
Gorilla WebSocketを使えば、割と簡単に作れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// handlers.go package handlers import ( "chat-websockets/domain" "log" "net/http" "github.com/CloudyKit/jet/v6" "github.com/gorilla/websocket" ) var views = jet.NewSet( ... ) // wsコネクションの基本設定 var upgradeConnection = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } // WebSocketsのエンドポイント func WsEndpoint(w http.ResponseWriter, r *http.Request) { // HTTPサーバーコネクションをWebSocketsプロトコルにアップグレード ws, err := upgradeConnection.Upgrade(w, r, nil) if err != nil { log.Println(err) } log.Println("OK Client Connecting") var response domain.WsJsonResponse response.Message = `<li>Connected to server</li>` err = ws.WriteJSON(response) if err != nil { log.Println(err) } } func Home(w http.ResponseWriter, r *http.Request) { ... } ... |
routes.go
先ほど作成したWebSocketsのエンドポイントをroutes.goに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// routes package main import ( ... ) func routes() http.Handler { mux := pat.New() mux.Get("/", http.HandlerFunc(handlers.Home)) mux.Get("/ws", http.HandlerFunc(handlers.WsEndpoint)) // 追加 ... } |
これで、「ws://127.0.0.1:8080/ws」にアクセス可能になりました。
WebSocketsオブジェクトの作成
JavaScrit側で、WebSocketsオブジェクトを作成し、サーバー側と通信してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// scripts.js let scoket = null; // 最初のHTMLの読み込みと解析が完了したとき、スタイルシート、 // 画像、サブフレームの読み込みが完了するのを待たずに発生。 document.addEventListener("DOMContentLoaded", function(){ // WebScoektオブジェクトの作成 // https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications socket = new WebSocket("ws://127.0.0.1:8080/ws") // 接続を確立 // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onopen socket.onopen = () => { console.log("Successfully connected") } }) |
接続に成功すれば「Successfully connected」がコンソール上に出力されるはずです。
ブラウザから「http://localhost:8080/」にアクセスし、F12でデベロッパーツールを開いて確認してみましょう。

OKですね。
サーバーからも以下のログが出力されています。
1 2 3 |
$ go run cmd/web/*.go 2021/04/02 06:50:24 Starting web server on port 8080 2021/04/02 06:50:29 OK Client Connecting |
WebSocketsイベントハンドラの追加
先ほどと同じ要領で、WebSocketsイベントのハンドラを追加していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// scripts.j document.addEventListener("DOMContentLoaded", function(){ // WebScoektオブジェクトの作成 ... // 接続を確立 ... // 接続がCLOSEDに変わった時に呼ばれる // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onclose socket.onclose = () => { console.log("connection closed") } // エラーが発生した時に呼び出される // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onerror socket.onerror = error => { console.log("there was an error") } // サーバーからメッセージが届いたときに呼び出される // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/onmessage socket.onmessage = msg => { let j = JSON.parse(msg.data) console.log(j) } }) |
WebSocketsのハンドリング
ここから少し、難しくなります。
ブラウザからリクエストを受信後、コネクションを確立しているユーザー全てにメッセージをブロードキャストする必要があるため、その為の処理を実装していきます。
新しい構造体を追加
connect.goに次の構造体を追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// connect.go package domain import "github.com/gorilla/websocket" // WebSocketsからの返却用データの構造体 // WebSocketsコネクション情報を格納 type WebScoketConnection struct { *websocket.Conn } // WebSockets送信データを格納 type WsPayload struct { Action string `json:"action"` Message string `json:"message"` Username string `json:"username"` Conn WebScoketConnection `json:"-"` } |
WebScoketConnectionは、WebSocketsのコネクション情報を格納するためのもです。一方、WsPayloadは、ブラウザから送信されたデータを格納します。
コネクション情報格納用の変数を宣言
チャットアプリでは、コネクション情報(ユーザーやPayload)を保持する必要があります。
そのため、handlers.goに以下の変数を宣言しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// handlers.go package handlers import (...) var ( // ペイロードチャネルを作成 wsChan = make(chan domain.WsPayload) // コネクションマップを作成 // keyはコネクション情報, valueにはユーザー名を入れる clients = make(map[domain.WebScoketConnection]string) ) |
コネクション情報の保持
先ほど作成したWsEndpoint関数に、次の処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// handlers.go ... // WebSocketsのエンドポイント func WsEndpoint(w http.ResponseWriter, r *http.Request) { // HTTPサーバーコネクションをWebSocketsプロトコルにアップグレード ... // コネクション情報を格納 conn := domain.WebScoketConnection{Conn: ws} // ブラウザが読み込まれた時に一度だけ呼び出されるのでユーザ名なし clients[conn] = "" err = ws.WriteJSON(response) ... } } |
この後、補足にも書きますが、WsEndpointはブラウザが読み込まれた時、一度だけ呼び出されます。この時はまだユーザー名を保持していないので、「clients[conn] = “”」としています。
ちなみに、ユーザー名は、以下のボックスに打ち込んだ時にWebSockets経由でサーバーに渡す予定です。

WobSocketsリッスン関数
次にWebSocketsをリッスンする関数を作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// handlers.go func ListenForWs(conn *WebSocketConnection) { defer func() { if r := recover(); r != nil { log.Println("Error", fmt.Sprintf("%v", r)) } }() var payload WsPayload for { err := conn.ReadJSON(&payload) if err != nil { // do nothing } else { payload.Conn = *conn wsChan <- payload } } } |
この関数は、WebSocketsのエンドポイントにアクセスした時、そのコネクション情報からPayloadを読み込んで、チャネルに格納します。
そして、無限ループでずっと起動させておきます。これにより、既にコネクションが確立されている通信は、ここから情報を取得することができます。
この関数は、非同期で動作させる必要があるので、WsEndpoint関数からgoroutineを使って呼び出しましょう。
1 2 3 4 5 6 7 8 9 10 11 |
func WsEndpoint(w http.ResponseWriter, r *http.Request) { ... conn := WebSocketConnection{Conn: ws} clients[conn] = "" err = ws.WriteJSON(response) ... } go ListenForWs(&conn) // goroutineで呼び出し } |
WsEndpoint関数の補足
WsEndpoint関数は、ブラウザから「http://localhost:8080/」にアクセスした時に、JavaScriptから一度だけ呼び出されます。
1 2 3 4 5 6 |
// DOMロード時に発動 document.addEventListener("DOMContentLoaded", function(){ ... socket = new WebSocket("ws://127.0.0.1:8080/ws") ... } |
逆にそれ以降は呼び出されないので、WebSocketsのリクエストを何らかの形で受け取る必要があります。
そこで、goroutineを使って、ListenForWs関数を別プロセスで呼び出すことで、ブラウザからの通信を常にキャッチし続ける状態を作っています。
ちなみに、goroutineを使うには関数の前に「go 」キーワードを指定するだけでOKです。
そして、ListenForWs関数のパラメータには、ポインタ(&)を使っているので、同じコネクション情報を参照します。
ブロードキャスト関数の作成
先ほど、変数として宣言したclientsには、全てのユーザー情報を格納します。
そして、チャットでは、メッセージの送信を検知したら、全てのユーザーに対してメッセージを送信する必要があります。
その為、次の関数を追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// handlers.go ... // 全てのユーザーにメッセージを返す func broadcastToAll(response domain.WsJsonResponse) { // clientsには全ユーザーのコネクション情報が格納されている for client := range clients { err := client.WriteJSON(response) if err != nil { log.Println("websockets err") _ = client.Close() // clients Mapからclientの情報を消す delete(clients, client) } } } |
wsChanチャネルリッスン関数
次に、wsChanチャネルをリッスンする関数を作成します。
wsChanチャネルにメッセージが格納されたら、先ほど実装したbroadcastToAll関数を使って、全てのユーザーにメッセージを送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// handlers.go ... func ListenToWsChannel() { var response domain.WsJsonResponse for { // メッセージが入るまで、ここでブロック e := <-wsChan // アクションによって処理を分ける必要があるが便宜的に以下を宣言 response.Action = "Got here" response.Message = fmt.Sprintf("Some message, and action was %s", e.Action) broadcastToAll(response) } } |
この関数は、wsChanチャネルからメッセージを読み取り、全ユーザーにメッセージをブロードキャストする性質上、Webサーバーとは別のプロセスで起動した方が良さそうです。
main関数からListenToWsChannelをgoroutineで呼び出しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// main.go package main import ( "chat-websockets/internal/handlers" "log" "net/http" ) func main() { mux := routes() log.Println("Starting channel listener") go handlers.ListenToWsChannel() log.Println("Starting web server on port 8080") _ = http.ListenAndServe(":8080", mux) } |
動作確認
JavaScriptの以下の処理で、メッセージをブラウザのコンソールから確認できるようにしています。
1 2 3 4 |
socket.onmessage = msg => { let j = JSON.parse(msg.data) console.log(j) } |
アプリを起動し直して、ブラウザからメッセージが送信されてくるか確認しましょう。
1 2 3 4 |
$ go run cmd/web/*.go 2021/04/03 07:43:42 Starting channel listener 2021/04/03 07:43:42 Starting web server on port 8080 2021/04/03 07:43:53 OK Client Connecting |

OKですね。
ちなみに、ページをリロードすると次のエラーが出ますが、これで正常です。
1 |
2021/04/03 07:43:53 Error repeated read on failed websocket connection |
このエラーは、ページリロード時に、コネクションを貼り直しに行くことで発生します。これにより、とあるバグが生み出されてしまいますが、次の記事で紹介したいと思います。
次回
次回も引き続き、WebSocketsを使って実装を進めていきます。
それでは、また!
コメントを残す
コメントを投稿するにはログインしてください。