post Image
ginでWebSocketとSession込みTw○○terDemo作ってみた。

KIT AdventCalender 15日目は
最近Go触り始めたのでせっかくなので書こうかと思います。
n番煎じかもしれない部分もありますが見逃してください(。- -。)
riot.jsでフロント管理しながら、サーバー側をginでさばいていきます。
SessionとWebsocketに関しても実装していきます。

スクリーンショット 2016-12-03 21.20.14.png

今回のデモここにあげときます。
見たい方しばしお持ち・・・。

ginの選定理由

まずお酒と可愛いgopherくんが好きでして..
revelもいいかなと思いましたが軽めの方が自分で色々カスタムできそうですし、初心者でも触りやすいくらいドキュメントが充実していてサンプルには持ってこいかなってことで選びました。深い意味はないです。

今回はざっくりこんな感じで作っています。

DemoApp
├── Gomfile
├── README.md
├── assets
│   ├── css
│   └── img
├── controllers
├── demo.db
├── main.go
├── models
├── public
│   ├── css
│   ├── front
│   └── js
├── runner.conf
├── session
├── templates
│   ├── app
│   ├── layout
│   └── tag
└── utils

必要な機能は?

DBとか設計とか抜きにして試しておきたい機能をあげると

  • Session
  • Websocket

が妥当かと思われます。(筆者がやったことなかったので)

Session

cookieかredisかどうしようか迷いましたがシンプルにcookieで持つことにします。
gin-gonic/contribというginを作ったところが出しているものを使います。

package session

import (
    "github.com/gin-gonic/contrib/sessions"
    "github.com/gin-gonic/gin"
    "reflect"
)

type SessionInfo struct {
    UserID         int
    UName          string
    IsSessionAlive bool
}

func Login(c *gin.Context, user interface{}) {
    vuser := reflect.ValueOf(user)
    session := sessions.Default(c)
    session.Set("alive", true)
    session.Set("userID", vuser.FieldByName("ID").Interface())
    session.Set("uname", vuser.FieldByName("UName").Interface())
    session.Save()
}

func ClearSession(c *gin.Context) {
    session := sessions.Default(c)
    session.Clear()
    session.Save()
}

func GetSessionInfo(c *gin.Context) SessionInfo {
    var info SessionInfo
    session := sessions.Default(c)
    user_id := session.Get("userID")
    uname := session.Get("uname")
    alive := session.Get("alive")
    if isNil(user_id) && isNil(uname) && isNil(alive) {
        info = SessionInfo{
            UserID: -1, UName: "", IsSessionAlive: false,
        }
    } else {
        info = SessionInfo{
            UserID:         user_id.(int),
            UName:          uname.(string),
            IsSessionAlive: alive.(bool),
        }
    }
    return info
}

func isNil(a interface{}) bool {
    return a == nil || reflect.ValueOf(a).IsNil()
}

これでSessionの書き込み、初期化、値の取得が可能です。

実際にログインしているかとか書くと

main.go
package main

import (
    "github.com/gin-gonic/contrib/sessions"
    "github.com/gin-gonic/gin"
    "html/template"
    "twitter_board/session"
)

var templates map[string]*template.Template

func init() {
    loadTemplates()
}

func main() {
    r := gin.Default()

    store := sessions.NewCookieStore([]byte("secret"))
    r.Use(sessions.Sessions("SessionName", store))

    r.GET("/", func(c *gin.Context) {
        info := session.GetSessionInfo(c)
        r.SetHTMLTemplate(templates["index"])
        c.HTML(200, "_base.html",
        gin.H{"SessionInfo": info})
    })
    r.Run("localhost:12312")
}

func loadTemplates() {
    var baseTemplate = "templates/layout/_base.html"
    templates = make(map[string]*template.Template)
    templates["index"] = template.Must(template.ParseFiles(baseTemplate, "templates/app/index.html"))
}
index.html
{{ define "title" }}Home{{ end }}

{{ define "content" }}

{{if .SessionInfo.IsSessionAlive}}
<ログイン中/>
{{else}}
<ログアウト中/>
{{end}}
{{end}}

WebSocket

olahol/melodyで試してみたいなと思います。
gorilla/websocketを扱いやすくしたやつみたいです。
golang.org/x/netとの違いに関してはこちらを参照

そもそもWebSocketいるのという方はTitleに注目すると

スクリーンショット 2016-12-13 16.51.38.png

他人のツイートが通知されているのがわかります。

これはどんな仕組みか考えてみると
ws.png
となるのではと思います。(憶測…)
iOSならAPNs、AndroidならGCMになるかもしれないです。

これを想定して考えてみます。

ざっくりまずclient側

<form onsubmit= { submit } >
  <input ref="text">
  <button type="submit" class="btn btn-outline-primary">ログイン</button>
</form>

<script>
  var tweetCnt = 0;
  var url = "ws://" + window.location.host + "/tweetsocket";
  var ws = new WebSocket(url);

  ws.onmessage = function (msg) {
    if ({{ .SessionInfo.UserID}} == num){
      tweetCnt++;
      var title = "(" + tweetCnt + ")" + " Tweet";
      document.title = title;
    }
  };

  submit(e) {
    var text = this.refs.text.value;
    if (text.value <= 140 && text.value != 0) {
      ws.send({{ .SessionInfo.UserID}});
    }
  };
