概要
- サーバーから情報をブラウザ側にプッシュする方法の紹介です
- プッシュする側のサーバーには Java Servletを使います
- いくつか手段がありますが、今回はおそらく一番手軽なSSE(Server-Sent Events)という方法を使います
やりかた
依存関係設定
以下ライブラリ(※)を追加する。
Maven
<dependency>
<groupId>org.riversun</groupId>
<artifactId>jetty-sse-helper</artifactId>
<version>1.0.0</version>
</dependency>
※ jeasseのミニマル版。SSEのプロトコルを実装。ソースは→https://github.com/riversun/jetty-sse-helper
コード
PUSHを送信するサーブレットのソースコード
@SuppressWarnings("serial")
public class SSEServlet extends HttpServlet {
private final SSEHelper mPushHelper = new SSEHelper();
/**
* (JSから)GETリクエストがあったら、リクエスト元をターゲットに追加する
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Added " + req + " to target to broadcast");
final EventTarget tgt = new EventTarget(req);
mPushHelper.addTarget(tgt);
}
/**
* POSTリクエストがあったら、登録されたターゲットにメッセージをブロードキャストする
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msgToSend = req.getParameter("message");
if (msgToSend == null) {
msgToSend = "";
}
mPushHelper.broadcast("message", msgToSend);
resp.setStatus(HttpServletResponse.SC_OK);
final PrintWriter out = resp.getWriter();
out.close();
}
}
- doGet側では、PUSHを受け取りたいクライアント(ブラウザ)の登録をしている。ブラウザ側のコードは下部に掲載。
- doPost側では、POSTを受け取ったら、登録されたすべてのクライアントにメッセージを送信している。
- もしすべてのクライアントに送りたくない場合は、
EventTarget#send
を呼び出せば指定されたターゲットのみにメッセージを送信できる
メインクラス=サーブレットコンテナを起動するコード
ここではJettyを使用。コードだけでサーブレットコンテナ、Webサーバーを起動することができる。
public class App {
public static void main(String[] args) {
new App().startWebServer();
}
public void startWebServer() {
final int port = 8080;
// サーブレットホスト用ハンドラ
ServletContextHandler sch = new ServletContextHandler(ServletContextHandler.SESSIONS);
ServletHolder sh = sch.addServlet(SSEServlet.class, "/sse");
sh.setAsyncSupported(true);
// 静的ページのホスト用ハンドラ
final ResourceHandler rh = new ResourceHandler();
rh.setResourceBase(System.getProperty("user.dir") + "/htdocs");
rh.setWelcomeFiles(new String[] { "index.html" });
rh.setCacheControl("no-store,no-cache,must-revalidate");// キャッシュオフ
final HandlerList hnList = new HandlerList();
hnList.addHandler(rh);
hnList.addHandler(sch);
final Server server = new Server(port);
server.setHandler(hnList);
try {
server.start();
System.out.println("Server started on port:" + port);
server.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Jettyを使うので、依存関係に以下も追加する。
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>9.4.18.v20190429</version>
</dependency>
サーバーPUSHを受け取るJavaScript
SSEのサーバーPUSHを受け取るには **new EventSource(url)**でEventSourceオブジェクトを作ってそこに、コールバックを設定すれば良い。コードは以下のようになる。
const eventSource = new EventSource('sse');//http://localhost:8080/sse
eventSource.addEventListener('message', (event) => {
console.info('SSE: ' + event.data);
});
これをHTMLに内包した全ソースは以下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSEのサンプル</title>
</head>
<body>
<h1>Server Sent Event(SSE) Example</h1>
・PUSH通知をうけとってメッセージを表示します<br>
・PUSH送信は↓のフォームから実行できます<br>
・別のブラウザでこのページを開いても、BroadcastするのにPUSH通知を受け取れます
<hr>
<h3>PUSH送信するメッセージ</h3>
<input id="text_message" value="テストメッセージ">
<button id="send_message">PUSH送信</button>
(エンターでもOK)
<br>
<br>
<small>(他のブラウザを開いても↓のメッセージが受信される)</small>
<hr>
<h3>Server PUSHを受信したメッセージ</h3>
<div id="messages"></div>
<script>
const eventSource = new EventSource('sse');//http://localhost:8080/sse
const msgContent = document.querySelector('#messages');
eventSource.addEventListener('message', (event) => {
console.info('SSE: ' + event.data);
msgContent.innerHTML += event.data + '<br>';
});
const msgText = document.querySelector('#text_message');
const msgPostBtn = document.querySelector('#send_message');
const funcSendData = (event) => {
sendData('sse', {message: msgText.value});
msgText.value = '';
};
msgText.addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
funcSendData(event);
}
});
msgPostBtn.addEventListener('click', funcSendData);
//https://developer.mozilla.org/ja/docs/Learn/HTML/Forms/Sending_forms_through_JavaScript
function sendData(url, data) {
const XHR = new XMLHttpRequest();
let urlEncodedData = '';
const urlEncodedDataPairs = [];
let name;
for (name in data) {
urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
}
urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');
XHR.addEventListener('load', function (event) {
});
XHR.addEventListener('error', function (event) {
});
XHR.open('POST', url);
XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
XHR.send(urlEncodedData);
}
</script>
</body>
</html>
サーバーPUSHを実行する
- App.java を起動すると、Webサーバーとサーブレットコンテナが立ち上がる
- http://localhost:8080 をブラウザで開く
- ブラウザを複数枚開いておくと良い
(1つのブラウザで入力した文字が他のブラウザにもサーバーPUSHによって即座に反映されるので) - テキストボックスに文字列を入力してエンターすると、その情報が他のブラウザにも即座に反映されるのがわかる
デモ、2枚のブラウザを開いた例。右側ブラウザで入力している。
補足:SSEの注意点
-
SSEは片方向(半二重:HALF DUPLEX)のメッセージングとなる。
- サーバーからクライアントへの通信しかケアされていないので、クライアントからサーバーへの通信を含む双方向(全二重:FULL DUPLEX)を実現するにはWebSocketのほうが適している場合がある。
- 既存のGET、POSTとセッションなどをつかって頑張る方法ももちろんある。
-
SSEでクライアントとサーバーのコネクションを張り続けた場合、クライアント=ブラウザの同時接続数制限にひっかかる。
- Chromeの同時接続数は6コネクションまで。(同一のサーバーへの接続数)
-
MSのブラウザであるIEやEdgeはSSEにデフォルト対応していないが、Polyfillが出ているので、そちらを活用すれば動作可能(Polyfill対応したサンプルコードはこちら)
まとめ
- Java Servletでサーバープッシュを行う方法を紹介しました
- サーバーで発生したイベントの通知や、サーバーからのストリーミングなどの用途ならWebSocketより手軽に実現できます
- 紹介した全ソースコードは https://github.com/riversun/jetty-sse-example にあります
サンプルコードの実行方法
mavenがあれば、すぐに実行して試せます
$ git clone https://github.com/riversun/jetty-sse-example.git
$ jetty-sse-example
$ mvn exec:java
または
$ mvn clean install exec:java
Server started on port:8080
サーバーが起動したら、http://localhost:8080 を開く