http://www.html5china.com/HTML5features/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 networknetworkDom = network.getView();
- networkDom.style.width = "100%";
- networkDom.style.height = "100%";
- document.body.appendChild(networkDom);
- windowwindow.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;
- }
- datadata = 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);
- }
- }
- }
完整代码
结构: