在Glassfish v2ur1 中测试grizzly comet chat demo

上一篇 / 下一篇  2008-04-17 21:59:07 / 个人分类:Glassfish

2006年Web 2.0最火的技术莫过于AJAX了,各种AJAX的框架层出不穷。正是应验一句老话:“三十年河东,三十年河西”,其实还不到三十年呢,B/S开发模式已经落伍了,现在C/S模式又吃香了,只不过client不是由OS承载,而是换成了web浏览器;client端的编程语言换成了JavaScript。AJAX似乎可以包办一切,唯一的缺陷是所有业务必须由client端来触发,Server端只能被动地响应。针对AJAX的先天缺陷,Comet技术应运而生,或者说是老树开新花,Server端的推送技术,使得Web浏览器真正成为理想的综合业务通用客户端平台。

Grizzly应用Java NIO框架,利用有限的线程为大量并发的socket连接提供服务,使得Server端有非常出色的扩展性。Grizzly Cometd为我们提供了针对Http请求的异步处理机制,加上对Bayeux协议的支持,我们可以在Cometd Servlet中创建消息队列,浏览器端可以灵活地订阅(Subscribe)消息,当Server端有新的消息需要分发时,将消息推送给订阅者;当然浏览器也可以主动地发布(Publish)消息,Server收到消息后,立即推送给此消息的订阅者(可以是Web浏览器,也可以是特殊的客户端或服务端的进程)。Glizzly Cometd的消息分发采用Bayeux协议。当前能够支持Grizzly+Bayeux的Servlet Container主要是Jetty6Glassfishv2,Tomcat还不支持。在浏览器端,只有Dojo Cometd支持Bayeux协议。

在Jetty下测试Cometd Chat Demo非常顺利,我下载了最新的Jetty 6.1.7,在WinXP(sp2)的DOS窗口中启动Server:
java -jar start.jar etc/jetty.xml etc/jetty-grizzly.xml
使用IE/FF访问 http://localhost:8888/test/chat。

Grizzly是Glassfish的子项目,Glassfish直接使用grizzly的源码,重新打包。我下载的版本是最新发布的V2UR1。Chat Demo(grizzly-cometd-chat.war)来自Jean-Francois Arcand的blog。下载并安装Glassfish V2UR1,配置domains/domain1/config/domain.xml,将ws/tcp注释掉,添加cometSupport支持:
Java代码复制代码
  1. <http-listener id="http-listener-1" address="0.0.0.0" port="8080" acceptor-threads="1" security-enabled="false" default-virtual-server="server" server-name="" xpowered-by="true" enabled="true" family="inet" blocking-enabled="false">   
  2.     <!-- property name="proxiedProtocols" value="ws/tcp"/ -->   
  3.     <property name="cometSupport" value="true"/>   
  4. </http-listener>  

将war文件部署到domains/domain1/autodeploy目录,能够看到Cometd Chat登录界面,输入用户名点击"Join"按钮,显示聊天页面(参见cometd1.PNG),但是未显示已登入聊天室的提示信息。检查glassfish的log文件,发现有异常抛出:
引用
StandardWrapperValve[Grizzly Cometd Servlet]: PWC1406: Servlet.service() for servlet Grizzly Cometd Servlet threw exception
java.lang.ClassCastException: java.lang.Double
at com.sun.grizzly.cometd.bayeux.VerbUtils.newHandshake(VerbUtils.java:126)
at com.sun.grizzly.cometd.bayeux.VerbUtils.parseMap(VerbUtils.java:98)
at com.sun.grizzly.cometd.bayeux.VerbUtils.parse(VerbUtils.java:71)
at com.sun.grizzly.cometd.EventRouterImpl.route(EventRouterImpl.java:90)
at com.demo.servlet.ChatDemoServlet.doPost(ChatDemoServlet.java:152)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:738)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:831)
at org.apache.catalina.core.ApplicationFilterChain.servletService(ApplicationFilterChain.java:411)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:290)
at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:271)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:202)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:632)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:577)
at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:94)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:206)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:632)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:577)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:571)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:1080)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:150)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:632)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:577)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:571)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:1080)
at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:272)
at com.sun.enterprise.web.connector.grizzly.DefaultProcessorTask.invokeAdapter(DefaultProcessorTask.java:637)
at com.sun.enterprise.web.connector.grizzly.comet.CometEngine.executeServlet(CometEngine.java:547)
at com.sun.enterprise.web.connector.grizzly.comet.CometEngine.handle(CometEngine.java:299)
at com.sun.enterprise.web.connector.grizzly.comet.CometAsyncFilter.doFilter(CometAsyncFilter.java:87)
at com.sun.enterprise.web.connector.grizzly.async.DefaultAsyncExecutor.invokeFilters(DefaultAsyncExecutor.java:175)
at com.sun.enterprise.web.connector.grizzly.async.DefaultAsyncExecutor.interrupt(DefaultAsyncExecutor.java:153)
at com.sun.enterprise.web.connector.grizzly.async.AsyncProcessorTask.doTask(AsyncProcessorTask.java:92)
at com.sun.enterprise.web.connector.grizzly.TaskBase.run(TaskBase.java:265)
at com.sun.enterprise.web.connector.grizzly.WorkerThreadImpl.run(WorkerThreadImpl.java:116)

