はじめに
この記事はGo 3 Advent Calendar 2020の19日目です。
勉強のためにgoroutine
を使うアプリを作りたいと思い、チャットアプリを作ろうと思いました。
web上でチャットアプリのようなインタラクティブなやり取りは、websocket
を使うことが多いかと思います。goで使用するとなるとgorilla/websocketのプラグインを使うと思いますが、そのリポジトリ内にあるチャットアプリのサンプルがとてもシンプルで、丁度良いレベルだったのでとても参考になりました。
そこで今回は、そのチャットアプリのサンプルコードを説明していこうと思います!
書くこと
goroutine
の基礎知識については全く書いてないです。代わりにチャットアプリのサンプルソースを通して、実際どのようにgoroutine
が使用されているかを書いていきます。
ファイル構成
サンプルアプリのファイルは以下となっております。
- main.go
- hub.go
- client.go
- home.html
各ファイルのポイントをピックアップして説明していこうと思います。
main.go
...
func main() {
flag.Parse()
hub := newHub()
go hub.run() // hubをgoroutineで起動
http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r) // クライアントサイド(js)とwebsocketを接続
})
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
main.go
では、webサーバーの立ち上げとindexページのテンプレート表示を行っております。hub.run()
をgoroutine
で起動して、ハブ(チャットルーム)の起動をしております。
hub.go
package main
// Hub maintains the set of active clients and broadcasts messages to the
// clients.
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the clients.
broadcast chan []byte
// Register requests from the clients.
register chan *Client
// Unregister requests from clients.
unregister chan *Client
}
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
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)
}
}
}
}
}
こちらではチャットアプリのハブ(チャットルーム)となる部分です。hub#run()
は前述したとおりmain.go
からgoroutine
で呼ばれます。websocket
作成時に入室処理が走りクライアントが登録され、メッセージが送られてくるまで待機します。送られてくればbroadcast(全員へ送信)し、メッセージが各クライアントへ渡されます。
client.go
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c // メッセージ取得のループを抜けたときは退室
c.conn.Close()
}()
...
for {
_, message, err := c.conn.ReadMessage() // クライアントサイドから送られてきたメッセージを取得
...
c.hub.broadcast <- message // broadcastする
}
}
func (c *Client) writePump() {
...
for {
select {
case message, ok := <-c.send: // broadcastで送られてきたメッセージ
...
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send) // メッセージをクライアントサイド(js)へ送信
}
...
}
}
}
...
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) // httpリクエストをwebsocketにアップグレード
if err != nil {
log.Println(err)
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client // 入室
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
go client.writePump() // broadcastされてきたメッセージをクライアントサイド(js)へ送る
go client.readPump() // クライアントサイド(js)から送られてきたメッセージをbroadcastする
}
serveWs()
は/ws
のエンドポイントへアクセスが来たときに呼ばれます。入室処理後にwritePump()
とreadPump()
をgoroutineで呼びます。
readPump()
では、websocket
でクライアントサイド(js)から送られてきたメッセージをbroadcastします。writePump()
では、broadcastされたメッセージをクライアントサイド(js)へ送ります。
home.html
こちらindexページのテンプレートです。一緒にjsも書かれてます。jsではformに書かれたメッセージをwebsocket
を利用してサーバーサイドと送受信してます。
...
document.getElementById("form").onsubmit = function () {
if (!conn) {
return false;
}
if (!msg.value) {
return false;
}
conn.send(msg.value); // submitが押されたらwebsocketでサーバーにメッセージ送信
msg.value = "";
return false;
};
if (window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + "/ws");
conn.onclose = function (evt) {
var item = document.createElement("div");
item.innerHTML = "<b>Connection closed.</b>";
appendLog(item);
};
conn.onmessage = function (evt) { // サーバーサイドからwebsocketでメッセージ取得時
var messages = evt.data.split('\n');
for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div");
item.innerText = messages[i];
appendLog(item); // チャット内容を表示
}
};
}
...
つまり、
- あるユーザーがメッセージ送信(js)
- メッセージを受信し各クライアント(送信者も含む)へbroadcast(go)
- 各クライアントはメッセージを受信し、jsへメッセージを送信(go)
- メッセージを表示(js)
となります。
さいごに
いかがだったでしょうか。実際に世の中で使用されているチャットアプリはもっと複雑だと思いますが、仕組み自体は結構シンプルだったのではないでしょうか。
goroutine
を勉強しようとしても、何を作ろうかと迷って行き詰まる方も多いかなと思います。そんなときは、今回紹介したサンプルアプリを参考にチャットアプリを作ってみてはいかがでしょうか。