こんにちは、 KOUKIです。
本記事では、掲示板アプリケーション開発のプロセスをハンズオン形式で記事にしています!
前回は、開発環境の準備、Webサーバー及び掲示板UIの実装を行いました。
今回は、WebSocketsを使って、コネクションの確立を行いたいと思います。
WebSocket API は、ユーザーのブラウザーとサーバー間で対話的な通信セッションを開くことができる先進技術です。この API によって、サーバーにメッセージを送信したり、応答をサーバーにポーリングすることなく、イベント駆動型のレスポンスを受信したりすることができます。
MDNより
<目次>
前回
まとめページ
サーバー側の実装
WebSockets対応を行うため、以下の作業が必要になります。
2. routeを作成する
3. WebSocketsコネクションを保持する仕組みを作る
4. WebSocketsをリッスンする
5. 投稿をブロードキャストする
6. payloadチャネルを別プロセスで読み込む
少々難しいですが、一つずつ段階を追っていけば理解できると思います^^
WebSocketsのエンドポイントを作成する
最初に、WebSocketsのエンドポイントを作成しましょう。
connect.goに、サーバー内で処理したデータをクライアント側へ返却するための構造体を実装します。
1 2 3 4 5 6 7 |
// connect.go package domain type WsJsonResponse struct { Action string `json:"action"` Post string `json:"post"` } |
「Action」には、クライアント側で実行するアクションを指定します。例えば、ユーザーが離脱したことを知らせるために「left」アクションを送ったりします。そして、「Post」には、ユーザーの投稿を格納します。
次に、handlers.goにエンドポイントを実装します。
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 |
// handlers.go package handlers import ( "bulletin-board/domain" "log" "net/http" "github.com/CloudyKit/jet/v6" "github.com/gorilla/websocket" ) var views = jet.NewSet(...) // WebSocketsの基本設定 // https://pkg.go.dev/github.com/gorilla/websocket#hdr-Overview 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 requestから*Connを取得 wsConn, err := upgradeConnection.Upgrade(w, r, nil) if err != nil { log.Println(err) } log.Println("WS Connecting....") // コネクション情報を格納 conn := domain.WebSocketConnection{Conn: wsConn} // ページを開いたときは、ユーザー名を持っていないので、空 clients[conn] = "" } func Home(w http.ResponseWriter, r *http.Request) {...} |
WebSocketsのエンドポイントを作成するために、Gorilla WebSocketを利用しました。これを利用するとWebSocket プロトコルを簡単に実装できます。
upgradeConnection変数には、WebSocketsプロトコルの基本設定を定義します。そして、この変数が提供する「Upgrade」メソッドで、HTTP RequestをWebSocketsへアップグレードします。
また、「CheckOrigin: func(r *http.Request) bool { return true },」の設定がない場合は、「websocket: request origin not allowed by Upgrader.CheckOrigin」エラーが表示され、クライアントからの接続に失敗(WebSocket connection to ‘ws://127.0.0.1:8080/ws’ failed: )します。
routeを作成する
エンドポイントへリクエストを送る処理を実装しましょう。
routes.goに、エントリポイントを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// routes.go package main import (...) func routes() http.Handler { mux := pat.New() mux.Get("/", http.HandlerFunc(handlers.Home)) mux.Get("/ws", http.HandlerFunc(handlers.WsEndpoint)) // 追加 // staticファイルの配信設定 ... } |
この設定により、クライアントから「ws://127.0.0.1:8080/ws」へGETリクエストを送れるようになりました。
本アプリでは、画面を開いた時にサーバへWebSocketsコネクションを張りに行くので、GETを指定しています。
WebSocketsコネクションを保持する仕組みを作る
掲示板アプリでは、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" type WsJsonResponse struct {...} // WebSocketコネクション情報格納 type WebSocketConnection struct { *websocket.Conn } // WebSockets受信データ type WsPayload struct { Action string `json:"action"` Post string `json:"Post"` Username string `json:"username"` Conn WebSocketConnection `json:"-"` } |
次に、handlers.goに以下の変数を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// handlers.go package handlers import (...) var ( // payloadのチャネルを作成 wsChan = make(chan domain.WsPayload) // key: コネクション情報 value: ユーザー名のマップ clients = make(map[domain.WebSocketConnection]string) ) |
wsChan変数では、payloadチャネルを作成しています。
WebSocketsのリクエストは非同期で受信する必要があり、goroutineを使ってWebサーバーとは別のプロセスで動かします。
goroutine間でデータの送受信を行うには「チャネル」が必要となるため、ここに定義しました。
clients変数は、WebSocketsコネクション情報をkeyにしたMapを指定しています。このようにユーザー情報を格納しておけば、全ユーザーにブロードキャストでデータを送れるようにできます。
この変数は、ページを開いた時に以下のように格納されるべきです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// WebSocketsのエンドポイント func WsEndpoint(w http.ResponseWriter, r *http.Request) { // HTTP requestから*Connを取得 ... // コネクション情報を格納 conn := domain.WebSocketConnection{Conn: wsConn} // ページを開いたときは、ユーザー名を持っていないので、空 clients[conn] = "" // クライアントに投稿を返す ... } |
「WsEndpoint」エンドポイントは、ページを開いた時に一度だけ呼ばれます。その時に、コネクション情報をclients変数のKeyとして保持します。
ユーザー名については、ブラウザの「User Name」に名前を入れた時、非同期でWebSocketsリクエストを送り、格納したいと思います。

WebSocketsをリッスンする
次は、WebSocketsをリッスンする処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func ListenForWs(wsConn *domain.WebSocketConnection) { defer func() { // パニックが起きた時に任意の処理ができる if r := recover(); r != nil { log.Println("Error", fmt.Sprintf("%v", r)) } }() var payload domain.WsPayload for { err := wsConn.ReadJSON(&payload) if err != nil { // no action } else { payload.Conn = *wsConn wsChan <- payload } } } |
ListenForWs関数は、パラメータにWebSocketsのコネクション情報を持ちます。この情報の中にpayload(クライアントから送られてきたデータ)が格納されているので、「ReadJSON」メソッドで読み取り、wsChanチャネルに格納します。
また、この関数は、forループで永続的に回しています。無限ループで回して大丈夫なの?と思われるかもしれませんが、「ReadJSON」メソッドが新しいpayloadを読み込むまで後続処理をブロックするので問題ありません。
無限ループで動かすという特性上、ListenForWs関数を別プロセスで動作させましょう。そうしない場合は、mainプロセスをブロックしてしまうので、他のユーザーのリクエストを受け付けられなくなります。
WsEndpoint関数からgoroutineを使って、ListenForWs関数を呼び出します。
1 2 3 4 5 6 7 8 9 |
// WebSocketsのエンドポイント func WsEndpoint(w http.ResponseWriter, r *http.Request) { // HTTP requestから*Connを取得 // コネクション情報を格納 // ページを開いたときは、ユーザー名を持っていないので、空 ... // goroutineでListenForWsを起動 go ListenForWs(&conn) } |
投稿をブロードキャストする
次は、ユーザーの投稿をブロードキャストする処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func broadcastToAllUser(response domain.WsJsonResponse) { // clientsには接続済みの全てのユーザー情報が格納されている for client := range clients { // 投稿をクライアントに返す err := client.WriteJSON(response) if err != nil { log.Println("WebSockets Error") // WebSocketsコネクションを破棄 _ = client.Close() // mapから削除 delete(clients, client) } } } |
投稿をユーザーに返す処理は、「WriteJSON」メソッドで実現可能です。
前述の通り、clients変数には全てのユーザーのWebSocketsコネクションが格納されているので、forループで回して、WriteJSONメソッドでレスポンスを返せます。
この関数を使うためには、実はもう一つ関数を実装する必要があります。
先ほど、Websocketsをリッスンし、新しいpayloadがコネクションに入ってきたら、wsChan チャネルに格納したと思います。
このチャネルからデータを取り出すして、ブロードキャストに渡す必要があります。
以下の関数を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func ListenToWsChannel() { var response domain.WsJsonResponse for { // wsChanにデータが入るまでブロックするので、無限ループでOK payload := <-wsChan // 後で、Action別の処理を実装する response.Action = "Sample Action" response.Post = fmt.Sprintf(` <div class='message"> <p>%s</p> <small class="name">%s</small> </div>`, payload.Username, payload.Post) broadcastToAllUser(response) } } |
ListenToWsChannel関数は、wsChanチャネルからpayloadを読み込み、任意のデータに加工後、broadcastToAllUser関数で全てのユーザーに対して、ブロードキャストを行います。
ここも無限ループで実装していますが、wsChanチャネルからデータを読み込むまで後続の処理をストップするので、これでOKです。
payloadチャネルを別プロセスで読み込む
ListenToWsChannel関数は、main関数とは別のプロセスで動かします。
main.goに以下の処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import ( "bulletin-board/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") log.Fatal(http.ListenAndServe(":8080", mux)) } |
これで、合計3つのプロセスが立ち上がることになります。
- Webサーバーのプロセス
- WebSocketsをリッスンするプロセス
- wsChanチャネルをリッスンするプロセス
JavaScript側の実装
次は、JavaScript側の処理を実装していきましょう。
2. 接続を確立する
WebSocketsオブジェクトを作成する
WebSocketsオブジェクトの作り方は、公式ページに詳しく載っています。
1 2 3 4 5 6 7 8 9 |
// scripts.js let socket = null; document.addEventListener('DOMContentLoaded', function(){ // WebSocketsオブジェクトの作成 socket = new WebSocket("ws://127.0.0.1:8080/ws") }) |
これで、自動的にサーバーへの接続が開かれます。
接続を確立する
WebSocketsのonopenメソッドは、WebSocketsのコネクションの確立がなされた時に呼び出されます。つまり、データを送受信する準備ができたことを表します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// scripts.js let socket = null; document.addEventListener('DOMContentLoaded', function(){ // WebSocketsオブジェクトの作成 ... // 接続を確立時 socket.onopen = () => { console.log("Successfully connectted!") } }) |
以下のコマンドでWebサーバーを起動し、ブラウザから「http://localhost:8080/」にアクセスしましょう。
1 2 3 4 5 |
# Mac / Linux make run # Windows go run cmd/web/*.go |

「Successfully connectted!」メッセージが表示されたので、接続は問題ないですね。
WebSocketsコネクションの確立が完了したので、今回はここまでにします。
次回
次回は、掲示板の投稿機能を実装したいと思います。
コメントを残す
コメントを投稿するにはログインしてください。