とある神戸大生の走り書き

主にプログラミングに関して、学んだことを走り書きとして残していきます。

Golangを始めて少し経ったので、WebSocketを使ってリアルタイムタスク管理アプリを作った。

Golang初めて1、2ヶ月ほど経ったので、Asana風なリアルタイムタスク管理アプリを作ってみました。 ちなみにAsanaはこんな感じのやつです。 デザインが好きなので、個人的に使ってます。 asana.com

目的

今回は実際にデプロイして使ってもらうことが目的ではなく、GitHubでコードを公開すること・Golangに慣れること・WebSocketに触れることを目的にしました。

作ったもの

Image from Gyazo

github.com

こんな感じのやつを作りました。 ユーザーごとにタスクを追加したり、削除したりできるようになっていて、それがリアルタイムで他のユーザーにも反映されるようになっています。 DBには接続していないので、CSSのクラス名でそれぞれのタスクを管理しています。

具体的には、作成した順番にタスクにCSSのクラス名としてIDが割り振られるようになっています。

例:

  • 最初に作ったタスクは、クラス名task1。
  • 二つ目に作ったタスクは、クラス名task2。
  • 次にtask1を削除して、三つ目のタスクを作るとクラス名がtask3になる。

デザインはRESUMEの感じが好きなので、それを参考にしています。

Golangで書いたコード

クリックしてコードを見ることができます。

handler.go

package main

import (
    "html/template"
    "net/http"
    "path/filepath"
    "sync"
)

/*
HTMLテンプレートをサーブするためのハンドラ
*/
type templateHandler struct {
    once     sync.Once          //HTMLテンプレートを1度だけコンパイルするための指定
    filename string             //テンプレートとしてHTMLファイル名を指定
    tmpl     *template.Template //テンプレート
}

/* templateHandlerをhttp.Handleに適合させるため、ServeHttpを実装する */
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

    // テンプレートディレクトリを指定する
    path, err := filepath.Abs("./templates/")
    if err != nil {
        panic(err)
    }

    // 指定された名称のテンプレートファイルを一度だけコンパイルする
    t.once.Do(
        func() {
            t.tmpl = template.Must(template.ParseFiles(path + t.filename))
        })

    t.tmpl.Execute(w, nil)
}

main.go

package main

import (
    "log"
    "net/http"
)

func main() {

    /* ルートへのアクセスに対してハンドラを貼り、group.htmlをサーブする */
    http.Handle("/", &templateHandler{filename: "/group.html"})

    group := newRoom()

    http.Handle("/room", group)

    go group.run()

    /* webサーバを開始する */
    log.Println("webサーバを開始します。")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalln("webサーバの起動に失敗しました。:", err)
    }
}

task.go

package main

type task struct {
    Name   string
    Detail string
    Delete string
}

group.go

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

const (
    socketBufferSize = 1024
    taskBufferSize   = 256
)

/* websocket用の変数 */
var upgrader = &websocket.Upgrader{
    ReadBufferSize:  socketBufferSize,
    WriteBufferSize: socketBufferSize,
}

type group struct {
    forward chan *task
    join    chan *client
    leave   chan *client
    clients map[*client]bool
}

/*
groupをhttp.handleに適合させる。
ここでは以下のことを実装する。
   ・websocketの開設
   ・clientの生成
*/
func (c *group) ServeHTTP(w http.ResponseWriter, r *http.Request) {

    /* websocketの開設 */
    socket, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatalln("websocketの開設に失敗しました。:", err)
    }

    /* クライアントの生成 */
    client := &client{
        socket: socket,
        send:   make(chan *task, taskBufferSize),
        room:   c,
    }

    c.join <- client
    defer func() {
        c.leave <- client
    }()

    go client.write()
    client.read()
}

func newRoom() *group {
    fmt.Println("groupが生成されました。:", t.Format(layout))
    return &group{
        forward: make(chan *task),
        join:    make(chan *client),
        leave:   make(chan *client),
        clients: make(map[*client]bool),
    }
}

func (c *group) run() {
    for {
        // チャネルの動きを監視し、処理を決定する
        select {

        /* joinチャネルに動きがあった場合(クライアントの入室) */
        case client := <-c.join:
            // クライアントmapのbool値を真にする
            c.clients[client] = true
            fmt.Printf("クライアントが入室しました。現在 %x 人のクライアントが存在します。\n",
                len(c.clients))

        /* leaveチャネルに動きがあった場合(クライアントの退室) */
        case client := <-c.leave:
            // クライアントmapから対象クライアントを削除する
            delete(c.clients, client)
            fmt.Printf("クライアントが退室しました。現在 %x 人のクライアントが存在します。\n",
                len(c.clients))

        /* forwardチャネルに動きがあった場合(タスクの受信) */
        case msg := <-c.forward:
            fmt.Println("タスクを受信しました。")
            // 存在するクライアント全てに対してタスクを送信する
            for target := range c.clients {
                select {
                case target.send <- msg:
                    fmt.Println("タスクの送信に成功しました。")
                default:
                    fmt.Println("タスクの送信に失敗しました。")
                    delete(c.clients, target)
                }
            }
        }
    }
}

大まかには以上です。

READMEに実行の仕方が書いてあるので、もし興味があれば見てみてください。

参考

[ golang ] WebSocketを使ったチャット機能を実装してみる。 | Wild Data Chase -データを巡る冒険-