覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Go言語 - melodyを利用してWebSocketサーバを立てる

melodyGinを使ってWebSocketサーバを立てて、簡易的なチャット画面を作ってみました。

実際には、melody Exampleを、ほぼそのまま流用しているだけです。


準備

以下を実行してmelodyを利用可能な状態にしておきます

go get gopkg.in/olahol/melody.v1


プログラムとhtml

WebSocketサーバと動作確認用チャット画面のindex.htmlとして以下を用意しました。

■WebSocketサーバ(Goプログラム)

package main

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

    "github.com/gin-gonic/gin"
    "gopkg.in/olahol/melody.v1"
)

func main() {
    log.Println("Websocket App start.")

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

    rg := router.Group("/sampleapp")
    rg.GET("/", func(ctx *gin.Context) {
        http.ServeFile(ctx.Writer, ctx.Request, "index.html")
    })

    rg.GET("/ws", func(ctx *gin.Context) {
        m.HandleRequest(ctx.Writer, ctx.Request)
    })

    m.HandleMessage(func(s *melody.Session, msg []byte) {
        m.Broadcast(msg)
    })

    m.HandleConnect(func(s *melody.Session) {
        log.Printf("websocket connection open. [session: %#v]\n", s)
    })

    m.HandleDisconnect(func(s *melody.Session) {
        log.Printf("websocket connection close. [session: %#v]\n", s)
    })

    // Listen and server on 0.0.0.0:8989
    router.Run(":8989")

    fmt.Println("Websocket App End.")
}


■index.html

<html>
  <head>
    <title>Chat powered by Melody</title>
  </head>

  <style>
    #chat {
      text-align: left;
      color:#ffffff;
      background: #113131;
      width: 400px;
      min-height: 300px;
      padding: 10px;
      font-family: 'Lucida Grande', 'Hiragino Kaku Gothic ProN', 'ヒラギノ角ゴ ProN W3', 'Meiryo', 'メイリオ', sans-serif;
      font-size: small;
    }
  </style>

  <body>

    <center>
      <h3>Sample Chat</h3>
      <pre id="chat"></pre>
      <label id="title"></label>
      <input placeholder="say something" id="text" type="text">
    </center>

    <script>
      var url = "ws://" + window.location.host + "/sampleapp/ws";
      var ws = new WebSocket(url);
      var name = "Guest-" + Math.floor(Math.random() * 1000);
      var chat = document.getElementById("chat");
      document.getElementById("title").innerText = name + ": ";
      
      var text = document.getElementById("text");
      var now = function () {
        return new Date().toLocaleString();
      };

      ws.onmessage = function (msg) {
        var line =  now() + " : " + msg.data + "\n";
        chat.innerText += line;
      };

      text.onkeydown = function (e) {
        if (e.keyCode === 13 && text.value !== "") {
          ws.send("[" + name + "] > " + text.value);
          text.value = "";
        }
      };
    </script>

  </body>
</html>


実行してみた

対象のアプリを実行します。実行すると以下のようなログがコンソールに出力されます

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /sampleapp/               --> main.main.func1 (3 handlers)
[GIN-debug] GET    /sampleapp/ws             --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8989


http://localhost:8989/sampleapp/にアクセスすることでチャット画面が利用できました


f:id:nini_y:20170523160815g:plain


というわけで、お遊び程度ですがかなりコード量少なくWebSocketのサーバサイドを実装することができました。



関連エントリ

Go言語 - Goの特徴

Go言語(Golang)の特徴(メリット)として以下のようなものが一般的に挙げられています


高パフォーマンス

一般的に軽量言語(LL)ではメモリ管理のコストなどに注意してコードを構成しなければパフォーマンスが極端に悪くなることがあります。
それに対して、Goは細部を気にせずにコードを書いても比較的パフォーマンスを引き出しやすい傾向があります。


メモリ管理からの解放

C/C++等はメモリ管理を手動で行う必要がありますが、Goではメモリ管理をGoランライムに任せることができます


コンパイル速度の速さ

C/C++等に比べてGoのコンパイル速度は極端に早いです。
コンパイル速度が速いので、コードを修正してビルド・実行という開発の流れを気軽に何度も実行することができます


スタイルやコード整形

プログラムをチーム開発している際に、どの書式スタイルに統一するかという議論がなされることがあります。
Goではスタイルの規定が最初からしっかりしていて、かつ、そのスタイルをサポートするためのコード整形ツールも揃っています。
このことにより、Goではコードのスタイルに関して質を簡単に保つことができます。


シンプルな言語仕様

Goは機能の割にとてもシンプルな仕様で作られています。
人によっては1日程度で言語仕様を理解することもできます。


シングルバイナリ

Goで書かれたプログラムは基本的には単体で実行可能なシングルバイナリとして生成されます。
一旦コンパイルしてしまえばLL系言語で必要なランタイムや依存関係の管理は必要なくなります。
アプリのインストール・デプロイといった作業も非常に簡単になります。



関連エントリ

Go言語 - Ginを使ってHello World

Go言語ではnet/httpを利用することでHTTPサーバを立てることができまが、GinといったWebアプリ用のフレームワークも存在しています。


事前準備

以下を実行してGinのパッケージ取得を行います

go get github.com/gin-gonic/gin


とりあえずHello World

以下 Hello World の文字列を返す例です。

Engine.GETにより、ハンドリング対象のパスとハンドラを登録します。


■サンプルコード

package main

import (
    "log"

    "github.com/gin-gonic/gin"
)

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

    router.GET("/hello", func(ctx *gin.Context) {
        ctx.String(200, "Hello World")
    })

    if err := router.Run(":8686"); err != nil {
        log.Fatal("Server Run Failed.: ", err)
    }
}


■実行結果

http://localhost:8686/helloにアクセスすると以下の結果が出力されます

Hello World


静的ページを返す

URLへのアクセスに対して静的ページを返す場合は、router.StaticFS 等を利用します。

以下サンプルでは、staticディレクトリにindex.htmlとuser.htmlというファイルを用意しているものとします

f:id:nini_y:20170519185348p:plain


■サンプルコード

package main

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
)

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

    router.StaticFS("/web", http.Dir("static"))

    if err := router.Run(":8686"); err != nil {
        log.Fatal("Server Run Failed.: ", err)
    }
}

■実行結果

  • http://localhost:8686/web/ に アクセスすると staticディレクトリのindex.html の内容が返される
  • http://localhost:8686/web/user.html に アクセスすると staticディレクトリのuser.html の内容が返される



関連エントリ

Go言語 - HTTPサーバでHello World

Go言語ではnet/httpを利用することでHTTPサーバを立てることができます。

とりあえず今回はお試しな感じでHTTPサーバを立ててみます


とりあえずHello World

以下 Hello World の文字列を返す例です。

http.HandleFuncにより、ハンドリング対象のパスとハンドラを登録します。その後にhttp.ListenAndServeでHTTPサーバを起動します

■サンプルコード

package main

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

func main() {

    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World")
    })

        // ポート = 8686
    if err := http.ListenAndServe(":8686", nil); err != nil {
        log.Fatal("ListenAndServe failed.", err)
    }
}


■実行結果

http://localhost:8686/helloにアクセスすると以下の結果が出力されます

Hello World


静的ページを返す

URLへのアクセスに対して静的ページを返す場合は、http.FileServer を利用します。

以下サンプルでは、staticディレクトリにindex.htmlとuser.htmlというファイルを用意しているものとします

f:id:nini_y:20170519174645p:plain


■サンプルコード

package main

import (
    "log"
    "net/http"
)

func main() {
    http.Handle("/", http.FileServer(http.Dir("static")))

    if err := http.ListenAndServe(":8686", nil); err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

■実行結果

  • http://localhost:8686/ に アクセスすると staticディレクトリのindex.html の内容が返される
  • http://localhost:8686/user.html に アクセスすると staticディレクトリのuser.html の内容が返される


URLからprefix部分を取り除いてハンドリングする場合

アクセスされたURLのPrefixを除いて、ハンドリングする場合は http.StripPrefix を利用します

■サンプルコード

package main

import (
    "log"
    "net/http"
)

func main() {
    
    // アクセスされたURLから /web 部分を取り除いてハンドリングする
    http.Handle("/web", http.StripPrefix("/web", http.FileServer(http.Dir("static"))))

    if err := http.ListenAndServe(":8686", nil); err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

■実行結果

  • http://localhost:8686/web/ に アクセスすると staticディレクトリのindex.html の内容が返される
  • http://localhost:8686/web/user.html に アクセスすると staticディレクトリのuser.html の内容が返される



関連エントリ

Go言語 - WindowsのアクティブなセッションのIDを取得する

Windowsではセッションという概念が存在しています。(セッション 0 の分離 - Windows 7 対応アプリケーションの互換性

複数セッションの中で現在アクティブになっている(物理コンソールにアタッチしている)セッションのセッションIDをGo言語で取得するサンプルです。


■サンプルコード

package main

import (
    "fmt"
    "log"
    "syscall"
)

var (
    kernel32, _ = syscall.LoadLibrary("kernel32.dll")

    wtsGetActiveConsoleSessionID, _ = syscall.GetProcAddress(kernel32, "WTSGetActiveConsoleSessionId")
)

func abort(funcname string, err error) {
    panic(fmt.Sprintf("%s failed: %v", funcname, err))
}

// ActiveSessionID : アクティブなセッションのIDを返す。アクティブなセッションが無い場合は-1を返す
func ActiveSessionID() int32 {
    r0, _, callErr := syscall.Syscall(uintptr(wtsGetActiveConsoleSessionID),
        2,
        0,
        0,
        0)
    if callErr != 0 {
        abort("Call WTSGetActiveConsoleSessionId", callErr)
    }
    return int32(r0)
}

func main() {
    log.Println("App start.")

    log.Printf("Active Session ID %d", ActiveSessionID())

    log.Println("App End.")
}

単純に、WindowsAPIのWTSGetActiveConsoleSessionIdを呼び出しているだけです。(WTSGetActiveConsoleSessionId function | Microsoft Docs


■実行結果

2017/05/18 05:28:32 App start.
2017/05/18 05:28:32 Active Session ID 1
2017/05/18 05:28:32 App End.



関連エントリ