LoginSignup
4
5

More than 3 years have passed since last update.

チャットアプリからgoroutineが実際にどう使われているか見てみよう!

Posted at

はじめに

この記事はGo 3 Advent Calendar 2020の19日目です。

勉強のためにgoroutineを使うアプリを作りたいと思い、チャットアプリを作ろうと思いました。

web上でチャットアプリのようなインタラクティブなやり取りは、websocketを使うことが多いかと思います。goで使用するとなるとgorilla/websocketのプラグインを使うと思いますが、そのリポジトリ内にあるチャットアプリのサンプルがとてもシンプルで、丁度良いレベルだったのでとても参考になりました。

そこで今回は、そのチャットアプリのサンプルコードを説明していこうと思います!

書くこと

goroutineの基礎知識については全く書いてないです。代わりにチャットアプリのサンプルソースを通して、実際どのようにgoroutineが使用されているかを書いていきます。

ファイル構成

サンプルアプリのファイルは以下となっております。

  • main.go
  • hub.go
  • client.go
  • home.html

各ファイルのポイントをピックアップして説明していこうと思います。

main.go

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

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を利用してサーバーサイドと送受信してます。

home.html

...

    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); // チャット内容を表示
            }
        };
    }

...

つまり、

  1. あるユーザーがメッセージ送信(js)
  2. メッセージを受信し各クライアント(送信者も含む)へbroadcast(go)
  3. 各クライアントはメッセージを受信し、jsへメッセージを送信(go)
  4. メッセージを表示(js)

となります。

さいごに

いかがだったでしょうか。実際に世の中で使用されているチャットアプリはもっと複雑だと思いますが、仕組み自体は結構シンプルだったのではないでしょうか。

goroutineを勉強しようとしても、何を作ろうかと迷って行き詰まる方も多いかなと思います。そんなときは、今回紹介したサンプルアプリを参考にチャットアプリを作ってみてはいかがでしょうか。

↓(おまけ)自分で作った簡単なチャットアプリ
Screen Recording 2020-12-19 at 1.46.18.mov.gif

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5