Ruby 版 Client

Ruby によるクライアント実装例を紹介します。

公開しているソースコードでは、client ディレクトリにクライアントの完成版を用意しています。利用にあたっては、consumer_key と consumer_secret の修正が必要です。試す場合は、最後の「メッセージの表示を修正」のコマンド実行例 2 ステップ(bundle exec ...)を実行してください。

Ruby 版クライアントの仕様

Ruby 版クライアントの大まかな機能は、次の通りです。

  • 部屋一覧の取得
  • 部屋を選択して入室
  • ルームの他の人の発言を見るビューワー
  • ルームに発言
  • 部屋にいる人の一覧取得

libmagellan

クライアントから Worker (MAGELLAN) へのアクセス部分は、libmagellan を使います。libmagellan は、MAGELLAN へアクセスするための MAGELLAN 公式ライブラリです。

現在は、以下の言語に対応しています。

  • ruby

MAGELLAN にアクセスするためには、consumer_key と consumer_secret が必要となります。これらの情報は、コンソールのプロジェクト詳細画面で確認できます。

サイドメニューの DefaultProject1 をクリックしてください(図をクリックすると、consumer_key、consumer_secret 表示部分が拡大します)。

プロジェクト DefaultProject1 詳細画面

consumer_secret は、伏せ字になっています。伏せ字を解除するには、伏せ字右横の 伏せ字解除ボタンの画像 をクリックしてください。

以降、consumer_key と consumer_secret の値は、次の内容で進めます。適宜、自身のプロジェクトの値に読み替えてください。

consumer_key consumer_secret
mfu6rjamyvqyjshk ********************************

libmagellan の利用方法

libmagellan では、Libmagellan クラスで MAGELLAN へアクセスする機能を定義しています。利用にあたっては、Libmagellan クラスのオブジェクトを生成して使います。

Libmagellan クラスのオブジェクト生成時(Libmagellan.new)の引数には、次の情報が必要となります。

項目名 説明
consumer_key mfu6rjamyvqyjshk アクセスの認証キー
consumer_secret ******************************** アクセスの認証シークレット
client_version DefaultStage1 クライアントバージョン
host nebula-001a-web.magellanic-clouds.net HTTP(S) のホスト名
port 443 HTTPS でアクセスするときのポート番号(HTTP では 80)
scheme https MAGELLAN にアクセスするときのスキーム
mqtt_host nebula-001a-mqtt.magellanic-clouds.net MQTT のホスト名
mqtt_port 1883 MQTT のポート番号

HTTP 型通信関連のメソッド

HTTP 型通信関連のメソッドには、次のものがあります。

メソッド 説明
ping MAGELLAN との導通を確認します。
request MAGELLAN へ任意のパスで、HTTP リクエストを送信します。 Worker への HTTP リクエストは、こちらを使用します。

Publish/Subscribe 型通信関連のメソッド

Publish/Subscribe 型通信関連のメソッドには、次のものがあります。

メソッド 説明
publish Publish を送信します。
subscribe Subscribe を登録します。ブロックが渡された場合は get_message を実行し、メッセージの待ち受けを開始します。
get_message Subscribe に登録したメッセージの待ち受けを開始します。ブロックが渡されなかった場合は、最後の 1 件のメッセージを返します。

ファイルの作成

それでは、クライアントの実装をしていきます。

まず、クライアントを実装するために、client というディレクトリを作成します。

$ mkdir client
$ cd client

tips用アイコン

以降の作業では、別ターミナルを開いての作業も行います。その際は、必ず今回作成した client ディレクトリで作業を行ってください。

使用する gem を指定するために Gemfile というファイルを作成し、以下を記述します。

source 'https://rubygems.org'

gem 'libmagellan'

Gemfile に記述した gem をインストールするために bundle install を実行します。

$ bundle install
Fetching gem metadata from https://rubygems.org/...........
Fetching version metadata from https://rubygems.org/..
Resolving dependencies...
Using i18n 0.7.0
Using json 1.8.3
Using minitest 5.8.0
Using thread_safe 0.3.5
Using tzinfo 1.2.2
Using activesupport 4.2.4
Using addressable 2.3.8
Using multipart-post 2.0.0
Using faraday 0.9.1
Using jwt 1.5.1
Using mqtt 0.3.1
Using multi_json 1.11.2
Using signet 0.5.1
Using libmagellan 0.2.4
Using bundler 1.10.6
Bundle complete! 1 Gemfile dependency, 15 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

