Goでチャットサーバーを作ってgoroutineとchannelを理解してみる

[シェアフル　Advent Calendar18日目](https://adventar.org/calendars/3523)　の記事になります。
最近Goを勉強したい熱が上がってきたので、今回はチャットサーバー的なのを作って　goroutineとchannelの仕組みについて理解してみました。

# はじめる前に
まだ学習中の身ですので、もし何かしら間違っている箇所や定義が曖昧な部分があったらバンバン指摘してもらえると助かります:pray:

### goroutineとは？　

- Goのランタイムによって管理されている処理実行用の軽量なスレッド
- 並行処理を可能にする
- 関数を呼び出す直前に`go`をつけると呼び出すことができる　(ex: `go functionName(ctx)`)
- 同じメモリのアドレス空間を共有している

### channelとは？

- goroutineで定義した並列で実行されている関数同士で値の送受信を可能にする型
- 実際に送受信する値の型を定義することができる

### なぜチャットサーバー？
単純に自分的にイメージがつきやすいかなーって思ったからです。
`time.Sleep`とかで試してみてある程度動きはわかるものの、実用的なケースはどういう物があるのかイメージがいまいち沸かなかったので。

## 構成図的な
かなりざっくりとした構成図ですが、最終的にはこんな感じになるかと思います。
クライアントがチャットサーバーにwebsocketで接続して、そのコネクションをgoroutine関数でハンドリングし、メッセージの送信を受け付けます。
送信されたメッセージはchannel型の変数に送り、別のgoroutine関数でメッセージを拾い、それをすべてのクライアントに送信するようにします。

![無題のプレゼンテーション (1).png](https://qiita-image-store.s3.amazonaws.com/0/39443/b6accd4d-6c76-44e6-e8f3-e68b84116bef.png)
## 作ってみる

今回出てくるソースコードは[こちら](https://scotch.io/bar-talk/build-a-realtime-chat-server-with-go-and-websockets)の記事のもの使用しております。

まずは適当なディレクトリを作ります

```shell
$ mkdir $GOPATH/src/github.com/username/chat-server
```

チャットサーバーにはwebsocketは必須なのでwebsocketのパッケージをインストールしましょう。今回はよく使われている[Gorilla Toolkit](http://www.gorillatoolkit.org/)をインストールします

```shell
$ go get github.com/gorilla/websocket
```

では実際にチャットサーバーの実装を進めていきましょう。
まずは必要なパッケージをインポートします。gorillaはもちろん、httpサーバとロギング用のパッケージもインポートします。

```go:main.go
package main

import (
        "log"
        "net/http"

        "github.com/gorilla/websocket"
)
```

次に定義する変数はこのチャットサーバーのメインとなる部分です。
１番目の変数はmapでwebsocketのコネクションに対するポインタを定義しています。２番目クライアント側から送られて来たメッセージをキューイングする役目を担うものになります。

```go:main.go
...
var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan Message)
```

次にアップグレーダを定義します。この変数はhttpコネクションを受け取ってwebsocketにアップグレードする関数を保持したオブジェクトになります。

```go:main.go
...
var upgrader = websocket.Upgrader{}
```

次にメッセージを保持するための構造体を定義します。
メールアドレス、ユーザ名とメッセージなどの情報を保持したシンプルなものになります。

```go:main.go
...
type Message struct {
        Email    string `json:"email"`
        Username string `json:"username"`
        Message  string `json:"message"`
}
```

これで必要最低限なものは定義しました。次にメインロジックを作っていきましょう。
静的ファイルを参照するファイルサーバの立ち上げやルーティングの紐づけなどがここに入ります。

```go:main.go
...
func main() {
        fs := http.FileServer(http.Dir("./public"))
        http.Handle("/", fs)

        http.HandleFunc("/ws", handleConnections)
        go handleMessages()

        log.Println("http server started on :8000")
        err := http.ListenAndServe(":8000", nil)
        if err != nil {
                log.Fatal("ListenAndServe: ", err)
        }
}

```

チャットサーバーのメインの機能はこの２つになります。最初に`/ws`にwebsocketの接続を始めるためのリクエストをハンドリングします。受け取ったリクエストは`handleConnections`という関数で処理しますが、これは後ほど作ります。
さらにここで`handleMessages`というgoroutineを開始させます。こちらも後ほど触れます。

```go:main.go
func main() {
...
        http.HandleFunc("/ws", handleConnections)
        go handleMessages()
...
}
```

まずはコネクションハンドラとして動いてくれる`handleConnections`の中身を作ります。
動作の流れとしては、GETリクエストをwebsocketにアップグレード→受け取ったリクエストをクライアントとして登録→websocketからのメッセージを待ち続け、受け取ったら`broadcast`チャネルに送る、といった感じです。
もし関数の中の処理が終わった場合はdeferを使って必ずwebsocketのコネクションを閉じるようにしています。

httpのルーティングハンドラも実はgoroutineとして動いているので、複数のコネクションを順番を待たずに同時に処理することを可能にしています。

```go:main.go
func handleConnections(w http.ResponseWriter, r *http.Request) {

	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Fatal(err)
	}

	defer ws.Close()

	clients[ws] = true

	for {
		var msg Message
		err := ws.ReadJSON(&msg)
		if err != nil {
			log.Printf("error: %v", err)
			delete(clients, ws)
			break
		}
		broadcast <- msg
	}
}
```

`broadcast`チャネルにメッセージが送信された時、それを取り出す役目を`handleMessages`が担っています。
`handleConnections`でハンドリングしているクライアントのどれかがメッセージを送信したら、それを取り出して現在接続しているクライアント全てにそのメッセージを送信するようになっています。

```go:main.go
func handleMessages() {
	for {
		msg := <-broadcast
		for client := range clients {
			err := client.WriteJSON(msg)
			if err != nil {
				log.Printf("error: %v", err)
				client.Close()
				delete(clients, client)
			}
		}
	}
}
```

全体的には以下のような感じになります

```go:main.go
package main

import (
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

var clients = make(map[*websocket.Conn]bool) // 接続されるクライアント
var broadcast = make(chan Message)           // メッセージブロードキャストチャネル

// アップグレーダ
var upgrader = websocket.Upgrader{}

// メッセージ用構造体
type Message struct {
	Email    string `json:"email"`
	Username string `json:"username"`
	Message  string `json:"message"`
}

func main() {
	// ファイルサーバーを立ち上げる
	fs := http.FileServer(http.Dir("./public"))
	http.Handle("/", fs)
	// websockerへのルーティングを紐づけ
	http.HandleFunc("/ws", handleConnections)
	go handleMessages()
	// サーバーをlocalhostのポート8000で立ち上げる
	log.Println("http server started on :8000")
	err := http.ListenAndServe(":8000", nil)
	// エラーがあった場合ロギングする
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

func handleConnections(w http.ResponseWriter, r *http.Request) {
	// 送られてきたGETリクエストをwebsocketにアップグレード
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Fatal(err)
	}
	// 関数が終わった際に必ずwebsocketnのコネクションを閉じる
	defer ws.Close()

	// クライアントを新しく登録
	clients[ws] = true

	for {
		var msg Message
		// 新しいメッセージをJSONとして読み込みMessageオブジェクトにマッピングする
		err := ws.ReadJSON(&msg)
		if err != nil {
			log.Printf("error: %v", err)
			delete(clients, ws)
			break
		}
		// 新しく受信されたメッセージをブロードキャストチャネルに送る
		broadcast <- msg
	}
}

func handleMessages() {
	for {
		// ブロードキャストチャネルから次のメッセージを受け取る
		msg := <-broadcast
		// 現在接続しているクライアント全てにメッセージを送信する
		for client := range clients {
			err := client.WriteJSON(msg)
			if err != nil {
				log.Printf("error: %v", err)
				client.Close()
				delete(clients, client)
			}
		}
	}
}
```

では最後に実際にメッセージのやり取りをするためのクライアントのインターフェイスを作ります。
ユーザ名とメールアドレスを登録したあと、チャットメッセージのやり取りが可能になる単純なものです。
`main.go`が置かれているディレクトリに`public`というディレクトリを作成し、以下の各静的ファイルを置きます。
今回フロント周りは　Vueを使っています。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Simple Chat</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="/style.css">

</head>
<body>
<header>
    <nav>
        <div class="nav-wrapper">
            <a href="/" class="brand-logo right">Simple Chat</a>
        </div>
    </nav>
</header>
<main id="app">
    <div class="row">
        <div class="col s12">
            <div class="card horizontal">
                <div id="chat-messages" class="card-content" v-html="chatContent">
                </div>
            </div>
        </div>
    </div>
    <div class="row" v-if="joined">
        <div class="input-field col s8">
            <input type="text" v-model="newMsg" @keyup.enter="send">
        </div>
        <div class="input-field col s4">
            <button class="waves-effect waves-light btn" @click="send">
                <i class="material-icons right">chat</i>
                Send
            </button>
        </div>
    </div>
    <div class="row" v-if="!joined">
        <div class="input-field col s8">
            <input type="email" v-model.trim="email" placeholder="Email">
        </div>
        <div class="input-field col s8">
            <input type="text" v-model.trim="username" placeholder="Username">
        </div>
        <div class="input-field col s4">
            <button class="waves-effect waves-light btn" @click="join()">
                <i class="material-icons right">done</i>
                Join
            </button>
        </div>
    </div>
</main>
<footer class="page-footer">
</footer>
<script src="https://unpkg.com/vue@2.1.3/dist/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/md5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js"></script>
<script src="/app.js"></script>
</body>
</head>
</html>
```
```css:style.css
body {
    display: flex;
    min-height: 100vh;
    flex-direction: column;
}

main {
    flex: 1 0 auto;
}

#chat-messages {
    min-height: 10vh;
    height: 60vh;
    width: 100%;
    overflow-y: scroll;
}
```
```javascript:app.js
new Vue({
    el: '#app',

    data: {
        ws: null,
        newMsg: '',
        chatContent: '',
        email: null,
        username: null,
        joined: false
    },
    created: function() {
        var self = this;
        this.ws = new WebSocket('ws://' + window.location.host + '/ws');
        this.ws.addEventListener('message', function(e) {
            var msg = JSON.parse(e.data);
            self.chatContent += '<div class="chip">'
                + '<img src="' + self.gravatarURL(msg.email) + '">'
                + msg.username
                + '</div>'
                + msg.message + '<br/>';

            var element = document.getElementById('chat-messages');
            element.scrollTop = element.scrollHeight;
        });
    },
    methods: {
        send: function () {
            if (this.newMsg != '') {
                this.ws.send(
                    JSON.stringify({
                            email: this.email,
                            username: this.username,
                            message: $('<p>').html(this.newMsg).text()
                        }
                    ));
                this.newMsg = '';
            }
        },
        join: function () {
            if (!this.email) {
                Materialize.toast('You must enter an email', 2000);
                return
            }
            if (!this.username) {
                Materialize.toast('You must choose a username', 2000);
                return
            }
            this.email = $('<p>').html(this.email).text();
            this.username = $('<p>').html(this.username).text();
            this.joined = true;
        },
        gravatarURL: function(email) {
            return 'http://www.gravatar.com/avatar/' + CryptoJS.MD5(email);
        }
    }
});
```

画面はこのような感じになります。
![chat-server1.png](https://qiita-image-store.s3.amazonaws.com/0/39443/9deddb1b-ffd0-6467-bea4-95239777dc57.png)

必要な情報を登録したらメッセージは送信できます。
![chat-server2.png](https://qiita-image-store.s3.amazonaws.com/0/39443/c3c3bfbf-929b-4aa6-67b0-9c4f121497f8.png)

２つのユーザ間でもメッセージのやり取りはできました。
![chat-server3.png](https://qiita-image-store.s3.amazonaws.com/0/39443/69e8c85b-6318-35de-db46-64e8ae0f83cb.png)


## 最後に

いかがでしょうか。goroutineで複数の処理を並列で実行し、データの共有をchannelを通してやるというのがある程度このチャットサーバーを作ることによってより感覚的にわかりやすくなったのではないかと思います。
ただ、他に色々なパターンやユースケースもあるのでそれも調べていきたいと思います。

## 参考
[Build a Realtime Chat Server With Go and WebSockets](https://scotch.io/bar-talk/build-a-realtime-chat-server-with-go-and-websockets)
[goの並列処理入門編 goroutineとchannel](https://qiita.com/marnie_ms4/items/cc2e49d4f11884e16611)
[Go の並行処理](http://jxck.hatenablog.com/entry/20130414/1365960707)