Phoenix Channelで作る最先端Webアプリ - topic-subtopic編 - Qiita
Phoenix Channelで作る最先端Webアプリ - ETS編 - Qiita
Phoenix Channelで作る最先端Webアプリ - Fault Tolerance編 - Qiita
Phoenix Channelで作る最先端Webアプリ - 地図(Geo)拡張編 - Qiita
Phoenix Channelで作る最先端Webアプリ - DynamicSupervisor編 - Qiita
Phoenix Channelで作る最先端Webアプリ - Elixier Application編 - Qiita
Phoenix Channelで作る最先端Webアプリ - Reactチャット編 - Qiita
■ Phoenix Channelをテーマにした過去記事
Phoenix Channelとelm-phoenixについて -Qiita
東京電力電力供給状況監視 - Phoenix Channel - Qiita
今回の技術的なポイントは以下の通りです。
1.Phoenix Channelのtopic/subtopicで独立した複数の部屋を作る
2.Phoenix Channelでauthorized?(join名の2重チェック、人数制限)を行う
channelにjoinするときにsubtopicを指定することで、チャットルームを別々にすることができます。Presenceも別々に管理されます。今回はPhoenix Channelで作る最先端Webアプリ - Reactチャット編 - Qiitaに少し修正を加え、topic-subtopicの仕様を確認しました。
1.実験結果を画像で確認する
ソースコードに簡単な修正を加え、実験した結果です。
2つブラウザを開いて、それぞれ「ジョン」と「ポール」というユーザ名で、「ビートルズ」というサブトピック名を入力しjoinします。参加者の横の「ジョン @ ビートルズ」が自分自身で「ビートルズ」というサブトピックに参加しているのがわかります。
もう2つブラウザを開き、それぞれ「山田隆夫」と「新井康弘」というユーザ名で、「ずうとるび」というサブトピック名を入力しjoinします。サブトピック名が異なると、Presence(参加者)も異なることがわかります。期待通り「ビートルズ」と「ずーとるび」というサブトピックのPresence(参加者)は独立しています。
しかし山田隆夫には伝わっていません。サブトピックが違うからですね。
2.ソースコードの修正
開発手順はPhoenix Channelで作る最先端Webアプリ - Reactチャット編 - Qiitaと同じです。ソースコードもほぼ同じですので、修正を加えた部分のみ説明します。但しプロジェクト名はReactChatからReactChatTopicに変えています。
まずchannel moduleですが、joinのtopic-subtopic引数を、subtopicの部分だけ変数で受けるようにします。subtopicは任意の文字列でokになります。
#
def join("room:"<>subtopic, %{"user_name" => user_name}, socket) do
IO.puts ("subtopic=#{subtopic}")
send(self(), {:after_join, user_name})
{:ok, socket}
end
#
以下Chat.jsですが、画面通りに修正した個所のみをピックアップします。
#
this.state = {
#
inputUser: "", //ユーザ名の入力
inputSubtopic: "", //サブトピックの入力
inputMessage: "", //メッセージの入力
#
}
}
#
handleInputSubtopic(event) {
this.setState({
inputSubtopic: event.target.value
})
}
#
// join処理
handleJoin(event) {
event.preventDefault();
if(this.state.inputUser!="" && this.state.inputSubtopic!="") {
#
this.channel = this.socket.channel("room:"+this.state.inputSubtopic, {user_name: this.state.inputUser});
#
// 画面表示
render() {
#
let form_jsx;
if(this.state.isJoined===false) {
form_jsx = (
<form onSubmit={this.handleJoin.bind(this)} >
<label>ユーザ名を指定してJoin</label>
<TextField hintText="ユーザ名" value = {this.state.inputUser} onChange = {this.handleInputUser.bind(this)} />
<TextField hintText="サブトピック名" value = {this.state.inputSubtopic} onChange = {this.handleInputSubtopic.bind(this)} />
<RaisedButton type="submit" primary={true} label="Join" />
</form>
);
} else {
form_jsx = (
<div>
<Paper style={style1}>
<label>参加者 : {this.state.inputUser} @ {this.state.inputSubtopic}</label>
<ul>
{presences_list}
</ul>
#
<Paper style={style1}>
<form onSubmit={this.handleSubmit.bind(this)}>
<label>チャット</label>
<TextField hintText="Chat Text" value = {this.state.inputMessage} onChange = {this.handleInputMessage.bind(this)} />
<RaisedButton type="submit" primary={true} label="Submit" />
</form>
#
3.5人目のビートルズは要らない
今度はチャットルームに入場制限をかけたいと思います。同じ名前でのjoinを禁止し、更に5人以上はjoinできないようにします。これらの制限がsubtopic単位で行われることに注意してください。
channel moduleに以下のようなコードを追加します。if authorized?(socket, user_name) doでjoinする前にチェックしています。下手な説明をするよりはソースを見ていただければわかりやすい思います。
#
def join("room:"<>subtopic, %{"user_name" => user_name}, socket) do
IO.puts ("subtopic=#{subtopic}")
if authorized?(socket, user_name) do
send(self(), {:after_join, user_name})
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
defp authorized?(socket, user_name) do
number_of_players(socket) < 4 && !existing_player?(socket, user_name)
end
defp number_of_players(socket) do
socket
|> Presence.list()
|> Map.keys()
|> length()
end
defp existing_player?(socket, user_name) do
socket
|> Presence.list()
|> Map.has_key?(user_name)
end
#
実際に動作を確認します。まず4人制限を見ます。画面でエラー処理をするのが面倒なので、iexシェルでエラー処理を確認します。最後の行でエラーになっているのが確認できます。5人目(ブライアン)を追加しようとしてエラーになっています。ビートルズに5人目は要りませんね。
subtopic=ビートルズ
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ジョン"}
[info] Replied room:ビートルズ :ok
subtopic=ビートルズ
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ポール"}
[info] Replied room:ビートルズ :ok
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "リンゴ"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :ok
subtopic=ビートルズ
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ジョージ"}
[info] Replied room:ビートルズ :ok
subtopic=ビートルズ
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ブライアン"}
[info] Replied room:ビートルズ :error
今度は5人目(ブライアン)が「ずーとるび」に参加してみます。これはokですので、この人数制限はサブトピック単位で行われていることがわかります。
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ジョン"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :ok
[info] GET /
[debug] Processing with ReactChatTopicWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 412μs
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ポール"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :ok
[info] GET /
[debug] Processing with ReactChatTopicWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 418μs
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "リンゴ"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :ok
[info] GET /
[debug] Processing with ReactChatTopicWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 377μs
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ジョージ"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :ok
[info] GET /
[debug] Processing with ReactChatTopicWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 415μs
[info] JOIN "room:ずーとるび" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ブライアン"}
subtopic=ずーとるび
[info] Replied room:ずーとるび :ok:
今度はジョンというユーザ名で「ビートルズ」サブトピックに2重にjoinしてみます。これはエラーになります。しかし「ずーとるび」サブトピックにならjoinできました。これもsubtopic単位で行われていることがわかりました。
[info] Sent 200 in 59ms
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ジョン"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :ok
[info] GET /
[debug] Processing with ReactChatTopicWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 386μs
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ポール"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :ok
[info] GET /
[debug] Processing with ReactChatTopicWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 520μs
[info] JOIN "room:ビートルズ" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ジョン"}
subtopic=ビートルズ
[info] Replied room:ビートルズ :error
[info] Sent 200 in 414μs
subtopic=ずーとるび
[info] JOIN "room:ずーとるび" to ReactChatTopicWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (2.0.0)
Serializer: Phoenix.Transports.V2.WebSocketSerializer
Parameters: %{"user_name" => "ジョン"}
[info] Replied room:ずーとるび :ok:
今回は以上となります。
4.修正ソースコードの全リスト
今回修正したソースの全リストを掲載します。
defmodule ReactChatTopicWeb.RoomChannel do
use ReactChatTopicWeb, :channel
alias ReactChatTopicWeb.Presence
def join("room:"<>subtopic, %{"user_name" => user_name}, socket) do
IO.puts ("subtopic=#{subtopic}")
if authorized?(socket, user_name) do
send(self(), {:after_join, user_name})
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
defp authorized?(socket, user_name) do
number_of_players(socket) < 4 && !existing_player?(socket, user_name)
end
defp number_of_players(socket) do
socket
|> Presence.list()
|> Map.keys()
|> length()
end
defp existing_player?(socket, user_name) do
socket
|> Presence.list()
|> Map.has_key?(user_name)
end
def handle_in("new_msg", %{"msg" => msg}, socket) do
user_name = socket.assigns[:user_name]
broadcast(socket, "new_msg", %{msg: msg, user_name: user_name})
{:reply, :ok, socket}
end
def handle_info({:after_join, user_name}, socket) do
push(socket, "presence_state", Presence.list(socket))
{:ok, _ref} = Presence.track(socket, user_name, %{online_at: now()})
{:noreply, assign(socket, :user_name, user_name)}
end
def terminate(_reason, socket) do
{:noreply, socket}
end
defp now do
System.system_time(:seconds)
end
end
import React from "react";
import {Socket, Presence} from "phoenix"
import RaisedButton from 'material-ui/RaisedButton';
import Paper from 'material-ui/Paper';
import Divider from 'material-ui/Divider';
import TextField from 'material-ui/TextField';
class Chat extends React.Component {
constructor() {
super();
this.state = {
isJoined: false, //joinしているかどうか。画面を切り替える。
inputUser: "", //ユーザ名の入力
inputSubtopic: "", //サブトピックの入力
inputMessage: "", //メッセージの入力
messages: [], //受け取ったメッセージの配列
presences: {} //presence(参加ユーザ)の状態
}
}
handleInputUser(event) {
this.setState({
inputUser: event.target.value
})
}
handleInputSubtopic(event) {
this.setState({
inputSubtopic: event.target.value
})
}
handleInputMessage(event) {
this.setState({
inputMessage: event.target.value
})
}
// join処理
handleJoin(event) {
event.preventDefault();
if(this.state.inputUser!="" && this.state.inputSubtopic!="") {
// assets/js/socket.jsのデフォルトの定義と同じ
this.socket = new Socket("/socket", {params:
{token: window.userToken}
});
this.socket.connect();
this.channel = this.socket.channel("room:"+this.state.inputSubtopic, {user_name: this.state.inputUser});
// Presences:現在のサーバの状態を初期状態として設定
this.channel.on('presence_state', state => {
let presences = this.state.presences;
presences = Presence.syncState(presences, state);
this.setState({ presences: presences })
console.log('state', presences);
});
// Presences:初期状態からの差分を更新していく
this.channel.on('presence_diff', diff => {
let presences = this.state.presences;
presences = Presence.syncDiff(presences, diff);
this.setState({ presences: presences })
console.log('diff', presences);
});
// メッセージを受け取る処理
this.channel.on("new_msg", payload => {
let messages = this.state.messages;
messages.push(payload)
this.setState({ messages: messages })
})
// channelにjoinする
this.channel.join()
.receive("ok", response => { console.log("Joined successfully", response) })
.receive('error', resp => { console.log('Unable to join', resp); });
this.setState({ isJoined: true })
}
}
// 退室の処理 socketを切断するだけ。これでいいのか?
handleLeave(event) {
event.preventDefault();
this.socket.disconnect();
this.setState({ isJoined: false })
}
// メッセージ送信の処理
handleSubmit(event) {
event.preventDefault();
this.channel.push("new_msg", {msg: this.state.inputMessage})
this.setState({ inputMessage: "" })
}
// 画面表示
render() {
const style1 = { margin: '16px 32px 16px 16px', padding: '10px 32px 10px 26px',};
const style2 = { display: 'inline-block', margin: '1px 8px 1px 4px',};
const messages = this.state.messages.map((message, index) => {
return (
<div key={index}>
<p><strong>{message.user_name}</strong> > {message.msg}</p>
</div>
)
});
let presences = [];
Presence.list(this.state.presences, (name, metas) => {
presences.push(name);
});
let presences_list = presences.map( (user_name, index) =>
<li key={index} style={style2}>{user_name}</li>
);
let form_jsx;
if(this.state.isJoined===false) {
form_jsx = (
<form onSubmit={this.handleJoin.bind(this)} >
<label>ユーザ名を指定してJoin</label>
<TextField hintText="ユーザ名" value = {this.state.inputUser} onChange = {this.handleInputUser.bind(this)} />
<TextField hintText="サブトピック名" value = {this.state.inputSubtopic} onChange = {this.handleInputSubtopic.bind(this)} />
<RaisedButton type="submit" primary={true} label="Join" />
</form>
);
} else {
form_jsx = (
<div>
<Paper style={style1}>
<label>参加者 : {this.state.inputUser} @ {this.state.inputSubtopic}</label>
<ul>
{presences_list}
</ul>
<div align="right">
<form onSubmit={this.handleLeave.bind(this)} >
<RaisedButton type="submit" primary={true} label="Leave" />
</form>
</div>
</Paper>
<Paper style={style1}>
<form onSubmit={this.handleSubmit.bind(this)}>
<label>チャット</label>
<TextField hintText="Chat Text" value = {this.state.inputMessage} onChange = {this.handleInputMessage.bind(this)} />
<RaisedButton type="submit" primary={true} label="Submit" />
</form>
<Divider />
<br />
<div>
{messages}
</div>
</Paper>
</div>
);
}
return (
<div>
{form_jsx}
</div>
)
}
}
export default Chat
以上です。