1. ホーム
  2. angular

SpringBoot + STOMPのチャットルーム(シングルチャット+マルチチャット)およびグループメッセージの詳細

2022-02-18 02:49:19

前回の連載記事 springboot+websocketでオンラインチャット(グループチャット+シングルチャット)構築

最近、長い接続を実装するためにstompプロトコルを発見しました、非常にシンプルです(springbootはもっとうまくカプセル化しています)。

このシリーズの記事

1. springboot+websocketでオンラインチャット(グループチャット+シングルチャット)構築

2. SpringBoot + STOMPチャットルーム(シングルチャット+マルチチャット)およびグループメッセージングの詳細情報

3. ウェブソケットstomp+rabbitmqによるメッセージのプッシング


1. STOMPの紹介

STOMP は WebSocket のサブプロトコルである STOMP, the Simple (or Streaming) Text Orientated Messaging Protocol であり、STOMP クライアントが任意の STOMP メッセージブローカーと相互運用できる接続フォーマットを提供する ( STOMP プロトコルは、そのシンプルな設計とクライアント開発の容易さから複数の言語と複数のプラットフォームで広く利用されている ) 。


STOMPは、HTTPのシンプルさにヒントを得て設計された、非常にシンプルで実装が容易なプロトコルである。STOMP プロトコルは,サーバ側での実装は難しいですが,クライアント側での実装は簡単です.例えば,Telnet を用いて任意の STOMP プロキシにログインし,STOMP プロキシと対話することができます.

STOMPサーバー側

STOMPサーバは、クライアントがメッセージを送信するための宛先アドレスの集合として設計されています。STOMPプロトコルは宛先アドレスのフォーマットを規定していないため、それを定義するのはプロトコルを使用するアプリケーションに任されています。

STOMPクライアント

STOMPプロトコルでは、クライアントは以下の2つの役割のいずれかを担う。

  • プロデューサーとして、SENDフレームで指定されたアドレスにメッセージを送信します。
  • コンシューマーとして、既知のアドレスにSUBSCRIBEフレームを送信することでメッセージを購読し、プロデューサーがこの購読アドレスにメッセージを送信すると、そのアドレスに購読している他のコンシューマーはMESSAGEフレームを介してそのメッセージを受信します。

事実上、WebSocketとSTOMPの組み合わせは、クライアントが上記の2つの役割を切り替えられるメッセージ配信キューを構築することと同じであり、サブスクリプションの仕組みにより、クライアントメッセージはサーバーを介して他の複数のクライアントにブロードキャストされ、プロデューサーとしてサーバーを介してP2Pメッセージを送信できることが保証されます。

STOMPフレーム構造

<ブロッククオート

コマンド
ヘッダー1:値1
header2:value2

本体^@。

行末を表す^@。

STOMPフレームは3つの部分から構成されています。コマンド、ヘッダ(ヘッダ情報)、ボディ(メッセージ本体)。

  • コマンドはUTF-8エンコーディング形式を使用し、コマンドはSEND、SUBSCRIBE、MESSAGE、CONNECT、CONNECTEDなどです。
  • ヘッダーもUTF-8エンコーディングで、HTTPヘッダーと同様にcontent-length,content-typeなどがあります。
  • ボディはバイナリまたはテキストです。BodyはHeaderと空白行(EOL)で区切られることに注意してください。

実際のフレームの例を見るには

送信
送信先:/broker/roomId/1
content-length:57
{"type":"ENTER","content":"o7jD64gNifq-wq-C13Q5CRisJx5E"} のように、「quot;type":"ENTER","」と入力します。

  • 1行目:このフレームがSENDフレームであること、COMMANDフィールドであることを示す。
  • 2行目 Headerフィールド、送信先アドレスは、相対アドレスです。
  • 3行目 ヘッダーフィールド、メッセージ本文の文字の長さ。
  • 4行目:空白行、HeaderとBodyの間隔を空ける。
  • 5行目 メッセージ本文、カスタムJSON構造。

STOMPプロトコルの詳細については、ここでは説明しませんが、興味のある方は、以下の公式サイトをご覧ください。 ストンプ

2. 環境の紹介

バックエンド:springboot 2.0.8

フロントエンド:angular7 (最近angularを勉強しているのでテストページ用に選びました :p)

