2018-05-01 BEX5内使用websocket实现前端数据实时同步

1 搭建运行websocket的环境(这里只用tomcat说明)

为了能让websocket运行起来,需要tomcat 7.0版本以上,但是目前X5使用的是tomcat6,可以通过以下两种方式达到条件

1.1 通过替换掉X5里面的tomcat来升级,替换步骤如下:

step1 下载解压版的tomcat 8

   https://tomcat.apache.org/download-80.cgi

step2 把tomcat 8拷贝到平台版本中把名字改为平台版本默认带的tomcat目录的名字

step3 把平台默认带的tomcat中的apache-tomcat\conf\context.xml文件和apache-tomcat\conf\Catalina\localhost下面的配置文件拷贝到tomcat 8中

step4 在自己的tomcat的lib中放数据库驱动,平台默认的tomcat的lib下带的数据库驱动如下: 

   jtds-1.2.jar、mysql-connector-java-5.1.36-bin.jar、ojdbc14.jar

step5 如果tomcat 8端口号不是8080,需要修改model同级的conf/server.xml中配置的地址中的端口号

step6 部署后输入http://IP:端口默认不会跳转到平台的页面中以及地址栏中图标是tomcat默认的,不是平台的蓝色图标;如果需要默认跳转到平台页面并且用平台的图标,需要把平台默认带的tomcat\webapps\ROOT下的index.html和favicon.ico两个文件拷贝到自己的tomcat\webapps\ROOT下

step7 需要在apache-tomcat\bin\startup.bat中配置java的环境变量

1 set JRE_HOME=..\..\java\jre1.8
2 set JAVA_HOME=
3 set CATALINA_BASE=..\..\apache-tomcat
4 set PATH=%JRE_HOME%\bin;%PATH%

 (如果按照以上步骤有疑问可以参考官网帖子http://docs.wex5.com/bex5-deploy-question-list-4001/)

 注意:安装以上步骤部署后开发工具点击运行时会出现以下报错

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/juli/logging/LogFactory
    at org.apache.catalina.startup.Bootstrap.<clinit>(Bootstrap.java:49)
Caused by: java.lang.ClassNotFoundException: org.apache.juli.logging.LogFactory
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

解决方法可参考http://docs.wex5.com/wex5-deploy-quetion-list-0006/

在Studio中,执行以下操作:

step1 在右上角选择“Java”,切换到Java视图

step2 选择“运行 –> 调试配置…”,弹出调试配置对话框

step3 在“调试配置”对话框中选择“Java应用程序 –> Tomat 5.x”, 选择“类路径”选项卡中,将%TOMCAT_HOME%\bin\tomcat-juli.jar添加至“用户条目”中,之后点“应用”

 

step4 之后运行Tomcat不能通过Tomcat图标快捷方式启动, 必须在Java视图中,使用下图中的“Tomcat 5.x”启动Tomcat

step5 启动tomcat后, 会出现以下错误

[JPivot] 13 六月 2016 17:29:26,072 ERROR [Session ] com.tonbeller.tbutils.res.JNDIResourceProvider#close: error closing context
javax.naming.OperationNotSupportedException: Context is read only
    at org.apache.naming.NamingContext.checkWritable(NamingContext.java:961)
    at org.apache.naming.NamingContext.close(NamingContext.java:761)
    at com.tonbeller.tbutils.res.JNDIResourceProvider.close(JNDIResourceProvider.java:68)
    at com.tonbeller.tbutils.res.CompositeResourceProvider.close(CompositeResourceProvider.java:56)
    at com.tonbeller.tbutils.res.ResourcesFactory.initialize(ResourcesFactory.java:163)
    at com.tonbeller.tbutils.res.ResourcesFactory.<init>(ResourcesFactory.java:92)
    at com.tonbeller.tbutils.res.ResourcesFactory.<clinit>(ResourcesFactory.java:89)
    at com.tonbeller.tbutils.res.ResourcesFactoryContextListener.contextInitialized(ResourcesFactoryContextListener.java:23)
    at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:5068)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5584)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:147)
    at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:899)
    at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:875)
    at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:652)
    at org.apache.catalina.startup.HostConfig.deployDescriptor(HostConfig.java:679)
    at org.apache.catalina.startup.HostConfig$DeployDescriptor.run(HostConfig.java:1966)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

处理方式:在%JUSTEP_HOME%\runtime\ReportServer\WEB-INF\classes目录下添加一个resfactory.properties文件,文件内容为

tbeller.usejndi=false

以上的操作貌似对tomcat8并不生效,而且也麻烦,所以个人建议最好采用另一种方法来满足运行需求

1.2 通过在X5之外部署新的tomcat7、8来运行websocket

step1 安装JRE,如果使用X5自带的则直接跳过这步

step2 解压安装tomcat8(下载地址参考上面的方法)

step3 配置环境变量(百度就知道),但是为了不影响同服务器的其他tomcat,可以使用上面方法的step7

step4 修改tomcat的端口,让它不与其他tomcat端口冲突


2 请求websocket使用ws还是wss的选择
当使用websocket的项目使用的是https,如果使用ws协议请求连接websocket会出现以下错误

Mixed Content: The page at 'https://xxx' was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint 'ws://xxx/websocket/?rid=18'. 
This request has been blocked; this endpoint must be available over WSS.

刚好发现有个博客遇到了同样的问题 https://mengkang.net/774.html

为了解决这一问题,提供了以下两种方法

2.1 让https请求中允许使用ws
在页面头信息增加

header('Content-Security-Policy: connect-src *;');

(这个方法我没测试过,可以自己根据那个博客试一下)

2.2 为websocket所在tomcat增加ssl证书

假如websocket部署的是跟X5部署的服务器是同一个,那么X5使用https的话,直接把证书配置进websocket的tomcat就行了;但是也有可能websocket所在的服务器和x5并不一定在同一个服务器,所以下面讲一下let‘s encrypt证书的申请方法。

(let‘s encrypt是什么自己百度就可以了,反正就是一个免费的好用的证书)

本方法是在windows服务器下申请证书,如果是其他系统,可以自行百度,一般使用openssl进行申请,我这里是采用证书官方提供的工具进行申请

step1 下载一个letsencrypt-win-simple工具,放入服务器解压

     https://pan.baidu.com/s/1g_Enw7CHrRGxkfwYBY8F-g

   

step2 点击letsencrypt运行

step3 填写个人邮箱以接收证书过期等信息

step4 同意一些啥协议(类似安装时候选的同意,还正只能选Y)

step5 选择证书,选择M

step6 填写域名

step7 填写域名对应服务的根目录文件路径,例如我用的端口80的是tomcat,那么我就要填tomcat的webapps下的ROOT路径

它会把一个文件放入到根路径下,并通过域名+根路径方式来访问,如果访问到了就会生成证书,证书地址它会在命令窗口显示出来

一般会生成到C:\Users\Administrator\AppData\Roaming\letsencrypt-win-simple,这个路径直接输入到系统地址栏跳转

step8 上面步骤通过后还会询问是否需要自动续证书(证书有效期3个月),如果需要,后面会要求填写服务器登陆的账号密码,这里就直接跳过,可以自己尝试

step9 得到证书后,就可以到tomcat 8 中增加以下代码

 <Connector port="443" protocol="org.apache.coyote.http11.Http11AprProtocol"
               maxThreads="150" SSLEnabled="true"  URIEncoding="UTF-8">
        <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
        <SSLHostConfig>
            <Certificate certificateKeyFile="C:/Users/Administrator/AppData/Roaming/letsencrypt-win-simple/httpsacme-v01.api.letsencrypt.org/zcit2018.cn-key.pem"
                         certificateFile="C:/Users/Administrator/AppData/Roaming/letsencrypt-win-simple/httpsacme-v01.api.letsencrypt.org/zcit2018.cn-crt.pem"
                         certificateChainFile="C:/Users/Administrator/AppData/Roaming/letsencrypt-win-simple/httpsacme-v01.api.letsencrypt.org/zcit2018.cn-chain.pem"
                         type="RSA" />
        </SSLHostConfig>
    </Connector>

这样就能通过https进行访问了