注意:如果使用Glassfish的版本不是V2UR1,可能会得到java.lang.NullPointerException异常。

初步分析错误出自grizzly cometd包对bayeux协议解析时发生数据转型错误。上网抓grizzly的sourcecode,先分析CometdServlet,在分析EventRouter和VerbUtils,发现Glassfish没有使用grizzly的最新版本,再上网抓glassfish的sourcecode。为了便于单步跟踪,在MyEclipse中创建一个Web工程,建立一个ChatDemoServlet替代CometdServlet,建立一个VerbUtils替代原来的VerbUtils,在ChatDemoServlet的doPost()方法中嵌入以下代码:

Java代码复制代码
  1. @Override  
  2. public void doPost(HttpServletRequest hreq, HttpServletResponse hres)   
  3.         throws ServletException, IOException {   
  4.         ......   
  5.         String[] messages = hreq.getParameterValues("message");      
  6.         if (messages != null && messages.length > 0){   
  7.             for(String message: messages){   
  8.                 Object verbObj = JSONParser.parse(message);   
  9.                 //Verb verb = VerbUtils.parse(verbObj);  
  10.                 VerbBase wellFormedVerb = null;   
  11.                 if (verbObj.getClass().isArray()){   
  12.                     int length = Array.getLength(verbObj);   
  13.                     for (int i=0; i < length; i++){   
  14.                         wellFormedVerb =    
  15.                                 VerbUtils.parseMap((Map)Array.get(verbObj,i), wellFormedVerb);   
  16.                      }   
  17.                 }   
  18.                    
  19.                 // Notify our listener;  
  20.             }   
  21.         }    
  22.         ......   
  23. }  


在MyEclipse中经过单步调试,发现Bug原因是:
JSONParser.parse()对请求参数中的数值进行转型,有小数点时转型为Double,否则转型为Long。而VerbUtils的newSubscribe()只能接收类型为String的参数。改进方案很简单,将VerbUtils的newSubscribe()方法中的以下代码
Java代码复制代码
  1. handshake.setVersion((String)map.get("version"));   
  2. handshake.setMinimumVersion((String)map.get("minimumVersion"));  

替换为
Java代码复制代码
  1. Object ov = map.get("version");   
  2. if (ov instanceof String)   
  3.     handshake.setVersion((String)ov);   
  4. else if (ov instanceof Number)   
  5.     handshake.setVersion(ov.toString());   
  6. else handshake.setVersion("1.0");   
  7. ov = map.get("minimumVersion");   
  8. if (ov instanceof String)   
  9.     handshake.setMinimumVersion((String)ov);   
  10. else if (ov instanceof Number)   
  11.     handshake.setMinimumVersion(ov.toString());   
  12. else handshake.setMinimumVersion("0.1");  


重新编译(技巧:在MyEclipse工程中建立一个com.sun.grizzly.cometd.bayeux包,将glassfish源文件的压缩包展开,将appserv-http-engine/src/main/java/com/sun/grizzly/cometd/bayeux目录下所有的java文件导入到此包中),得到VerbUtils.class文件替换glassfish安装路径的lib目录下的appserv-rt.jar文件中的同名文件。