(html + jsでももちろん大丈夫です。 githubにある私のWebSocketTest.htmlをご自由にご覧ください。  )

3. バックエンド

ここではまず、STOMPの通信プロセス全体について説明します。

1. まず、クライアントはサーバーとHTTPハンドシェイク接続を確立し、WebSocketMessageBrokerを介して接続ポイントEndPointが設定されます。(これは、以下の操作の前提条件となります)。
2. クライアントはサーバーからメッセージトピック("/topic" or "/all")をsubscribeで購読します。このデモのトピックはチャットルーム(シングル+マルチチャット)の購読トピックで、allはトピックへのグループ購読です。
3. クライアントはstompClient.sendでサーバーにメッセージを送ることができ、メッセージは/app/chatまたは/appAllのパスでサーバーに到達し、サーバーは対応するControllerに転送します(Controllerの設定@MessageMapping("/ chat")か@MessageMapping("/chaAll")に従ってメッセージを転送します)。
4. サーバーからメッセージが送信されると、該当するトピックを購読しているクライアントにプッシュされます (Controller の @SendTo("/topic") または @SendTo("/all ") は、メソッドが返すメッセージが /topic トピックにプッシュされることを意味します). また、messagingTemplateを使って指定した宛先に送信する方法もあります(messagingTemplate.convertAndSend(destination, response))。

この後のコードを理解するために、以下の一般的なフローを参考にするとよいでしょう。

3.1. 依存関係

        <parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.8.RELEASE</version>
		<relativePath/> <! -- lookup parent from repository -->
	    </parent>






        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

        <! --websocket related dependencies-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>webjars-locator-core</artifactId>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>sockjs-client</artifactId>
			<version>1.0.2</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>stomp-websocket</artifactId>
			<version>2.3.3</version>
		</dependency>
		<! --websocket-related-dependencies-->



3.2. アプリケーション・プロパティ

server.port = 8080

3.3. WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        /*
         * Users can subscribe to messages from "/topic" and "/user".
         * In the Controller, the send target can be specified via the @SendTo annotation so that the server can send the message to the client that subscribed to the relevant message
         *
         * In this demo, use topic to achieve the chat room effect (single chat + multi-chat) and use all for the group sending effect
         *
         * Clients can only subscribe to topics with these two prefixes
         */
        config.enableSimpleBroker("/topic","/all");

        /*
         * The message sent by the client needs to be prefixed with "/app" and then forwarded by the Broker to the responding Controller
         */
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {

        /*
         * The path "/websocket" is registered as a STOMP endpoint, exposed to the public, and clients access the WebSocket service through this path
         */
        registry.addEndpoint("/websocket").setAllowedOrigins("*").withSockJS();
    }


}


注意事項

@EnableWebSocketMessageBroker , このアノテーションを使用して、WebSocket を有効にするブローカーを識別します。つまり、メッセージを処理するためにブローカーを使用します。

3.4. メッセージクラス

RequestMessageです。

public class RequestMessage {

	private String sender;//the sender of the message
	private String room;//room number
	private String type;//message type
	private String content;//message content

    public RequestMessage() {
    }
    public RequestMessage(String sender,String room, String type, String content) {
        this.sender = sender;
        this.room = room;
        this.type = type;
        this.content = content;         
    }

    
    public String getSender() {
        return sender;
    }
    public String getRoom() {
        return room;
    }
    public String getType() {
        return type;
    }
    public String getContent() {
        return content;
    }

    
    public void setSender(String sender) {
    	this.sender = sender;
    }
    public void setReceiver(String room) {
    	this.room = room;
    }
    public void setType(String type) {
    	this.type = type;
    }
    public void setContent(String content) {
    	this.content = content;
    }

ResponseMessageです。

public class ResponseMessage {

    private String sender;
	private String type;
	private String content;

    public ResponseMessage() {
    }
    public ResponseMessage(String sender, String type, String content) {
    	this.sender = sender;
    	this.type = type;
        this.content = content;
    }

