月別アーカイブ: 2013年9月

Go言語:go-gtkでネットワーク対戦型○×ゲームのネイティブアプリケーションを実装する

screen_shot

Go言語のGUIライブラリgo-gtkを利用してネットワークを介して対戦できる○×ゲームのネイティブアプリケーションを実装した。

コード:
github.com/inatus/noughts-and-crosses-go

使い方

事前準備

  • PC2台と対戦してくれる友達を用意する
  • GTK+2のインストール
    Macならbrew install gtk+でインストールするのが楽。
  • 6392ポートを開けておく。
    通信に勝手にこのポートを使う。

ビルド

$ go get github.com/inatus/noughts-and-crosses-go
$ cd $GOPATH/src/github.com/inatus/noughts-and-crosses-go
$ go build
$ ./noughts-and-crosses-go

解説

実装上の特徴は以下のとおり。

  • GUIにGTKを利用。つまりマルチプラットフォームに動作する(はず)。
    ライブラリはgo-gtkを利用。
  • 対戦のための通信にはUDPを利用。ポート番号は決め打ち。
    標準ライブラリnetパッケージを利用。

go-gtkライブラリには説明がなくて分かりにくかったが、他のGTK APIの解説サイトで解決。
これが役に立った。だいたい似ているメソッド名になっている。
GTK+ リファレンスマニュアル

ネットワーク対戦に必要な通信は下記の3つで成り立っている。

  • 他端末に自分の存在を知らせるために一定間隔でブロードキャストを繰り返すためのgoroutineを起動
  • 他端末から送られてくる通信を受信するためのgoroutineを起動
  • Startボタンや手を決めるボタンのクリック時に対戦相手にデータを送信

Go言語でもこういったネイティブで動くGUIアプリが作れるとなるとかなり用途が広がるだろう。

Go言語:goroutineとchannelを使ってWebSocketでブラウザと通信するXMPPチャットWebクライアントを実装する

screen_shot

Go言語でGoogle Hangout(旧Google Talk)などで利用できるXMPPチャットクライアントのWebアプリを実装した。
github.com/inatus/xmpp-web-client-go

使用した技術要素は以下のとおり。

  • Webブラウザとの通信はWebSocketを利用
  • チャットサーバとの通信はXMPPを利用
  • 受信データはGo言語のgoroutine、channelを用いて並行処理を行う

WebSocketには準標準パッケージのcode.google.com/p/go.net/websocketを利用する。

XMPPはcode.google.com/p/rsc/xmppを使おうと思ったが、Roster(コンタクトリスト)がうまく取得できなかったため、github.com/agl/xmppを利用することにした。

方針

本プログラムでは以下のように、異なるプロトコルの通信を扱う必要がある。

xmpp_chat_client_overview

Webブラウザから入力されたメッセージは、WebSocket経由で受信し(①)、XMPPサーバへ送信する(②)。
チャット先の相手がメッセージを送信した際には、XMPPサーバからメッセージを受信し(③)、WebSocket経由でWebブラウザに送信する(④)。
このようなリモートからの通信(①、③)を受ける際には並行処理が必要になる。

今回は、以下のようにWebSocket、XMPPからの通信を受けるgoroutineを別々に立て、メインの処理を走らせるgoroutineに集めて処理を行う方法をとった。

goroutine_structure

Go言語では、goroutine、channelを用いることで、こういった並行処理を驚くほど簡単に実現できる。

実装

まずコネクションを1つの型にまとめる。
goroutineとして実行する関数は、この型のメソッドとして定義する。(後述)

type session struct {
    talk *xmpp.Conn
    ws   *websocket.Conn
}

その上で、それぞれのコネクションからの受信データを格納するchannelを定義し、これを引数にしてgoroutineを起動する。
メインのgoroutineはそのままselect-case構文によりchannelからのデータを待ち受ける。
case文にそれぞれのchannelを指定してやれば複数のchannelでデータを待ち受けることが可能。
なお、okにはchannelが開いていればtrue、閉じていればfalseが返る。

