您的位置:首页 > 编程语言 > Java开发

使用 JSR 356 API 构建 Java WebSocket 应用

2017-08-09 17:03 931 查看

HTTP与WEBSOCKET

大家都知道这样一个事实,那就是HTTP(Hypertext Transfer Protocol)是一个无状态的请求-响应式协
议。HTTP协议的这种简单设计使它颇具扩展性却不够高效,并且不适合于频繁交互的实时网络应用。HTTP被设
计用来进行文档共享而不是用来建立频繁交互的网络应用。HTTP天生就不太正规,对每一个http请求/响应,
都要通过线路传输许多头信息。

在HTTP 1.1版本之前,每一个提交到服务器的请求都会创建一个新的链接。这种情况在HTTP 1.1中通过引入
HTTP持久化连接得以改进。持久化连接允许web浏览器复用同样的连接来获取图片,脚本等等。

HTTP被设计成半双工的,这意味着同一时刻只允许向一个方向上传输数据。Walkie-talkie是一个半双工设施
的例子,因为一个时刻只能有一个人说话。开发者们已经创造出了一些工作方法或者应对方法来克服HTTP的这个
缺点。这些工作方法包括轮询,长效轮询和流。

使用轮询时,客户端采用同步调用来从服务器获取信息。如果服务器有新信息,它会在响应中返回数据。否则,就
不会有信息返回给客户端,然后客户端会在一段时间之后再次创建一个新的连接。这是一种很低效却很简单的获得
实时行为的方式。长效轮询是另一种工作方法,客户端会对服务器发起一个连接,服务器会保持该连接直到有可用
数据或者超过了指定超期时间。长效轮询也被称为comet。由于同步的HTTP和这些异步应用之间的不匹配,这些方
案易于复杂化,无标准化和低效化。

随着时间的推移,对于在客户端和服务器之间建立基于标准的、双向的、全双工通道的需求,已经增长了。在本文
中,我们会看到WebSockets是如何帮助解决这些问题的,然后了解一下如何在Java中使用JSR 356 API来构建
基于WebSocket的应用。

请注意本文不会讨论到OpenShift WebSocket支持。如果你想了解OpenShift WebSocket支持,请参考
Marek Jelen的这篇文章。


什么是WebSocket?

一个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不适合于实时应用的问题,并且它通过避免复杂的工作方式使得应用结构很简单。


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
应用的开发工作变得很容易。


步骤 5: 创建并启动一个WebSocket服务器

我们需要一个服务器来部署我们的WebSocket @ServerEndpoint。该服务器是使用如下所示的tyrus服务器
API来创建的。它会运行在8025端口。WordgameServerEndpoint可以通过
ws://localhost:8025/websockets/game来访问。


package com.shekhar.wordgame.server;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import org.glassfish.tyrus.server.Server;

public class WebSocketServer {

public static void main(String[] args) {
runServer();
}

public static void runServer() {
Server server = new Server("localhost", 8025, "/websockets", WordgameServerEndpoint.class);

try {
server.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Please press a key to stop the server.");
reader.readLine();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
server.stop();
}
}
}


如果你在使用Eclipse,你可以通过把它作为一个Java application(ALT+SHIFT+X,J)来运行,来启动该服
务器。你会看到如下所示的日志。


Jul 26, 2013 1:39:37 PM org.glassfish.tyrus.server.ServerContainerFactory create
INFO: Provider class loaded: org.glassfish.tyrus.container.grizzly.GrizzlyEngine
Jul 26, 2013 1:39:38 PM org.glassfish.grizzly.http.server.NetworkListener start
INFO: Started listener bound to [0.0.0.0:8025]
Jul 26, 2013 1:39:38 PM org.glassfish.grizzly.http.server.HttpServer start
INFO: [HttpServer] Started.
Jul 26, 2013 1:39:38 PM org.glassfish.tyrus.server.Server start
INFO: WebSocket Registered apps: URLs all start with ws://localhost:8025
Jul 26, 2013 1:39:38 PM org.glassfish.tyrus.server.Server start
INFO: WebSocket server started.
Please press a key to stop the server.


步骤 6 : 启动WebSocket客户端

现在服务器已经启动了并且WebSocket @ServerEndpoint也部署好了,我们准备把客户端作为一个Java应用
来启动。我们会创建一个ClientManager实例,然后连接到服务器上的endpoint,如下所示。


