中国领先的IT技术网站
|
|

用JavaEE7、Websockets和GlassFish4打造聊天室(一)

Java EE 7已经在今年正式发布了,新增加了很多新的功能和特性,如新增或更新了不少的JSR标准。其中特别受到关注的是Websockets。它的一个好处之一是减少了不必要的网络流量。它主要是用于在客户机和服务器之间建立单一的双向连接。

作者:廖煜嵘译来源:51CTO|2013-11-27 10:46

开发者大赛路演 | 12月16日,技术创新,北京不见不散


Java EE 7已经在今年正式发布了,新增加了很多新的功能和特性,如新增或更新了不少的JSR标准。其中特别受到关注的是Websockets。它的一个好处之一是减少了不必要的网络流量。它主要是用于在客户机和服务器之间建立单一的双向连接。这意味着客户只需要发送一个请求到服务端,那么服务端则会进行处理,处理好后则将其返回给客户端,客户端则可以在等待这个时间继续去做其他工作,整个过程是异步的。在本系列教程中,将指导用户如何在JAVA EE 7的容器GlassFish 4中,使用JAVA EE 7中的全新的解析Json API(JSR-353),以及综合运用jQuery和Bootstrap。本文要求读者有一定的HTML 5 Websocket的基础原理知识。

效果图

我们先来看下在完成这个教程后的效果图,如下所示:

准备工作

我们使用的是JDK 7 和MAVN 3进行库的构建工作,首先看pom.xml中关于Jave EE 7的部分:

  1.  <properties> 
  2.     <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir> 
  3.     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
  4. </properties> 
  5.   
  6. <dependencies> 
  7.     <dependency> 
  8.         <groupId>javax</groupId> 
  9.         <artifactId>javaee-api</artifactId> 
  10.         <version>7.0</version> 
  11.         <scope>provided</scope> 
  12.     </dependency> 
  13. </dependencies> 
  14.   
  15. <build> 
  16.     <plugins> 
  17.         <plugin> 
  18.             <groupId>org.apache.maven.plugins</groupId> 
  19.             <artifactId>maven-compiler-plugin</artifactId> 
  20.             <version>3.1</version> 
  21.             <configuration> 
  22.                 <source>1.7</source> 
  23.                 <target>1.7</target> 
  24.                 <compilerArguments> 
  25.                     <endorseddirs>${endorsed.dir}</endorseddirs> 
  26.                 </compilerArguments> 
  27.             </configuration> 
  28.         </plugin> 
  29.         <plugin> 
  30.             <groupId>org.apache.maven.plugins</groupId> 
  31.             <artifactId>maven-war-plugin</artifactId> 
  32.             <version>2.3</version> 
  33.             <configuration> 
  34.                 <failOnMissingWebXml>false</failOnMissingWebXml> 
  35.             </configuration> 
  36.         </plugin> 
  37.         <plugin> 
  38.             <groupId>org.apache.maven.plugins</groupId> 
  39.             <artifactId>maven-dependency-plugin</artifactId> 
  40.             <version>2.6</version> 
  41.             [..] 
  42.         </plugin> 
  43.     </plugins> 
  44. </build> 

同时,为了能使用GlassFish 4,需要增加如下的插件:

  1. plugin> 
  2.     <groupId>org.glassfish.embedded</groupId> 
  3.     <artifactId>maven-embedded-glassfish-plugin</artifactId> 
  4.     <version>4.0</version> 
  5.     <configuration> 
  6.         <goalPrefix>embedded-glassfish</goalPrefix> 
  7.         <app>${basedir}/target/${project.artifactId}-${project.version}.war</app> 
  8.         <autoDelete>true</autoDelete> 
  9.         <port>8080</port> 
  10.         <name>${project.artifactId}</name> 
  11.         <contextRoot>hascode</contextRoot> 
  12.     </configuration> 
  13.     <executions> 
  14.         <execution> 
  15.             <goals> 
  16.                 <goal>deploy</goal> 
  17.             </goals> 
  18.         </execution> 
  19.     </executions> 
  20. </plugin> 

设置Websocket的Endpoint

