求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
HTML5 WebSocket 应用示例
 

发布于2012-5-21

 

继续上一篇《HTML5 WebSocket 技术介绍》的内容,本篇将以示例说明WebSocket的使用,这个示例同时结合了TWaver HTML5的使用,场景如下:后台提供拓扑数据,并以JSON格式通过WebSocket推送到各个客户端,客户端获取到拓扑信息后,通过TWaver HTML5的Network组件呈现于界面,客户端可以操作网元,操作结果通过WebSocket提交到后台,后台服务器更新并通知所有的客户端刷新界面,此外后台服务器端还会不断产生告警,并推送到各个客户端更新界面。

大体结构

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/HTML Code复制内容到剪贴板

<?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); 
      } 
   } 
} 

完整代码

代码:webSocketDemo

结构:

相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
 
分享到
 
 
     


十天学会DIV+CSS(WEB标准)
HTML 5的革新:结构之美
介绍27款经典的CSS框架
35个有创意的404错误页面
最容易犯的13个JavaScript错误
设计易理解和操作的网站
更多...   


设计模式原理与应用
从需求过渡到设计
软件设计原理与实践
如何编写高质量代码
单元测试、重构及持续集成
软件开发过程指南


东软集团 代码重构
某金融软件服务商 技术文档
中达电通 设计模式原理与实践
法国电信 技术文档编写与管理
西门子 嵌入式设计模式
中新大东方人寿 技术文档编写
更多...