@ClientEndpoint
public class WordgameClientEndpoint {

private static CountDownLatch latch;

private Logger logger = Logger.getLogger(this.getClass().getName());

@OnOpen
public void onOpen(Session session) {
// same as above
}

@OnMessage
public String onMessage(String message, Session session) {
// same as above
}

@OnClose
public void onClose(Session session, CloseReason closeReason) {
logger.info(String.format("Session %s close because of %s", session.getId(), closeReason));
latch.countDown();
}

public static void main(String[] args) {
latch = new CountDownLatch(1);

ClientManager client = ClientManager.createClient();
try {
client.connectToServer(WordgameClientEndpoint.class, new URI("ws://localhost:8025/websockets/game"));
latch.await();

} catch (DeploymentException | URISyntaxException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}


我们使用CountDownLatch来确保在代码执行之后,主线程不能退出问题。当时间锁去减少onClose()方法里的
计数器时,主线程会保持等待。然后程序结束。在main()方法里,我们创建了ClientManager实例,它被用来
连接位于ws://localhost:8025/websockets/game的@ServerEndpoint。

把客户端作为一个Java应用来运行(ALT + SHIFT + X , J),你会看到下列日志信息。


Jul 26, 2013 1:40:26 PM com.shekhar.wordgame.client.WordgameClientEndpoint onOpen
INFO: Connected ... 95f58833-c168-4a5f-a580-085810b4dc5a
Jul 26, 2013 1:40:26 PM com.shekhar.wordgame.client.WordgameClientEndpoint onMessage
INFO: Received ....start
随意发送消息比如 "hello world",它会被重复并返回给你,如下所示。

INFO: Received ....start
hello world
Jul 26, 2013 1:41:04 PM com.shekhar.wordgame.client.WordgameClientEndpoint onMessage
INFO: Received ....hello world
发送"quit"消息,WebSocket连接就会被关闭。

INFO: Received ....hello world
quit
Jul 26, 2013 1:42:23 PM com.shekhar.wordgame.client.WordgameClientEndpoint onClose
INFO: Session 95f58833-c168-4a5f-a580-085810b4dc5a close because of CloseReason[1000,Game ended]


步骤 7 : 添加游戏逻辑

现在我们准备添加游戏逻辑,发送一个排序错乱的单词给客户端,然后当从客户端接收到一个恢复后单词时,检查
该单词是否正确。像下面这样改进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 unscrambledWord, Session session) {
switch (unscrambledWord) {
case "start":
logger.info("Starting the game by sending first word");
String scrambledWord = WordRepository.getInstance().getRandomWord().getScrambledWord();
session.getUserProperties().put("scrambledWord", scrambledWord);
return scrambledWord;
case "quit":
logger.info("Quitting the game");
try {
session.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "Game finished"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
String scrambledWord = (String) session.getUserProperties().get("scrambledWord");
return checkLastWordAndSendANewWord(scrambledWord, unscrambledWord, session);
}

@OnClose
public void onClose(Session session, CloseReason closeReason) {
logger.info(String.format("Session %s closed because of %s", session.getId(), closeReason));
}

private String checkLastWordAndSendANewWord(String scrambledWord, String unscrambledWord, Session session) {
WordRepository repository = WordRepository.getInstance();
Word word = repository.getWord(scrambledWord);

String nextScrambledWord = repository.getRandomWord().getScrambledWord();

session.getUserProperties().put("scrambledWord", nextScrambledWord);

String correctUnscrambledWord = word.getUnscrambbledWord();

if (word == null || !correctUnscrambledWord.equals(unscrambledWord)) {
return String.format("You guessed it wrong. Correct answer %s. Try the next one .. %s",
correctUnscrambledWord, nextScrambledWord);
}
return String.format("You guessed it right. Try the next word ...  %s", nextScrambledWord);
}
}


重启服务器和客户端,享受游戏吧。


结论

在本文中,我们看到了JSR 356 WebSocket API是如何帮助我们构建实时的全双工的Java应用程序的。JSR
356 WebSocket API非常简单,并且基于注解的开发模式使得构建WebSocket应用非常容易。在下篇博文中,
我们会看一下Undertow,一个来自JBoss的Java编写的灵活的高性能web服务器。


翻译原文:http://www.oschina.net/translate/how-to-build-java-websocket-applications-using-the-jsr-356-api

英文原文:https://blog.openshift.com/how-to-build-java-websocket-applications-using-the-jsr-356-api/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息