Java WebSocket编程 开发、部署和保护动态Web应用 参考

来源:互联网 发布:淘宝推广渠道 编辑:程序博客网 时间:2024/06/05 18:44

1.1 创建第一个WebSocket应用

《Java WebSocket编程 开发、部署和保护动态Web应用》第1章 Java WebSocket基本原理,本章介绍Java WebSocket API并粗略了解其功能。本章将深入介绍一个示例应用,此示例应用的服务器端简单地回显客户端发给它的任意消息。我们将通过此示例来阐述Java WebSocket API的主要特性。本书节为大家介绍创建第一个WebSocket应用。

第1章 Java WebSocket基本原理

本章介绍Java WebSocket API并粗略了解其功能。本章将深入介绍一个示例应用,此示例应用的服务器端简单地回显客户端发给它的任意消息。我们将通过此示例来阐述Java WebSocket API的主要特性。这样做,本章将建立本书其他部分所需要描述的主要特性的基础。如果你需要复习WebSocket协议的主要概念,请在继续学习之前先看看"前言"部分。

1.1  创建第一个WebSocket应用

既然WebSocket协议的核心是使得两个对等节点间能够进行消息通信,那么我们将从最简单的示例(EchoServer应用)来开始介绍Java WebSocket API。

EchoServer应用是一个客户端/服务器应用。它的客户端是运行在Web浏览器上的一小段JavaScript,其服务器是一个WebSocket端点。此端点通过使用Java WebSocket API进行编写,作为Web应用的一部分部署并运行在应用服务器上。当用户通过浏览器载入Web页面时,在用户的命令下,这个JavaScript代码片段会被执行。此代码执行时首先要做的事是连接存在于应用服务器上的Java EchoServer WebSocket端点并在连接之后立刻发送一条消息。运行在应用服务器上的Java EchoServer端点等待入站的连接,并且在其接收到一个已连接的客户端发送的消息时,立刻对其进行回复,确认已收到消息。

通过此应用,也就是EchoServer,可以阐述已经创建的WebSocket端点的关键方面。虽然该示例特别简单,但是充分理解其创建和部署过程将有助于我们迅速理解Java WebSocket API的关键特性。在本示例中,我们不会详述任何关键特性,而是会给出一个全局图,可以将该示例作为学习Java WebSocket API的一个很好的起点,在本书的后续章节将会更详细地阐述这些特性。

此示例大体上包括两大部分:第一部分同时也是最重要的是开发并部署在服务器上的Java EchoServer端点;第二部分是运行于浏览器中的Web页面,它包含JavaScript WebSocket客户端。图1-1展示了该示例的部署总图。


1.1.1  创建WebSocket端点

幸运的是,创建第一个WebSocket端点并部署到应用服务器中非常简单。你所要做的全部事情是开发一个Java类,实现想让此端点所拥有的功能,并且了解怎样将Java WebSocket API中定义的少数几个Java注解应用于开发的Java类上。对于示例EchoServer来说,我们想要其实现从一个已连接客户端接收消息并立即将这个消息返回给此客户端的功能。

让我们立刻开始实现该功能。首先创建一个如代码清单1-1所示的名为EchoServer的Java类,然后为其添加一个方法,此方法的参数是String类型的,用于保存客户端发送的任意入站消息。此方法的返回值也是String类型。无论客户端何时发送一个入站消息,我们都希望此返回值可以用来将此消息发送回客户端。

代码清单1-1:简单的Java对象(POJO)

  1. public class EchoServer {   
  2. public String echo(String incomingMessage) {   
  3. return "I got this (" + incomingMessage + ")"   
  4. + " so I am sending it back !";   
  5. }  
  6. }  

此时,你也许乐于知道你已经实现了此应用所需要的所有逻辑。所剩下的工作就是使用Java WebSocket API将此简单传统的Java对象转换成一个WebSocket。为此,仅仅需要Java WebSocket API中的两个Java注解:@ServerEndpoint和@OnMessage。

注解@ServerEndpoint是类级别的注解,用于告诉Java平台它注解的类实际上要成为一个WebSocket端点。此注解的唯一强制参数是相对URI(Uniform Resource Identifier,统一资源标识符),开发人员希望这个端点在此相对URL之下可用。这个场景有点类似于给别人电话号码,使得大家可以通过电话号码打电话。为了使示例更加简单,将使用URI /echo来发布示例创建的新端点。

因此,对EchoServer类添加如代码清单1-2所示的注解:

代码清单1-2:演化为WebSocket端点的POJO类

  1. import javax.websocket.server.ServerEndpoint;  
  2.  
  3. @ServerEndpoint("/echo")  
  4. public class EchoServer {   
  5. public String echo(String incomingMessage) {   
  6. return "I got this (" + incomingMessage + ")"   
  7. + " so I am sending it back !";   
  8. }  
  9. }  

注意属性的名称是value,它定义了端点将被发布在其下的路径,这意味着甚至不必将属性名称写到注解定义中。

现在代码已经包含了WebSocket实现能够运行的足够的信息,它将知道发布端点的URI空间。你可能想知道相对URI到底是相对于什么URI:它相对于包含示例EchoServer端点的Web应用的上下文根。然而,因为并未告诉WebSocket实现当端点接收到消息时将会调用哪些方法,所以将Java类转换成WebSocket端点的过程还未结束。因为EchoServer类只有一个方法,在此示例中这点也许非常明显。然而,在更复杂的示例中,实现WebSocket端点的Java类有多个方法,你可能希望一些方法在WebSocket事件发生而不是在WebSocket消息发生时被调用,一些方法不需要直接和WebSocket事件有任何直接关系。无论如何,为了将实现方法标记为随时准备处理任何入站消息,需要使用方法级注解@OnMessage。

不管你相信与否,你刚刚已经编写完了如代码清单1-3所示的第一个WebSocket端点。

代码清单1-3:WebSocket端点

  1. import javax.websocket.server.ServerEndpoint;  
  2. import javax.websocket.OnMessage;  
  3.  
  4. @ServerEndpoint("/echo")  
  5. public class EchoServer {   
  6.  
  7. @OnMessage   
  8. public String echo(String incomingMessage) {   
  9. return "I got this (" + incomingMessage + ")"   
  10. + " so I am sending it back !";   
  11. }  
  12. }  

1.1.2  部署端点

部署EchoServer WebSocket端点特别简单。你需要编译源文件,并将编译后的类文件包含在WAR文件中,最后部署WAR文件。Web容器将检测到WAR文件中包含的WebSocket端点,并且完成必要的设置和部署工作。一旦完成这些步骤后,就可以首次调用WebSocket端点了。我们很快就会看到一些其他的端点,为了部署它们可以有更多的设置工作,但是暂时可以先看看调用刚刚创建的端点的客户端代码。

1.1.3  创建WebSocket客户端

在本示例以及本书的众多示例中,我们将用来调用服务器端点的客户端将以利用JavaScript WebSocket API的JavaScript代码片段的形式存在。这些代码片段将被嵌入Web页面,并且被打包在包含WebSocket端点的WAR文件中。Java WebSocket API也有客户端API的特性,开发人员可以使用Java WebSocket API创建应用的客户端,代替使用Web页面上的JavaScript连接到WebSocket服务器端点。第3章中将介绍WebSocket应用的Java客户端。然而,在本示例以及本书中的其他几个示例中,将利用简单的JavaScript API进行访问。因为所有支持WebSocket的浏览器都支持JavaScript API,所以它是一个广泛的选择。

在本示例应用中,Web客户端是一个带有按钮的Web页面。当按钮被按下时,将导致WebSocket客户端创建一个到EchoServer端点的WebSocket连接,并发送一条消息。每当JavaScript WebSocket接收到一条消息时,它将消息显示在Web页面中,这样用户能够看到它,同时也会关闭WebSocket连接。在本书后面章节中将能够看到生命周期更长的WebSocket连接,目前仅仅简单阐述其第一次交互。

代码清单1-4所示是将调用EchoServer的Web页面的代码,包括最重要的JavaScript WebSocket客户端代码。

