0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

仕事でWebSocket通信を用いたアプリケーションの要件定義をする機会があり、実際に開発した経験がなかったため理解を深めるために簡易アプリを作ってみました。
やっていることはこちらの記事と同じですが、言語がKotlinだったり、他にいくつか変更点や解説を加えているので記事を書いてみることにしました。

実装

ディレクトリ構成

スクリーンショット 2024-06-23 22.33.21.png

SpringBootはデフォルトでsrc/main/resources/staticディレクトリが静的なリソースの格納場所として設定されているため、index.htmlresources/static/直下に置いてください。

プロジェクトの作成

スクリーンショット 2024-06-23 21.45.51.png
SpringInitializrでプロジェクトを作成。
私はいつもGradleで開発を行っているのでこれにしましたが、お好みで大丈夫です。
また、DependenciesにWebSocketを追加しています。

ライブラリの依存関係を追加

build.gradle.ktsにライブラリの依存関係を追加します。

build.gradle.kts
dependencies {
	implementation("org.springframework.boot:spring-boot-starter-websocket")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")
	implementation("org.webjars:webjars-locator-core") //追加
	implementation("org.webjars:sockjs-client:1.0.2") //追加
	implementation("org.webjars:stomp-websocket:2.3.3") //追加
	implementation("org.webjars:bootstrap:3.3.7") //追加
	implementation("org.webjars:jquery:3.1.1-1") //追加
}

WebJarsはBootstrapやjQueryなどのライブラリを、Java(Kotlin)の依存関係として管理することができ、静的リソースとしてサーバーから提供することも可能になるというものです。
(WebSocket通信とは関係ないので、これ以上の説明は割愛。)

Modelを定義

下記はクライアントからのリクエストのモデルになります。

HelloMessage.kt
package testwebsocket.Model

data class HelloMessage(
        var name: String,
        var message: String
)

下記はサーバーからのレスポンスのモデルになります。

Greeting.kt
package testwebsocket.Model

data class Greeting(
        var content: String
)

Controllerを定義

WebSocket通信を用いたメッセージの送受信を行うクラスになります。

GreetingController.kt
package testwebsocket.Controller

import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.SendTo
import org.springframework.stereotype.Controller
import org.springframework.web.util.HtmlUtils
import testwebsocket.Model.Greeting
import testwebsocket.Model.HelloMessage

@Controller
class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    fun greeting(message: HelloMessage): Greeting {
        Thread.sleep(1000) //処理を1秒間待機
        return Greeting(
                HtmlUtils.htmlEscape(message.name)
                        + " : "
                        + HtmlUtils.htmlEscape(message.message)
        )
    }
}

WebSocket通信のアノテーションであるMessageMappingSendToについて簡単に解説します。

  • @MessageMapping("/hello")というアノテーションは、クライアントから/helloというエンドポイントにメッセージが送られてきたときに、greetingメソッドを実行します

  • @SendTo("/topic/greetings")というアノテーションは、greetingメソッドの処理が終わった後、その結果を/topic/greetingsというエンドポイントに送信します

greetingメソッドの処理内容についても簡単に解説します。

  • 引数としてHelloMessage型のmessageを受け取り、その中のnamemessageを取り出しています
  • HtmlUtils.htmlEscapeを用いてエスケープ処理を行った後にそれらを結合して、新たなGreetingオブジェクトを作成して返却しています

Configクラスを定義

WebSocket通信の設定クラスです。
Springフレームワークで使用されるWebSocketとSTOMPプロトコル1を設定しています。

WebSocketConfig.kt
package testwebsocket.Config

import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer

@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {

    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        config.enableSimpleBroker("/topic")
        config.setApplicationDestinationPrefixes("/app")
    }

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry.addEndpoint("/gs-guide-websocket").withSockJS()
    }
}
  • @EnableWebSocketMessageBrokerはSpringフレームワークにおいてWebSocketメッセージブローカーを有効にするアノテーションであり、WebSocketを用いたメッセージングの設定や制御が可能になります

  • configureMessageBrokerメソッドはメッセージブローカー2の設定を行っています

    • config.enableSimpleBroker("/topic")は、/topicというエンドポイントを持つシンプルなメッセージブローカーを有効にします。これにより、クライアントは/topicを購読3してメッセージを受信することができます
    • config.setApplicationDestinationPrefixes("/app")は、サーバーへのメッセージの送信先のプレフィックスを"/app"に設定します
  • registerStompEndpointsメソッドは、STOMPプロトコル1のエンドポイントを登録します

    • registry.addEndpoint("/gs-guide-websocket").withSockJS()は、/gs-guide-websocketというエンドポイントを追加し、SockJSを使用することを示しています。SockJSは、WebSocketが利用できない場合でも、可能な限りWebSocketと同等の機能を提供するためのJavaScriptライブラリです

フロントエンド(HTML,JS)の実装

index.html
<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Chat</title>
    <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="/main.css" rel="stylesheet">
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">WebSocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">Name</label>
                    <input type="text" id="name" class="form-control" placeholder="Your name here...">
                </div>
                <div class="form-group">
                    <label for="name">Message</label>
                    <input type="text" id="message" class="form-control" placeholder="Hello">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</button>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Messages</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

app.js
let stompClient = null;

const setConnected = (connected) => {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    } else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
};

const connect = () => {
    const socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, (frame) => {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greetings', (greeting) => {
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
};

const disconnect = () => {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
};

const sendMessage = () => {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val(),'message': $("#message").val()}));
    $("#message").val('');
};

const showGreeting = (message) => {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
};

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $("#connect").click(() => connect());
    $("#disconnect").click(() => disconnect());
    $("#send").click(() => sendMessage());
});

setTimeout(() => connect(), 3000);
  • stompClientという変数は、WebSocketの接続を管理するためのものです
  • connectメソッドでは、サーバーとのWebSocket接続を確立し、サーバーからのメッセージを購読3します
  • disconnectメソッドでは、WebSocketの接続を切断します
  • sendMessageメソッドでは、サーバーにメッセージを送信します。送信内容は、namemessageの入力フィールドから取得します
  • ページが読み込まれた3秒後にconnectメソッドを呼び出して、WebSocketの接続を確立します

見た目を整える

main.css
body {
    background-color: #f5f5f5;
}

#main-content {
    max-width: 940px;
    padding: 2em 3em;
    margin: 0 auto 20px;
    background-color: #ffffff;
    border: 1px solid #e5e5e5;
    -webkit-border-radius: 5px;
    -moz-border-radius: 5px;
    border-radius: 5px;
}

アプリを立ち上げる

プロジェクト直下でbootRunを実行して、localhost:8080にアクセスするとページを閲覧できます。

$ ./gradlew bootRun

タイトルなし.gif

Tips

久々にプライベートPCでIntelliJを使用したら、JAVAのバージョン不一致のエラーが出ました。
プロジェクト作成時に選んだバージョンがJAVA17でしたが、IntelliJのJVMがJAVA11を使用していたようです。

Caused by: org.gradle.internal.component.resolution.failure.exception.VariantSelectionException: Dependency requires at least JVM runtime version 17. This build uses a Java 11 JVM.

なので、IntelliJの設定を開いて、JAVA17のGradleJVMをインストール。
私はM1Macを使用しているのですが、AmazonCorretto17を選択しました。
スクリーンショット 2024-06-23 23.48.21.png

参考文献

  1. STOMPプロトコルとはシンプルなテキストベースのメッセージングプロトコルのことで、主にクライアントとサーバー間のメッセージ交換を行うために使用される。WebSocketと組み合わせて使用されることが多い。 2

  2. メッセージブローカーとは、異なるプロセスやアプリケーション間でメッセージをやり取りする役割を担うソフトウェアのこと。メッセージの送信者からメッセージを受け取り、それを受信者に配信する。
    これにより、大量のメッセージ通信を効率的に処理したり、システム間の通信をスムーズにしたりすることが可能になる。

  3. サーバーから送られてくるメッセージを受け取るための待機状態に入るという意味。 2

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?