    public String getSender() {
        return sender;
    }
    public String getType() {
        return type;
    }
    public String getContent() {
        return content;
    }
 
    
    public void setSender(String sender) {
    	this.sender = sender;
    }
    public void setType(String type) {
    	this.type = type;
    }
    public void setContent(String content) {
    	this.content = content;
    }
}

3.5. WebSocketTestController

@RestController
public class WebSocketTestController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    /* Chat room (single chat + multi chat)
     *
     * @CrossOrigin cross-domain
     *
     * The @MessageMapping annotation method can use the following parameters:
     * * Use the @Payload method parameter to get the payload (i.e., the content of the message) in the message
     * * Use the @Header method parameter to get a specific header
     * * Use the @Headers method parameter to get all the headers stored in a map
     * Principal method parameter to get the user information used in the websocket handshake phase
     * @param requestMessage
     * @throws Exception
     */
    @CrossOrigin
    @MessageMapping("/chat")
    public void messageHandling(RequestMessage requestMessage) throws Exception {
        String destination = "/topic/" + HtmlUtils.htmlEscape(requestMessage.getRoom());

        String sender = HtmlUtils.htmlEscape(requestMessage.getSender()); //htmlEscape converted to HTML escape character representation
        String type = HtmlUtils.htmlEscape(requestMessage.getType());
        String content = HtmlUtils.htmlEscape(requestMessage.getContent());
        ResponseMessage response = new ResponseMessage(sender, type, content);

        messagingTemplate.convertAndSend(destination, response);
    }

    /**
     * Group message
     * @param requestMessage
     * @return
     * @throws Exception
     */
    @CrossOrigin
    @MessageMapping("/chatAll")
   public void messageHandlingAll(RequestMessage requestMessage) throws Exception {
        String destination = "/all";
        String sender = HtmlUtils.htmlEscape(requestMessage.getSender()); //htmlEscape converted to HTML escape character representation
        String type = HtmlUtils.htmlEscape(requestMessage.getType());
        String content = HtmlUtils.htmlEscape(requestMessage.getContent());
        ResponseMessage response = new ResponseMessage(sender, type, content);

        messagingTemplate.convertAndSend(destination, response);
    }


}


注意事項

1. MessageMappingアノテーションを使用して、"/chat" 宛に送信されたすべてのメッセージが、処理のためにこのメソッドにルーティングされることを識別します。

2. SendToアノテーションを使用して、このメソッドが返す結果が指定した宛先、"/topic"に送信されることを識別します。 

3. 渡されたパラメータ RequestMessage requestMessage は、クライアントから送信されたメッセージで、自動的にバインドされます。

バックエンドが書かれてるんですね、簡単でしょう〜。

4. フロントエンド

angular7プロジェクトを新規に作成します。

前回の記事は以下をご参照ください。

angular7チュートリアル(1) - angular7の初期理解とプロジェクト構築_小牛呼噜噜的博客- CSDN博客_angular7

次に、新しいコンポーネントwebsocketを作成します。

ng generate component websocket


ルーティングを設定します。

