LoginSignup
2
2

More than 3 years have passed since last update.

【Java】今から10分ではじめるサーバープッシュ ~TomcatやJettyでSSE(Server-Sent Events)する方法~

Last updated at Posted at 2019-05-08

概要

  • サーバーから情報をブラウザ側にプッシュする方法の紹介です
  • プッシュする側のサーバーには Java Servletを使います
  • いくつか手段がありますが、今回はおそらく一番手軽なSSE(Server-Sent Events)という方法を使います

やりかた

依存関係設定

以下ライブラリ(※)を追加する。

Maven

POM.xmlへの依存関係
<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を送信するサーブレットのソースコード

SSEServlet.java
@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サーバーを起動することができる。

App.java
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を使うので、依存関係に以下も追加する。

POM.xmlに追加(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に内包した全ソースは以下

index.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枚のブラウザを開いた例。右側ブラウザで入力している。

push.gif

補足: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があれば、すぐに実行して試せます

コマンドラインでSSEのサンプルコード実行
$ 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 を開く

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2