</script>

ざっくりserver側

import (
    "github.com/gin-gonic/gin"
    "github.com/olahol/melody"
    "encoding/binary"
    "twitter_board/models"
)

func main() {
    var(
        user   models.User
    )

    r := gin.Default()
    m := melody.New()

    .GET("/", func(c *gin.Context) {
        info := session.GetSessionInfo(c)
        r.SetHTMLTemplate(templates["index"])
        c.HTML(200, "_base.html",
            gin.H{"SessionInfo": info})
    })

    r.GET("/tweetsocket", func(c *gin.Context) {
        m.HandleRequest(c.Writer, c.Request)
    })

    m.HandleMessage(func(s *melody.Session, msg []byte) {
        data := binary.BigEndian.Uint64(msg)
        gdb.Where("id = ?", data).Find(&user)
        followIDs := user.FollowIDs() //return []uint64
        for  _, val := range followIDs {
            m.Broadcast([]byte(myInt))
        }
    })
}

server側は
idを受け取ったら[]byteからencodingしてuint64型に直します。
(ここは端折りましたが)そしてdbにアクセスしてFollowerを探して[]uint64で用意します。
最後に[]uint64に対して[]byteに直して送ります。

client側は
submitを押すとcacheのUserIDをserverに送ります。
受け取りではidが一致しているか確認して

思ったより、簡単だった!

Clientで使ったもの

Riot.js

component管理として入れています。とは言ってもEventHandlerとObserbleしか利用してないです(^^;;

fetch

HTML の新たな標準仕様らしくて使ってみようと思った次第。

ginと一緒に使いたいライブラリ達

fresh
go run main.go
でもいけますがいちいちbuildが正直長いです。
変更時に自動でそこだけbuildしてくれるありがたいやつです!
gom
rubyでいうgemみたいなものです。
今回は他環境でも試せるようにここにまとめておきます。
gorm
ORMです。
db処理が楽になりますね。
go-bindata
今回は使っていませんが、
ファイルをバイナリに変換して呼び出したりできるやつです。

Go or Gin での小ネタ

なんだかんだ使える部分を抜粋しておきます!

gin.HandleFunc

db処理でいちいち開閉はする人はいないんじゃないんでしょうか。(本当?)
main.goで宣言したものはgin.HandleFuncを通して通すことが可能です。

main.go
import (
    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"
    "twitter_board/models"
    "twitter_board/controllers"
    _ "github.com/jinzhu/gorm/dialects/sqlite"
)

func main() {
    gdb := initDB()
    r := gin.Default()
    r.Use(ApiMiddleware(gdb))

    v1 := r.Group("/api/v1")
    {
        v1.POST("/signup", controllers.Login)
    }
    r.Run("localhost:12312")
}

func initDB() *gorm.DB {
    db, err := gorm.Open("sqlite3", "demo.db")
    if err != nil {
        panic("failed to connect database")
    }
    db.AutoMigrate(&models.User{}, &models.Tweet{}, &models.Follower{})
    return db
}


func ApiMiddleware(db *gorm.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("databaseConn", db)
        c.Next()
    }
}

controllers/login.go
import (
    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"
    "twitter_board/models"
)

type Login struct {
    UName    string `json:"uname" binding:"required"`
    Password string `json:"pass" binding:"required"`
}

func LogIn(c *gin.Context) {
    var (
        user  models.User
        login Login
    )
    gdb, _ := c.MustGet("databaseConn").(*gorm.DB)
    gdb.Where("uname = ? AND password = ?", login.UName, login.Password).Find(&user)
}

のようにkeyと型を指定することでmain.goでセットしたものが簡単に持ってこれます。

interface{}がnilかはreflectを使う

文字列とわかっていればbyte数でわかるので

len(str) > 0

とできるのですが
確実にnil検知するには

import "reflect"

func isNil(a interface{}) bool {
    return a == nil || reflect.ValueOf(a).IsNil()
}

としたほうがいい。

正規表現

regexpを使ってみます。
基本的にはre2ベースで作られているとのこと。
今回はこれでひとまず作ってみます。

import "regexp"

func check_regexp(reg, str string) bool {
    return regexp.MustCompile(reg).Match([]byte(str))
}

func CheckUserName(txt string) bool {
    return check_regexp(`\-|\w`, txt)
}

func CheckPassWord(pass string) bool {
    return check_regexp(`[A-Za-z0-9]`, pass)
}

日本語他多種の言語にも対応しているようで。(ASCIIでもできるんでなくてもどうにかできるんだけども)

リッチだけど処理が遅いということでこんな声こんな声があります。
想像以上ですね。stringsでしっかりカバーしなければですね。

参考資料:

The Go Programming Language
gin-gonic/gin
Riot.js
olahol/melody
絶対ハマる、不思議なnil
Go Websockets (Gin-gonic + Gorilla)
Go+gin+dbrでセッション管理(ログイン・ログアウト)をする話+HTMLテンプレートにセッション情報を埋め込んで制御する

最後に

さぁ今日のGo&Roit_Lifeしていこう!


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。