import { NgModule } from '@angular/core';
import { LoginComponent } from '. /login/login.component';
import { WebsocketComponent } from '. /websocket/websocket.component';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' }
  { path: 'login', component: LoginComponent },
  { path: 'websocket', component: WebsocketComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

詳しくは、こちらをご覧ください。

角型7教程(2)--新建築页面_小牛呼噜噜的博客-CSDN博客_角型新建築页面

stomp関連の依存関係をインストールする。

npm install stompjs --save
npm install sockjs-client --save

websocket.component.ts。

import { Component, OnInit } from '@angular/core';
import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';



@Component({
  selector: 'app-websocket',
  templateUrl: '. /websocket.component.html',
  styleUrls: ['. /websocket.component.scss']
})
export class WebsocketComponent implements OnInit {

  public stompClient;

  public serverUrl = "http://localhost:8080/websocket";

  public room;//channel number

  constructor() { 
    
  }

  ngOnInit() {
      this.connect();
  }

  connect() {
    const ws = new SockJS(this.serverUrl);
    this.stompClient = Stomp.over(ws);
    const that = this;
    this.stompClient.connect({}, function (frame) {

      that.stompClient.subscribe('/topic/' + that.room, (message) => {
        if (message.body) {

          const sender = JSON.parse(message.body)['sender'];
          const language = JSON.parse(message.body)['language'];
          const content = JSON.parse(message.body)['content'];
          const type = JSON.parse(message.body)['type'];

        }
          
      });
      

    });

  }
  


}


プロジェクトを実行します。

ng serve --port 4300

エラーの報告
でERROR。/node_modules/stompjs/lib/stomp-node.js でエラーが発生しました。
モジュールが見つかりませんでした。Error: E:......」にある「net」を解決できません。

解決方法

npm i net -S

ブラウザを開き、デバッグモードをオンにする

Found エラーが報告されました。

browser-crypto.js:3 Uncaught ReferenceError: グローバルが定義されていません。

解決方法

での polyfills.ts ファイルを手動で入力します。

// Add global to window, assigning the value of window itself.
(window as any).global = window;

成功

フロントエンドがバックエンドとのウェブソケット接続を確立できるようになったので、フロントエンドページを充実させてみましょう。

PrimeNGというUIコンポーネントフレームワークを使いました。PrimeNGはfont-awesome cssを使っているので、font-awesome icon libraryも使っています(これを見つけるのに時間がかかりました)。

依存関係をダウンロードします。

npm install primeng --save
npm install primeicons --save

npm install font-awesome --save 

cssをインポートする(srcディレクトリのsingle styles.scssを修正する)。

/* You can add global styles to this file, and also import other style files */

@import 
"... /node_modules/font-awesome/css/font-awesome.min.css", //is used by the style of ngprime
"... /node_modules/primeng/resources/primeng.min.css", // this is needed for the structural css
"... /node_modules/primeng/resources/themes/omega/theme.css",
"... /node_modules/primeicons/primeicons.css";
// ". /node_modules/primeflex/primeflex.css";

次に、app.module.tsを修正して、必要なモジュールを追加します。

import {ButtonModule} from 'primeng/button';


imports: [
    ... ,
    ButtonModule
  ],

フロントエンドの表示クラスを新規に作成します。

item.ts.

これは、チャットルームのメッセージを定義するために使用されます


export class Item {
    type: string;
    user: String;
    content: string;

    constructor(type: string, user: String, content: string) {
        this.type = type;
        this.user = user;
        this.content = content;
    }

}

別のatim.tsを作成します。

これは、グループメッセージを定義するために使用されます

export class Item {
    type: string;
    user: String;
    content: string;

    constructor(type: string, user: String, content: string) {
        this.type = type;
        this.user = user;
        this.content = content;
    }

}

次に、websocket.htmlを変形させます。

<p>
  websocket works!
</p>

<div class="ui-g ui-fluid">
  <div class="ui-g-12 ui-md-4">
    <div class="ui-inputgroup">
        <span class="ui-inputgroup-addon"><i class="fa fa-user"></i></span>
        <input type="text" pInputText placeholder="Sender" [(ngModel)]='sender'> 
        {

{sender}}     
    </div>

    <br>
    <div class="ui-inputgroup">
        <span class="ui-inputgroup-addon">$$$</span>
        <input type="text" pInputText placeholder="Room" [(ngModel)]='room'> 
        <p-button label="Connect" (click)="connect()"></p-button>
        <p-button label="Disconnect" (click)="disconnect()"></p-button>
       {
{room}}
    </div>
    <br>
    <br>
    <br>
    <div class="ui-inputgroup">
      <span class="ui-inputgroup-addon">--</span>
      <input type="text" pInputText placeholder="Type" [(ngModel)]='type'> 
     {
{type}}
    </div>

    <br>
    <div class="ui-inputgroup">
      <span class="ui-inputgroup-addon"><i class="fa fa-credit-card"></i></span>  
      <input type="text" pInputText placeholder="Message" [(ngModel)]='message'> 
      <p-button label="Send" (click)="sendMessage()"></p-button>
     {
{message}}
    </div>

    <br>
    <div class="ui-inputgroup">
      <span class="ui-inputgroup-addon"><i class="fa fa fa-cc-visa"></i></span>  
      <input type="text" pInputText placeholder="MessageAll" [(ngModel)]='messageAll'> 
      <p-button label="GroupMessage" (click)="sendMessageToAll()"></p-button>
    </div>

    <br>
    = = = = = = = = =
    <p> This is a message from the chat room: </p>
    <br>
  
    <div >
      <li *ngFor="let item of items">
          {
{item.user}} - {
{item.content}} 
      </li>
    </div>

    = = = = = = = = =
    <p> This is the group message: </p>
    <br>
  
    <div >
      <li *ngFor="let atem of atems">
          {
{atem.user}} - {
{atem.content}} 
      </li>
    </div>


  </div>
</div>


次にwebsocket.component.tsを変換します。

import { Component, OnInit } from '@angular/core';
import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';
import { Item } from '. /entity/item';
import { Atem } from '. /entity/atem';

@Component({
  selector: 'app-websocket',
  templateUrl: '. /websocket.component.html',
  styleUrls: ['. /websocket.component.scss']
})
export class WebsocketComponent implements OnInit {

  public stompClient;

  public serverUrl = "http://localhost:8080/websocket";

  public room;//channel number

  public sender;//sender

  public type;//the type of the message

  public message;//message content

  public messageAll;//content of the group message

  items = [];

  atems = [];

  constructor() { 
    
  }

  ngOnInit() {
      // this.connect();
  }

  connect() {

    if(this.sender===undefined) {
      alert("Sender cannot be null")
      return
    }

    if(this.room===undefined) {
      alert("Room number cannot be empty")
      return
    }


    const ws = new SockJS(this.serverUrl);
    this.stompClient = Stomp.over(ws);

    const that = this;
    this.stompClient.connect({}, function (frame) {

     //get the chat room message
      that.stompClient.subscribe('/topic/' + that.room, (message) => {
        if (message.body) {

          const sender = JSON.parse(message.body)['sender'];
          // const language = JSON.parse(message.body)['language'];
          const content = JSON.parse(message.body)['content'];
          const type = JSON.parse(message.body)['type'];

          const newitem = new Item(
           type,
           sender,
           content
          );

          that.items.push(newitem);

        }else{

          return
        }
          
      });

       //get group messages
    that.stompClient.subscribe('/all', (message) =>{
      if (message.body) {

        const sender = JSON.parse(message.body)['sender'];
        // const language = JSON.parse(message.body)['language'];
        const content = JSON.parse(message.body)['content'];
        const type = JSON.parse(message.body)['type'];

        const newatem = new Atem(
         type,
         sender,
         content
        );

        that.atems.push(newatem);

      }else{

        return
      }
    })
      
    });

   
  }

  // method for disconnecting
  disconnect() {
      if (this.stompClient ! == undefined) {
        this.stompClient.disconnect();
      }else{
        alert("No websocket is currently connected")
      }
      this.stompClient = undefined;
      alert("Disconnected");
  }

  // send a message (single chat)
  sendMessage() {
    if(this.stompClient===undefined) {
      alert("websocket is not connected")
      return
    };

    if(this.type===undefined) {
      alert("Message type cannot be null")
      return
    };

    if(this.message===undefined) {
      alert("Message content cannot be empty")
      return
    };

      this.stompClient.send(
        '/app/chat',
        {},
        JSON.stringify({
          'sender': this.sender,
          'room': this.room,
          'type': this.type,
          'content': this.message })
      );
    }

    // Send a group message
    sendMessageToAll() {
      if(this.stompClient===undefined) {
        alert("websocket not yet connected")
        return
      };
  
      if(this.messageAll===undefined) {
        alert("Group message content cannot be empty")
        return
      };
  
        this.stompClient.send(
          '/app/chatAll',
          {},
          JSON.stringify({
            'sender': this.sender,
            'room': "00000",
            'type': "00000",
            'content': this.messageAll })
        );

      }
    

  
}


テスト+最終結果画像。

チャットルームの機能テスト(シングルチャット+マルチチャット)。

グループ投稿機能のテスト。

これで、STOMPを使ったチャットルーム(シングルチャット+マルチチャット)とグループ投稿の機能の実装が完了しました。

皆さんは、STOMPプロトコルがネイティブのwebsocketプロトコルよりずっと簡単で速く開発できると感じていますか?

前の記事 springboot+websocket build online chat room (グループチャット+シングルチャット)

WebSocketはどのようにしてユーザーログインの安全な認証を可能にするのか、rabbitmqと組み合わせてどのようにしてメッセージのプッシュを可能にするのか、これらの簡単な疑問について考えてみてください。

参考にしてください。

ストンプ

https://stomp.github.io/stomp-specification-1.2.html

https://juejin.im/post/5b7071ade51d45665816f8c0

Spring+STOMPでWebSocketのブロードキャスト購読、パーミッション認証、1対1通信(ソースコード付き)_Jamin Jaminのブログ - CSDN Blog_stompのログイン認証