WebSocket and Web Application Framework


[WebSocket](https://tools.ietf.org/html/rfc6455) はネットワークの双方向通信の規格で、リアルタイム通信の用途で利用されています。
**Web Application Framework** の [Phoenix](http://www.phoenixframework.org) は **WebSocket** を標準でサポートしています。
[Rails](http://rubyonrails.org) も [Action Cable](https://github.com/rails/actioncable) で **WebSocket** の利用がサポートされるようになります。

そこで [Express](http://expressjs.com/) を合わせて３つの **Framework** で **WebSocket** のサンプルを作成しました。
( *bash* コマンドの `brew` は  **Mac OS X** で利用できます。他OSは適宜のコマンドを利用してください。 )

---

## Node.js on Express

![logo-express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/websocket/logo/express.png)

**Express** は JavaScript が言語の **Web Application Framework** です。
[Socket.IO](http://socket.io/) という **WebSocket** のライブラリが有名です。

### Setup

* [Node.js](https://nodejs.org) をインストールすると `npm` のコマンドが利用できます。
* `npm` コマンドで **Express** の **Generator** をインストールします。

```bash
$ brew install node
$ npm install express-generator -g
```

* `express` コマンドで **Web Application** を作成します。
* `npm` コマンドで **Socket.IO** のパッケージをインストールします。

```bash
$ express express-socketio-example
$ cd express-socketio-example && npm install
$ npm install socket.io --save
```

### Making

**Socket.IO** の実装は[こちら](http://socket.io/docs/#using-with-express-3/4)が参考になります。

#### Server

サーバーサイドに **WebSocket** の接続処理を記述します。

* **Express** の実行ファイルの最後に **Socket.IO** の設定を追加します。
    * `io.on` は接続の処理です。
    * `io.emit` でメッセージが送信されます。
    * `socket.on` でメッセージが受信されます。

`bin/www`:

```
var io = require('socket.io')(server);
io.on('connection', function(socket){
  socket.on('message from client', function(message){
    io.emit('message from server', message);
  });
});
```

#### Client

フロントエンドにも **WebSocket** の接続処理を記述します。

* *View* のファイルに **Socket.IO** の設定を追加します。
    * `socket = io` で **Socket** を設定します。
    * `socket.emit` でメッセージが送信されます。
    * `socket.on` でメッセージが受信されます。
* *DOM* の操作は **JQuery** を利用します。
* メッセージは `<div id="messages"></div>` に出力します。

`views/index.jade`:

```
extends layout

block content
  form
    input#content
    button Send
  #messages
  script(src='/socket.io/socket.io.js')
  script(src='http://code.jquery.com/jquery-1.11.3.min.js')
  script().
    var socket = io();
    $('form').submit(function(){
      socket.emit('message from client', $('#content').val());
      $('#content').val('');
      return false;
    });
    socket.on('message from server', function(message){
      $('#messages').append($('<p>').text(message));
    });
```

### Start

* アプリケーションを起動します。

```bash
$ DEBUG=express-socketio-example:* npm start
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/websocket/example/express.png)
[サンプルコード](https://github.com/dddrb/express-socketio-example)

これだけで簡単なチャットアプリケーションができます。

---

## Elixir on Phoenix

![logo-phoenix](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/websocket/logo/phoenix.png)

**Phoenix** は [Elixir](http://elixir-lang.org) が言語の **Web Application Framework** です。
**WebSocket** が標準の機能で利用することができます。
**Elixir** は Erlang VM で実行されます。外部DSLが簡単に実装できます。

### Setup

* **Elixir** をインストールすると `mix` のコマンドが利用できます。
* `mix` コマンドで **Phoenix** をインストールします。
* `mix archive.install` は[ここ](https://github.com/phoenixframework/phoenix/releases/)から *[URL]* を取得します。

```bash
$ brew install elixir
$ mix local.hex
$ mix archive.install https://[URL]/phoenix_new-x.x.x.ez
```

* デフォルトのデータベースに **PostgreSQL** が利用されます。

```bash
$ brew install postgresql
$ createuser -d -P postgres
$ postgres -D /usr/local/var/postgres
```

* `mix phoenix.new` コマンドで **Web Application** を作成します。
* `mix phoenix.gen.html` コマンドで *Message* の **Generator** を実行します。

```bash
$ mix phoenix.new phoenix_channel_example
$ cd phoenix_channel_example
$ mix ecto.create
$ mix phoenix.gen.html Message messages content:text
```

* **Router** に *Message* を追加します。（よく忘れます。）

`web/router.ex`:

```
resources "/messages", MessageController
```

### Making

**Channels** の実装は[こちら](http://www.phoenixframework.org/docs/channels)が参考になります。

#### Server

* **Channels** のルーティングを設定します。

`web/channels/user_socket.ex`:

```
channel "rooms:*", PhoenixChannelExample.RoomChannel
```

* **Channels** の設定します。
    * `join` でチャンネルを設定します。
    * `handle_out` でメッセージが送信されます。
    * `handle_in` でメッセージが受信されます。

`web/channels/room_channel.ex`:

```
defmodule PhoenixChannelExample.RoomChannel do
  use Phoenix.Channel

  def join("rooms:lobby", auth_msg, socket) do
    {:ok, socket}
  end

  def join("rooms:" <> _private_room_id, _auth_msg, socket) do
    {:error, %{reason: "unauthorized"}}
  end

  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast! socket, "new_msg", %{body: body}
    {:noreply, socket}
  end

  def handle_out("new_msg", payload, socket) do
    push socket, "new_msg", payload
    {:noreply, socket}
  end
end
```

#### Client

* **Channels** の設定します。
    * `chan = socket.channel` でチャンネルを設定します。
    * `chan.push` でメッセージが送信されます。
    * `chan.on` でメッセージが受信されます。

`web/static/js/app.js`:

```
import {Socket} from "deps/phoenix/web/static/js/phoenix"

let message_content   = $("#message_content")
let messagesContainer = $("#messages")

let socket = new Socket("/socket")
socket.connect()
let chan = socket.channel("rooms:lobby", {})

message_content.on("keypress", event => {
  if(event.keyCode === 13){
    chan.push("new_msg", {body: message_content.val()})
    message_content.val("")
    return false
  }
})

chan.on("new_msg", payload => {
  messagesContainer.append(`<br/>${payload.body}`)
})

chan.join().receive("ok", chan => {
  console.log("Welcome to Phoenix Chat!")
})
```

* *DOM* の操作は **JQuery** を利用します。

`web/templates/layout/app.html.eex`:

```
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
```

* メッセージは `<div id="messages"></div>` に出力します。

`web/templates/message/new.html.eex`:

```
<div id="messages"></div>
```

### Run

* アプリケーションを起動します。

```bash
$ mix ecto.migrate
$ mix phoenix.server
```

![phoenix](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/websocket/example/phoenix.png)
[サンプルコード](https://github.com/dddrb/phoenix-channel-example)

手順は増えましたが、高度なチャットアプリケーションが簡単にできます。

---

## Ruby on Rails

![logo-rails](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/websocket/logo/rails.png)

**Rails** は [Ruby](https://www.ruby-lang.org) が言語の **Web Application Framework** です。
**WebSocket** は **ActionCable** の機能で利用することができます。
**Ruby** はもちろん、外部DSLが簡単に実装できます。

### Setup

* **Ruby** をインストールすると `gem` のコマンドが利用できます。
* 今回は `gem` コマンドで **Rails** をインストールしません。
* `gem` コマンドで [Bundler](http://bundler.io/) をインストールします。

```bash
$ brew install ruby
$ gem install bundler
```

* 最新バージョンの **Rails** をインストールします。
* `rails` コマンドで **Web Application** を作成します。
* `--edge` オプションを指定すると *Gemfile* が **GitHub** を参照します。

```bash
$ git clone https://github.com/rails/rails.git
$ cd rails
$ bundle install
$ cd ..
$ rails/railties/exe/rails new rails-actioncable-example --edge
$ cd rails-actioncable-example
```

* **ActionCable** で **Redis** を利用します。

```bash
$ brew install redis
```

* **Rails** で利用するライブラリを `Gemfile` に記述します。

`Gemfile`:

```
gem 'actioncable', github: 'rails/actioncable'
gem 'puma'
gem 'foreman', require: false
```

* `rails generate` コマンドで *Message* の **Generator** を実行します。

```
$ bundle
$ bundle exec rails generate scaffold Message content:text
```

### Making

*ActionCable* の実装は[こちら](https://github.com/rails/actioncable-examples)が参考になります。

#### Server

* 接続のスーパークラスを記述します。（ジェネレーターまだかなぁ）

`app/channels/application_cable/connection.rb`:

```
module ApplicationCable
  class Connection < ActionCable::Connection::Base
  end
end
```

* チャンネルのスーパークラスを記述します。（ジェネレーターまだかなぁ）

`app/channels/application_cable/channel.rb`:

```
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
```

* チャンネルのSubscribe(購読)を設定します。

`app/channels/messages_channel.rb`:

```
class MessagesChannel < ApplicationCable::Channel
  def subscribed
    stream_from "messages"
  end
end
```

* `POST /messages` の `server.broadcast` でメッセージが送信されます。

`app/controllers/messages_controller.rb`:

```
def create
  ActionCable.server.broadcast 'messages', message: params[:message][:content]
  head :ok
end
```

* **ActionCable** の **Rack** の設定を記述します。

`config/racks/cable.ru`:

```
require ::File.expand_path('../../environment',  __FILE__)
Rails.application.eager_load!

require "action_cable/process/logging"

run ActionCable.server
```

* **ActionCable** 設定を **yml** 記述します。

`config/redis/cable.yml`:

```
production: &base
  :url: redis://localhost:6379
  :host: localhost
  :port: 6379
  :timeout: 1
  :inline: true
development: *base
test: *base
```

* **Rails** と **ActionCable** の実行コマンドを記述します。

`Procfile`:

```
web: bundle exec rails server
cable: bundle exec puma -p 28080 config/racks/cable.ru
```

#### Client

* `App.cable = Cable.createConsumer` でチャンネルを設定ます。

`app/assets/javascripts/channels/index.coffee`:

```
#= require cable
#= require_self
#= require_tree .

@App = {}
App.cable = Cable.createConsumer "ws://localhost:28080"
```

* `received` でメッセージが受信されます。
* *DOM* の操作は **JQuery** を利用します。

`app/assets/javascripts/messages.coffee`:

```
App.messages = App.cable.subscriptions.create 'MessagesChannel',
  received: (data) ->
    $('#messages').append($('<p>').text(data.message))
```

* `form_for` の `remote: true` でページリロードを抑制します。
* `Submit` でメッセージが送信されます。

`app/views/messages/_form.html.erb`:

```
<%= form_for(message, remote: true) do |f| %>
```

* メッセージは `<div id="messages"></div>` に出力します。

`app/views/messages/new.html.erb`:

```
<div id='messages'></div>
```

### Run

* アプリケーションを起動します。

```bash
$ rake db:migrate
$ bundle exec foreman start
```

![rails](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/websocket/example/rails.png)
[サンプルコード](https://github.com/dddrb/rails-actioncable-example)

かなり手順は増えましたが、このチャットアプリケーションはスケールアウトに対応しています。

## Tips

### ActionCable with ActiveJob

**Node.js** や **Elixir** は非同期処理のメカニズムがあり、多くのリクエストを処理するのに向いています。
**Ruby** で非同期処理を実装するには **EventMachine** のライブラリを利用するのですが、さらに **Rails** では **ActiveJob** のライブラリを利用したり、 **Redis** の `Pub/Sub` を利用して非同期処理を実現しています。

* まず、 **sidekiq** のライブラリを `Gemfile` に記述します。

`Gemfile`:

```
gem 'sidekiq'
```

* 次に、メッセージを送信する処理を `Job` に記述します。

`app/jobs/message_job.rb`

```
class MessageJob < ApplicationJob
  queue_as :default

  def perform(*args)
    ActionCable.server.broadcast 'messages', message: args[0]
  end
end
```

* さきほどの `POST /messages` に非同期の設定を記述します。

`app/controllers/messages_controller.rb`:

```
def create
  MessageJob.perform_later(params[:message][:content])
  head :ok
end
```

*Enjoy WebSocket*