(在网上看到个差不多的证书申请博客 https://blog.csdn.net/qq_27424559/article/details/67661220)

3 websocket代码实现

3.1 后端服务

(项目名为ZCServer)
step1 增加一个监听用的websocket

 1 package com.zc.websocket;
 2 
 3 import java.io.IOException;
 4 import java.util.HashMap;
 5 import java.util.Map;
 6 import java.util.concurrent.CopyOnWriteArraySet;
 7 
 8 import javax.websocket.OnClose;
 9 import javax.websocket.OnOpen;
10 import javax.websocket.Session;
11 import javax.websocket.server.ServerEndpoint;
12 
13 
14 @ServerEndpoint("/listener")
15 public class Listener {
16     
17     private static int onlineCount = 0;
18     
19     private static Map<String,CopyOnWriteArraySet<Listener>> webSocketMap = new HashMap<String,CopyOnWriteArraySet<Listener>>();
20     
21     private String fEventID = null;
22     
23     //与某个客户端的连接会话,需要通过它来给客户端发送数据
24     private Session session;
25     
26     
27     public static CopyOnWriteArraySet<Listener> getwebSocketSet(String fEventID){
28         
29         return Listener.webSocketMap.get(fEventID);
30     }
31      
32     /**
33      * 连接建立成功调用的方法
34      * 
35      * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
36      * 
37      */
38     @OnOpen
39     public void onOpen(Session session) {
40         this.session = session;
41         
42         this.fEventID = session.getRequestParameterMap().get("fEventID").get(0);
43         
44         CopyOnWriteArraySet<Listener> socekts = webSocketMap.get(this.fEventID);
45         if(socekts==null){ 
46             socekts = new CopyOnWriteArraySet<Listener>();
47             webSocketMap.put(this.fEventID,socekts); 
48         }
49         
50         socekts.add(this);
51         
52         addOnlineCount();   
53         System.out.println("有新监听加入!当前监听数为" + getOnlineCount());
54     }
55     
56     /**
57      * 连接关闭调用的方法
58      */
59     @OnClose
60     public void onClose() {
61         
62         webSocketMap.get(this.fEventID).remove(this);
63         
64         
65          subOnlineCount();
66          System.out.println("有一监听关闭!当前监听数为" + getOnlineCount());
67     }
68     
69     /**
70      * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
71      * 
72      * @param message
73      * @throws IOException
74      */
75     public void sendMessage(String message) throws IOException {
76         this.session.getBasicRemote().sendText(message);
77     }
78     
79     
80      public static synchronized int getOnlineCount() {
81                  return onlineCount;
82             }
83         
84              public static synchronized void addOnlineCount() {
85                  Listener.onlineCount++;
86             }
87          
88             public static synchronized void subOnlineCount() {
89                 Listener.onlineCount--;
90      }
91     
92 }

代码使用注解的方式,不需要进行配置,使用类变量记录需要监听的对象,实现分组管理不同的监听

step2 增加一个用于通知监听的websocket(也可以用一个servlet)

package com.zc.websocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import com.alibaba.fastjson.JSONObject;


@ServerEndpoint("/notifier")
public class Notifier {
    
    
    /**
     * 收到客户端消息后调用的方法
     * 
     * @param message
     *            客户端发送过来的消息
     * @param session
     *            可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        
        System.out.print("有新的通知:"+message);
        
        
        JSONObject values = JSONObject.parseObject(message);
        
        CopyOnWriteArraySet<Listener> websocketSet = Listener.getwebSocketSet(values.getString("fEventID"));
        
        if(websocketSet==null){
            System.out.println(" 监听数量:0");
            return;
        }else{
            System.out.println(" 监听数量:"+websocketSet.size());
        }
        
        // 群发消息
        for (Listener item : websocketSet) {
            try {
                item.sendMessage(values.getString("message"));
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
    }
    
}

3.2 前端调用

step1 定义一个js工具

define(function(require) {
    
    var Model = function() {
        this.callParent();
    };
    
    var URI = "wss://zcit2018.cn";
//    var URI = "ws://baxd.ys100.com:8999";
    
    var err = {
        "type":"连接失败",
        "URI":URI,
        "address":"/UI2/B/M000_Core/process/Utils/WebSocketUtils.js"
    };
    
    /**
     * 注册一个fEventID的消息接收监听
     * @param fEventID 监听标识
     * @params callback 回调函数,带参数
     * @returns websocket对象
     */
    Model.setListener = function(fEventID,callback){
        if(!fEventID){
            throw "fEventID 不能为空!";
        }
        
        var websocket = null;
        
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
            try{
            websocket = new WebSocket(URI+"/ZCServer/listener?fEventID="+fEventID);
            }catch(e){
                console.log(err);
                return false;
            }
        } else {
            console.log('当前浏览器 Not support websocket');
            return false;
        }
        //连接发生错误的回调方法
        websocket.onerror = function() {
            
            console.log(err);
            
        };
        
        //连接成功建立的回调方法
        websocket.onopen = function() {};
        //接收到消息的回调方法
        websocket.onmessage = function(event) {
            if(callback&& typeof callback =="function"){
                callback(event);
            }
        };
        
        //连接关闭的回调方法
        websocket.onclose = function() {};
        
        return websocket;
    };    
    
    /**
     * 给fEventIDs发送通知message
     * @params fEventIDs 监听者IDs数组
     * @params message 消息内容
     * 
     */
    Model.sendNotic = function(fEventIDs,message){
        if(!Array.isArray(fEventIDs)){
            throw "fEventIDs 必须为数组!";
        }
        
        var websocket = null;
        
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
            try{
                websocket = new WebSocket(URI+"/ZCServer/notifier");
            }catch(e){
                console.log(err);
                return false;
            }
        } else {
            console.log('当前浏览器 Not support websocket');
            return false;
        }
        
        

        websocket.onopen = function() {

            for ( var i in fEventIDs) {
                websocket.send(JSON.stringify({
                    "fEventID" : fEventIDs[i],
                    "message" : message?message:""
                }));
            }
            websocket.close();
        };
        
        
    };
    

    return Model;
});

增加监听器的代码(引入工具js就不描述了)

1 websocket.setListener("10e0495c-650c-486a-8d65-552f103aa243",function(re){
2      // 处理逻辑
3  });

增加通知的代码

websocket.sendNotic(['10e0495c-650c-486a-8d65-552f103aa243'],message);

一个处理可能需要引起多个监听器的响应,所以参数使用的数组

整个websocket基本就是这样,但是有时候进行通知的未必是通过前端操作的,所以还需要提供一个java工具用于通知

posted @ 2018-05-01 19:23  JaminLee  阅读(814)  评论(0编辑  收藏  举报