重新测试,server端log文件中没有异常记录,但是登录和发消息都没有看到预期的消息显示在对话内。采用MyEclipse Web2.0浏览器进行跟踪,发现chat.js工作不正常,没有发出http请求到服务端。由于本人对AJAX/Dojo的功力不足,未进一步分析是何原因。与Jetty6的chat demo对比,发现两者使用dojo版本不同,所以想到使用移花接木的手段,尝试用Jetty6的客户端访问Glassfish的服务端:
1)在MyEclipse工程中WebRoot目录下创建chat目录,导入index.html、chat.js和chat.css(参见附加文件)。
2)下载最新的Dojo API包:dojo-release-1.0.2.zip。解压缩后部署到WebRoot目录下。
3)修改index.html,内容如下:
Java代码复制代码
  1. <html>   
  2. <head>   
  3.     <title>Cometd chat</title>   
  4.     <script type="text/javascript" src="../dojo/dojo.js"></script>   
  5.     <script type="text/javascript" src="../dojox/cometd.js"></script>   
  6.     <script type="text/javascript" src="chat.js"></script>   
  7.     <link rel="stylesheet" type="text/css" href="chat.css">   
  8. </head>   
  9. <body>   
  10. <h1>Cometd Chat</h1>   
  11.   
  12. <div id="chatroom">   
  13.  <div id="chat"></div>   
  14.  <div id="input">   
  15.    <div id="join" >   
  16.      Username: <input id="username" type="text"/><input id="joinB" class="button" type="submit" name="join" value="Join"/>   
  17.    </div>   
  18.    <div id="joined" class="hidden">   
  19.      Chat: <input id="phrase" type="text"></input>   
  20.      <input id="sendB" class="button" type="submit" name="join" value="Send"/>   
  21.      <input id="leaveB" class="button" type="submit" name="join" value="Leave"/>   
  22.    </div>   
  23.   </div>   
  24.  </div>   
  25. </body>  