代码清单1-4:Echo JavaScript客户端

  1. <!DOCTYPE html> 
  2. <html>   
  3. <head>   
  4. <meta http-equiv="Content-Type" content="text/html;   
  5. charset=UTF-8">   
  6. <title>Web Socket JavaScript Echo Client</title>   
  7. <script language="javascript" type="text/javascript">   
  8. var echo_websocket;   
  9.  
  10. function init() {   
  11. output = document.getElementById("output");   
  12. }   
  13.  
  14. function send_echo() {   
  15. var wsUri = "ws://localhost:8080/echoserver/echo";   
  16. writeToScreen("Connecting to " + wsUri);   
  17. echo_websocket = new WebSocket(wsUri);   
  18. echo_websocket.onopen = function (evt) {   
  19. writeToScreen("Connected !");   
  20. doSend(textID.value);   
  21. };   
  22. echo_websocket.onmessage = function (evt) {   
  23. writeToScreen("Received message: " + evt.data);   
  24. echo_websocket.close();   
  25. };   
  26. echo_websocket.onerror = function (evt) {   
  27. writeToScreen('<span style="color: red;"> 
  28. ERROR:</span> ' + evt.data);   
  29.  
  30. echo_websocket.close();   
  31. };   
  32. }   
  33.  
  34. function doSend(message) {   
  35. echo_websocket.send(message);   
  36. writeToScreen("Sent message: " + message);   
  37. }   
  38.  
  39. function writeToScreen(message) {   
  40. var pre = document.createElement("p");   
  41. pre.style.wordWrap = "break-word";   
  42. pre.innerHTML = message;   
  43. output.appendChild(pre);   
  44. }   
  45. window.addEventListener("load", init, false);   
  46.  
  47. </script>   
  48. </head>   
  49. <body>   
  50. <h1>Echo Server</h1>   
  51.  
  52. <div style="text-align: left;">   
  53. <form action="">   
  54. <input onclick="send_echo()" value="Press to send" type="button">   
  55. <input id="textID" name="message" value=  
  56. "Hello Web Sockets" type="text">   
  57. <br>   
  58. </form>   
  59. </div>   
  60. <div id="output"></div>   
  61. </body> 
  62. </html> 

现在如果运行该应用,将得到如图1-2所示的输出。

目前为止,你已经创建、部署并且运行了第一个WebSocket应用,我们可以不必关注代码,休息一下,转而看看这个非常简单的应用中包含的有关Java WebSocket API的基本概念。

1.2  WebSocket端点

我们使用术语"端点"来表示WebSocket对话的一端。当通过@ServerEndpoint来注解EchoServer类时,它将简单传统的Java类转换成一个逻辑上的WebSocket端点。当部署包含EchoServer类的应用时,WebSocket实现首先扫描包含EchoServer类的WAR文件,找到其中的WebSocket端点。然后,它将此类注册成WebSocket端点。当客户端通过URI /echo来尝试建立到端点的连接时,因为该URI匹配请求中的URI,WebSocket实现将创建EchoServer类的一个新实例,并使得此EchoServer类的实例为后续的WebSocket对话服务。当按下示例Web页面中的按钮时将调用此实例,其存活时间和WebSocket连接的活跃时间保持一致。

注意:

端点是Java WebSocket API组件模型的中心:每一个使用Java WebSocket API所创建的WebSocket应用都有WebSocket端点的参与。

目前为止,我们已经了解了使用Java WebSocket API创建端点的两种方式中的一种。EchoServer示例阐述了如何使用Java WebSocket API注解将简单Java类转换成WebSocket端点。第二种方式是继承Java WebSocket API中的Endpoint抽象类。若如此,此子类将成为WebSocket端点,下一节将马上对其进行描述。

注意:

目前存在两种Java WebSocket端点:注解式端点和编程式端点。注解式端点是通过Java WebSocket API注解将Java类转换成Java WebSocket端点。编程式端点是通过继承Java WebSocket API中的Endpoint类来实现。

在讨论什么情况下选择注释式端点和什么情况下选择编程式端点之前,你应该知道,无论采用哪种方法,总的来说都已经可以利用Java WebSocket API的大多数特性

1.3  编程式端点

前面已经介绍过注解式端点,这里主要介绍编程式端点。下一个示例和之前描述过的EchoServer示例一样,因为前面已经介绍过,所以你将会很快熟悉它。但它是以编程式端点编写,而不是以注解式端点编写。

为了创建编程式端点类型的EchoServer服务器端端点,我们需要做的第一件事情是编写Java类,此类继承Java WebSocket API中的Endpoint类。为了这样做,Endpoint类要求实现它的onOpen()方法。此方法的目的在于,只要客户端连接上所创建的端点实例,就为它注入生命。传入此方法的参数是Java WebSocket API的核心类。在详细介绍此API之前,先来看看代码。

此处不再列出客户端代码,如果仔细检查编程式EchoServer应用的JavaScript客户端,你会发现它和注解式EchoServer几乎完全一样。唯一不同的地方是它使用了不同的URI来连接编程式EchoServer端点。

  1. ws://localhost:8080/programmaticechoserver/programmaticecho 

就像我们即将看到的,服务器上的编程式端点映射成/programmaticecho,而不是/echo。

既然明确了这一点,让我们看看代码清单1-5所示的编程式端点。

代码清单1-5:编程式端点示例

  1. import java.io.IOException;  
  2. import javax.websocket.Endpoint;  
  3. import javax.websocket.EndpointConfig;  
  4. import javax.websocket.MessageHandler;  
  5. import javax.websocket.Session;  
  6.  
  7. public class ProgrammaticEchoServer extends Endpoint {   
  8.  
  9. @Override   
  10. public void onOpen(Session session, EndpointConfig   
  11. endpointConfig) {   
  12. final Session mySession = session;   
  13. mySession.addMessageHandler  
  14. (new MessageHandler.Whole<String>() {   
  15. @Override   
  16. public void onMessage(String text) {   
  17. try {   
  18. mySession.getBasicRemote().sendText("I got this (  
  19. " +text + ") so I am sending it back !");   
  20. } catch (IOException ioe) {   
  21. System.out.println("oh dear, something went wrong   
  22. trying to send the message back: " +   
  23. ioe.getMessage());   
  24. }   
  25. }   
  26. });   
  27. }  
  28. }  

对于编程式端点首先要注意的是其代码长度比注解式端点要长很多。一些开发人员经常选择创建注解式端点,但是另一些开发人员通常对编程式的处理方式更加熟悉。无论你最终更偏爱哪种处理方式,学习一下此处的示例都是非常有用的,因为即使是如此简单的示例也能够让我们密切接触API中的一些关键对象(虽然编程式端点看起来似乎比注解式端点的版本更复杂一些)。两个强制传入onOpen()方法的对象是Session和EndpointConfig对象。即使你在创建WebSocket应用时经常选择使用注解式方式而不是编程式方式,这些也都是需要理解的非常重要的对象,因为你经常需要它们。

Java WebSocket API基础对象

Java WebSocket API中的Session对象给了开发人员关于打开的WebSocket连接的视图。每一个到ProgrammaticEchoServer端点的客户端连接都由Session接口的唯一实例表示。它拥有一些方式用于调整连接属性;而且,也许最重要的是,它为端点提供了一个方式来获取对RemoteEndpoint对象的访问。RemoteEndpoint对象表示WebSocket对话的另一端,尤其是提供了一种方式使得开发人员可以把消息发送回客户端。

方法onOpen()的另一个参数是EndpointConfig对象。EndpointConfig接口表示WebSocket实现用来配置端点的一些信息。由于示例中未用到此参数,因此现阶段我们不会讨论此接口。读者如果想了解细节的话,可以参考第4章,那里详细介绍了API端点配置。

回到示例程序,onOpen()方法实现做的第一件事情是为会话添加一个MessageHandler对象。MessageHandler接口(及其后代)定义了编程式端点接收入站消息时注册其兴趣的所有方式。例如,开发人员可以使用MessageHandler接口来选择是接收文本消息还是二进制消息,也可以选择接收整个消息或者是当消息到达时接收较小的片段(这点对交换非常大的消息的应用开发人员特别有用)。示例中实现了最简单的MessageHandler接口:MessageHandler. Whole<String>接口。此接口定义了如何注册一个兴趣来整体接收文本消息。它要求开发人员实现一个单一方法:

  1. public void onMessage(String text) 

此方法在每次客户端的WebSocket文本消息抵达时被WebSocket实现调用。

示例中,当WebSocket实现传递这样的一个文本消息时,我们可以立刻通过调用Session对象上的方法:

  1. public RemoteEndpoint.Basic getBasicRemote() 

获得对RemoteEndpoint的一个引用。通过使用它可以马上返回一个消息给客户端。

值得注意的是存在两种类型的RemoteEndpoint:RemoteEndpoint.Basic和RemoteEndpoint.Async。RemoteEndpoint的每个子接口都提供了一种不同的方式来发送消息给它所表示的客户端。RemoteEndpoint.Async接口提供了一系列方法来异步发送消息;也就是说,这些方法初始化消息发送行为,但并不等待消息发送完成就返回。这样,开发人员可以在其应用中忙于做一些其他工作,而不需要阻塞当前工作线程直到消息事实上被发送完毕。简单一些的RemoteEndpont.Basic接口定义了一系列方法用于同步发送消息给客户端;也就是说,每一个send方法调用只有在消息发送完成后才返回。示例中,我们选择使用了此种简单一些的方式。RemoteEndpoint的方法调用发送文本消息给客户端。

  1. public void sendText(String text) throws IOException 

你应该看到它要求开发人员处理受检异常IOException,此异常在发送消息过程中底层连接出现问题时抛出。

现在我们已经浏览了代码,在编程式端点部署后,当客户端连接到此端点时发生的事情是WebSocket实现调用刚刚介绍过的ProgrammaticEchoServer类的onOpen()方法。方法onOpen()的实现创建MessageHandler的实现,此实现在处理客户端消息时,将客户端发送的任何文本消息原样返回。然后,添加MessageHandler实例到表示客户端连接的会话上。一旦完成这些工作,此方法就结束了。下次当文本消息从客户端连接到来时,它将被WebSocket实现路由到此MessageHandler的onMessage()方法中。

所有的事情似乎都同注解式端点示例完全类似。编程式端点唯一缺少的一部分是没有部署此端点到服务器上的路径。你应该记得注解式端点示例中此路径是注解@ServerEndpoint的一个属性。在编程式端点的情况中,分派路径稍微复杂一些。为了部署此示例,我们将不得不告诉WebSocket实现如何部署此端点。为了做到这一点,我们需要提供ServerApplicationConfig接口的实现,它将提供WebSocket实现部署端点时需要的这块缺失的信息。

ServerApplicationConfig接口定义了两个方法来允许开发人员配置应用中的端点。首先来看看第一个方法,因为它相当简单:

  1. public Set<Class<?>> getAnnotatedEndpointClasses(  
  2. Set<Class<?>> scanned) 

此方法在部署应用时被WebSocket实现调用。其传入的参数scanned是一个集合,它包含所有通过@ServerEndpoint注解的Java类。换句话说,它传入了所有的注解式端点。开发人员实现此方法,通过它返回其实际希望部署的所有的注解式端点的集合。一般来说,返回的集合将允许WebSocket实现部署WAR文件中的所有注解式端点。例如,此示例应用中没有注解式端点,所以传入的参数集合scanned将为空。

ServerApplicationConfig接口的第二个方法是我们需要实现用来部署编程式端点的方法:

  1. public Set<ServerEndpointConfig> getEndpointConfigs(  
  2. Set<Class<? extends Endpoint>> endpointClasses) 

同上述注解式端点的方法类似,此方法在应用部署阶段被调用。传入方法的参数是应用中继承了Endpoint的所有类的集合。也就是说,它是应用中所有编程式端点的集合。开发人员必须实现此方法,以便其返回ServerEndpointConfig对象的集合。此返回的对象集合对应于开发人员希望WebSocket实现部署的所有编程式端点。所以在我们的示例中,此方法将被调用并传入一个集合参数,其中的类当然是ProgrammaticEchoServer类。这里所需要做的事情是创建ServerEndpointConfig对象并将其返回,WebSocket实现后续将使用此对象部署端点。代码清单1-6如下所示。

代码清单1-6:Echo示例中的ServerApplicationConfig

  1. import java.util.HashSet;  
  2. import java.util.Set;  
  3. import javax.websocket.Endpoint;   
  4. import javax.websocket.server.ServerApplicationConfiguration;  
  5. import javax.websocket.server.ServerEndpointConfiguration;  
  6. import javax.websocket.server.  
  7. ServerEndpointConfigurationBuilder;  
  8.  
  9. public class ProgrammaticEchoServerAppConfig   
  10. implements ServerApplicationConfiguration {   
  11. @Override   
  12. public Set<ServerEndpointConfiguration> getEndpointConfigurations(Set<Class<? extends Endpoint>>   
  13. endpointClasses   
  14.  
  15. ) {   
  16. Set configs = new HashSet<ServerEndpointConfiguration>();   
  17. ServerEndpointConfiguration sec = ServerEndpointConfigurationBuilder.create(   
  18. ProgrammaticEchoServer.class, "/programmaticecho")   
  19. .build();   
  20. configs.add(sec);   
  21. return configs;   
  22. }   
  23.  
  24. @Override   
  25. public Set<Class<?>> getAnnotatedEndpointClasses(  
  26. Set<Class<?>> scanned){   
  27. return scanned;   
  28. }  
  29. }  

你应该从方法getAnnotatedEndpointClasses()中可以看到其只是返回了集合scanned,因为WAR文件中不包含注解式端点,所以此示例中该集合是空集。对于方法getEndpointConfigurations()来说,它首先创建了一个端点配置对象,其拥有希望部署的端点的路径以及端点的实现类。然后,我们将其添加到需要此应用部署的所有的端点配置集合中并返回。它将指示WebSocket实现部署此编程式端点到给定的路径/programmaticecho。

然后,我们像部署注解式端点示例一样,将WAR文件部署到应用服务器上,而且运行应用时可以得到类似的结果。

也有额外的证据表明,在某种程度上选择创建编程式端点的开发工作量更大。在示例中,为了完成与注解式端点同样的工作,编程式端点本身的代码量更大。当想要部署注解式端点时,可注意到你无须给出更多的信息或者是编写任何额外的配置代码。然而,当想要部署编程式端点时,则不得不实现一个接口用于提供相关配置信息。

在某种程度上,无论你是否坚持使用注解式端点、编程式端点或者两者混合使用,它仅涉及个人风格。一些开发人员喜欢更传统的显式使用API的处理方式。另一方面,一些开发人员更喜欢简洁的注解式处理方式,以及通过改变注解而不是重写配置类而带来的快速改变配置和设置信息的额外灵活性。从功能上来说,虽然有些场景中编程式处理方式比注解式处理方式提供了更多的控制手段和更多的特性,但是两种处理方式基本等价。

本书倾向于使用注解式方式,同时也一定会在示例中混合编程式处理方式。为了能够探索哪种方式将更适合你要编写的应用,以及确定你的个人风格,本书推荐你以开放的心态学习两种方式的基础知识。

1.4.1 部署阶段

《Java WebSocket编程 开发、部署和保护动态Web应用》第1章 Java WebSocket基本原理,本章介绍Java WebSocket API并粗略了解其功能。本章将深入介绍一个示例应用,此示例应用的服务器端简单地回显客户端发给它的任意消息。我们将通过此示例来阐述Java WebSocket API的主要特性。本书节为大家介绍部署阶段。

1.4  深入Echo示例

现在我们已经开发、部署和运行了第一个应用,值得花一些时间通过考察在部署和运行应用时实际发生的事情来理解我们已经做了些什么工作。示例应用中的简单功能暴露了Java WebSocket API中的一些主要元素。因为这些元素几乎在每一个你创建的WebSocket应用中以不同的形式重复出现,因此值得花一些时间来看看这些重要的构建部分。

1.4.1  部署阶段

当我们将包含端点的WAR文件部署到应用服务器时,为了让应用准备好服务其第一个连接,发生了一系列的事情。第一件事情是WebSocket实现将检查WAR文件,尝试定位可能需要部署的所有端点。首先,此检查将定位被@ServerEndpoint注解的任何Java类和扩展Java WebSocket API中Endpoint类的任何Java类。它也将定位实现了ServerApplicationConfig接口的任何类;这些类将告知怎样部署端点。

一旦WebSocket实现获取了这些信息,它将使用这些信息来构建一个要部署的端点集合。在EchoServer示例中,WAR文件仅包含一个注解式端点。如果WAR文件中不存在ServerApplicationConfig接口的实现,WebSocket实现将自动部署所有的注解式端点。这确实是我们的注解式端点示例的场景,所以那是那个端点如何部署的方式。在编程式Echo示例中,WAR文件包含了一个ServerApplicationConfig接口的实现,所以WebSocket实现在部署阶段实例化此类一次,并查询其方法来了解哪些端点需要部署。在我们的编程式端点示例中,ServerApplicationConfig接口要求我们编写的单一的编程式端点被部署。

Java WebSocket API也有一些开发人员使用的其他部署选项。也可以将多个ServerApplicationConfig实现打包在同一个WAR文件中。

一旦WebSocket实现决定了WAR文件中需要部署的WebSocket端点集合,多数WebSocket实现在进一步处理之前将在端点上运行其他检测,并且仅部署完整的通过检测的应用。不同的WebSocket实现在部署阶段可能在不同的地方以不同的方式捕获并且报告可能的配置或者编程错误(例如,两个端点映射到同一个路径或者WebSocket注解应用于错误的语义层级),这些都依赖于其架构和设计。一些WebSocket实现可能将故障报告输出到日志文件。一些实现可能提供图形界面化的工具来输出部署过程中的有帮助的信息。不管怎样,如果一切顺利,应用是有效的,WebSocket实现将关联这些端点到其声明的URI上,然后应用将准备接收入站连接。


1.4.2  接收第一个连接

你应该还记得"前言"中讲到当初始化一个WebSocket连接时,不得不发生的第一件事情是最初的HTTP请求/响应交互。此交互称为WebSocket打开阶段握手(WebSocket opening handshake)。许多WebSocket开发人员将永远不需要理解交互工作的细节,就像你不必理解电话交换或者蜂窝网络的机制而能够打电话一样。不管怎样,我们将在第4章涉及此主题。在这里,只要说那些希望连接的客户端装备有完整的URI地址(包括主机名称以及从主机名到端点发布位置的相对路径),并且它颁发一个特别构造的HTTP请求到那个URI就够了。此时此刻,在打开阶段握手的客户端也能够关联其他参数,这是我们第4章将讨论的话题。当服务器接收到打开阶段握手请求时,它检查请求,而且可能在客户端执行一系列检查(例如,颁发请求的客户端真的是来自其声称的客户端吗?客户端是否已授权处理?)。当一切顺利时,服务器将返回一个特殊格式化的HTTP响应。此响应将告诉客户端服务器是否希望接受客户端的入站连接。在典型场景中,所有这一切发生在"幕后",远远无需WebSocket开发人员的关注。尽管在更高级的场景中,Java WebSocket开发人员可能拦截此HTTP请求和响应交互来进行一些自定义工作。图1-3阐述了打开阶段握手的概念。

如果一切正常,服务器中确实会有一个WebSocket端点注册到打开阶段握手提供的地址,建立连接。无论是注解式端点还是编程式端点,WebSocket实现将创建新的端点实例。此实例将致力于同现在已经连接的单一客户端进行交互。这意味着,如果WebSocket端点最终有许多的客户端连接,WebSocket实现将实例化端点许多次,连接的每个新客户端都会初始化一次。图1-4展示了打开阶段握手请求的一个示例。

示例中,客户端请求连接托管在http://server.example.com的服务器上且相对于服务器根的URI路径为/mychat的一个WebSocket。你应该注意到WebSocket协议使用了HTTP升级机制。该机制也被浏览器用于将HTTP连接升级到安全的HTTPS连接,只是示例中客户端请求的协议是WebSocket协议。客户端发送一个唯一标识符,此标识符将在响应中使用。而且你应该注意到图中有一些其他的头定义了客户端希望使用的WebSocket协议的版本,以及希望使用的一些特殊子协议及扩展。我们将在后面章节接触其精确定义和使用WebSocket协议的子协议及扩展。目前,简单地知道它们指示了一种特定应用可以调整协议来更好地适应其需求的方法即可。

最后,打开阶段握手请求可能也声明了Origin头。例如,如果请求由浏览器创建,它经常是包含WebSocket客户端代码的Web页面的网站的因特网地址。

图1-5展示了打开阶段握手响应的示例。示例中,服务器同意"升级"连接为WebSocket连接。它发送回一个安全标识符,客户端可以使用其来验证它接收的响应是来自于对其发送的同一个请求的回应。示例中的服务器决定使用打开阶段握手请求中列出的一系列协议中的chat子协议。由于它同时支持两种WebSocket协议扩展(称为compress和mux--分别表示压缩和多路技术),因此它也同意在打开阶段握手的客户端和服务器之间已经建立的连接中使用这些扩展。

1.4.3  WebSocket消息通信

现在传输控制协议(TCP)连接已建立。WebSocket协议在TCP之上定义了消息协议的最小帧。这些在TCP连接上来回发送的不同的WebSocket协议帧定义了WebSocket的生命周期事件,例如打开和关闭连接,同时也定义了在连接上应用创建的文本和二进制消息的传输方式。

当WebSocket实现接收入站连接时,它保证存在一个可以处理连接的端点的实例,并且将其与发起连接的客户端相关联。在下一章中你将学到更多关于客户端连接与端点的实例相关联的知识。此时,每当客户端发送消息给端点时,都是端点实例的实现在与客户端相关联,接收以负载中消息为参数的方法回调。端点可以从Session对象中获取一个到RemoteEndpoint对象的引用。此引用唯一表示连接上的客户端,可用于给客户端发送消息。同样的关联也隐式存在于处理注解式端点的消息处理方法返回值的场景中。图1-6展示了编程式端点的典型服务器端部署中的对象模型图。

图中,我们可以看到服务器上部署了一个单一的逻辑端点。此端点有两个实例分别由两个Endpoint框表示,每个实例处理来自两个独立客户端的消息。每个端点实例已经注册了一系列的MessageHandler实例来处理入站消息。你也可以看到每个端点都有一个对表示到每个客户端的连接的Session对象的引用。每个会话引用一个RemoteEndpoint对象实例,它使得端点具备发送消息给客户端的能力。

对于注解式端点来说,其运行时建立的对象模型大体相同,端点一直都能够利用Session和RemoteEndpoint对象。然而,对于注解式端点来说有一个简化,其MessageHandler对象由WebSocket实现产生;开发人员永远不需要创建、注册或者引用它们。

当然,此图仅仅表示连接存活时的对象的快照。因此WebSocket端点的全图就如同两个人在电话中进行谈话的一张照片是一个电话对话的完整描述一样。为了更完整地理解两个端点间完整的WebSocket会话,我们需要探究WebSocket端点的生命周期。而这将是下一章讨论的主题。

1.5  本章小结

本章中已经看到如何使用Java WebSocket API来创建服务器端的元素并使用JavaScript API来创建客户端元素,从而创建简单的客户端/服务器WebSocket应用。同样也看到如何使用Java WebSocket API中的Java注解或者继承Java WebSocket API中的Endpoint类来创建WebSocket端点。本章也提及了Java WebSocket API中一些最重要的对象:Session、RemoteEndpoint和MessageHandler对象。同时阐述了将WebSocket端点作为标准Web应用的一部分部署的两种方法。

2.1 WebSocket协议

《Java WebSocket编程 开发、部署和保护动态Web应用》第2章Java WebSocket生命周期,本章将阐述WebSocket端点的生命周期。WebSocket端点的生命周期为开发人员提供了一个框架来管理端点所需的资源,也提供了一个框架来拦截消息。本书节为大家介绍WebSocket协议。

第2章 Java WebSocket生命周期

本章将阐述WebSocket端点的生命周期。WebSocket端点的生命周期为开发人员提供了一个框架来管理端点所需的资源,也提供了一个框架来拦截消息。我们将仔细查看其生命周期事件的顺序和语义,以及Java WebSocket API如何提供API和注解来支持处理这些事件。我们将看到如何在示例应用中以注解式和编程式两种方式来使用它们。

2.1  WebSocket协议

我们首先介绍WebSocket协议本身的背景。为了能够使用Java WebSocket API,读者无须知晓本节的每一个细节,但是它能作为有益的背景资料帮助理解WebSocket技术以及Java WebSocket API形成的原因。

与基于HTTP的技术不同,WebSocket具有生命周期。此生命周期由WebSocket协议进行支撑。例如,在Servlet技术中,底层协议仅仅定义了简单的请求/响应交互,此交互完全独立于下一次交互。事实上,在大部分情况下携带交互数据的底层网络连接将被完全弱化。一些技术(例如Java Servlet API)必须在请求/响应交互模型之上构建会话,这有助于开发人员创建比单一的隔离的交互生存时间更长的应用:HttpSession和Java Servlet组件生命周期就是其最佳示例。

相反,WebSocket协议定义了客户端和服务器间长时间存活的专用的TCP连接的正因为如此,在定义更长时间的生命周期方面,它比传统的Web请求/响应模型更进一步。此外,WebSocket协议定义了WebSocket连接上往返传输的数据的各个块的格式。一旦连接已经建立,这些传输的元数据帧描述了其用途。WebSocket协议中包含两种主要类型的帧:控制帧和数据帧。控制帧是用于执行协议的一些内部功能逻辑的数据传输。例如,协议定义了关闭帧(close frame)。关闭帧是一种特殊的传输,它意味着发送者准备关闭连接。其他控制帧是Ping帧和Pong帧。Ping帧和Pong帧是用来服务于连接健康性检测的数据传输。如果WebSocket希望检查其到WebSocket对等节点的连接的健康性,它可以发送Ping帧。当WebSocket对等节点收到Ping帧后,必须响应Pong帧。同乒乓球游戏一样,连接的健康性(多快或者其功能是否正常)可以在任何时候进行检测。

WebSocket协议定义的另一类帧是数据帧。数据帧定义了携带应用数据的WebSocket传输的种类。第1章中发送的回显消息就是文本数据帧的示例。数据帧分为两种基本类型:文本型和二进制型,一种用来携带文本消息,另一种用来携带二进制数据(例如图像数据)。WebSocket协议的一个特性是它能够通过多个独立的传输来发送消息,不管消息是文本类型还是二进制类型。这样的单独传输携带了完整消息的一部分,同时也作为此传输序列的一部分,其被称为部分帧(partial frame)。此种将消息作为部分帧序列的技术在WebSocket实现传递特别大的消息时或者是发送正在制定中的消息时特别有用。虽然此协议经常仅用来发送短消息,但是它并没有限制WebSocket消息的大小,所以也有一些应用选择使用WebSocket消息来传输大量的数据,例如视频或者金融数据的大型存档文件。

你将看到,低层级的WebSocket协议帧的提示在一些Java WebSocket API调用中是很明显的,然而开发人员实际上不需要理解其低层级的细节。

2.2  Java WebSocket生命周期

既然你已经对WebSocket会话期间连接上往返交互的过程有了一定的了解,下面就来看看Java WebSocket API如何对端点生命周期进行建模。

所有Java WebSocket端点生命周期的第一个事件是打开通知(open notification),它用来指示到WebSocket会话另一端的连接已经建立。使用电话呼叫来作一个类比,在第1章中已经谈到此握手交互与因特网电话呼叫建立过程中发生的交互过程类似:电话号码路由、选择使用的连接速度,以及是否仅使用语音还是同时使用视频连接。打开通知类似于电话铃声响,你接听电话并单击建立打开的连接。

一旦打开通知被WebSocket对话的两端都接收到,参与的任意WebSocket后续就可以发送消息了。发送多少消息、何时发送、发送顺序、发送内容当然都高度依赖于应用。与打电话类似,打电话期间,任何一方都可以有机会在电话中说其想说的话。

在WebSocket对话期间的任何时候,都可能出现一些消息传递的错误。接收消息的WebSocket端点本身就可能会产生错误(例如,它接收消息但不知如何合理地处理消息),或者WebSocket实现本身在某些情况下也会产生错误(例如,接收的消息比它能够处理的消息还要大)。WebSocket端点生命周期的这一阶段可能会产生两种结果:错误是致命错误,这将导致连接被关闭,无法再传递消息;错误是非致命错误,此时端点将有能力自己决定是否继续发送和接收消息。和打电话类似,消息可能来自对话的任意一端,它可能暂时被干扰,但随后又变得连续,或者一些更严重的错误可能事实上导致对话被中断。

不管WebSocket对话的哪端准备结束对话,它都可以初始化关闭事件。在这种情况下,WebSocket实现与对话的另一端进行通信,通知其连接将被关闭。和打电话类似,这个过程类似于电话中的一方准备说"再见"来结束对话。事实上,WebSocket无须等待另一方确认此通知事件,它类似于对话伙伴已经说了再见但没来得及以同样的方式响应时立刻听到了电话滴答声。或许对打电话的人来说这种行为有些无礼,但是对于WebSocket端点来说却完全合适。

借助于图2-1,我们回顾一下WebSocket对话的生命周期。在这里我们可以自顶向下看看这个图,一旦客户端和服务端的WebSocket连接被建立,WebSocket对话的两端都被通知会话打开了。一旦会话被打开,WebSocket对话的两端会接收到其对方发来的消息。

在此阶段中,如图中的情况下,可能出现的一些错误状态是可恢复的。因为服务端端点在接收错误并且在处理完错误之后发送了另一条消息。图中,WebSocket对话的一端已经决定结束对话,将导致两端接收到关闭通知(close notification)。此后除非建立新的连接,否则客户端和服务端间不会再发送消息。

2.3  Java WebSocket API中的WebSocket生命周期

既然已经基本了解了WebSocket端点生命周期,下面就从Java组件的视角来看看其生命周期是如何呈现的。在上一节中,我们已经识别了WebSocket端点的如下4个生命周期事件。

打开事件:此事件发生在端点上建立新连接时并且在任何其他事件发生之前。

消息事件:此事件接收WebSocket对话中另一端发送的消息。它可以发生在WebSocket端点接收了打开事件之后并且在接收关闭事件关闭连接之前的任意时刻。

错误事件:此事件在WebSocket连接或者端点发生错误时产生。

关闭事件:此事件表示WebSocket端点的连接目前正在部分地关闭,它可以由参与连接的任意一个端点发出。

幸运的是,这些事件几乎一对一地映射到Java方法上,使得你可以编写代码在Java组件中拦截这些事件。首先,我们看看注解式WebSocket端点。一旦完成了这些,将看看同样的事件在编程式端点上的展现方式。

2.3.1  注解式端点事件处理(1)

为了将Java类声明成WebSocket端点,对于服务器端端点来说需要使用一个类级别注解@ServerEndpoint(第1章中已经描述过),对于客户端端点来说需要使用类似的@ClientEndpoint注解(将在第3章中描述)。现在假设我们正在开发一个端点,此端点将部署在服务器上并且将等待来自一个或者更多客户端的入站连接。

对于注解式端点来说,为了拦截不同的生命周期事件,我们需要利用方法级注解:@OnOpen、@OnMessage、@OnError和@OnClose。我们将看到每个WebSocket生命周期事件都伴随着不同的限定信息。因此,对于被生命周期注解注解的Java方法可能的方法签名,有许多选项。

1. @OnOpen

首先介绍第一个注解@OnOpen。此注解用于注解式端点的方法,指示当此端点建立新的连接时调用此方法。需要一个方法来处理打开事件的主要原因是,使得开发人员能够设置在WebSocket对话时可能需要的任何信息。有关这一点的合适示例是如果你的WebSocket准备使用数据库来获取或者存储一些WebSocket对话中的信息,你可能希望为准备数据库而执行一些花费昂贵的必要操作,例如在处理打开事件的方法中打开数据库连接。此事件伴随着三部分信息:WebSocket Session对象,用于表示已经建立好的连接;配置对象(EndpointConfig的实例),包含了用来配置端点的信息;一组路径参数,用于打开阶段握手时WebSocket端点匹配入站URI。使用WebSocket注解的好处是如果你不需要的话,不必使用此事件的所有信息。因此此类使用@OnOpen注解的方法是任何没有返回值的公有方法,这些方法有一个可选的Session参数、一个可选的EndpointConfig参数,以及任意数量的被@PathParam注解的String参数。另外,这些参数的顺序可以任意排列。我们将在第6章回到路径参数主题,因此此处不在其上花费过多时间。这意味着如果仅仅希望能够引用Session对象,可以编写如下的代码清单2-1。

代码清单2-1:打开事件处理方法示例

  1. @OnOpen  
  2. public void init(Session session) {   
  3. // initialization code  
  4. }  

此外,如果你希望查询配置对象(由EndpointConfig类的实例表示),同样可以进行如代码清单2-2所示的声明:

代码清单2-2:打开事件处理方法示例

  1. @OnOpen  
  2. public void init(Session session, EndpointConfig config) {   
  3. // initialization code  
  4. }  

你已经看到,不仅能够灵活地命名方法来处理打开事件,而且能够灵活地决定事件关联的数据中有多少可用。

2. @OnMessage

通常,你应该希望WebSocket端点能够在连接建立后处理一些或者所有的入站消息。为此,我们使用@OnMessage注解。此注解允许你装饰你希望处理入站消息的方法。Java WebSocket API中的消息事件伴随的信息是Session对象(它表示消息抵达时的连接)、EndpointConfig对象、打开阶段握手中从匹配入站URI过程中获取的路径参数以及最重要的消息本身。与@OnOpen注解一样,方法参数可以以任意顺序排列,并且你仅需要包含你想知道的消息事件的那些部分的方法参数。

连接上的消息将以3种基本形式抵达:文本消息、二进制消息或Pong消息。Java WebSocket API提供了一系列的选项使得能够以这些形式接收消息。最基本的形式是选择使用带String参数的方法来处理文本消息;使用带ByteBuffer或者是byte[]参数的方法来处理二进制消息;若你的消息将仅仅处理Pong消息,则可使用Java WebSocket API中的PongMessage接口的一个实例。

例如,如果你希望以最简单的形式来处理文本消息,可以使用如下所示的代码清单2-3:

代码清单2-3:文本消息处理方法示例

  1. @OnMessage  
  2. public void handleTextMessages(String textMessage) {   
  3. // process the textMessage here  
  4. }  

如果你希望处理二进制消息,并且希望获取表示消息到来时连接的会话的引用,可使用如下所示的代码清单2-4:

代码清单2-4:二进制消息处理方法示例

  1. @OnMessage  
  2. public void processBinary(byte[] messageData, Session session) {   
  3. // process binary data here  
  4. }   

尽管如此,对于文本和二进制消息来说有一些更加高级的选项。例如,你可以选择当消息到来时分批地接收文本或者二进制消息。若如此,可以使用一对参数来表示到来的消息分片(partial message),如代码清单2-5所示。对于文本消息分片来说是String和boolean,如代码清单2-6所示。String表示文本消息分片,boolean是一个标志位,当其被设置为false时表示后续还有整个文本消息序列中的更多的消息分片到来,当设置为true时表示当前的消息分片是序列中的最后一个消息分片。对于二进制消息分片来说,可以选择一对参数ByteBuffer和boolean,其中ByteBuffer表示二进制消息分片,boolean表示到来的此二进制消息分片是否为构成完整消息的序列中的最后一个消息分片。对于二进制消息来说,也可以使用一对参数byte[]和boolean,它与参数对ByteBuffer和boolean一样,但其使用字节数组而不是ByteBuffer来存储二进制消息分片。

代码清单2-5:二进制消息分片处理方法示例

  1. @OnMessage  
  2. public void processVideoFragment(byte[] partialData,   
  3. boolean isLast) {   
  4. if (!isLast) {   
  5. // there is more to come !   
  6. } else {   
  7. // now we have the whole message !   
  8. }  
  9. }  

代码清单2-6:文本消息分片处理方法示例

  1. @OnMessage  
  2. public void catchDocumentPart(String text, boolean isLast) {   
  3. // pass on to feed elsewhere  
  4. }  
即使使用此消息分片处理选项,无论使用哪种处理方式,短消息仍然可能以一个片段到达。长消息可能以任意数量的片段到达。一般说来,它取决于WebSocket实现如何将消息发送到API:不同的实现可能会将消息拆分成更小一些的片段。尽管如此,一般说来如果你希望能够在消息一到来时就开始处理,则它是处理大消息的一个有用选项。

2.3.1  注解式端点事件处理(2)

使用@OnMessage注解来处理输入消息的另一个选项是选择使用Java I/O流:使用java.io.Reader来处理文本消息,使用java.io.InputStream来处理二进制消息。如代码清单2-7所示。试图使用一些使用了I/O的Java类库来处理消息时,它们是有用的。

代码清单2-7:二进制I/O消息处理方法示例

  1. @OnMessage  
  2. public void handleBinary(InputStream is) {   
  3. // read  
  4. }  

事实上,处理消息还有更多的选项:你甚至可以让WebSocket实现把入站消息转换成自己选择的对象。我们将在第3章回到此话题,描述更多的细节。

WebSocket应用一般是异步的双向消息。换言之,典型应用并不总是立即响应入站消息。尽管如此,在一些场景下你希望立刻响应入站消息:第1章中的Echo示例应用就是关于这一点的极好示例。因此,通过@OnMessage注解的此类方法上有一个额外选项:方法可以有返回类型或者返回为空。当使用@OnMessage注解的方法有返回类型时,WebSocket实现立即将返回值作为消息返回给刚刚在方法中处理的消息的发送者。它在你需要设计一个应用显式确认收到WebSocket消息的特殊情况下是有用的。如代码清单2-8所示。

代码清单2-8:包含返回值的消息处理方法示例

  1. @OnMessage  
  2. public byte[] dealWithRequest(String requestMessage) {   
  3. byte[] ack = {0};   
  4. return ack;   
  5. }  

在示例中,当拥有此方法的端点接收任意类型的文本消息时,端点立刻返回包含0个字节负载的二进制消息。通过@OnMessage注解的方法的返回类型中最基本的类型是String、byte[]或者ByteBuffer。还有更多的选项,具体可参见下一章。

3. @OnError

错误事件比消息事件稍微简单一些。@OnError可以用来注解WebSocket端点的方法,使其可以处理WebSocket实现处理入站消息时发生的任何错误。如代码清单2-9所示。如果你不想让WebSocket端点处理这些类型的错误的话,就可以不处理。尽管如此,还是建议在WebSocket端点中包括处理错误的方法,否则你在试图追踪客户端发送了但没有传达的消息时可能以浪费时间而结束。伴随错误事件的信息是错误信息、发生错误的会话以及与建立连接的打开阶段握手相关联的任何一个路径参数。再次重申,我们将在下一章回到路径参数的主题。错误信息由java.lang.Throwable类表示,会话由Java WebSocket API中的Session接口表示。和其他Java WebSocket API中的方法级注解一样,错误处理方法中包含哪些参数依赖于错误发生时你希望方法所接收的信息。与其他方法级注解一样,方法中的参数顺序可以任意排列。

代码清单2-9:错误处理方法示例

  1. @OnError  
  2. public void errorHandler(Throwable t) {   
  3. // log error here  
  4. }  

处理入站消息时,可能会发生3种基本的错误类型。首先,WebSocket实现产生的错误可能会发生,例如如果该WebSocket端点的入站消息是非正常的。这些错误都属于SessionException类型。其次,错误可能发生在当WebSocket实现试图将入站消息解码成开发人员所要求的某个对象时。我们将在第3章回到此话题,此处只需要知道此类型错误都是DecodeException就足够了。最后是由WebSocket端点的其他方法产生的运行时错误。

如果你选择不在应用中包含错误处理方法,那么WebSocket实现将记录WebSocket端点操作过程中产生的任何错误,以便后续查看。当然,从哪里能够找到这些信息以及信息是否足够详细都依赖于所使用的WebSocket实现。

4. @OnClose

现在我们来看看WebSocket生命周期中的最后一个事件:关闭事件。如果你在WebSocket对话中使用了一些昂贵的资源并且希望释放和恰当地关闭它们,拦截关闭事件是一个很好的主意。它对于在WebSocket连接关闭时做其他的通用清理工作也是有用的。@OnClose可以用来注解多种不同类型的方法来处理关闭事件。如代码清单2-10所示。伴随关闭事件的信息是关闭信息、与建立连接的打开阶段握手相关联的任意一个路径参数,以及一些描述连接关闭原因的信息。最后的关闭原因信息以Java WebSocket API中CloseReason类的形式存在。连接关闭存在多种原因,但是最典型的原因是WebSocket对话的任意一方完成了所需要的所有工作或者是由于不活跃导致连接超时。按惯例,方法参数都是可选的,而且可以以任意顺序出现。

代码清单2-10:关闭事件处理方法示例

  1. @OnClose  
  2. public void goodbye(CloseReason cr) {   
  3. // log the reason for posterity   
  4. // close database connection  
  5. }   
现在,我们将所有这些概念放在一起,以示例的形式解释这些主要概念。

2.3.2  Lifecycle示例(1)

Lifecycle示例是一个JavaScript WebSocket客户端。它与Java WebSocket端点进行交互,并且使用所有的生命周期注解。当LifecycleEndpoint注解式端点运行于服务器上处理WebSocket生命周期事件时,它发送消息给客户端使得客户端能够显示一系列交通信号灯。当应用启动时,连接是关闭的。点击Web页面上的按钮将一步步地带着用户通过WebSocket生命周期中的关键阶段,如图2-2所示。你将注意到有两个按钮能够关闭连接:一个按钮导致客户端初始化关闭事件;另一个按钮导致服务器初始化关闭事件。

1. Lifecycle JavaScript客户端

JavaScript代码使用JavaScript WebSocket API拦截来自Java类LifecycleEndpoint发送的消息,并解析消息来告知哪个信号灯亮着以及需要给用户显示什么消息。

首先来看看JavaScript代码,仅仅关注主要的客户端方法调用即可。按惯例,如代码清单2-11所示,我们创建了JavaScript WebSocket实例,为其添加事件处理代码使得每当服务器组件发送回消息时都将被调用,并将指示客户端如何显示交通信号灯。

代码清单2-11:创建一个JavaScript WebSocket

  1. function open_connection() {   
  2. lifecycle_websocket = new   
  3. WebSocket("ws://localhost:8080/lifecycle/lights");   
  4. lifecycle_websocket.onmessage = function (evt) {   
  5. update_for_message(evt.data);   
  6. update_buttons();   
  7. };   
  8. lifecycle_websocket.onclose = function (evt) {   
  9. update_buttons();   
  10. };   
  11. }  

如你所见,当JavaScript WebSocket接收到消息时,它将调用方法update_for_message()并且更新按钮的启用/禁用状态。另外,当JavaScript WebSocket关闭时,你将看到也会调用同样的方法更新按钮状态。如代码清单2-12所示。

其次,我们有两个方法来解析来自服务器的消息。这些消息的一个示例是"3:Just opened"。第一个方法去除消息部分,仅返回数字;第二个方法去除开始的数字,仅返回消息部分。

代码清单2-12:解析生命周期消息的JavaScript

  1. function get_light_index(message) {   
  2. return message.substring(0, 1)  
  3. }   
  4.  
  5. function get_display_message(message) {   
  6. return message.substring(2, message.length)  
  7. }   

下一个方法基于其在交通信号灯中的位置(light_index)和应该打开还是关闭(light_on_index)来计算交通信号灯中每个灯的颜色。如代码清单2-13所示。

代码清单2-13:计算交通信号灯颜色的JavaScript

  1. function get_color(light_index, light_on_index) {   
  2. if (light_index == 1 && light_on_index == 1) {   
  3. return "red"   
  4. } else if (light_index == 2 && light_on_index == 2) {   
  5. return "yellow"   
  6. } else if (light_index == 3 && light_on_index == 3) {   
  7. return "green"   
  8. } else {   
  9. return "grey"   
  10. }   
  11. }  

最后,这里有一个方法将所有的一切放在一起:它被调用时的参数是服务器发送的消息中解析出来的部分,并且其目的是更新交通信号灯以及信号灯下的消息。如代码清单2-14所示。

代码清单2-14:响应生命周期消息更新页面的JavaScript

  1. function update_display(light_index, display_message) {   
  2. var old = traffic_light_display.firstChild;   
  3. var pre = document.createElement("pre");   
  4. pre.style.wordWrap = "break-word";   
  5. pre.innerHTML = "<b><font   
  6. face='Arial'>"+display_message+"</font></b>";   
  7. if (traffic_light_display.firstChild != null) {   
  8. traffic_light_display.replaceChild(pre,   
  9. traffic_light_display.firstChild);   
  10. } else {   
  11. traffic_light_display.appendChild(pre)   
  12. }   
  13. var context = document.getElementById  
  14. ('myDrawing').getContext('2d');   
  15. context.beginPath();   
  16. context.fillStyle = "black"   
  17. context.fillRect(65,0,70,210);   
  18. context.fill();   
  19.  
  20. context.beginPath();   
  21. context.fillStyle = get_color(1, light_index);   
  22. context.arc(100,35,25,0,(2*Math.PI), false)   
  23. context.fill();   
  24.  
  25. context.beginPath();   
  26. context.fillStyle = get_color(2, light_index);   
  27. context.arc(100,105,25,0,(2*Math.PI), false)   
  28. context.fill();   
  29.  
  30. context.beginPath();   
  31. context.fillStyle = get_color(3, light_index);   
  32. context.arc(100,175,25,0,(2*Math.PI), false)   
  33. context.fill();   
  34. }  

我们注意到此示例中使用了新的绘画画布。它和WebSocket一样,也是属于HTML5规范的一部分。

2. Lifecycle注解式端点

目前我们已经了解了应用客户端所做的工作,下面回到WebSocket端点生命周期的主题。让我们来看看我们创建的服务器组件Lifecycle注解式端点。代码清单2-15如下所示。

代码清单2-15:Lifecycle Java WebSocket端点

  1. import java.io.*;  
  2. import java.io.IOException;  
  3. import javax.websocket.OnClose;  
  4. import javax.websocket.OnError;  
  5. import javax.websocket.OnMessage;  
  6. import javax.websocket.OnOpen;  
  7. import javax.websocket.Session;  
  8. import javax.websocket.server.ServerEndpoint;   
  9.  
  10. @ServerEndpoint("/lights")  
  11. public class LifecycleEndpoint {   
  12. private static String START_TIME = "Start Time";   
  13. private Session session;   
  14.  
  15. @OnOpen   
  16. public void whenOpening(Session session) {   
  17. this.session = session;   
  18. session.getUserProperties().put(START_TIME, System.currentTimeMillis());   
  19. this.sendMessage("3:Just opened");   
  20. }   
  21.  
  22. @OnMessage   
  23. public void whenGettingAMessage(String message) {   
  24. if (message.indexOf("xxx") != -1) {   
  25. throw new IllegalArgumentException("xxx not   
  26. allowed !");   
  27. } else if (message.indexOf("close") != -1) {   
  28. try {   
  29. this.sendMessage("1:Server closing after "   
  30. + this.getConnectionSeconds() + " s");   
  31. session.close();   
  32. } catch (IOException ioe) {   
  33. System.out.println("Error closing session "   
  34. + ioe.getMessage());   
  35. }   
  36. return;   
  37. }   
  38. this.sendMessage("3:Just processed a message");   
  39. }   
  40.  
  41. @OnError   
  42. public void whenSomethingGoesWrong(Throwable t) {   
  43. this.sendMessage("2:Error: " + t.getMessage());   
  44. }   
  45.  
  46. @OnClose   
  47. public void whenClosing() {   
  48. System.out.println("Goodbye !");   
  49. }   
  50.  
  51. void sendMessage(String message) {   
  52. try {   
  53. session.getBasicRemote().sendText(message);   
  54. } catch (Throwable ioe) {   
  55. System.out.println("Error sending message "   
  56. + ioe.getMessage());   
  57. }   
  58. }   
  59.  
  60. int getConnectionSeconds() {   
  61. long millis = System.currentTimeMillis()   
  62. - ((Long) this.session.getUserProperties().  
  63. get(START_TIME));   
  64. return (int) millis / 1000;   
  65. }  
  66. }  

2.3.2  Lifecycle示例(2)

首先,我们注意到此服务器端点使用了@ServerEndpoint注解,它被映射到路径/lights上。你可能想知道在不看需要它来初始化连接的客户端代码的情况下如何计算此端点的完整URI。此完整URI是ws:// URI,其中URI由主机名称和端口号,加上包含此WebSocket端点的Web应用的上下文路径,再加上此端点的相对URI构成。我们将在第6章回到此话题,然而由于此处包含Lifecycle端点的Web应用的上下文路径是/lifecycle,因此意味着此WebSocket端点的完整URL是:

  1. ws://localhost:8080/lifecycle/lights 

你将看到示例中用@OnOpen注解指定的方法用来处理WebSocket端点的打开事件,代码清单2-16如下所示。

代码清单2-16:Lifecycle示例中打开事件处理方法签名

  1. @OnOpen  
  2. public void whenOpening(Session session) 

我们已经选择此方法接收一个Session对象的引用,此对象将在后面使用。通过查看此方法的实现,我们将看到端点中的此方法在接收到打开事件之后所做的事情是记录当前的时间并将其添加到Session对象的用户属性字典中,然后给客户端发送消息指示将第三个灯(绿灯)打开来表明消息通信现在可以流动了。

你在示例中也会看到每当接收到消息时,都将调用如代码清单2-17所示的方法。

代码清单2-17:Lifecycle示例中消息事件处理方法签名

  1. @OnMessage  
  2. public void whenGettingAMessage(String message) 

我们已经选择仅接收文本消息(任何抵达此端点的二进制消息将被忽略)并且以String对象的形式接收。与前面谈到的选择以消息分片或者是以java.io.Reader形式接收文本消息相比,由于此处的消息都是短消息,此形式是完全合适的。事实上,在很多场景中,很可能你会选择以这种最简单的形式来接收文本消息。此消息事件处理方法的实现中首先检查消息是否有不良字符的特殊序列(毕竟这些交通信号灯是在一个家族中)。如果发现不正确,将产生一个包含解释的运行时异常。如果发现正确,方法将检查消息查看客户端是否请求服务器端端点关闭连接,此种场景下方法将在给客户端发送一条指示将灯变红的消息后关闭连接。最后,如果消息是其他种类的消息,此方法的实现将发送一个消息给客户端,指示信号灯应该继续保持绿色。

如果消息中有不良字符,它将创建一个需要被处理的错误事件。因此Lifecycle端点声明如代码清单2-18所示的方法来处理任何错误事件。

代码清单2-18:Lifecycle示例中错误事件处理方法签名

  1. @OnError  
  2. public void whenSomethingGoesWrong(Throwable t) 

此方法简单地给客户端发送一条消息,此消息包含错误描述并且请求客户端将交通信号灯变黄。当运行此应用时,如果你发送一条坏消息,Lifecycle示例中的消息处理方法将被调用。这将产生一个异常,它接着导致错误处理方法被调用,并接着将交通信号灯中的黄灯点亮。因为错误事件并没有导致会话被关闭,黄灯意味着消息通信仍将流动,在这种场景下是合适的。当然,在错误处理方法中也可以做一些其他的事情,例如调用session.close()方法来关闭连接。

最后,每当客户端连接关闭时,都将调用Lifecycle端点声明的如代码清单2-19所示的方法。

代码清单2-19:Lifecycle示例中关闭事件处理方法签名

  1. @OnClose  
  2. public void whenClosing() 
你将注意到无论是Lifecycle客户端还是服务器端点终止连接,都将调用LifecycleEndpoint类的whenClosing()方法。


2.3.3  编程式端点生命周期

在继续讨论此示例的其他方面之前,简单地看一下编程式WebSocket端点生命周期。

1. 生命周期事件

你应该记得第1章中提到过编程式端点都必须继承javax.websocket.Endpoint类。可以通过提供Endpoint方法的自定义实现来拦截编程式端点的打开、关闭和错误事件。既然你已经学习了WebSocket生命周期注解,那么应该相当熟悉Endpoint类的如表2-1所示的方法。

表2-1  端点方法

事    件

端 点 方 法

打开

public abstract void onOpen(Session ses, EndpointConfig config)

错误

public void onError(Session ses, Throwable thr)

关闭

public void onClose(Session ses, CloseReason cr)

2. 处理消息

到目前为止你可能想知道如何拦截消息事件,当然你已经看到这一点。因为在Java WebSocket API中开发人员有广泛的选项可以用来处理消息,因此此工作有一些轻微的差别。

如第1章所见,为了处理入站消息,你需要提供MessageHandler实现。如同注解式端点,这里存在广泛的选项用于创建不同的MessageHandler实现来以不同的方式处理不同类型的WebSocket消息。我们将在第3章中查看这些参数的全集,现在仅关注处理基本文本和二进制消息。对此仅需要实现下列这些:对于文本消息,使用MessageHandler.Whole<String>;对于二进制消息,使用MessageHandler.Whole<ByteBuffer>。

一旦你实现了上述一个或者两个接口来定义希望消费消息的方式,你需要做的所有事情是通过调用

  1. session.addMessageHandler(myMessageHandler) 

在第一个消息到达之前的某一时刻注册你的消息处理程序到代表你有兴趣侦听的连接的Session对象上。通常,端点将在onOpen()方法中添加其消息处理程序,因此可以确保不遗漏任何消息。

3. 编程式Lifecycle

代码清单2-20所示是Lifecycle端点的编程式版本:

代码清单2-20:编程式Lifecycle WebSocket端点

  1. import java.io.IOException;  
  2. import javax.websocket.CloseReason;  
  3. import javax.websocket.Endpoint;  
  4. import javax.websocket.EndpointConfig;  
  5. import javax.websocket.MessageHandler;  
  6. import javax.websocket.Session;  
  7.  
  8. public class ProgrammaticLifecycleEndpoint extends Endpoint {   
  9. private static String START_TIME = "Start Time";   
  10. private Session session;   
  11.  
  12. @Override   
  13. public void onOpen(Session session, EndpointConfig config) {   
  14. this.session = session;   
  15. final Session mySession = session;   
  16. this.session.addMessageHandler(  
  17. new MessageHandler.Whole<String>() {   
  18. @Override   
  19. public void onMessage(String message) {   
  20. if (message.indexOf("xxx") != -1) {   
  21. throw new IllegalArgumentException(  
  22. "xxx not allowed !");   
  23. } else if (message.indexOf("close") != -1) {   
  24. try {   
  25. sendMessage("1:Server closing after " + getConnectionSeconds() + " s");   
  26. mySession.close();   
  27. } catch (IOException ioe) {   
  28. System.out.println("Error closing session "   
  29. + ioe.getMessage());   
  30. }   
  31. return;   
  32. }   
  33. sendMessage("3:Just processed a message");   
  34. }   
  35. });   
  36. session.getUserProperties().put(START_TIME,   
  37. System.currentTimeMillis());   
  38. this.sendMessage("3:Just opened");   
  39. }   
  40.  
  41. @Override   
  42. public void onClose(Session session, CloseReason closeReason) {   
  43. System.out.println("Goodbye !");   
  44. }   
  45.  
  46. @Override   
  47. public void onError(Session session, Throwable thr) {   
  48. this.sendMessage("2:Error: " + thr.getMessage());   
  49. }   
  50.  
  51. void sendMessage(String message) {   
  52. try {   
  53. session.getBasicRemote().sendText(message);   
  54. } catch (IOException ioe) {   
  55. System.out.println("Error sending message " + message);   
  56. }   
  57. }   
  58.  
  59. int getConnectionSeconds() {   
  60. long millis = System.currentTimeMillis() -   
  61. ((Long) this.session.getUserProperties().  
  62. get(START_TIME));   
  63. return (int) millis / 1000;   
  64. }  
  65. }  

2.3.4  实例数目及线程机制

此示例中,你可能想知道的一件事情是为什么端点使用实例变量来保存对Session对象的引用。你应该记得Session对象表示到单一客户端的连接。你应该也注意到在Lifecycle示例中,我们在端点的生命周期中的两个地方使用了会话对象:一个地方是在客户端建立新连接时用于记录事件发生的时间,另一个地方是在方法whenGettingAMessage()中当服务器端点准备关闭到客户端的连接时计算连接打开的时间。

因为可以很容易地如代码清单2-21所示编写关闭事件处理方法,所以我们无须在打开事件处理方法中将会话存储为实例变量以便后续访问。

代码清单2-21:另一种Lifecycle关闭事件处理方法签名

  1. @OnClose  
  2. public void whenGettingAMessage(String message,  
  3. Session session) 

我们可以简单地要求WebSocket实现传入这些参数。

Lifecycle示例中将会话存储为实例变量的原因是,我们可以使用它来阐述WebSocket端点生命周期中的一个更重要的问题。如果你重新启动Lifecycle应用,但是这一次打开第二个浏览器窗口到同样的首页,你将看到两组交通信号灯。假如你开始按下任意一个浏览器窗口的生命周期按钮,那么将看到每组信号灯都可以是不同的状态。这是因为每个浏览器窗口对Lifecycle WebSocket端点来说都充当一个独立的客户端,并且WebSocket实现为每个连接的客户端使用不同的LifecycleEndpoint实例。

这意味着对于每一个WebSocket端点(不管是注解式Java类还是编程式端点)定义来说,WebSocket容器在每次有新的客户端连接时会实例化端点的一个新的实例。这样做的结果是每个WebSocket端点实例仅能够永远"看到"同样的会话实例:此实例表示从唯一的客户端连接到那个端点实例的唯一连接。

同样,可以编写Lifecycle示例将会话对象作为参数传递给whenGettingAMessage()方法。若如此,你将发现传递的实例与传递给whenOpening(Session session)方法的Session对象是一样的。

WebSocket实现也为你提供了另外一个重要的保证:同一个会话(或者是连接)中不允许两个事件线程同时调用一个端点实例。这可能听起来很抽象,但是这意味着端点实例永远不会在某时被WebSocket实现的一个以上的线程调用。它意味着如果客户端发送多条消息,WebSocket实现必须调用端点每次处理一条消息。知道这一点特别重要,因为这意味着你永远不需要担心为端点实例的并发访问进行编程。这也是Java WebSocket编程模型与Java Servlet 编程模型的关键差异,Java Servlet实例可能被多个线程同时调用,每个线程用于处理不同客户端的请求/响应交互。这意味着WebSocket编程明显更加容易。

2.4  本章小结

本章中学习了管理WebSocket端点生命周期的事件。不仅学习打开、消息、错误和关闭事件的顺序和语义,而且也学习了那些允许开发人员处理这些事件的方法和注解。最后,还学习了WebSocket实现调用开发人员创建的WebSocket端点时所使用的线程策略,它意味着WebSocket开发人员通常不需要处理多个线程同时对端点的调用。




原创粉丝点击