这是一篇翻译的文章, 版本虽不是最新,但有参考价值。
转自:http://www.oschina.net/translate/how-to-build-java-websocket-applications-using-the-jsr-356-api
大家都知道这样一个事实,那就是HTTP(Hypertext Transfer Protocol)是一个无状态的请求-响应式协议。HTTP协议的这种简单设计使它颇具扩展性却不够高效,并且不适合于频繁交互的实时网络应用。HTTP被设计用来进行文档共享而不是用来建立频繁交互的网络应用。HTTP天生就不太正规,对每一个http请求/响应,都要通过线路传输许多头信息。
在HTTP 1.1版本之前,每一个提交到服务器的请求都会创建一个新的链接。这种情况在HTTP 1.1中通过引入HTTP持久化连接得以改进。持久化连接允许web浏览器复用同样的连接来获取图片,脚本等等。
HTTP被设计成半双工的,这意味着同一时刻只允许向一个方向上传输数据。Walkie-talkie是一个半双工设施的例子,因为一个时刻只能有一个人说话。开发者们已经创造出了一些工作方法或者应对方法来克服HTTP的这个缺点。这些工作方法包括轮询,长效轮询和流。
一个WebSocket是通过一个独立的TCP连接实现的、异步的、双向的、全双工的消息传递实现机制。WebSockets不是一个HTTP连接,却使用HTTP来引导一个WebSocket连接。一个全双工的系统允许同时进行双向的通讯。陆地线路电话是一个全双工设施的例子,因为它们允许两个通话者同时讲话并被对方听到。最初WebSocket被提议作为HTML5规范的一部分,HTML5承诺给现代的交互式的web应用带来开发上的便利和网络效率,但是随后WebSocket被移到一个仅用来存放WebSockets规范的独立的标准文档里。它包含两件事情 — WebSocket协议规范,即2011年12月发布的RFC 6455,和WebSocket JavaScript API。
WebSocket协议利用HTTP 升级头信息来把一个HTTP连接升级为一个WebSocket连接。HTML5 WebSockets 解决了许多导致HTTP不适合于实时应用的问题,并且它通过避免复杂的工作方式使得应用结构很简单。
最新的浏览器都支持WebSockets,如下图所示。该信息来自于http://caniuse.com/#feat=websockets.
WebSocket是如何工作的?
每一个WebSocket连接的生命都是从一个HTTP请求开始的。HTTP请求跟其他请求很类似,除了它拥有一个Upgrade头信息。Upgrade头信息表示一个客户端希望把连接升级为不同的协议。对WebSockets来说,它希望升级为WebSocket协议。当客户端和服务器通过底层连接第一次握手时,WebSocket连接通过把HTTP协议转换升级为WebSockets协议而得以建立。一旦WebSocket连接成功建立,消息就可以在客户端和服务器之间进行双向发送。
WebSockets带来了性能,简单化和更少带宽消耗
-
WebSockets比其它工作方式比如轮询更有效也更高效。因为它需要更少的带宽并且降低了延时。
-
WebSockets简化了实时应用的结构体系。
-
WebSockets在点到点发送消息时不需要头信息。这显著的降低了带宽。
WebSocket使用案例
一些可能的WebSockets使用案例有:
-
聊天应用
-
多人游戏
-
股票交易和金融应用
-
文档合作编辑
-
社交应用
在Java中使用WebSockets
在Java社区中下面的情形很普遍,不同的供应商和开发者编写类库来使用某项技术,一段时间之后当该技术成熟时它就会被标准化,来使开发者可以在不同实现之间互相操作,而不用冒供应商锁定的风险。当JSR 365启动时,WebSocket就已经有了超过20个不同的Java实现。它们中的大多数都有着不同的API。JSR 356是把Java的WebSocket API进行标准化的成果。开发者们可以撇开具体的实现,直接使用JSR 356 API来创建WebSocket应用。WebSocket API是完全由事件驱动的。
JSR 356 — WebSockets的Java API
JSR 356,WebSocket的Java API,规定了开发者把WebSockets 整合进他们的应用时可以使用的Java API — 包括服务器端和Java客户端。JSR 356是即将出台的Java EE 7标准中的一部分。这意味着所有Java EE 7兼容的应用服务器都将有一个遵守JSR 356标准的WebSocket协议的实现。开发者也可以在Java EE 7应用服务器之外使用JSR 356。目前Apache Tomcat 8的开发版本将会增加基于JSR 356 API的WebSocket支持。
一个Java客户端可以使用兼容JSR 356的客户端实现,来连接到WebSocket服务器。对web客户端来说,开发者可以使用WebSocket JavaScript API来和WebSocket服务器进行通讯。WebSocket客户端和WebSocket服务器之间的区别,仅在于两者之间是通过什么方式连接起来的。一个WebSocket客户端是一个WebSocket终端,它初始化了一个到对方的连接。一个WebSocket服务器也是一个WebSocket终端,它被发布出去并且等待来自对方的连接。在客户端和服务器端都有回调监听方法 — onOpen , onMessage , onError, onClose。后面我们创建一个应用的时候再来更详细的了解这些。
Tyrus — JSR 356 参考实现
Tyrus是JSR 356的参考实现。我们会在下一节中以独立模式用Tyrus开发一个简单应用。所有Tyrus组件都是用Java SE 7编译器进行构建的。这意味着,你也至少需要不低于Java SE 7的运行环境才能编译和运行该应用示例。它不能够在Apache Tomcat 7中运行,因为它依赖于servlet 3.1规范。
使用WebSockets开发一个单词游戏
现在我们准备创建一个非常简单的单词游戏。游戏者会得到一个字母排序错乱的单词,他或她需要把这个单词恢复原样。我们将为每一次游戏使用一个单独的连接。
本应用的源代码可以从github获取 https://github.com/shekhargulati/wordgame
步骤 1 : 创建一个模板Maven项目
开始时,我们使用Maven原型来创建一个模板Java项目。使用下面的命令来创建一个基于Maven的Java项目。
$ mvn archetype:generate -DgroupId=com.shekhar -DartifactId=wordgame -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
步骤 2 : 向pom.xml中添加需要的依赖
正如上节中提到的,你需要Java SE 7来构建使用Tyrus的应用。要在你的maven项目中使用Java 7,你需要在配置中添加maven编译器插件来使用Java 7,如下所示。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerVersion>1.7</compilerVersion>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
下面,添加对JSR 356 API的依赖。javax.websocket-api的当前版本是 1.0。
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0</version>
</dependency>
下面我们将要添加与Tyrus JSR 356实现相关的依赖。tyrus-server包提供了JSR 356服务端WebSocket API实现,tyrus-client包提供了JSR356客户端WebSocket API实现。
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-server</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-client</artifactId>
<version>1.1</version>
</dependency>
最后,我们添加tyrus-container-grizzly依赖到我们的pom.xml中。这将提供一个独立的容器来部署WebSocket应用。
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-container-grizzly</artifactId>
<version>1.1</version>
</dependency>
你可以在这里查看完整的pom.xml文件。
步骤 3 : 编写第一个JSR 356 WebSocket服务器终端
现在我们的项目已经设置完毕,我们将开始编写WebSocket服务器终端。你可以通过使用@ServerEndpoint注解来把任何Java POJO类声明为WebSocket服务器终端。开发者也可以指定用来部署终端的URI。URI要相对于WebSocket容器的根路径,必须以”/”开头。在如下所示的代码中,我们创建了一个非常简单的WordgameServerEndpoint。
package com.shekhar.wordgame.server;
import java.io.IOException; import java.util.logging.Logger;
import javax.websocket.CloseReason; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.CloseReason.CloseCodes; import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/game") public class WordgameServerEndpoint {
private Logger logger = Logger.getLogger(this.getClass().getName());
@OnOpen public void onOpen(Session session) {
logger.info("Connected ... " + session.getId());
}
@OnMessage public String onMessage(String message, Session session) { switch (message) { case "quit": try {
session.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "Game ended"));
} catch (IOException e) { throw new RuntimeException(e);
} break;
} return message;
}
@OnClose public void onClose(Session session, CloseReason closeReason) {
logger.info(String.format("Session %s closed because of %s", session.getId(), closeReason));
}
}
@OnOpen注解用来标注一个方法,在WebSocket连接被打开时它会被调用。每一个连接都有一个和它关联的session。在上面的代码中,当onOpen()方法被调用时我们打印了一下session的id。对每一个WebSocket连接来说,被@OnOpen标注的方法只会被调用一次。
@OnMessage注解用来标注一个方法,每当收到一个消息时它都会被调用。所有业务代码都需要写入该方法内。上面的代码中,当从客户端收到”quit”消息时我们会关闭连接,其它情况下我们只是把消息原封不动的返回给客户端。所以,在收到“quit”消息以前,一个WebSocket连接将会一直打开。当收到退出消息时,我们在session对象上调用了关闭方法,告诉它session关闭的原因。在示例代码中,我们说当游戏结束时这是一个正常的关闭。
@OnClose注解用来标注一个方法,当WebSocket连接关闭时它会被调用。
步骤 4 : 编写第一个JSR 356 WebSocket客户端终端
@ClientEndpoint注解用来标记一个POJO WebSocket客户端。类似于javax.websocket.server.ServerEndpoint,通过@ClientEndpoint标注的POJO能够使它的那些使用了网络套接字方法级别注解的方法,成为网络套接字生命周期方法。
package com.shekhar.wordgame.client;
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; import java.util.concurrent.CountDownLatch; import java.util.logging.Logger;
import javax.websocket.ClientEndpoint; import javax.websocket.CloseReason; import javax.websocket.DeploymentException; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session;
import org.glassfish.tyrus.client.ClientManager;
@ClientEndpoint public class WordgameClientEndpoint {
private Logger logger = Logger.getLogger(this.getClass().getName());
@OnOpen public void onOpen(Session session) {
logger.info("Connected ... " + session.getId()); try {
session.getBasicRemote().sendText("start");
} catch (IOException e) { throw new RuntimeException(e);
}
}
@OnMessage public String onMessage(String message, Session session) {
BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in)); try {
logger.info("Received ...." + message);
String userInput = bufferRead.readLine(); return userInput;
} catch (IOException e) { throw new RuntimeException(e);
}
}
@OnClose public void onClose(Session session, CloseReason closeReason) {
logger.info(String.format("Session %s close because of %s", session.getId(), closeReason));
}
}
在上面的代码中,当WebSocket 连接被打开时,我们发送了一个“start”消息给服务器。每当从服务器收到一个消息时,被@OnMessage注解标注的onMessage方法就会被调用。它首先记录下消息让后等待用户的输入。用户的输入随后会被发送给服务器。最后,当WebSocket 连接关闭时,用@OnClose标注的onClose()方法被被调用。正如你所看到的,客户单和服务器端的代码编程模式是相同的。这使得通过JSR 356 API来编写WebSocket应用的开发工作变得很容易。
********************************************************************************************************************************************************
最后附上oracle官网关于JSR 356 websocket api 如何整合进应用说明, http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html:
JSR 356, Java API for WebSocket
by Johan Vos
Learn how to integrate WebSockets into your applications.
Published April 2013
For many Web-based client-server applications, the old HTTP request-response model has its limitations. Information has to be transmitted from the server to the client in between requests, rather than upon request only.
A number of “hacks” have been used in the past to circumvent this problem, for example, long polling and Comet. However, the need for a standards-based, bidirectional and full-duplex channel between clients and a server has only increased.
In 2011, the IETF standardized the WebSocket protocol as RFC 6455. Since then, the majority of the Web browsers are implementing client APIs that support the WebSocket protocol. Also, a number of Java libraries have been developed that implement the WebSocket protocol.
The WebSocket protocol leverages the HTTP upgrade technology to upgrade an HTTP connection to a WebSocket. Once it is upgraded, the connection is capable of sending messages (data frames) in both directions, independent of each other (full duplex). No headers or cookies are required, which considerably lowers the required bandwidth. Typically, WebSockets are used to periodically send small messages (for example, a few bytes). Additional headers would often make the overhead larger than the payload.
JSR 356
JSR 356, Java API for WebSocket, specifies the API that Java developers can use when they want to integrate WebSockets into their applications—both on the server side as well as on the Java client side. Every implementation of the WebSocket protocol that claims to be compliant with JSR 356 must implement this API. As a consequence, developers can write their WebSocket-based applications independent of the underlying WebSocket implementation. This is a huge benefit, because it prevents a vendor-lock and allows for more choices and freedom of libraries and application servers.
JSR 356 is a part of the upcoming Java EE 7 standard; hence, all Java EE 7–compliant application servers will have an implementation of the WebSocket protocol that adheres to the JSR 356 standard. Once they are established, WebSocket client and server peers are symmetrical. The difference between a client API and a server API is, therefore, minimal. JSR 356 defines a Java client API as well, which is a subset of the full API required in Java EE 7.
A client-server application leveraging WebSockets typically contains a server component and one or more client components, as shown in Figure 1:

Figure 1
In this example, the server application is written in Java, and the WebSocket protocol details are handled by the JSR 356 implementation contained in the Java EE 7 container.
A JavaFX client can rely on any JSR 356–compliant client implementation for handling the WebSocket-specific protocol issues. Other clients (for example, an iOS client and an HTML5 client) can use other (non-Java) implementations that are compliant with RFC 6455 in order to communicate with the server application.
Programming Model
The Expert Group that defined JSR 356 wanted to support patterns and techniques that are common to Java EE developers. As a consequence, JSR 356 leverages annotations and injection.
In general, two different programming models are supported:
-
Annotation-driven. Using annotated POJOs, developers can interact with the WebSocket lifecycle events.
-
Interface-driven. Developers can implement the Endpoint interface and the methods that interact with the lifecycle events.
Lifecycle Events
The typical lifecycle event of a WebSocket interaction goes as follows:
-
One peer (a client) initiates the connection by sending an HTTP handshake request.
-
The other peer (the server) replies with a handshake response.
-
The connection is established. From now on, the connection is completely symmetrical.
-
Both peers send and receive messages.
-
One of the peers closes the connection.
Most of the WebSocket lifecycle events can be mapped to Java methods, both in the annotation-driven and interface-driven approaches.
Annotation-Driven Approach
An endpoint that is accepting incoming WebSocket requests can be a POJO annotated with the @ServerEndpoint annotation. This annotation tells the container that the given class should be considered to be a WebSocket endpoint. The required value element specifies the path of the WebSocket endpoint.
Consider the following code snippet:
@ServerEndpoint("/hello")
public class MyEndpoint { }
This code will publish an endpoint at the relative path hello. The path can include path parameters that are used in subsequent method calls; for example, /hello/{userid} is a valid path, where the value of {userid} can be obtained in lifecycle method calls using the @PathParam annotation.
In GlassFish, if your application is deployed with the contextroot mycontextroot in a Web container listening at port 8080 of localhost, the WebSocket will be accessible using ws://localhost:8080/mycontextroot/hello.
An endpoint that should initiate a WebSocket connection can be a POJO annotated with the @ClientEndpoint annotation. The main difference between @ClientEndpoint and a ServerEndpoint is that the ClientEndpoint does not accept a path value element, because it is not listening to incoming requests.
@ClientEndpoint
public class MyClientEndpoint {}
Initiating a WebSocket connection in Java leveraging the annotation-driven POJO approach can be done as follows:
javax.websocket.WebSocketContainer container =
javax.websocket.ContainerProvider.getWebSocketContainer();
container.conntectToServer(MyClientEndpoint.class,
new URI("ws://localhost:8080/tictactoeserver/endpoint"));
Hereafter, classes annotated with @ServerEndpoint or @ClientEndpoint will be called annotated endpoints.
Once a WebSocket connection has been established, a Session is created and the method annotated with @OnOpen on the annotated endpoint will be called. This method can contain a number of parameters:
-
A javax.websocket.Session parameter, specifying the created Session
-
An EndpointConfig instance containing information about the endpoint configuration
-
Zero or more string parameters annotated with @PathParam, referring to path parameters on the endpoint path
The following method implementation will print the identifier of the session when a WebSocket is “opened”:
@OnOpen
public void myOnOpen (Session session) {
System.out.println ("WebSocket opened: "+session.getId());
}
A Session instance is valid as long as the WebSocket is not closed. The Session class contains a number of interesting methods that allow developers to obtain more information about the connection. Also, the Session contains a hook to application-specific data, by means of the getUserProperties() method returning a Map<String, Object>. This allows developers to populate Session instances with session- and application-specific information that should be shared among method invocations.
When the WebSocket endpoint receives a message, the method annotated with @OnMessage will be called. A method annotated with @OnMessage can contain the following parameters:
-
The javax.websocket.Session parameter.
-
Zero or more string parameters annotated with @PathParam, referring to path parameters on the endpoint path.
-
The message itself. See below for an overview of possible message types.
When a text message has been sent by the other peer, the content of the message will be printed by the following code snippet:
@OnMessage
public void myOnMessage (String txt) {
System.out.println ("WebSocket received message: "+txt);
}
If the return type of the method annotated with @OnMessage is not void, the WebSocket implementation will send the return value to the other peer. The following code snippet returns the received text message in capitals back to the sender:
@OnMessage
public String myOnMessage (String txt) {
return txt.toUpperCase();
}
Another way of sending messages over a WebSocket connection is shown below:
RemoteEndpoint.Basic other = session.getBasicRemote();
other.sendText ("Hello, world");
In this approach, we start from the Session object, which can be obtained from the lifecycle callback methods (for example, the method annotated with @OnOpen). The getBasicRemote() method on the Session instance returns a representation of the other part of the WebSocket, the RemoteEndpoint. That RemoteEndpoint instance can be used for sending text or other types of messages, as described below.
When the WebSocket connection is closing, the method annotated with @OnClose is called. This method can take the following parameters:
-
The javax.websocket.Session parameter. Note that this parameter cannot be used once the WebSocket is really closed, which happens after the @OnClose annotated method returns.
-
A javax.websocket.CloseReason parameter describing the reason for closing the WebSocket, for example, normal closure, protocol error, overloaded service, and so on.
-
Zero or more string parameters annotated with @PathParam, referring to path parameters on the endpoint path.
The following code snippet will print the reason why a WebSocket is closing:
@OnClose
public void myOnClose (CloseReason reason) {
System.out.prinlnt ("Closing a WebSocket due to "+reason.getReasonPhrase());
}
To be complete, there is one more lifecycle annotation: in case an error is received, the method annotated with @OnError will be called.
Interface-Driven Approach
The annotation-driven approach allows us to annotate a Java class and methods with lifecycle annotations. Using the interface-driven approach, a developer extends javax.websocket.Endpoint and overrides the onOpen, onClose, and onError methods:
public class myOwnEndpoint extends javax.websocket.Endpoint {
public void onOpen(Session session, EndpointConfig config) {...}
public void onClose(Session session, CloseReason closeReason) {...}
public void onError (Session session, Throwable throwable) {...}
}
In order to intercept messages, a javax.websocket.MessageHandler needs to be registered in the onOpen implementation:
public void onOpen (Session session, EndpointConfig config) {
session.addMessageHandler (new MessageHandler() {...});
}
MessageHandler is an interface with two subinterfaces: MessageHandler.Partial and MessageHandler.Whole. The MessageHandler.Partial interface should be used when the developer wants to be notified about partial deliveries of messages, and an implementation of MessageHandler.Whole should be used for notification about the arrival of a complete message.
The following code snippet listens to incoming text messages and sends the uppercase version of the text message back to the other peer:
public void onOpen (Session session, EndpointConfig config) {
final RemoteEndpoint.Basic remote = session.getBasicRemote();
session.addMessageHandler (new MessageHandler.Whole<String>() {
public void onMessage(String text) {
try {
remote.sendString(text.toUpperCase());
} catch (IOException ioe) {
// handle send failure here
}
}
});
}
Message Types, Encoders, and Decoders
The Java API for WebSocket is very powerful, because it allows any Java object to be sent or received as a WebSocket message.
Basically, there are three different types of messages:
-
Text-based messages
-
Binary messages
-
Pong messages, which are about the WebSocket connection itself
When using the interface-driven model, each session can register at most one MessageHandler for each of these three different types of messages.
When using the annotation-driven model, for each different type of message, one @onMessage annotated method is allowed. The allowed parameters for specifying the message content in the annotated methods are dependent on the type of the message.
The Javadoc for the @OnMessage annotation clearly specifies the allowed message parameters based on the message type (the following is quoted from the Javadoc):
-
“if the method is handling text messages:
-
String to receive the whole message
-
Java primitive or class equivalent to receive the whole message converted to that type
-
String and boolean pair to receive the message in parts
-
Reader to receive the whole message as a blocking stream
-
any object parameter for which the endpoint has a text decoder (Decoder.Text or Decoder.TextStream).
-
if the method is handling binary messages:
-
if the method is handling pong messages:
Any Java object can be encoded into a text-based or binary message using an encoder. This text-based or binary message is transmitted to the other peer, where it can be decoded into a Java object again—or it can be interpreted by another WebSocket library. Often, XML or JSON is used for the transmission of WebSocket messages, and the encoding/decoding then comes down to marshaling a Java object into XML or JSON and back.
An encoder is defined as an implementation of the javax.websocket.Encoder interface, and a decoder is an implementation of the javax.websocket.Decoder interface. Somehow, the endpoint instances need to know what the possible encoders and decoders are. Using the annotation-driven approach, a list of encoders and decoders is passed via the encoder and decoder elements in the @ClientEndpoint and @ServerEndpoint annotations.
The code in Listing 1 shows how to register a MessageEncoder class that defines the conversion of an instance of MyJavaObject to a text message. A MessageDecoder class is registered for the opposite conversion.
@ServerEndpoint(value="/endpoint", encoders = MessageEncoder.class, decoders= MessageDecoder.class)
public class MyEndpoint {
...
}
class MessageEncoder implements Encoder.Text<MyJavaObject> {
@override
public String encode(MyJavaObject obj) throws EncodingException {
...
}
}
class MessageDecoder implements Decoder.Text<MyJavaObject> {
@override
public MyJavaObject decode (String src) throws DecodeException {
...
}
@override
public boolean willDecode (String src) {
// return true if we want to decode this String into a MyJavaObject instance
}
}
Listing 1
The Encoder interface has a number of subinterfaces:
-
Encoder.Text for converting Java objects into text messages
-
Encoder.TextStream for adding Java objects to a character stream
-
Encoder.Binary for converting Java objects into binary messages
-
Encoder.BinaryStream for adding Java objects to a binary stream
Similarly, the Decoder interface has four subinterfaces:
-
Decoder.Text for converting a text message into a Java object
-
Decoder.TextStream for reading a Java object from a character stream
-
Decoder.Binary for converting a binary message into a Java object
-
Decoder.BinaryStream for reading a Java object from a binary stream
Conclusion
The Java API for WebSocket provides Java developers with a standard API to integrate with the IETF WebSocket standard. By doing so, Web clients or native clients leveraging any WebSocket implementation can easily communicate with a Java back end.
The Java API is highly configurable and flexible, and it allows Java developers to use their preferred patterns.