4)改编chat.js,内容如下:
Java代码复制代码
  1. dojo.require("dojox.cometd");   
  2. //dojo.require("dojox.cometd.timesync");  
  3.   
  4. var room = {   
  5.     _last: "",   
  6.     _username: null,   
  7.     _connected: true,   
  8.   
  9.     join: function(name){   
  10.            
  11.         if(name == null || name.length==0 ){   
  12.             alert('Please enter a username!');   
  13.         }else{   
  14.            
  15.             dojox.cometd.init(new String(document.location).replace(/http:\/\/[^\/]*/,'').replace(/\/chat\/.*$/,'')+"/cometd");   
  16.             // dojox.cometd.init("http://127.0.0.2:8080/cometd");  
  17.             this._connected=true;   
  18.            
  19.             this._username=name;   
  20.             dojo.byId('join').className='hidden';   
  21.             dojo.byId('joined').className='';   
  22.             dojo.byId('phrase').focus();   
  23.            
  24.             // subscribe and join  
  25.             dojox.cometd.startBatch();   
  26.             dojox.cometd.subscribe("/chat/demo", room, "_chat");   
  27.             dojox.cometd.publish("/chat/demo", { user: room._username, join: true, chat : room._username+" has joined"});   
  28.             dojox.cometd.endBatch();   
  29.            
  30.             // handle cometd failures while in the room  
  31.             room._meta=dojo.subscribe("/cometd/meta",dojo.hitch(this,function(event){   
  32.                 console.debug(event);      
  33.                 if (event.action=="handshake") {   
  34.                 room._chat({data:{join:true,user:"SERVER",chat:"reinitialized"}});   
  35.                     dojox.cometd.subscribe("/chat/demo", room, "_chat");   
  36.                 } else if (event.action=="connect") {   
  37.             if (event.successful && !this._connected)   
  38.                         room._chat({data:{leave:true,user:"SERVER",chat:"reconnected!"}});   
  39.             if (!event.successful && this._connected)   
  40.                         room._chat({data:{leave:true,user:"SERVER",chat:"disconnected!"}});   
  41.             this._connected=event.successful;   
  42.             }   
  43.             }));   
  44.         }   
  45.     },   
  46.   
  47.     leave: function(){   
  48.         if (room._username==null)   
  49.             return;   
  50.            
  51.     if (room._meta)   
  52.             dojo.unsubscribe(room._meta);   
  53.     room._meta=null;   
  54.        
  55.         dojox.cometd.startBatch();   
  56.         dojox.cometd.unsubscribe("/chat/demo", room, "_chat");   
  57.         dojox.cometd.publish("/chat/demo", { user: room._username, leave: true, chat : room._username+" has left"});   
  58.         dojox.cometd.endBatch();   
  59.   
  60.         // switch the input form.  
  61.         dojo.byId('join').className='';   
  62.         dojo.byId('joined').className='hidden';   
  63.         dojo.byId('username').focus();   
  64.         room._username=null;   
  65.         dojox.cometd.disconnect();   
  66.     },   
  67.          
  68.     chat: function(text){   
  69.         if(!text || !text.length){ return false; }   
  70.         dojox.cometd.publish("/chat/demo", { user: room._username, chat: text});   
  71.     },   
  72.   
  73.     _chat: function(message){   
  74.         var chat=dojo.byId('chat');   
  75.         if(!message.data){   
  76.             alert("bad message format "+message);   
  77.             return;   
  78.         }   
  79.         var from=message.data.user;   
  80.         var special=message.data.join || message.data.leave;   
  81.         var text=message.data.chat;   
  82.         if(!text){ return; }   
  83.   
  84.         if( !special && from == room._last ){   
  85.             from="...";   
  86.         }else{   
  87.             room._last=from;   
  88.             from+=":";   
  89.         }   
  90.   
  91.         if(special){   
  92.             chat.innerHTML += "<span class=\"alert\"><span class=\"from\">"+from+" </span><span class=\"text\">"+text+"</span></span><br/>";   
  93.             room._last="";   
  94.         }else{   
  95.             chat.innerHTML += "<span class=\"from\">"+from+" </span><span class=\"text\">"+text+"</span><br/>";   
  96.         }    
  97.         chat.scrollTop = chat.scrollHeight - chat.clientHeight;       
  98.     },   
  99.      
  100.   _init: function(){   
  101.         dojo.byId('join').className='';   
  102.         dojo.byId('joined').className='hidden';   
  103.         dojo.byId('username').focus();   
  104.        
  105.         var element=dojo.byId('username');   
  106.         element.setAttribute("autocomplete","OFF");    
  107.         dojo.connect(element, "onkeyup", function(e){      
  108.             if(e.keyCode == dojo.keys.ENTER){   
  109.                 room.join(dojo.byId('username').value);   
  110.                 return false;   
  111.             }   
  112.             return true;   
  113.     });   
  114.      
  115.         element=dojo.byId('joinB');   
  116.         element.onclick = function(){   
  117.             room.join(dojo.byId('username').value);   
  118.             return false;   
  119.     }   
  120.      
  121.         element=dojo.byId('phrase');   
  122.         element.setAttribute("autocomplete","OFF");   
  123.         dojo.connect(element, "onkeyup", function(e){      
  124.             if(e.keyCode == dojo.keys.ENTER){   
  125.                 room.chat(dojo.byId('phrase').value);   
  126.                 dojo.byId('phrase').value='';   
  127.                 return false;   
  128.             }   
  129.             return true;   
  130.     });   
  131.      
  132.         element=dojo.byId('sendB');   
  133.         element.onclick = function(){   
  134.           room.chat(dojo.byId('phrase').value);   
  135.           dojo.byId('phrase').value='';   
  136.     }   
  137.      
  138.         element=dojo.byId('leaveB');   
  139.         element.onclick = function(){   
  140.           room.leave();   
  141.     }   
  142.     }    
  143. };   
  144.   
  145. dojo.addOnLoad(room, "_init");   
  146. dojo.addOnUnload(room,"leave");  

5)将此工程打包为cometd.war(Context Root Path必须是/cometd)部署到glassfish的domains/domain1/autodeploy/目录下。

重新测试http://localhost:8080/cometd/chat/index.html,期待已久的Cometd聊天室可以正常工作啦!

TAG:

引用 删除 Tony   /   2008-09-23 13:57:24
 

评分:0

我来说两句

显示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

日历

« 2008-10-11  
   1234
567891011
12131415161718
19202122232425
262728293031 

数据统计

  • 访问量: 18738
  • 日志数: 171
  • 影音数: 3
  • 建立时间: 2008-02-28
  • 更新时间: 2008-05-19

RSS订阅

Open Toolbar