クライアントはひとつのファイルで実装するため、まずは client.rb というファイルを作成します。

# coding: utf-8

require 'json'
require 'libmagellan'

また、MAGELLAN にアクセスするための情報を定数で定義します。

MAGELLAN_CONSUMER_KEY = "mfu6rjamyvqyjshk"
MAGELLAN_CONSUMER_SECRET = "********************************"
MAGELLAN_CLIENT_VERSION = "DefaultStage1"
MAGELLAN_HTTP_SERVER = "https://nebula-001a-web.magellanic-clouds.net"
MAGELLAN_MQTT_SERVER_HOST = "nebula-001a-mqtt.magellanic-clouds.net"
MAGELLAN_MQTT_SERVER_PORT = 1883

ルーム一覧の取得と表示

Worker で実装したルーム一覧取得 API /rooms を呼び出してルーム一覧を取得します。

準備として Libmagellan.new でクライアントインスタンスを生成します。 このとき必要なオプションを指定します。

client = Libmagellan.new(MAGELLAN_HTTP_SERVER,
                         consumer_key: MAGELLAN_CONSUMER_KEY,
                         consumer_secret: MAGELLAN_CONSUMER_SECRET,
                         client_version: MAGELLAN_CLIENT_VERSION,
                         mqtt_host: MAGELLAN_MQTT_SERVER_HOST,
                         mqtt_port: MAGELLAN_MQTT_SERVER_PORT)
response = client.request("/rooms")
body = response.body
data = JSON.parse(body)
puts data

bundle execを使ってclient.rbを実行してみます。

$ bundle exec ruby client.rb
{"id"=>1, "name"=>"Room000", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>2, "name"=>"Room001", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>3, "name"=>"Room002", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>4, "name"=>"Room003", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>5, "name"=>"Room004", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>6, "name"=>"Room005", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>7, "name"=>"Room006", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>8, "name"=>"Room007", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>9, "name"=>"Room008", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>10, "name"=>"Room009", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}

MAGELLAN にアクセスしてデータが取得できました。

この後、その他の機能を実装していくため、クラス化しておきます。

# coding: utf-8

require 'json'
require 'libmagellan'

MAGELLAN_CONSUMER_KEY = "mfu6rjamyvqyjshk"
MAGELLAN_CONSUMER_SECRET = "********************************"
MAGELLAN_CLIENT_VERSION = "DefaultStage1"
MAGELLAN_HTTP_SERVER = "https://nebula-001a-web.magellanic-clouds.net"
MAGELLAN_MQTT_SERVER_HOST = "nebula-001a-mqtt.magellanic-clouds.net"
MAGELLAN_MQTT_SERVER_PORT = 1883

class MagellanClient

  class << self
    def run(args)
      instance = MagellanClient.new
      instance.run(args)
    end
  end

  def run(args)
    command = args.shift
    if command
      case command.to_sym
      when :rooms then get_rooms()
      else help()
      end
    else
      help()
    end
  end

  # Get rooms list.
  # @return
  def get_rooms
    rooms = parse_body(http_request("/rooms").body, [])
    puts rooms
  end


  private

  # Create and return MAGELLAN client instance.
  # @return [Libmagellan] MAGELLAN client
  def client
    @client_ ||= Libmagellan.new(MAGELLAN_HTTP_SERVER,
                                 consumer_key: MAGELLAN_CONSUMER_KEY,
                                 consumer_secret: MAGELLAN_CONSUMER_SECRET,
                                 client_version: MAGELLAN_CLIENT_VERSION,
                                 mqtt_host: MAGELLAN_MQTT_SERVER_HOST,
                                 mqtt_port: MAGELLAN_MQTT_SERVER_PORT)
  end

  def http_request(path, method=:get, body="", headers={})
    client.request(path, method, body, headers)
  end

  def parse_body(body, default_value=nil)
    JSON.parse(body)
  rescue JSON::ParserError
    default_value
  end

  # Show help
  def help
    puts <<__HELP__