ses := session{
    talk: talk, // XMPPのコネクション
    ws:   ws, // WebSocketのコネクション
}

stanzaChan := make(chan xmpp.Stanza) // xmpp.Stanza型のchannelを定義
go ses.receiveMessage(stanzaChan) // goroutineの起動

webSocketChan := make(chan string) // string型のchannelを定義
go ses.receiveWebSocket(webSocketChan) // goroutineの起動

for {
    select {
    case receivedStanza, ok := <-stanzaChan: // XMPP用channelからのデータを待ち受ける
        if !ok {
            // エラー処理
        }
        // 処理
    case receivedMessage, ok := <-webSocketChan: // WebSocket用channelからのデータを待ち受ける
        if !ok {
            // エラー処理
        }
        // 処理
    }
}

XMPP、WebSocketからの通信を待ち受けるgoroutineで行うことは、受け取ったデータを引数のchannelに渡すことのみである。

func (ses *session) receiveMessage(stanzaChan chan<- xmpp.Stanza) {
    defer close(stanzaChan)
    for {
        receivedStanza, err := ses.talk.Next() // XMPPからのデータを待ち受ける
        if err != nil {
            // エラー処理
        }
        stanzaChan <- receivedStanza // channelにデータを渡す
    }
}

func (ses *session) receiveWebSocket(webSocketChan chan<- string) {
    defer close(webSocketChan)
    for {
        var receivedMessage string
        if err := websocket.Message.Receive(ses.ws, &receivedMessage); err != nil {  // WebSocketからのデータを待ち受ける
            // エラー処理
        }
        webSocketChan <- receivedMessage // channelにデータを渡す
    }
}

たったこれだけで上記の図に示すような並行処理を組むことができる。

Go言語:sshパッケージを使って対話型SSHクライアントを実装する

code.google.com/p/go.crypto/sshで提供されているsshパッケージを使って、標準のsshコマンドと同等の対話型SSHクライアントを実装してみた。

コードは以下。
https://github.com/inatus/ssh-client-go

API・コード例はGoDocに記載されている。
http://godoc.org/code.google.com/p/go.crypto/ssh

しかし、Sessionを作るところまでは例があるからいいとして、その先がよくわからない。
セッションを開始するメソッドと以下のようなものがあるようだ。

  • Run: コマンドを引数に指定。コマンドの実行が完了するまでWaitが掛かり、結果はSessionのStdio、Stderrに出力される。
  • Start: コマンドを引数に指定。コマンドの実行完了を待たずに先に進む。結果はSessionのStdio、Stderrに出力される。つまり、Startメソッドを実行した直後には結果が出力されない。
  • Shell: コマンドを実行せずにシェルを開始する。
  • Output: コマンドを引数に指定。コマンドの実行が完了するまでWaitが掛かり、結果が戻り値に返る。
  • CombinedOutput: コマンドを引数に指定。コマンドの実行が完了するまでWaitが掛かり、結果が戻り値に返る。標準出力に加え標準エラー出力が返る。

また、Sessionに対してこれらのメソッドは一度しか実行できない。
これらのメソッドの中で、Shell以外は非対話型的な実行を想定していると思われる。したがって、Shellを用いる。

Shellメソッドを実行すると、ログイン直後の出力がStdioに出力される。これをOSの標準出力に出力するようにした。

session.Stdout = os.Stdout
session.Stderr = os.Stderr

次に、ログイン後のコマンド受付処理を実装する。コマンドはStdinに受け付けられる。端末の標準入力から入力されたコマンドをStdinPipeを使ってSessionのStdinに伝える。

in, _ := session.StdinPipe()
for {
  reader := bufio.NewReader(os.Stdin)
  str, _ := reader.ReadString('n')
  fmt.Fprint(in, str)
}

この際、以下に注意する。

  • StdinPipeはセッションを開始する前に取得すること。
  • コマンドの末尾には改行コード(n)を付けること。

これで通常のsshコマンドと同等な対話型SSHクライアントが構築できる。
ただし、パスワードのマスクやテキストエディタの起動などがうまくできない。