繼續上一篇《HTML5 WebSocket 技術介紹》的內容,本篇將以示例說明WebSocket的使用,這個示例同時結合了TWaver HTML5的使用,場景如下:后台提供拓撲數據,並以JSON格式通過WebSocket推送到各個客戶端,客戶端獲取到拓撲信息后,通過TWaver HTML5的Network組件呈現於界面,客戶端可以操作網元,操作結果通過WebSocket提交到后台,后台服務器更新並通知所有的客戶端刷新界面,此外后台服務器端還會不斷產生告警,並推送到各個客戶端更新界面。
大體結構
准備
需要用到jetty和twaver html5,可自行下載:
jetty :http://www.eclipse.org/jetty/
twaver html5
jetty目錄結構
jetty下載解壓后是下面的結構,運行start.jar(java -jar start.jar)啟動jetty服務器,web項目可以發布在/webapps目錄中,比如本例目錄/webapps/alarm/
后台部分
后台使用jetty,其使用風格延續servlet的api,可以按Serlvet的使用和部署方式來使用,本例中主要用到三個類
- WebSocketServlet – WebSocket服務類
- WebSocket – 對應一個WebSocket客戶端
- WebSocket.Conllection – 代表一個WebSocket連接
WebSocketServlet
全名為org.eclipse.jetty.websocket.WebSocketServlet,用於提供websocket服務,繼承於HttpServlet,增加了方法public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol),在客戶端第一次請求websocket連接時會調用該方法,如果允許建立連接,則返回一個WebSocket實例對象,否則返回null。
本例中將定義一個AlarmServlet類,繼承於WebSocketServlet,並實現doWebSocketConnect方法,返回一個AlarmWebSocket實例,代表一個客戶端。
AlarmServlet
AlarmWebSocket中有個clients屬性,用於維持一個客戶端(AlarmWebSocket)列表,當與客戶端建立連接時,會將客戶端對應的AlarmWebSocket實例添加到這個列表,當客戶端關閉時,則從這個列表中刪除。
public class AlarmServlet extends org.eclipse.jetty.websocket.WebSocketServlet { private final Set<AlarmWebSocket> clients;//保存客戶端列表 public AlarmServlet() { initDatas();//初始化數據 } @Override public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { return new AlarmWebSocket(); } //... }
AlarmWebSocket
來看看AlarmWebSocket的實現,這里定義的是一個內部類,實現了接口org.eclipse.jetty.websocket.WebSocket.OnTextMessage的三個方法:onOpen/onMessage/onClose,這三個方法分別在連接建立,收到客戶端消息,關閉連接時回調,如果需要向客戶端發送消息,可以通過Connection#sendMessage(…)方法,消息統一使用JSON格式,下面是具體實現:
class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage { WebSocket.Connection connection; @Override public void onOpen(Connection connect) { this.connection = connect; clients.add(this); sendMessage(this, "reload", loadDatas()); } @Override public void onClose(int code, String message) { clients.remove(this); } @Override public void onMessage(String message) { Object json = JSON.parse(message); if(!(json instanceof Map)){ return; } //解析消息,jetty中json數據將被解析成map對象 Map map = (Map)json; //通過消息中的信息,更新后台數據模型 ... //處理消息,通知到其他各個客戶端 for(AlarmWebSocket client : clients){ if(this.equals(client)){ continue; } sendMessage(client, null, message); } } } private void sendMessage(AlarmWebSocket client, String action, String message){ try { if(message == null || message.isEmpty()){ message = "\"\""; } if(action != null){ message = "{\"action\":\"" + action + "\", \"data\":" + message + "}"; } client.connection.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } }
后台配置
后台配置如serlvet相同,這里設置的url名稱為/alarmServer
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" metadata-complete="false" version="3.0"> <servlet> <servlet-name>alarmServlet</servlet-name> <servlet-class>web.AlarmServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>alarmServlet</servlet-name> <url-pattern>/alarmServer</url-pattern> </servlet-mapping> </web-app>
前台部分
看看前台的大體結構,創建websocket連接,監聽相關事件,比如onmessage事件,可以收到后台發送的信息(JSON格式),解析后更新到界面,詳細的處理函數將稍后介紹
function init(){ window.WebSocket = window.WebSocket || window.MozWebSocket; if (!window.WebSocket){ alert("WebSocket not supported by this browser"); return; } var websocket = new WebSocket("ws://127.0.0.1:8080/alarm/alarmServer"); websocket.onopen = onopen; websocket.onclose = onclose; websocket.onmessage = onmessage; ... } function onmessage(evt){ var data = evt.data; if(!data){ return; } data = stringToJson(data); if(!data){ return; } ... } function jsonToString(json){ return JSON.stringify(json); } function stringToJson(str){ try{ str = str.replace(/\'/g, "\""); return JSON.parse(str); }catch(error){ console.log(error); } }
WebSocket前后台流程
業務實現
數據模型
本例需要用到三種業務類型,節點,連線和告警,后台分別提供了實現類,並定義了名稱,位置,線寬等屬性,此外還提供了導出json數據的功能。
interface IJSON{ String toJSON(); } class Data{ String name; public Data(String name){ this.name = name; } } class Node extends Data implements IJSON{ public Node(String name, double x, double y){ super(name); this.x = x; this.y = y; } double x, y; public String toJSON(){ return "{\"name\":\"" + name + "\", \"x\":\"" + x + "\",\"y\":\"" + y + "\"}"; } } class Link extends Data implements IJSON{ public Link(String name, String from, String to, int width){ super(name); this.from =from; this.to = to; this.width = width; } String from; String to; int width = 2; public String toJSON(){ return "{\"name\":\"" + name + "\", \"from\":\"" + from + "\", \"to\":\"" + to + "\", \"width\":\"" + width + "\"}"; } } class Alarm implements IJSON{ public Alarm(String elementName, String alarmSeverity){ this.alarmSeverity = alarmSeverity; this.elementName = elementName; } String alarmSeverity; String elementName; @Override public String toJSON() { return "{\"elementName\": \"" + elementName + "\", \"alarmSeverity\": \"" + alarmSeverity + "\"}"; } }
后台維持三個數據集合,分別存放節點,連線和告警信息,此外elementMap以節點名稱為鍵,便於節點的快速查找
Map<String, Data> elementMap = new HashMap<String, AlarmServlet.Data>(); List<Node> nodes = new ArrayList<AlarmServlet.Node>(); List<Link> links = new ArrayList<AlarmServlet.Link>(); List<Alarm> alarms = new ArrayList<AlarmServlet.Alarm>();
初始化數據
在servlet構造中,我們添加了些模擬數據,在客戶端建立連接時(AlarmWebSocket#onOpen(Connection connection)),后台將節點連線和告警信息以JSON格式發送到前台(sendMessage(this, “reload”, loadDatas());)
public AlarmServlet() { initDatas(); ... } public void initDatas() { int i = 0; double cx = 350, cy = 230, a = 250, b = 180; nodes.add(new Node("center", cx, cy)); double angle = 0, perAngle = 2 * Math.PI/10; while(i++ < 10){ Node node = new Node("node_" + i, cx + a * Math.cos(angle), cy + b * Math.sin(angle)); elementMap.put(node.name, node); nodes.add(node); angle += perAngle; } i = 0; while(i++ < 10){ Link link = new Link("link_" + i, "center", "node_" + i, 1 + random.nextInt(10)); elementMap.put(link.name, link); links.add(link); } } private String loadDatas(){ StringBuffer result = new StringBuffer(); result.append("{\"nodes\":"); listToJSON(nodes, result); result.append(", \"links\":"); listToJSON(links, result); result.append(", \"alarms\":"); listToJSON(alarms, result); result.append("}"); return result.toString(); } class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage { ... @Override public void onOpen(Connection connect) { this.connection = connect; clients.add(this); sendMessage(this, "reload", loadDatas()); } ... }
初始數據前台展示
初始數據通過后台的sendMessage(…)方法推送到客戶端,客戶端可以在onmessage回調函數中收到,本例我們使用twaver html5組件來展示這些信息。TWaver組件的使用流程一如既往,先作數據轉換,將JSON數據轉換成TWaver的網元類型,然后填充到ElementBox數據容器,最后關聯上Network拓撲圖組件,代碼如下:
<!DOCTYPE html> <html> <head> <title>TWaver HTML5 Demo - Alarm</title> <script type="text/javascript" src="./twaver.js"></script> <script type="text/javascript"> var box, network, nameFinder; function init(){ network = new twaver.network.Network(); box = network.getElementBox(); nameFinder = new twaver.QuickFinder(box, "name"); var networkDom = network.getView(); networkDom.style.width = "100%"; networkDom.style.height = "100%"; document.body.appendChild(networkDom); window.WebSocket = window.WebSocket || window.MozWebSocket; if (!window.WebSocket){ alert("WebSocket not supported by this browser"); return; } var websocket = new WebSocket("ws://127.0.0.1:8080/alarm/alarmServer"); ... websocket.onmessage = onmessage; } ... function onmessage(evt){ var data = evt.data; if(!data){ return; } data = stringToJson(data); if(!data){ return; } var action = data.action; if(!action){ return; } if(action == "alarm.clear"){ box.getAlarmBox().clear(); return; } data = data.data; if(!data){ return; } if(action == "reload"){ reloadDatas(data); return; } if(action == "alarm.add"){ newAlarm(data) return; } if(action == "node.move"){ modeMove(data); return; } } function reloadDatas(datas){ box.clear(); var nodes = datas.nodes; var links = datas.links; var alarms = datas.alarms; for(var i=0,l=nodes.length; i < l; i++){ var data = nodes[i]; var node = new twaver.Node(); node.setName(data.name); node.setCenterLocation(parseFloat(data.x), parseFloat(data.y)); box.add(node); } for(var i=0,l=links.length; i < l; i++){ var data = links[i]; var from = findFirst(data.from); var to = findFirst(data.to); var link = new twaver.Link(from, to); link.setName(data.name); link.setStyle("link.width", parseInt(data.width)); box.add(link); } var alarmBox = box.getAlarmBox(); for(var i=0,l=alarms.length; i < l; i++){ newAlarm(alarms[i]); } } function findFirst(name){ return nameFinder.findFirst(name); } function newAlarm(data){ var element = findFirst(data.elementName); var alarmSeverity = twaver.AlarmSeverity.getByName(data.alarmSeverity); if(!element || !alarmSeverity){ return; } addAlarm(element.getId(), alarmSeverity, box.getAlarmBox()); } function addAlarm(elementID,alarmSeverity,alarmBox){ var alarm = new twaver.Alarm(null, elementID,alarmSeverity); alarmBox.add(alarm); } function modeMove(datas){ for(var i=0,l=datas.length; i<l; i++){ var data = datas[i]; var node = findFirst(data.name); if(node){ var x = parseFloat(data.x); var y = parseFloat(data.y); node.setCenterLocation(x, y); } } } ... </script> </head> <body onload="init()" style="margin:0;"></body> </html>
界面效果
后台推送告警,前台實時更新
增加后台推送告警的代碼,這里我們在后台起了一個定時器,每隔兩秒產生一條隨機告警,或者清除所有告警,並將信息推送給所有的客戶端
后台代碼如下:
public AlarmServlet() { ... Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { if(random.nextInt(10) == 9){ alarms.clear(); sendMessage ("alarm.clear", ""); return; } sendMessage("alarm.add", randomAlarm()); } }, 0, 2000); } public void sendMessage(String action, String message) { for(AlarmWebSocket client : clients){ sendMessage(client, action, message); } } private Random random = new Random(); private Data getRandomElement(){ if(random.nextBoolean()){ return nodes.get(random.nextInt(nodes.size())); } return links.get(random.nextInt(links.size())); } String[] alarmSeverities = new String[]{"Critical", "Major", "Minor", "Warning", "Indeterminate"}; private String randomAlarm(){ Alarm alarm = new Alarm(getRandomElement().name, alarmSeverities[random.nextInt(alarmSeverities.length)]); alarms.add(alarm); return alarm.toJSON(); }
前台代碼:
客戶端接收到消息后,需要對應的處理,增加對”alarm.clear”和”alarm.add”的處理,這樣告警就能實時更新了
function onmessage(evt){ ... if(action == "alarm.clear"){ box.getAlarmBox().clear(); return; } data = data.data; if(!data){ return; } ... if(action == "alarm.add"){ newAlarm(data) return; } ... }
客戶端拖拽節點,同步到其他客戶端
最后增加拖拽同步,監聽network網元拖拽監聽,在網元拖拽放手后,將節點位置信息發送給后台
前台代碼:
network.addInteractionListener(function(evt){ var moveEnd = "MoveEnd"; if(evt.kind.substr(-moveEnd.length) == moveEnd){ var nodes = []; var selection = box.getSelectionModel().getSelection(); selection.forEach(function(element){ if(element instanceof twaver.Node){ var xy = element.getCenterLocation(); nodes.push({name: element.getName(), x: xy.x, y: xy.y}); } }); websocket.send(jsonToString({action: "node.move", data: nodes})); } });
后台接收到節點位置信息后,首先更新后台數據(節點位置),然后將消息轉發給其他客戶端,這樣各個客戶端就實現了同步操作
后台代碼:
class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage { ... @Override public void onMessage(String message) { Object json = JSON.parse(message); if(!(json instanceof Map)){ return; } Map map = (Map)json; Object action = map.get("action"); Object data = map.get("data"); if("node.move".equals(action)){ if(!(data instanceof Object[])){ return; } Object[] nodes = (Object[])data; for(Object nodeData : nodes){ if(!(nodeData instanceof Map) || !((Map)nodeData).containsKey("name") || !((Map)nodeData).containsKey("x") || !((Map)nodeData).containsKey("y")){ continue; } String name = ((Map)nodeData).get("name").toString(); Data element = elementMap.get(name); if(!(element instanceof Node)){ continue; } double x = Double.parseDouble(((Map)nodeData).get("x").toString()); double y = Double.parseDouble(((Map)nodeData).get("y").toString()); ((Node)element).x = x; ((Node)element).y = y; } }else{ return; } for(AlarmWebSocket client : clients){ if(this.equals(client)){ continue; } sendMessage(client, null, message); } } }
完整代碼
結構: