この記事では、リアルタイム動的チャートの表示機能を実現する、WebSocket に基づくリアルタイム データ双方向通信の小規模なアプリケーションを紹介します。実際、これがチャートの動的な更新を実現する唯一の方法ではありません。ユーザーページのjsハートビートポーリングもバックグラウンドで最新データを取得できますが、擬似リアルタイムな気がします。
WebSocket は、HTML5 が提供し始めた、単一の TCP 接続で全二重通信を行うためのプロトコルです。 WebSocket の通信プロトコルは 2011 年に IETF によって標準 RFC 6455 として制定される予定であり、WebSocket API は W3C によって標準規格として定められています。 WebSocket API では、ブラウザとサーバーはハンドシェイク アクションを実行するだけで、ブラウザとサーバーの間に高速チャネルが形成されます。 データは両者間で直接送信できます。または、国内の Zhihu からの説明をご覧ください: https://www.zhihu.com/question/20215561
プロジェクト要件:
実際には、タイトルこの問題を解決するには、アイコンのリアルタイムの動的更新を実現する必要があることが明らかになりました。また、私のデータ ソースは MQ (Message Queue) からのものです。つまり、メッセージが消費される場所でデータ プッシュがトリガーされます。
STOMP について:
ここで STOMP について言及する必要があります。これも Spring の調査中に発見した新しいプロトコルです。正式名: Simple Text-Orientated Messaging Protocol。プロトコルの公式 Web サイト: http://jmesnil.net/stomp-websocket/doc/。個人的には、これは WebSocket プロトコルのカプセル化された実装であると理解しています。もちろん、Spring は STOMP の実装をうまくカプセル化しており、公式ドキュメントの説明も非常に包括的です。
システム構成:
システム全体は JavaConfig モードで構成されます。したがって、指定された構成メソッドはすべてクラス ファイルです。XML 構成メソッドを使用したい場合は、それを自分で変換できます。
Java
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); //接受请求前缀 registry.enableSimpleBroker("/topic"); //返回请求前缀 } public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/getLoanPoints").withSockJS(); }}
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistryregistry) { registry.setApplicationDestinationPrefixes("/app"); //接受请求前缀 registry.enableSimpleBroker("/topic"); //返回请求前缀 } public void registerStompEndpoints(StompEndpointRegistryregistry) { registry.addEndpoint("/getLoanPoints").withSockJS(); } }
WebSocket メッセージ送信インターフェース
WebSocket メッセージ処理インターフェイス
Java
public interface WebSocketCommonHandler<T> { /** * WebSocket发送消息方法 * * @param t */ void send(T t);}
public interface WebSocketCommonHandler<T> { /** * WebSocket发送消息方法 * * @param t */ void send(T t);}
WebSocket メッセージ処理インターフェイス抽象クラス
Java
public abstract class AbstractWebSocketCommonHandlerimplements WebSocketCommonHandler {
@Autowiredprivate SimpMessagingTemplate template;/** * 设置消息返回路由 * * @return */public abstract String setTopic();/** * WebSocket发送消息方法 * * @param o */public void send(T o) { String topic = setTopic(); if (StringUtils.isEmpty(topic) || o == null) { throw new RuntimeException("Topic is Empty or Object is null!"); } this.template.convertAndSend(topic, o);}
}
public abstract class AbstractWebSocketCommonHandler<T> implements WebSocketCommonHandler<T> { @Autowired private SimpMessagingTemplatetemplate; /** * 设置消息返回路由 * * @return */ public abstract String setTopic(); /** * WebSocket发送消息方法 * * @param o */ public void send(T o) { String topic = setTopic(); if (StringUtils.isEmpty(topic) || o == null) { throw new RuntimeException("Topic is Empty or Object is null!"); } this.template.convertAndSend(topic, o); }}
WebSocket メッセージ送信実装クラス
WebSocket メッセージ処理実装クラス
Java
@Componentpublic class DemoWebSocketHandler extends AbstractWebSocketCommonHandler<DataVo> { /** * 设置消息返回路由 * * @return */ @Override public String setTopic() { return "/topic/addLoanPoint"; }}
@Componentpublic class DemoWebSocketHandler extends AbstractWebSocketCommonHandler<DataVo> { /** * 设置消息返回路由 * * @return */ @Override public String setTopic() { return "/topic/addLoanPoint"; }}
メッセージ処理と WebSocket データプッシュ
メッセージ消費リスナーと WebSocket データ プッシュ
Java
public class DemoMessageListener implements MessageListener { private static final Logger LOGGER = LoggerFactory.getLogger(RepayMessageListener.class); @Autowired private DemoWebSocketHandler demoWebSocketHandler; public void onMessage(List<Message> list) throws Exception { for (Message message : list) { LOGGER.info("还款消息体是:" + message.getText()); Gson gson = new Gson(); Demo demo = gson.fromJson(message.getText(), Demo.class); DataVo dataVo = new DataVo(); dataVo.setType(2); dataVo.setDate(demo.getTime()); dataVo.setValue(demo.getValue()); dataVo.setName(demo.getName()); demoWebSocketHandler.send(dataVo); } }}
public class DemoMessageListener implements MessageListener { private static final LoggerLOGGER = LoggerFactory.getLogger(RepayMessageListener.class); @Autowired private DemoWebSocketHandlerdemoWebSocketHandler; public void onMessage(List<Message> list) throws Exception { for (Messagemessage : list) { LOGGER.info("还款消息体是:" + message.getText()); Gsongson = new Gson(); Demodemo = gson.fromJson(message.getText(), Demo.class); DataVodataVo = new DataVo(); dataVo.setType(2); dataVo.setDate(demo.getTime()); dataVo.setValue(demo.getValue()); dataVo.setName(demo.getName()); demoWebSocketHandler.send(dataVo); } }}
これまでのところ、これらはバックグラウンド システムの一部です 関連する実装また、上記のメッセージ消費については、メッセージ ミドルウェアが異なれば実装方法も異なる場合がありますが、ここでの一般的な考え方は、メッセージを受信し、メッセージ Json をオブジェクトに変換し、それに応じて処理し、その後応答データを処理するというものにすぎません。ハンドラーの send メソッドによって、対応するルーティング アドレスに送信されます。
次にフロントエンド、サーバーに接続してルーティングアドレスを監視する方法ですが、冒頭でフロントエンドのWebSocket管理をSTOMPを使って実装したと書きましたので、jsライブラリが2つあります。前のセクションで使用した 2 つ:socketjs-1.0.3.js と stomp.js。Google で検索すると、特定のダウンロード アドレスを取得できます。
WebSocket ツール js
JavaScript
var websocket = ( function() {
var stompClient = null;/** * 创建WebSocket链接 * * @param url * @param databackurl * @param callback */var createConnectFunc = function connect(url, databackurl, callback) { var socket = new SockJS(url); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe(databackurl, function (response) { if (typeof callback === "function") { callback(response); } else { console.log("Not Function!"); } }); });};/** * 断开WebSocket链接 */var disconnectFunc = function disconnect() { if (stompClient != null) { stompClient.disconnect(); } console.log("WebSocket has Disconnected!");};/** * 发送数据到服务端 * * @param url * @param data */var sendDataFunc = function sendDate(url, data) { stompClient.send("/app" + url, {}, JSON.stringify(data));};/** * 判断是否已经链接 * * @returns {boolean} */var hasConnectedFunc = function hasConnected(){ if (stompClient != null) { return true; } return false;};return { createConnect: createConnectFunc, sendData: sendDataFunc, disconnect: disconnectFunc, hasConnected: hasConnectedFunc}
})();
var websocket = (function () { var stompClient = null; /** * 创建WebSocket链接 * * @param url * @param databackurl * @param callback */ var createConnectFunc = function connect(url, databackurl, callback) { var socket = new SockJS(url); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe(databackurl, function (response) { if (typeof callback === "function") { callback(response); } else { console.log("Not Function!"); } }); }); }; /** * 断开WebSocket链接 */ var disconnectFunc = function disconnect() { if (stompClient != null) { stompClient.disconnect(); } console.log("WebSocket has Disconnected!"); }; /** * 发送数据到服务端 * * @param url * @param data */ var sendDataFunc = function sendDate(url, data) { stompClient.send("/app" + url, {}, JSON.stringify(data)); }; /** * 判断是否已经链接 * * @returns {boolean} */ var hasConnectedFunc = function hasConnected(){ if (stompClient != null) { return true; } return false; }; return { createConnect: createConnectFunc, sendData: sendDataFunc, disconnect: disconnectFunc, hasConnected: hasConnectedFunc }})();
と、demo.js はページのビジネス メソッドです。たとえば、次のとおりです。 echarts の線図の作成方法、ルーティング データが受信されて処理されている
JavaScript
var Demon = (function () { // Based準備された dom 上で、初期化 echarts インスタンス var myChart = echarts.init(document.getElementById('main'));
var loanDataValues = [];var repayDataValues = [];// 使用刚指定的配置项和数据显示图表。var showChartFunc = function () { myChart.setOption({ title: { show: false, text: '图表详情' }, tooltip: { trigger: 'item', formatter: function (params) { var date = new Date(params.value[0]); data = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds(); return data + '<br/>' + '金额:' + params.value[1] + '<br/>' + '公司:' + params.value[2]; } }, legend: { data: ['Demo1金额', 'Demo2金额'] }, toolbox: { show: true, feature: { mark: {show: true}, dataView: {show: true, readOnly: false}, restore: {show: true}, saveAsImage: {show: true} } }, xAxis: [ { type: 'time', splitNumber:10, boundaryGap: ['20%', '20%'], min: 'dataMin', max: 'dataMax' } ], yAxis: [ { type: 'value', scale: true, name: '金额(元)', min: 0, boundaryGap: ['20%', '20%'] } ], dataZoom: { type: 'inside', start: 0, end: 100 }, series: [ { name: 'Demo1金额', type: 'line', smooth: true, symbol: 'circle', data: loanDataValues }, { name: 'Demo2金额', type: 'line', smooth: true, symbol: 'rect', data: repayDataValues } ] });};/** * 实时接受消息并绘制图标 * * @param message */var addPointFunc = function addPoint(message) { var dataVo = JSON.parse(message.body); addData(dataVo); showChartFunc();};function addData(dataVo) { if (dataVo.type == 1) { loanDataValues.push([dataVo.date, dataVo.value, dataVo.name]); } else if (dataVo.type == 2) { repayDataValues.push([dataVo.date, dataVo.value, dataVo.name]); }}/** * WebSocket连接 */var connectFunc = function connect() { websocket.createConnect("/getLoanPoints", "/topic/addLoanPoint", addPointFunc);};/** * 发送数据到服务器(暂时不用) */var sendValueFunc = function sendValue() { var value = document.getElementById('name').value; websocket.sendData("/getLoanPoints", value);};/** * 获取当日借贷信息 */var getLoanFunc = function () { $.getJSON('getLoanInfo').done(function (data) { if (data.success) { loanDataValues = data.loanInfos.datas; repayDataValues = data.repayInfos.datas; showChartFunc(); } else { alert(data.message); } });};return { getLoan: getLoanFunc, connect: connectFunc}
})();
var demo = (function () { // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById('main')); var loanDataValues = []; var repayDataValues = []; // 使用刚指定的配置项和数据显示图表。 var showChartFunc = function () { myChart.setOption({ title: { show: false, text: '图表详情' }, tooltip: { trigger: 'item', formatter: function (params) { var date = new Date(params.value[0]); data = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds(); return data + '<br/>' + '金额:' + params.value[1] + '<br/>' + '公司:' + params.value[2]; } }, legend: { data: ['Demo1金额', 'Demo2金额'] }, toolbox: { show: true, feature: { mark: {show: true}, dataView: {show: true, readOnly: false}, restore: {show: true}, saveAsImage: {show: true} } }, xAxis: [ { type: 'time', splitNumber:10, boundaryGap: ['20%', '20%'], min: 'dataMin', max: 'dataMax' } ], yAxis: [ { type: 'value', scale: true, name: '金额(元)', min: 0, boundaryGap: ['20%', '20%'] } ], dataZoom: { type: 'inside', start: 0, end: 100 }, series: [ { name: 'Demo1金额', type: 'line', smooth: true, symbol: 'circle', data: loanDataValues }, { name: 'Demo2金额', type: 'line', smooth: true, symbol: 'rect', data: repayDataValues } ] }); }; /** * 实时接受消息并绘制图标 * * @param message */ var addPointFunc = function addPoint(message) { var dataVo = JSON.parse(message.body); addData(dataVo); showChartFunc(); }; function addData(dataVo) { if (dataVo.type == 1) { loanDataValues.push([dataVo.date, dataVo.value, dataVo.name]); } else if (dataVo.type == 2) { repayDataValues.push([dataVo.date, dataVo.value, dataVo.name]); } } /** * WebSocket连接 */ var connectFunc = function connect() { websocket.createConnect("/getLoanPoints", "/topic/addLoanPoint", addPointFunc); }; /** * 发送数据到服务器(暂时不用) */ var sendValueFunc = function sendValue() { var value = document.getElementById('name').value; websocket.sendData("/getLoanPoints", value); }; /** * 获取当日借贷信息 */ var getLoanFunc = function () { $.getJSON('getLoanInfo').done(function (data) { if (data.success) { loanDataValues = data.loanInfos.datas; repayDataValues = data.repayInfos.datas; showChartFunc(); } else { alert(data.message); } }); }; return { getLoan: getLoanFunc, connect: connectFunc }})();
while page 必要なのは、ページ要素をロードした後にdemo.connect()を呼び出し、WebSocketリンクを作成し、データがプッシュされるのを待ってから、グラフを描画することだけです。この時点で、簡単なリアルタイム ダイナミック チャートの描画が完了しました。ご不明な点がございましたら、お気軽にメッセージを残してください。 O(∩_∩)O