Usage:

  $ bundle exec ruby client.rb COMMAND [ARGS,]

Commands:

  client.rb rooms       # Get rooms list

__HELP__
  end

end

MagellanClient.run(ARGV.dup)

先ほどとやることは変わりませんが、クラス化してメソッドを分割しました。 また、実行時の引数でコマンドを指定し、処理を分岐できるようにしました。

$ bundle exec ruby client.rb
Usage:

  $ bundle exec ruby client.rb COMMAND [ARGS,]

Commands:

  client.rb rooms       # Get rooms list

$ bundle exec ruby client.rb rooms
{"id"=>1, "name"=>"Room000", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>2, "name"=>"Room001", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>3, "name"=>"Room002", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>4, "name"=>"Room003", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>5, "name"=>"Room004", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>6, "name"=>"Room005", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>7, "name"=>"Room006", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>8, "name"=>"Room007", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>9, "name"=>"Room008", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}
{"id"=>10, "name"=>"Room009", "created_at"=>"2015-08-24T23:26:32.000Z", "updated_at"=>"2015-08-24T23:26:32.000Z"}

ルーム一覧の表示を少し整えます。

puts rooms の部分を変更します。

    rooms.each do |room|
      puts "[#{room['id'].to_s.rjust(2)}] #{room['name']}"
    end
$ bundle exec ruby client.rb rooms
[ 1] Room000
[ 2] Room001
[ 3] Room002
[ 4] Room003
[ 5] Room004
[ 6] Room005
[ 7] Room006
[ 8] Room007
[ 9] Room008
[10] Room009

部屋選択と入室

入室 API はルーム ID と入室ユーザの名前を指定して、POST でリクエストを送信します。

  # Enter room with username
  # @param [Array] Arguments
  def post_login(args)
    room_id = args.shift
    if room_id.nil? or room_id.empty?
      puts "Please enter room id"
      exit(0)
    end
    username = args.shift
    if username.nil? or username.empty?
      puts "Please enter your name"
      exit(0)
    end

    res = parse_body(http_request("/rooms/#{room_id}/users", :post, {name: username}).body, {})
    if res['result']
      puts "Login Success"
    else
      error = res['error']
      puts "Login Failured: #{error}"
    end
  end

簡易なバリデーションチェックを行なった後、必要なパラメータ(name)を POST で送信します。 レスポンスをチェックして、reuslttrue の場合は正常に入室できているので、メッセージを表示します。 resultfalse のときは、入室に失敗しているのでその旨を表示します。

また、run メソッドの case 文に以下を追記します。

      when :login then post_login(args)

Subscribe の開始

このままでは入室したという情報だけで、実際にそのルームのメッセージを受け取ることはできません。 入室に成功したら、続けてメッセージの購読(Subscribe)を開始するようにします。

Subscribe を開始するにはクライアントの subscribe メソッドを利用します。 引数は購読を開始するトピック名で、ブロックを渡したときにはそのままメッセージの待ち受けを行ないます。

メッセージを受け取ったときにブロックが実行されますが、そのときの引数はメッセージが送られたトピック名と、送信されたメッセージの内容となります。

post_login メソッドの入室成功(puts "Login Success")の後に以下の処理を記述します。

      # Start subscribe
      target_topic = "worker/rooms/#{room_id}"
      client.subscribe(target_topic) do |topic, message|
        puts "[#{topic}] #{message}"
      end

worker/rooms/#{room_id} というトピックで待ち受けを開始します。

クライアントを実行してみます。

Room000yourname という名前で入室します。

$ bundle exec ruby client.rb login 1 yourname
Login Success

と表示されて待ち受けを開始します。

テストとして irb を使ってメッセージを送信してみます。 別画面でターミナルウィンドウを開きます。

