zoukankan      html  css  js  c++  java
  • 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-tomcatconfcontext.xml文件和apache-tomcatconfCatalinalocalhost下面的配置文件拷贝到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默认的,不是平台的蓝色图标;如果需要默认跳转到平台页面并且用平台的图标,需要把平台默认带的tomcatwebappsROOT下的index.html和favicon.ico两个文件拷贝到自己的tomcatwebappsROOT下

    step7 需要在apache-tomcatinstartup.bat中配置java的环境变量

    1 set JRE_HOME=....javajre1.8
    2 set JAVA_HOME=
    3 set CATALINA_BASE=....apache-tomcat
    4 set PATH=%JRE_HOME%in;%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%in omcat-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% untimeReportServerWEB-INFclasses目录下添加一个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:UsersAdministratorAppDataRoamingletsencrypt-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工具用于通知

  • 相关阅读:
    设计模式——观察者模式
    LeetCode OJ——Word Ladder
    unorder_set<typename T> 学习
    LeetCode OJ——Text Justification
    LeetCode OJ——Two Sum
    LeetCode OJ——Longest Valid Parentheses
    路飞学城Python-Day16
    路飞学城Python-Day15
    路飞学城Python-Day14(practise)
    路飞学城Python-Day14
  • 原文地址:https://www.cnblogs.com/WongHugh/p/9262042.html
Copyright © 2011-2022 走看看