我们先来看服务端Websocket的代码如下,然后再做进一步解析:

  1. package com.hascode.tutorial; 
  2.   
  3. import java.io.IOException; 
  4. import java.util.logging.Level; 
  5. import java.util.logging.Logger; 
  6.   
  7. import javax.websocket.EncodeException; 
  8. import javax.websocket.OnMessage; 
  9. import javax.websocket.OnOpen; 
  10. import javax.websocket.Session; 
  11. import javax.websocket.server.PathParam; 
  12. import javax.websocket.server.ServerEndpoint; 
  13.   
  14. @ServerEndpoint(value = "/chat/{room}", encoders = ChatMessageEncoder.class, decoders = ChatMessageDecoder.class
  15. public class ChatEndpoint { 
  16.     private final Logger log = Logger.getLogger(getClass().getName()); 
  17.   
  18.     @OnOpen 
  19.     public void open(final Session session, @PathParam("room"final String room) { 
  20.         log.info("session openend and bound to room: " + room); 
  21.         session.getUserProperties().put("room", room); 
  22.     } 
  23.   
  24.     @OnMessage 
  25.     public void onMessage(final Session session, final ChatMessage chatMessage) { 
  26.         String room = (String) session.getUserProperties().get("room"); 
  27.         try { 
  28.             for (Session s : session.getOpenSessions()) { 
  29.                 if (s.isOpen() 
  30.                         && room.equals(s.getUserProperties().get("room"))) { 
  31.                     s.getBasicRemote().sendObject(chatMessage); 
  32.                 } 
  33.             } 
  34.         } catch (IOException | EncodeException e) { 
  35.             log.log(Level.WARNING, "onMessage failed", e); 
  36.         } 
  37.     } 

面分析下上面的代码:

使用@ ServerEndpoint定义一个新的endpoint,其中的值指定了URL并且可以使用PathParams参数,就象在JAX-RS中的用法一样。

所以值“/chat/{room}”允许用户通过如下形式的URL去连接某个聊天室:ws://0.0.0.0:8080/hascode/chat/java

在大括号中的值(即room),可以通过使用javax.websocket.server.PathParam,在endpoint的生命周期回调方法中以参数的方式注入。

此外,我们要使用一个编码和解码的类,因为我们使用的是一个DTO形式的类,用于在服务端和客户端传送数据。

当用户第一次连接到服务端,输入要进入聊天室的房号,则这个房号以参数的方式注入提交,并且使用session.getUserProperties将值保存在用户的属性map中。

当一个聊天参与者通过tcp连接发送信息到服务端,则循环遍历所有已打开的session,每个session被绑定到指定的聊天室中,并且接收编码和解码的信息。

如果我们想发送简单的文本信息或和二进制格式的信息,则可以使用session.getBasicRemote().sendBinary() 或session.getBasicRemote().sendText()

接下来我们看下用于代表信息传递实体(DTO:Data Transfer Object)的代码,如下:

  1. package com.hascode.tutorial; 
  2.   
  3. import java.util.Date; 
  4.   
  5. public class ChatMessage { 
  6.     private String message; 
  7.     private String sender; 
  8.     private Date received; 
  9.   
  10.     // 其他getter,setter方法 

聊天消息的转换

在这个应用中,将编写一个编码和解码类,用于在聊天信息和JSON格式间进行转换。

先来看下解码类的实现,这将会把传递到服务端的聊天信息转换为ChatMessage实体类。在这里,使用的是Java API for JSON Processing(JSR353)(参考:

http://jcp.org/en/jsr/detail?id=353)规范去将JSON格式的信息转换为实体类,代码如下,其中重写的willDecode方法,这里默认返回为true。

  1. package com.hascode.tutorial; 
  2.   
  3. import java.io.StringReader; 
  4. import java.util.Date; 
  5.   
  6. import javax.json.Json; 
  7. import javax.json.JsonObject; 
  8. import javax.websocket.DecodeException; 
  9. import javax.websocket.Decoder; 
  10. import javax.websocket.EndpointConfig; 
  11.   
  12. public class ChatMessageDecoder implements Decoder.Text<ChatMessage> { 
  13.     @Override 
  14.     public void init(final EndpointConfig config) { 
  15.     } 
  16.   
  17.     @Override 
  18.     public void destroy() { 
  19.     } 
  20.   
  21.     @Override 
  22.     public ChatMessage decode(final String textMessage) throws DecodeException { 
  23.         ChatMessage chatMessage = new ChatMessage(); 
  24.         JsonObject obj = Json.createReader(new StringReader(textMessage)) 
  25.                 .readObject(); 
  26.         chatMessage.setMessage(obj.getString("message")); 
  27.         chatMessage.setSender(obj.getString("sender")); 
  28.         chatMessage.setReceived(new Date()); 
  29.         return chatMessage; 
  30.     } 
  31.   
  32.     @Override 
  33.     public boolean willDecode(final String s) { 
  34.         return true
  35.     } 

同样再看下编码类的代码,这个类相反,是将ChatMessage类转换为Json格式,代码如下:

  1. package com.hascode.tutorial; 
  2.   
  3. import javax.json.Json; 
  4. import javax.websocket.EncodeException; 
  5. import javax.websocket.Encoder; 
  6. import javax.websocket.EndpointConfig; 
  7.   
  8. public class ChatMessageEncoder implements Encoder.Text<ChatMessage> { 
  9.     @Override 
  10.     public void init(final EndpointConfig config) { 
  11.     } 
  12.   
  13.     @Override 
  14.     public void destroy() { 
  15.     } 
  16.   
  17.     @Override 
  18.     public String encode(final ChatMessage chatMessage) throws EncodeException { 
  19.         return Json.createObjectBuilder() 
  20.                 .add("message", chatMessage.getMessage()) 
  21.                 .add("sender", chatMessage.getSender()) 
  22.                 .add("received", chatMessage.getReceived().toString()).build() 
  23.                 .toString(); 
  24.     } 

这里可以看到JSR-353的强大威力,只需要调用Json.createObjectBuilder就可以轻易把一个DTO对象转化为JSON了。

通过Bootstrap、Javacsript搭建简易客户端

最后,我们综合运用著名的Bootstrap、jQuery框架和Javascript设计一个简易的客户端。我们在src/main/weapp目录下新建立index.html文件,代码如下:

  1. <!DOCTYPE html> 
  2. <html lang="en"> 
  3. <head> 
  4. [..] 
  5. <script> 
  6.     var wsocket; 
  7.     var serviceLocation = "ws://0.0.0.0:8080/hascode/chat/"
  8.     var $nickName; 
  9.     var $message; 
  10.     var $chatWindow; 
  11.     var room = ''
  12.   
  13.     function onMessageReceived(evt) { 
  14.         //var msg = eval('(' + evt.data + ')'); 
  15.         var msg = JSON.parse(evt.data); // native API 
  16.         var $messageLine = $('<tr><td class="received">' + msg.received 
  17.                 + '</td><td class="user label label-info">' + msg.sender 
  18.                 + '</td><td class="message badge">' + msg.message 
  19.                 + '</td></tr>'); 
  20.         $chatWindow.append($messageLine); 
  21.     } 
  22.     function sendMessage() { 
  23.         var msg = '{"message":"' + $message.val() + '", "sender":"' 
  24.                 + $nickName.val() + '", "received":""}'; 
  25.         wsocket.send(msg); 
  26.         $message.val('').focus(); 
  27.     } 
  28.   
  29.     function connectToChatserver() { 
  30.         room = $('#chatroom option:selected').val(); 
  31.         wsocket = new WebSocket(serviceLocation + room); 
  32.         wsocket.onmessage = onMessageReceived
  33.     } 
  34.   
  35.     function leaveRoom() { 
  36.         wsocket.close(); 
  37.         $chatWindow.empty(); 
  38.         $('.chat-wrapper').hide(); 
  39.         $('.chat-signin').show(); 
  40.         $nickName.focus(); 
  41.     } 
  42.   
  43.     $(document).ready(function() { 
  44.         $nickName = $('#nickname'); 
  45.         $message = $('#message'); 
  46.         $chatWindow = $('#response'); 
  47.         $('.chat-wrapper').hide(); 
  48.         $nickName.focus(); 
  49.   
  50.         $('#enterRoom').click(function(evt) { 
  51.             evt.preventDefault(); 
  52.             connectToChatserver(); 
  53.             $('.chat-wrapper h2').text('Chat # '+$nickName.val() + "@" + room); 
  54.             $('.chat-signin').hide(); 
  55.             $('.chat-wrapper').show(); 
  56.             $message.focus(); 
  57.         }); 
  58.         $('#do-chat').submit(function(evt) { 
  59.             evt.preventDefault(); 
  60.             sendMessage() 
  61.         }); 
  62.   
  63.         $('#leave-room').click(function(){ 
  64.             leaveRoom(); 
  65.         }); 
  66.     }); 
  67. </script> 
  68. </head> 
  69.   
  70. <body> 
  71.   
  72.     <div class="container chat-signin"> 
  73.         <form class="form-signin"> 
  74.             <h2 class="form-signin-heading">Chat sign in</h2> 
  75.             <label for="nickname">Nickname</label> <input type="text" 
  76.                 class="input-block-level" placeholder="Nickname" id="nickname"> 
  77.             <div class="btn-group"> 
  78.                 <label for="chatroom">Chatroom</label> <select size="1" 
  79.                     id="chatroom"> 
  80.                     <option>arduino</option> 
  81.                     <option>java</option> 
  82.                     <option>groovy</option> 
  83.                     <option>scala</option> 
  84.                 </select> 
  85.             </div> 
  86.             <button class="btn btn-large btn-primary" type="submit" 
  87.                 id="enterRoom">Sign in</button> 
  88.         </form> 
  89.     </div> 
  90.     <!-- /container --> 
  91.   
  92.     <div class="container chat-wrapper"> 
  93.         <form id="do-chat"> 
  94.             <h2 class="alert alert-success"></h2> 
  95.             <table id="response" class="table table-bordered"></table> 
  96.             <fieldset> 
  97.                 <legend>Enter your message..</legend> 
  98.                 <div class="controls"> 
  99.                     <input type="text" class="input-block-level" placeholder="Your message..." id="message" style="height:60px"/> 
  100.                     <input type="submit" class="btn btn-large btn-block btn-primary" 
  101.                         value="Send message" /> 
  102.                     <button class="btn btn-large btn-block" type="button" id="leave-room">Leave 
  103.                         room</button> 
  104.                 </div> 
  105.             </fieldset> 
  106.         </form> 
  107.     </div> 
  108. </body> 
  109. </html> 

在上面的代码中,要注意如下几点:

在Javascript端要调用websocket的话,要用如下的方式发起连接即可:ws://IP:PORT/CONTEXT_PATH/ENDPOINT_URL e.g ws://0.0.0.0:8080/hascode/chat/java

创建一个Websocket连接的方法很简单,使用的是var wsocket = new WebSocket(‘ws://0.0.0.0:8080/hascode/chat/java’);

要获得来自服务端返回的信息,只需要在回调函数wsocket.onmessage中设置对应的获取返回信息的方法即可。

发送一个Websocket消息到服务端,使用的方法是wsocket.send(),其中可以发送的消息可以文本或者二进制数据。

关闭连接使用的是wsocket.close()。

Websocket中还有其他很多种用法,具体的可以参考其标准规范文档(http://tools.ietf.org/html/rfc6455)

最后,我们通过

mvn package embedded-glassfish:run

进行代码的部署,然后就可以看到本文开始部分截图的效果。

本文的代码可以通过git获得:

git clone https://bitbucket.org/hascode/javaee7-websocket-chat.git

读者也可以通过这个地址下载可部署的war包:

原文链接:http://www.hascode.com/2013/08/creating-a-chat-application-using-java-ee-7-websockets-and-glassfish-4/

【编辑推荐】

  1. JavaEE7新特性JMS 2.0公共审查草案发布
  2. JavaEE7发布进入倒计时
  3. Apache Tomcat 8.0 到来,支持JavaEE 7
  4. HTML5 WebSockets node.js实例教程
【责任编辑:chensf TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

Windows用户态程序高效排错

本书是一本介绍Windows系统上的用户态程序排错方法和技巧的书。本书分为4个章节,先介绍最重要的、通用的思考方法,以便制定排错步骤;再介...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