$ irb -r libmagellan
irb> c = Libmagellan.new("https://nebula-001a-web.magellanic-clouds.net", consumer_key: "mfu6rjamyvqyjshk", consumer_secret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", client_version: "DefaultStage1", mqtt_host: "nebula-001a-mqtt.magellanic-clouds.net", mqtt_port: 1883)
irb> c.ping
irb> c.publish("worker/rooms/1", "Hello, MAGELLAN.")

と送信すると、client.rb を実行している画面に、

[worker/rooms/1] Hello, MAGELLAN.

と表示されます。

ルームに発言する

先ほどのテストで行なったように、libmagellanpublish メソッドを使うことでメッセージが送信できるので、client.rb にも発言するためのメソッドを実装します。

  # Publish message to room
  # @param [Array] Arguments
  def publish_message(args)
    room_id = args.shift
    if room_id.nil? or room_id.empty?
      puts "Please enter room id"
      exit(0)
    end
    message = (args.shift || "").dup
    target_topic = "worker/rooms/#{room_id}"
    client.publish(target_topic, message)
  end

また、run メソッドの case 文に以下を追記します。

      when :chat then publish_message(args)

実行するために先ほどの subscribe を開始しているターミナルウィンドウとは別のターミナルウィンドウを開きます。このとき、client ディレクトリに移動することを忘れないでください。

$ cd /path/to/your-directory/client
$ bundle exec ruby client.rb chat 1 "Hello, MAGELLAN."

と実行すると、Subscribeしているウィンドウに以下のように表示されます。

[worker/rooms/1] Hello, MAGELLAN.

入室処理の修正

このままでは、メッセージを送信する度にルーム ID を入力しないといけないのと、誰が発言したのか分からないため、入室時に入室ユーザとルーム ID をファイルに保存するようにします。

発言時にはこのファイルを読み込み入室しているユーザとルーム ID を取得するようにします。

post_login メソッドの Start subscribe の前に以下の処理を追加します。

      # Save room id and username
      data = {room_id: room_id, username: username}
      File.open(".rails-worker-example", "w") do |io|
        io.write(data.to_json)
      end

Subscribe しているターミナルの処理を Ctrl+C で終了します(エラーが表示されますが、ここでは無視します)。

再度入室処理を実行します。

$ bundle exec ruby client.rb login 1 yourname
Login Success

メッセージ送信処理の修正

続けて publish_message を修正します。 room_id を引数から取得している部分を入室時に保存した .rails-worker-example というファイルから取得するようにします。 また、メッセージの送信データを、メッセージだけではなくユーザ名も含めるようにするため、JSON データにします。

    # Publish message to room
    # @param [Array] Arguments
    def publish_message(args)
-     room_id = args.shift
-     if room_id.nil? or room_id.empty?
-       puts "Please enter room id"
-       exit(0)
-     end
      message = (args.shift || "").dup
+     data = parse_body(File.read(".rails-worker-example"), {})
+     if data.empty?
+       puts "Please login before publish message."
+       exit(0)
+     end
+
+     room_id = data['room_id']
+     username = data['username']
+     payload = {username: username, message: message}.to_json
      target_topic = "worker/rooms/#{room_id}"
-     client.publish(target_topic, message)
+     client.publish(target_topic, payload)
    end
$ bundle exec ruby client.rb chat 'Hello, MAGELLAN.'

メッセージを送信すると、Subscribe しているターミナルウィンドウには以下のような表示がされます。

[worker/rooms/1] {"username":"yourname","message":"Hello, MAGELLAN."}

メッセージの表示を修正

メッセージが JSON 形式で送信されたので、最後に表示を調整します。

表示形式としては、

[YYYY-MM-DD hh:mm:ss] {username}: {message}

とします。

post_login メソッドの client.subscribe ブロックを修正します。

-      client.subscribe(target_topic) do |topic, message|
-        puts "[#{topic}] #{message}"
+      client.subscribe(target_topic) do |topic, payload|
+        data = parse_body(payload)
+        if not data.nil? and data.is_a?(::Hash)
+          username = data['username']
+          message = data['message']
+          puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{username.rjust(16)}: #{message}"
+        else
+          puts payload
+        end
       end

Subscribe しているターミナルウィンドウを Ctrl+C で終了し、再度起動します。

$ bundle exec ruby client.rb login 1 yourname
Login Success

メッセージの送信を別ウィンドウで行ないます。このとき、client ディレクトリに移動することを忘れないでください。

$ cd /path/to/your-directory/client
$ bundle exec ruby client.rb chat 'Hello, My name is Yourname.'

Subscribe ウィンドウに、

[2015-08-25 09:23:12]         yourname: Hello, My name is Yourname.

と表示されます。

以上で、Ruby 版クライアントの完成です。