本文聚焦「架構與 WebSocket 設定」,說明為什麼用 STOMP/WebSocket、端點與前綴怎麼切、首次訂閱如何回推一次快照。
次日會介紹「實際推送內容」:市場摘要、訂單簿、逐筆成交、最近成交等 DTO 與 payload實際內容。
背景:為什麼不用 REST 輪詢?
效能差:每秒輪詢造成大量重複查詢。
延遲高:行情講究秒級/毫秒級,輪詢跟不上。
解法:STOMP over WebSocket,前端「訂閱一次、持續接收」。
端點:/ws(支援 SockJS 降級)
Application Prefix:/app(對應 @MessageMapping)
Broker Prefix:/topic、/queue(前端訂閱用)
消息推送:SimpMessagingTemplate.convertAndSend(...)
首次訂閱即回推:使用 @SubscribeMapping 在訂閱當下回一次快照
端點 /ws、/app 前綴、/topic//queue broker
以下為我的程式碼節錄內容
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue"); // 前端訂閱
config.setApplicationDestinationPrefixes("/app"); // 後端處理入口
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // 有需要可移除 SockJS 僅保留原生 WS
}
}
設定重點
/ws:前端 WebSocket/SockJS 連線入口。
/app:前端送指令到後端(@MessageMapping)。
/topic:後端推送資料的訂閱目的地(convertAndSend("/topic/…", data))。
首次訂閱即推一次,之後根據排程設定的時間做訊息推播,並提供心跳與手動刷新
@Controller
@RequiredArgsConstructor
@Slf4j
public class WebSocketController {
private final MarketDataService marketDataService;
// === 訂閱 ===
@SubscribeMapping("/topic/market")
public void subscribeMarketData() {
log.info("客戶端訂閱市場數據");
marketDataService.pushMarketData(); // 首次訂閱回一次
}
@SubscribeMapping("/topic/trades")
public void subscribeRealtimeTrades() {
log.info("客戶端訂閱實時成交數據");
marketDataService.pushRecentTrades(); // 先把最近成交補齊
}
@SubscribeMapping("/topic/trades/recent")
public void subscribeRecentTrades() {
log.info("客戶端訂閱最近成交記錄");
marketDataService.pushRecentTrades();
}
@SubscribeMapping("/topic/orderbook")
public void subscribeOrderBook() {
log.info("客戶端訂閱訂單簿數據");
marketDataService.pushOrderBook(); // 推一份即時簿
}
// === 指令 ===
@MessageMapping("/heartbeat")
@SendTo("/topic/heartbeat")
public String handleHeartbeat(String message) {
log.debug("收到心跳消息: {}", message);
return "pong";
}
@MessageMapping("/market/refresh")
public void handleMarketRefresh() {
log.info("客戶端請求刷新市場數據");
marketDataService.pushMarketData();
marketDataService.pushOrderBook();
}
}
訂閱/指令一覽
訂閱:/topic/market、/topic/orderbook、/topic/trades、/topic/trades/recent
指令:/app/heartbeat(回 /topic/heartbeat)、/app/market/refresh
筆者為後端工程,對於前端熟悉度較低,這邊採用我學過的js簡單串接方法做介紹
<script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs/lib/stomp.min.js"></script>
<script>
const sock = new SockJS('/ws');
const client = Stomp.over(sock); // 開發期可關閉 debug: client.debug = () => {};
client.connect({}, () => {
// 訂閱主題
client.subscribe('/topic/market', m => console.log('market', JSON.parse(m.body)));
client.subscribe('/topic/orderbook', m => console.log('orderbook', JSON.parse(m.body)));
client.subscribe('/topic/trades', m => console.log('trade', JSON.parse(m.body)));
client.subscribe('/topic/trades/recent', m => console.log('recent', JSON.parse(m.body)));
client.subscribe('/topic/heartbeat', m => console.log('heartbeat', m.body));
// 心跳 + 手動刷新一次
client.send('/app/heartbeat', {}, 'ping');
client.send('/app/market/refresh', {});
});
</script>
啟動 order-service;瀏覽器/前端接上 /ws。
確認訂閱 /topic/market 時,立即收到一次快照。
送 /app/heartbeat 應回 "pong" 到 /topic/heartbeat。
送 /app/market/refresh,應收到市場/訂單簿最新資料。
明天會把「實際推送內容」全部攤開:來源、DTO、payload 例子、撮合事件觸發的即時推播流程。