HTML5 WebSocket 應用示例


繼續上一篇《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);
		}
	}
}

完整代碼

代碼:webSocketDemo

 

結構:

 

  

  

  

  

  

  

  

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM