- 按【Connect】連線到 server。
- 輸入名稱後按【Send】,將名稱送到 server。
- server 收到後,再將名稱回覆給 Angular。
- Angular 收到回覆後顯示「Greetings」表示成功。
- 按【Disconnect】終止與 server 的連線。
文章的來源是「Spring Boot Angular Websocket」,我直接執行裡面的程式會有問題,我修正後整理如下。
- Angular 程式
- install
npm install stompjs --save
npm install sockjs --save
What is STOMP
STOMP stands for Streaming Text Oriented Messaging Protocol. As per wiki, STOMP is a simple text-based protocol, designed for working with message-oriented middleware (MOM). It provides an interoperable wire format that allows STOMP clients to talk with any message broker supporting the protocol.This means when we do not have STOMP as a client, the message sent lacks of information to make Spring route it to a specific message handler method. So, if we could create a mechanism that can make Spring to route to a specific message handler then probably we can make websocket connection without STOMP.
STOMP 即 Simple (or Streaming) Text Orientated Messaging Protocol,簡單(流)文本定向消息協議,它提供一個可互操作的連接格式,允許 STOMP 客戶端與任意 STOMP 訊息代理 (broker) 進行交互傳送訊息。STOMP 協議由於設計簡單,易於開發客戶端,因此在多種語言和多種平台上得到廣泛應用。
- app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule, FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
- app.component.html
<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="button" [disabled]="disabled" (click)="connect()">Connect</button>
<button id="disconnect" class="btn btn-default" type="button" [disabled]="!disabled" (click)="disconnect()">Disconnect
</button>
</div>
</form>
</div>
<div class="col-md-6">
<form class="form-inline" name="test-form">
<div class="form-group">
<label for="name">What is your name?</label>
<input type="text" id="name" name="name" class="form-control" placeholder="Your name here..." [(ngModel)]="name">
</div>
<button id="send" class="btn btn-default" type="button" (click)="sendName()">Send</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12" *ngIf="showConversation">
<table id="conversation" class="table table-striped">
<thead>
<tr>
<th>Greetings</th>
</tr>
</thead>
<tbody *ngFor="let greeting of greetings" >
<tr><td> </td></tr>
</tbody>
</table>
</div>
</div>
</div>
- app.component.ts
import { Component } from '@angular/core';
import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
greetings: string[] = [];
showConversation: boolean = false;
ws: any;
name: string;
disabled: boolean;
constructor(){}
connect() {
let socket = new WebSocket("ws://localhost:8080/greeting");
this.ws = Stomp.over(socket);
let that = this;
this.ws.connect({}, function(frame) {
that.ws.subscribe("/errors", function(message) {
alert("Error " + message.body);
});
that.ws.subscribe("/topic/reply", function(message) {
console.log(message)
that.showGreeting(message.body);
});
that.disabled = true;
}, function(error) {
alert("STOMP error " + error);
});
}
disconnect() {
if (this.ws != null) {
this.ws.ws.close();
}
this.setConnected(false);
console.log("Disconnected");
}
sendName() {
let data = JSON.stringify({
'name' : this.name
})
this.ws.send("/app/message", {}, data);
}
showGreeting(message) {
this.showConversation = true;
this.greetings.push(message)
}
setConnected(connected) {
this.disabled = connected;
this.showConversation = connected;
this.greetings = [];
}
}
- server 提供的 broker endpoint 為 /greeting,所以一開始要先從這個介面與 server 端建立連線。
- 傳送資料是傳送到 /app/message,回覆資料是以 call back 的方式回覆,所以要訂閱。
- 上面可以看到我們訂閱了兩個訊息 "/topic/reply"、"/errors",分別是正確時回覆的訊息及當發生錯誤時回覆的錯誤訊息。
- spring boot Server 端程式
- build.gradle
buildscript {
ext {
springBootVersion = '2.0.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'idv.steven.demowebsocket'
version = '1.0'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-websocket')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
}
- SecurityConfig.java
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll();
}
}
- WebSocketConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic/", "/queue/");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/greeting").setAllowedOrigins("*");
}
}
- WebSocketController.java
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;
import com.google.gson.Gson;
@Controller
public class WebSocketController {
@Autowired
private SimpMessageSendingOperations messagingTemplate;
@MessageMapping("/message")
@SendTo("/topic/reply")
public String processMessageFromClient(@Payload String message) throws Exception {
String name = new Gson().fromJson(message, Map.class).get("name").toString();
return name;
}
@MessageExceptionHandler
public String handleException(Throwable exception) {
messagingTemplate.convertAndSend("/errors", exception.getMessage());
return exception.getMessage();
}
}
- @MessagMapping("/message") 是 client 傳過來時的路徑,要注意,在前面我們有設定 prefixes 為 "app",所以實際的路徑為"/app/message"。
- @SendTo("...") 是回覆給 client 的路徑,client 想收到回覆訊息,要先訂閱。
- @MessageExceptionHandler 是發生錯誤時會執行的 method,client 端想收到錯誤訊息,要訂閱 "/errors"。
- DemowebsocketApplication.java
@SpringBootApplication
public class DemowebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(DemowebsocketApplication.class, args);
}
}
沒有留言:
張貼留言