Erlang WebサーバCowboy - WebSocket編
2014年10月8日
WebSocketとは
WebSocket(ウェブソケット)は、コンピュータ・ネットワーク用の通信規格の1つです。インターネットの標準化団体であるW3CとIETFがウェブサーバーとウェブブラウザとの間の通信のために規定を予定している双方向通信用の技術規格であり、APIはW3Cが、WebSocket プロトコルはIETFが策定に関与している。プロトコルの仕様は RFC 6455。TCP上で動く。
WebSocketを使うことで、サーバがリクエストをされなくても、クライアントにデータを送信することができます。
目的
Twitterホームの様なアプリケーションを作る。
動作としては、新しいメッセージが投稿されたら、ページを見てるクライアントはWebSocket経由で自動的にそのメッセージが表示される。
利用するコンポネントは。
また、わかりやすい様にJiffyのJSONエンコードにマップを利用するので、Erlang 17以上が必要となります。
メッセージウォール
早速、Cowboyアプリケーションのフォルダーを作成してみましょう。
erlang.mk
をダウンロードします。
アプリケーションベースファイルを生成する。
このコマンドで下記のファイルが生成されました。
Cowboy、JiffyとGprocをプロジェクトに追加するため、Makefile
を下記のように変更。
PROJECT = hello_erlang
DEPS = cowboy jiffy gproc
dep_gproc = git https://github.com/uwiger/gproc master
include erlang.mk
Gprocはerlang.mk
のパッケージに登録されていないため、dep_gproc
でGprocのリポジトリを指定します。
アプリケーションリソースファイルsrc/message_wall.app.src
にCowboy、JiffyとGprocの依存を追加します。
{application, message_wall, [
{description, ""},
{vsn, "0.1.0"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib,
cowboy,
jiffy,
gproc
]},
{mod, {message_wall_app, []}},
{env, []}
]}.
make
コマンドでCowboy等の依存パッケージを一括にダウンロードし、コンパイルします。
アプリケーションのメインソースファイルであるsrc/message_wall_app.erl
にCowboyを初期化します。
-module(message_wall_app).
-behaviour(application).
-export([start/2]).
-export([stop/1]).
start(_Type, _Args) ->
% ETSテーブルを初期化
ets:new(message_wall, [ordered_set, named_table, public]),
% ルート宣言
Dispatch = cowboy_router:compile([
{'_', [
% cowboy_staticはパスマッチに対して、静的ファイルを読み込む
% index.htmlを読み込む
{"/", cowboy_static, {priv_file, message_wall, "index.html"}},
% /websocketのリクエストをws_handlerに渡す
{"/websocket", message_wall_handler, []}
]}
]),
% Cowboyを起動
{ok, _} = cowboy:start_http(http, 100,
[{port, 8001}],
[
{env, [{dispatch, Dispatch}]}
]),
message_wall_sup:start_link().
stop(_State) ->
ok.
解説:
ets:new(message_wall, [ordered_set, named_table, public])
はメッセージを保存するETSテーブルを作成します。{"/", cowboy_static, {priv_file, message_wall, "index.html"}},
はサイトインデックス(/
)にpriv/index.html
の静的ファイルをレスポンスします。
重要なポイントはETSテーブルの初期化です、メッセージはアプリケーションプロセスのETSテーブルで保存します。
ETSテーブルは軽く、簡単に使えるので今回のデモアプリケーションに最適です。
一つ注意点はETSがメモリ内のテーブルのため、message_wall_app
プロセスが終了する際にテーブル内のデータは消えます。
ETSをファイルシステムで保存するDETSもあります、重要なデータの場合はETSを使わない方がいいです。
Erlangとデータベース
Erlangと一緒にデータベースが付いています。
- ETS: メモリ内のシンプルなテーブル。
- DETS: ディスクETS、ETSテーブルと同じですが、ディスクでデータが保存されます。
- Mnesia: 分散型データベース。
機能と使い方がそれぞれ分かれていますので、興味がある方はぜひ「Learn you some Erlang for great good!」のBears, ETS, Beets とMnesiaと記憶術を読みください。
外部データベースへのドライバーもあります。
フロントエンド - HTML[frontend-html]
一番イメージが湧きやすいフロントエンドページを先に作ります。
フロントエンドはシンプルに
- 投稿エリア
- メッセージ一覧エリア
のみとします。
フロントエンドで利用するライブラリは以下です。
- Bootstrap: CSSプロトタイプ
- jQuery: Javascriptライブラリ
- moment.js: 時間計算Javascriptライブラリ
- ua-parser.js: ユーザエージェント解析ライブラリ
{priv_file, message_wall, "index.html"}
をアプリケーションに登録しましたので、 インデックスファイルはpriv/index.html
となります。
priv
フォルダーがまだ存在しないため、mkdir priv
等のコマンドで作成します。
body
タグ内はbootstrapのベーシックテンプレートをベースに、下記のソースにします。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>メッセージウォール(WebSocketデモ)</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<!-- bootstrap -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<!-- ua-parser.js -->
<script src="http://cdn.jsdelivr.net/ua-parser.js/0.7.0/ua-parser.min.js"></script>
<!-- moment.js -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.3/moment.min.js"></script>
<script>
// WebSocket Javascriptはここに入ります。
</script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<!-- ナビゲーション -->
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">メッセージウォール</a>
</div>
</div>
</div>
<div class="container">
<!-- 投稿エリア -->
<p><textarea class="form-control" id="message" rows="3"></textarea></p>
<p class="text-right"><button type="submit" class="btn btn-default" id="send_message">送信</button></p>
<hr />
<!-- メッセージはここに入ります -->
<div id="wall">
</div>
<!-- メッセージのテンプレート -->
<div id="message_template">
<div class="list-group list-group-item">
<h4 class="list-group-item-heading heading"><span class="date">date</span> <span class="ua">UA</span> <span class="ip">IP</span></h4>
<p class="list-group-item-text message">Message</p>
</div>
</div>
</div><!-- /.container -->
</body>
</html>
WebSocket関連の処理はJavascriptで行いますが、フロントエンドの実装の前に、ハンドラーを作ります。
ハンドラー
erlang.mk
のテンプレートシステムを利用して、ハンドラーのベースコードを生成します。
このコマンドでsrc/message_wall_handler.erl
が作成されます。
ソースは下記のようになります。
-module(message_wall_handler).
-behaviour(cowboy_websocket_handler).
-export([init/3]).
-export([websocket_init/3]).
-export([websocket_handle/3]).
-export([websocket_info/3]).
-export([websocket_terminate/3]).
-record(state, {
}).
init(_, _, _) ->
{upgrade, protocol, cowboy_websocket}.
websocket_init(_, Req, _Opts) ->
Req2 = cowboy_req:compact(Req),
{ok, Req2, #state{}}.
websocket_handle({text, Data}, Req, State) ->
{reply, {text, Data}, Req, State};
websocket_handle({binary, Data}, Req, State) ->
{reply, {binary, Data}, Req, State};
websocket_handle(_Frame, Req, State) ->
{ok, Req, State}.
websocket_info(_Info, Req, State) ->
{ok, Req, State}.
websocket_terminate(_Reason, _Req, _State) ->
ok.
解説:
init
関数はプロセスを初期化し、接続をWebSocketにアップグレード。websocket_init
関数はWebSocketハンドラーを初期化する。websocket_handle
関数はクライアントからのメッセージを処理する。websocket_info
関数は他のプロセスからのメッセージを処理する。websocket_terminate
関数はプロセスが終了された時に実行される。
今回クライアントからWebSocketメッセージは2種類です。
get_list
: 最新の10メッセージをクライアントに返す。get_list
以外のメッセージを保存し、接続している全てクライアントに新しいメッセージを送ります。
get_listメッセージ
ハンドラーはget_list
メッセージを受けた際にETSテーブルに保存されているメッセージの最新の10通を返します。 クライアントからのメッセージとなりますのでwebsocket_handle
関数を利用します。
% get_listメッセージの場合はメッセージのリストを返します
websocket_handle({text, <<"get_list">>}, Req, State) ->
% 最新のメッセージを取得する
RawMessages = get_recent_messages(10),
% メッセージをJiffyが変換できる形式に変更
Messages = format_messages(RawMessages),
% JiffyでJsonレスポンスを生成
JsonResponse = jiffy:encode(#{
<<"type">> => <<"message_list">>,
<<"messages">> => Messages
}),
% JSONを返す
{reply, {text, JsonResponse}, Req, State};
解説:
get_recent_messages/1
はETSテーブルから最新のメッセージを取得。format_messages
はETS結果をマップに変換する関数です。jiffy:encode
はレスポンスのJsonを生成します。
get_recent_messages
関数
% 最新のNumberメッセージを取得する
get_recent_messages(Number) ->
case ets:last(?TABLE) of
'$end_of_table' -> [];
Key -> get_recent_messages(Key, Number, [])
end.
get_recent_messages(_Key, 0, Messages) -> lists:reverse(Messages);
get_recent_messages('$end_of_table', _Number, Messages) -> lists:reverse(Messages);
get_recent_messages(Key, Number, Messages) ->
Message = ets:lookup(?TABLE, Key),
PreviousKey = ets:prev(?TABLE, Key),
get_recent_messages(PreviousKey, Number-1, [Message|Messages]).
解説:
?TABLE
はテーブル名のマクロ、マクロの定義は-define(TABLE, message_wall).
でできます。get_recent_messages/1
は末尾再帰関数get_recent_messages/3
を呼び出します。- ETSに10メッセージ以内が登録されている場合はテーブルの終わりに
'$end_of_table'
アトムが返されます。
% ETS結果メッセージをJiffyが変換できる形式に変更
format_message([{Time, Message, Ip, Ua}]) ->
#{
<<"date">> => unicode:characters_to_binary(iso8601(Time)),
<<"ip">> => unicode:characters_to_binary(format_ip(Ip)),
<<"text">> => Message,
<<"ua">> => Ua
}.
% IPタプルを文字列に変換
format_ip({I1,I2,I3,I4}) ->
io_lib:format("~w.~w.~w.~w",[I1,I2,I3,I4]);
format_ip(Ip) -> Ip.
% erlangのdatetimeをISO8601形式に変換
iso8601(Time) ->
{{Year, Month, Day},{Hour, Minut, Second}} = calendar:now_to_universal_time(Time),
io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Year, Month, Day, Hour, Minut, Second]).
解説:
iso8601
はtimestamp()
をISO8601形式に変換する。format_ip
はIPタプルを文字列形式に変換する。iolib:format
はiolist()
形式を返し、Jiffyにはユニコードバイナリが必要なため、unicode:characters_to_binaryでiolist()
をbinary()
に変換する。
一般メッセージ
get_list
以外の場合はメッセージをETSテーブルに保存し、 接続中のクライアントにそのメッセージをプッシュします。
処理の流れ。
- クライアントから
get_list
以外のメッセージリクエストを受けます。 - メッセージをETSテーブルに保存。
gproc
へnew_message
イベントを送ります。gproc
に登録されているプロセスにnew_message
メッセージを送ります。- プロセスがWebSocket経由で新しいメッセージをクライアントにプッシュします。
% get_list以外のメッセージは保存する
websocket_handle({text, Text}, Req, #state{ua=Ua, ip=Ip} = State) ->
Time = now(),
Message = {Time, Text, Ip, Ua},
save_message(Message),
% gprocにイベントを公開し、
% 全ての接続クライアントにwebsocket_info({gproc_ps_event, new_message, Time}, Req, State)を呼び出します
gproc_ps:publish(l, new_message, Time),
{ok, Req, State};
% ETSにメッセージを保存する
save_message({Time, Text, Ip, Ua}) ->
ets:insert(?TABLE, {Time, unicode:characters_to_binary(Text), Ip, Ua}).
解説:
- テーブルキーには絶対に重複しない
erlang:now/0
を利用します。 save_message
は単純にets:insert
を実行します。gproc_ps:publish(l, new_message, Time)
はgproc
のPubSubを利用して、他のクライエントに新しいメッセージが届いたのを知らせます。
gproc_ps:publish
を利用するためにプロセスをgproc
に登録することが必要です。
gproc
の登録はwebsocket_init
で行います。
% websocket_init はwebsocket接続が開始された時に実行されます
websocket_init(_, Req, _Opts) ->
% プロセスをgproc pubsubに登録する
gproc_ps:subscribe(l, new_message),
% stateを設定する
Ip = get_ip(Req),
{UserAgent, _Req} = cowboy_req:header(<<"user-agent">>, Req),
State = #state{ip=Ip, ua=UserAgent},
% WebSocketリクエストは長くなる可能性があるため
% 不要なデータをReqから削除
Req2 = cowboy_req:compact(Req),
% 自動切断を10分に設定する(60万ミリ秒)
{ok, Req2, State, 600000, hibernate}.
解説:
gproc_ps:subscribe(l, new_message)
はgproc
PubSubのnew_message
イベントにプロセスを登録します。- メッセージが投稿される際にIPとユーザエージェントを一緒に保存するため、IPとユーザエージェントはプロセスステートに保存します。
ステートはレコードで保存します、利用するステートレコードは-record(state, {ip, ua}).
でできます。 cowboy_req:compact
はリクエストから不要なデータを削除します。
WebSocketの様に長く続くリクエストの場合はできるだけプロセスのメモリを軽くしたいです。{ok, Req2, State, 600000, hibernate}
のhibernate
はプロセスのメモリを軽くします。(ドキュメンテーション)
gproc
からのメッセージを受ける処理は下記の様になります。
% websocket_infoは本プロセスにErlangメッセージが届いた時に実行されます
% gprocからnew_messageメッセージの場合はそのメッセージをWebSocketに送信します
websocket_info({gproc_ps_event, new_message, Key}, Req, State) ->
RawMessage = ets:lookup(?TABLE, Key),
% ETS結果をマップに変換
Message = format_message(RawMessage),
JsonResponse = jiffy:encode(#{
<<"type">> => <<"new_message">>,
<<"message">> => Message
}),
{reply, {text, JsonResponse}, Req, State};
解説:
gproc
からのメッセージはErlangデータのため、websocket_info
が利用されます。new_message
と一緒に、メッセージのキーである作成時間をTime
変数で渡されます。format_message
でETS結果をマップに変更します。jiffy:encode
でJsonを生成します。
ハンドラーコード
src/message_wall_handler.erl
-module(message_wall_handler).
-behaviour(cowboy_websocket_handler).
-export([init/3]).
-export([websocket_init/3]).
-export([websocket_handle/3]).
-export([websocket_info/3]).
-export([websocket_terminate/3]).
% プロセスステートにIPアドレスとユーザエージェントを保存
-record(state, {ip, ua}).
% テーブル名をマクロで定義
-define(TABLE, message_wall).
% ========================
% 初期化
% ========================
init(_, _, _) ->
% 接続をWebSocketにアップグレード
{upgrade, protocol, cowboy_websocket}.
% websocket_init はwebsocket接続が開始された時に実行されます
websocket_init(_, Req, _Opts) ->
% プロセスをgproc pubsubに登録する
gproc_ps:subscribe(l, new_message),
% stateを設定する
Ip = get_ip(Req),
{UserAgent, _Req} = cowboy_req:header(<<"user-agent">>, Req),
State = #state{ip=Ip, ua=UserAgent},
% WebSocketリクエストは長くなる可能性があるため
% 不要なデータをReqから削除
Req2 = cowboy_req:compact(Req),
% 自動切断を10分に設定する(60万ミリ秒)
{ok, Req2, State, 600000, hibernate}.
% ========================
% メッセージハンドリング
% ========================
% get_listメッセージの場合はメッセージのリストを返します
websocket_handle({text, <<"get_list">>}, Req, State) ->
% 最新のメッセージを取得する
RawMessages = get_recent_messages(10),
% メッセージをJiffyが変換できる形式に変更
Messages = format_messages(RawMessages),
% JiffyでJsonレスポンスを生成
JsonResponse = jiffy:encode(#{
<<"type">> => <<"message_list">>,
<<"messages">> => Messages
}),
% JSONを返す
{reply, {text, JsonResponse}, Req, State};
% それ以外のメッセージは保存する
websocket_handle({text, Text}, Req, #state{ua=Ua, ip=Ip} = State) ->
Time = now(),
Message = {Time, Text, Ip, Ua},
save_message(Message),
% gprocにイベントを公開し、
% 全ての接続クライアントにwebsocket_info({gproc_ps_event, new_message, Time}, Req, State)を呼び出します
gproc_ps:publish(l, new_message, Time),
{ok, Req, State};
websocket_handle(_Frame, Req, State) ->
{ok, Req, State}.
% websocket_info は本プロセスにErlangメッセージが届いた時に実行されます
% gprocからnew_messageメッセージの場合はそのメッセージをWebSocketに送信します
websocket_info({gproc_ps_event, new_message, Key}, Req, State) ->
RawMessage = ets:lookup(?TABLE, Key),
% ETSエントリーをマップに変換
Message = format_message(RawMessage),
JsonResponse = jiffy:encode(#{
<<"type">> => <<"new_message">>,
<<"message">> => Message
}),
{reply, {text, JsonResponse}, Req, State};
websocket_info(_Info, Req, State) ->
{ok, Req, State}.
websocket_terminate(_Reason, _Req, _State) ->
ok.
% ========================
% メッセージ関連
% ========================
% ETSにメッセージを保存する
save_message({Time, Text, Ip, Ua}) ->
ets:insert(?TABLE, {Time, unicode:characters_to_binary(Text), Ip, Ua}).
% 最新のNumberメッセージを取得する
get_recent_messages(Number) ->
case ets:last(?TABLE) of
'$end_of_table' -> [];
Key -> get_recent_messages(Key, Number, [])
end.
get_recent_messages(_Key, 0, Messages) -> lists:reverse(Messages);
get_recent_messages('$end_of_table', _Number, Messages) -> lists:reverse(Messages);
get_recent_messages(Key, Number, Messages) ->
Message = ets:lookup(?TABLE, Key),
PreviousKey = ets:prev(?TABLE, Key),
get_recent_messages(PreviousKey, Number-1, [Message|Messages]).
% IP取得
get_ip(Req) ->
% プロキシ経由対応
case cowboy_req:header(<<"x-real-ip">>, Req) of
{undefined, _Req} ->
{{Ip, _Port}, _Req} = cowboy_req:peer(Req),
Ip;
{Ip, _Req} -> Ip
end.
% ETS結果メッセージをJiffyが変換できる形式に変更
format_messages(RawMessages) ->
lists:map(fun(Message) -> format_message(Message) end, RawMessages).
% ETS結果メッセージをJiffyが変換できる形式に変更
format_message([{Time, Message, Ip, Ua}]) ->
#{
<<"date">> => unicode:characters_to_binary(iso8601(Time)),
<<"ip">> => unicode:characters_to_binary(format_ip(Ip)),
<<"text">> => Message,
<<"ua">> => Ua
}.
% IPタプルを文字列に変換
format_ip({I1,I2,I3,I4}) ->
io_lib:format("~w.~w.~w.~w",[I1,I2,I3,I4]);
format_ip(Ip) -> Ip.
% erlangのdatetimeをISO8601形式に変換
iso8601(Time) ->
{{Year, Month, Day},{Hour, Minut, Second}} = calendar:now_to_universal_time(Time),
io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Year, Month, Day, Hour, Minut, Second]).
フロントエンド - Javascript
最後はフロントエンドのJavascriptを作ります。
WebSocketの基本JavascriptチュートリアルはMozillaのサイトにあります。
下記のスクリプトは[フロントエンド - HTML][frontend-html]の// WebSocket Javascriptはここに入ります。
の代わりに入ります。(フールソース)
// Websocketエンドポイント宣言
var ws_url = "ws://" + window.location.host + "/websocket";
// WebSocket接続変数
var ws_connection;
// JS初期化
$(document).ready(function(){
ws_init();
// 送信ボタンを押した際の処理
$("#send_message").click(function(){
var message = $.trim( $("#message").val() );
if( message != "" ){
ws_connection.send(message);
}
// テキストエリアを空に
$("#message").val("");
// ボタンのフォカスをはすず
$(this).blur();
return false;
});
});
// WebSocketの初期化
function ws_init(){
if(!("WebSocket" in window)){
// WebSocketが対応しないブラウザの場合にアラートを表示。
display_alert("お使いのブラウザはWebSocketを対応していません。");
} else {
connect();
}
}
// WebSocket接続関数
function connect(){
ws_connection = new WebSocket(ws_url);
// 接続が出来たら、メッセージのリストを取得します。
ws_connection.onopen = function(e){
// CowboyのWebSocketにget_listテキストを送信
ws_connection.send('get_list');
};
// WebSocketからメッセージが届くときに実行される関数
ws_connection.onmessage = function (e) {
var msg = JSON.parse(e.data);
switch(msg.type) {
case "message_list":
populate_wall(msg.messages);
break;
case "new_message":
add_message(msg.message);
break;
}
};
// WebSocketが切断されるときにアラートを表示
ws_connection.onclose = function(e) {
display_alert("WebSocket接続は切断されました、ページをリロードしてください。");
}
}
function display_alert(message) {
$("#wall").prepend('<div class="alert alert-danger" role="alert">'+message+'</div>');
}
function populate_wall(messages){
for(var i=messages.length-1; i>=0; i--){
add_message(messages[i]);
}
}
// メッセージをウォールに追加する
function add_message(message){
// テンプレートHTMLをコピー
var template = $("#message_template>div").clone();
$.ua.set(message.ua);
var message_time = moment(message.date).format("YYYY/M/D H:mm:ss");
// テンプレートにメッセージの情報を入れる
template.find(".date").text(message_time);
template.find(".message").text(message.text);
template.find(".ua").text($.ua.browser.name+" "+$.ua.browser.major+"("+$.ua.os.name+")");
template.find(".ip").text(message.ip);
$("#wall").prepend(template);
$("#wall>div:gt(9)").remove();
}
解説:
ws_connection
はWebSocketオブジェクトとなります。- WebSocketにメッセージを送信する場合は
ws_connection.send()
を利用します。 - 接続ができた際に
.onopen
関数は実行されます。
今回は.onopen
でハンドラーにget_list
メッセージを送信し、最新のメッセージリストを取得します。 - ハンドラーからメッセージを受信すると
.onmessage
関数が実行されます。
case
文でget_list
とnew_message
は別関数で処理します。 - 接続が切断された際に
.onclose
関数が実行されます。
切断がわかるようにアラートを表示します。 display_alert
はアラートを表示します。 WebSocket未対応のクライアント、または切断された場合に利用します。populate_wall
はメッセージのリストをウォールに追加します。add_message
は単独メッセージのリストをウォールに追加します。
動作確認
ソースをコンパイルします。
リリースを起動します。
http://localhost:8001にアクセスすれば、メッセージウォールをを試すことができます。
同時に2つのブラウザで開けば、一つのブラウザで投稿したメッセージが自動的に他のブラウザにプッシュされます。
Nginxと組み合わせる
nginx.conf
のserver
文を以下の様に設定すれば、NginxをCowboyアプリのプロキシとして使えます。
server {
listen 80;
# 利用するドメイン名
server_name mw.l-lab.jp;
location / {
proxy_pass http://localhost:8001;
# WebSocketが切断されるまでの秒数
proxy_read_timeout 600s;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}