Golangを始めて少し経ったので、WebSocketを使ってリアルタイムタスク管理アプリを作った。
Golang初めて1、2ヶ月ほど経ったので、Asana風なリアルタイムタスク管理アプリを作ってみました。 ちなみにAsanaはこんな感じのやつです。 デザインが好きなので、個人的に使ってます。 asana.com
目的
今回は実際にデプロイして使ってもらうことが目的ではなく、GitHubでコードを公開すること・Golangに慣れること・WebSocketに触れることを目的にしました。
作ったもの
こんな感じのやつを作りました。 ユーザーごとにタスクを追加したり、削除したりできるようになっていて、それがリアルタイムで他のユーザーにも反映されるようになっています。 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 -データを巡る冒険-