Clojure と Immutant で Heroku の WebSocket を使用する
最終更新日 2022年02月09日(水)
Table of Contents
このチュートリアルでは、WebSocket を使用する Immutant と共に Clojure アプリケーションを構築する方法について学びます。その後、アプリケーションを Heroku にデプロイする方法について学びます。
デモアプリケーションのサンプルコードは GitHub で入手できます。編集や機能強化を歓迎します。単にリポジトリをフォークし、変更を加えて、プルリクエストを送信してください。
前提条件
- Java、Leiningen、および Heroku CLI (Heroku CLI のセットアップに関する記事)で説明しているとおり)
- Heroku ユーザーアカウント。 無料ですぐにサインアップできます。
WebSocket アプリの作成
次のように、Leiningen を足場にしてプロジェクトを生成することから始めます。
$ lein new demo
Generating a project called demo based on the 'default' template.
The default template is intended for library projects, not applications.
To see other templates (app, plugin, etc), try `lein help new`.
次に、Immutant の依存関係を project.clj
ファイルに追加し、次の設定を追加して Immutant を Heroku 用に準備します。
(defproject demo "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.6.0"]
[org.immutant/web "2.0.0-beta2"]
[compojure "1.1.8"]
[ring/ring-core "1.3.0"]
[environ "1.0.0"]]
:main demo.core
:uberjar-name "demo-standalone.jar"
:profiles {:uberjar {:aot [demo.core]}}
:min-lein-version "2.4.0")
これで、アプリケーションに動作を追加する準備ができました。
機能
このサンプルアプリケーションは、バックエンドへの WebSocket を開く単純な Web ページをレンダリングします。クライアントは、文字列を含むペイロードを WebSocket 経由で送信します。サーバーは文字列を反転して、結果をクライアントに送り返します。
このやり取りには、2 つの重要な部分があります。サーバー側のコールバック関数と、WebSocket を開く JavaScript ファイルです。
サーバー
プロジェクトの src/demo/core.clj
ファイルで、次の依存関係をファイルの先頭に追加します (既存の名前空間ステートメントを置き換えます)。
(ns demo.core
(:require
[immutant.web :as web]
[immutant.web.async :as async]
[immutant.web.middleware :as web-middleware]
[compojure.route :as route]
[environ.core :refer (env)]
[compojure.core :refer (ANY GET defroutes)]
[ring.util.response :refer (response redirect content-type)])
(:gen-class))
その後に、WebSocket コールバック関数を定義します。
(def websocket-callbacks
"WebSocket callback functions"
{:on-open (fn [channel]
(async/send! channel "Ready to reverse your messages!"))
:on-close (fn [channel {:keys [code reason]}]
(println "close code:" code "reason:" reason))
:on-message (fn [ch m]
(async/send! ch (apply str (reverse m))))})
websocket-callbacks
関数では、3 つの関数のマップを定義します。これらの関数は、それぞれソケットを開く、ソケットを閉じる、メッセージを受信するために実行されます。
次に、アプリケーションの Web ルートを定義します。
(defroutes routes
(GET "/" {c :context} (redirect (str c "/index.html")))
(route/resources "/"))
最後に、アプリケーションへのエントリポイントとして main 関数を作成します。
(defn -main [& {:as args}]
(web/run
(-> routes
(web-middleware/wrap-session {:timeout 20})
;; wrap the handler with websocket support
;; websocket requests will go to the callbacks, ring requests to the handler
(web-middleware/wrap-websocket websocket-callbacks))
(merge {"host" (env :demo-web-host), "port" (env :demo-web-port)}
args)))
サーバー側では、クライアントからのメッセージを処理する準備ができています。
クライアント
クライアント側では、単純な HTML ページを使用してデモを制御します。resources/public/index.html
ファイルを作成し、その中に次のコードを記述します。
<html>
<head>
<meta charset="utf-8">
<title>WebSocket Demo</title>
</head>
<body>
<h1>WebSocket Demo</h1>
<div>
<input type="text" id="input" value="Enter text to reverse!" />
</div>
<div>
<button type="button" id="open">Open</button>
<button type="button" id="send">Send</button>
<button type="button" id="close">Close</button>
</div>
<div id="messages"></div>
<script src="js/app.js"></script>
</body>
</html>
このページにはテキストボックス、開くボタン、送信ボタン、閉じるボタン、一部のメッセージ用の領域があります。しかし、これらはまだ機能しません。これらに動作を追加するには、何らかの JavaScript が必要になります。resources/public/js/app.js
ファイルを作成し、その中に次のコードを記述します。
window.onload = function() {
var input = document.getElementById('input');
var openBtn = document.getElementById('open');
var sendBtn = document.getElementById('send');
var closeBtn = document.getElementById('close');
var messages = document.getElementById('messages');
var socket;
};
これにより、window.onload
関数が定義されます。また、テキストボックスの内容、ボタン、メッセージ領域、通信に使用するソケットを表す変数も定義されます。
次に、window.onload
の本体に、messages
変数を更新するための関数 (関数内の関数) を追加します。
function output(style, text){
messages.innerHTML += "<br/><span class='" + style + "'>" + text + "</span>";
}
これは、ソケットと通信するときにコールバックとして使用されます。この関数の後 (ただし、まだ window.onload
関数の内側) に次のコードを追加して、開くボタンの動作を定義します。
openBtn.onclick = function(e) {
e.preventDefault();
if (socket !== undefined) {
output("error", "Already connected");
return;
}
var uri = "ws://" + location.host + location.pathname;
uri = uri.substring(0, uri.lastIndexOf('/'));
socket = new WebSocket(uri);
socket.onerror = function(error) {
output("error", error);
};
socket.onopen = function(event) {
output("opened", "Connected to " + event.currentTarget.url);
};
socket.onmessage = function(event) {
var message = event.data;
output("received", "<<< " + message);
};
socket.onclose = function(event) {
output("closed", "Disconnected: " + event.code + " " + event.reason);
socket = undefined;
};
};
開くボタンをクリックすると、この機能が実行されます。これは新しい WebSocket を作成し、ソケット操作のそれぞれに対してコールバック関数を定義します。いずれの場合も、これは messages
テキスト領域にメッセージを表示します。
さらに、(引き続き window.onload
関数の内側で) 次のコードを追加して、送信ボタンの動作を定義します。
sendBtn.onclick = function(e) {
if (socket == undefined) {
output("error", 'Not connected');
return;
}
var text = document.getElementById("input").value;
socket.send(text);
output("sent", ">>> " + text);
};
開く操作と同様に、この関数はソケットで send
メソッドを呼び出し、送信された値を使用して output
関数を呼び出します。
最後に、window.onload
関数の終了前に次のコードを追加して、閉じるボタンの動作を定義します。
closeBtn.onclick = function(e) {
if (socket == undefined) {
output('error', 'Not connected');
return;
}
socket.close(1000, "Close button clicked");
};
これは、閉じるボタンがクリックされたらソケットを閉じます。
アプリケーションを実行する準備ができました。
アプリのローカル実行
アプリをローカルで実行するには、まず次のコマンドを実行してアプリをコンパイルする必要があります。
$ lein uberjar
これにより、次のコマンドで起動できる実行可能 JAR ファイルが生成されます。
$ java -jar target/demo-standalone.jar host 0.0.0.0 port 5000
上記のコマンドを実行してから、ブラウザで http://localhost:5000
を開きます。開く、送信、閉じるボタンを備えた WebSocket ページが表示されます。何かテキストを入力してテストします。これで、クラウドにデプロイする準備ができました。
Heroku へのアプリのデプロイ
次の内容でプロジェクトルートに Procfile
を作成します。
web: java $JVM_OPTS -jar target/demo-standalone.jar host 0.0.0.0 port $PORT
Heroku にこのファイルが必要なのは、アプリケーションを起動するために実行するコマンドを指示するためです。お気付きのように、これはローカルで実行したコマンドとよく似ています。
次に、すべてのコードを Git リポジトリに追加します。
$ git init
$ git add .
$ git commit -m "first commit"
デプロイ先の Heroku アプリを作成します。
$ heroku create
次に、コードをデプロイします。
$ git push heroku master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (20/20), 7.03 KiB | 0 bytes/s, done.
Total 20 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Clojure (Leiningen 2) app detected
remote: -----> Installing OpenJDK 1.8...done
remote: -----> Installing Leiningen
remote: Downloading: leiningen-2.5.0-standalone.jar
remote: Writing: lein script
remote: -----> Building with Leiningen
remote: Running: lein uberjar
remote: Retrieving org/clojure/clojure/1.6.0/clojure-1.6.0.pom from central
...
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing... done, 58.2MB
remote: -----> Launching... done, v3
remote: https://still-hamlet-4310.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/immutant-feature-demo.git
* [new branch] master -> master
以上で、Web アプリが Heroku で稼働するようになりました。アプリケーションにアクセスして動作を確認してみましょう。
$ heroku open
heroku open
コマンドは HTTPS URL を開くことに注意してください。Firefox を使用している場合、WebSocket を開こうとするとブラウザでセキュリティエラーが発生します。URL を HTTP URL に変更するか、別のブラウザを使用する必要があります。
Immutant で WebSocket を使用する方法についての詳細は、Immutant API のドキュメントを参照してください。ただし、Immutant のすべての機能が Heroku で使用できるわけではないことに注意してください。シングルトンサービスやセッション複製など、クラスターを必要とする機能は正しく動作しません。 代わりに、Heroku Scheduler アドオンやキャッシュ用の Memcached アドオンなど、その他のソリューションを使用することをお勧めします。