这篇博客文章描述了用于WebSocket协议的Java API(JSR 356) (这是Java EE 7平台的四个最新JSR之一),并提供了部署在WildFly 8上并可以在OpenShift上在线获得的具体应用程序。
- [FR]版本的法语( HTML或PDF )在AngularJS上发布了基于Java API的示范性演示。
阅读这篇文章后,您将能够了解Arun Gupta对WebSocket技术可以做什么的定义。
“ WebSocket通过单个TCP为您提供双向的全双工通信通道。”
— Arun Gupta(Java EE布道士Chez Oracle)–英国Devoxx 2013
Java EE 7概述
Java平台企业版于2013年6月在版本7(Java EE 7)中发布。 与先前的两个版本(Java EE 5和Java EE 6)保持一致, Java EE 7始终建议简化开发人员的工作。 此版本用3个主要目标装饰了以前的版本:
- 包含HTML5 (WebSocket API,JSON-P API,JAX-RS)
- 为开发人员(JMS)提供更高的生产率
- 满足企业需求 (批处理API,并发实用程序)
Java平台企业版7(JSR 342)可以总结为:
- 4个最新规范:
Java API for WebSocket 1.0
Java API for JSON Processing 1.0
,Batch Applications 1.0
和Concurrency Utilities for Java EE 1.0
- 3个主要更新的规范:
JMS 2.0
,JAX-RS 2.0
和EL 3.0
- 和6个规范(进行了较小的更新):
JPA 2.1
,Servlet 3.1
,EJB 3.2
,CDI 1.1
,JSF 2.2
和Bean Validation 1.1
演示:AngularJS –在WildFly 8(OpenShift)上部署HTML5 / JSR-356 API应用程序
如果要立即查看它的外观,可以访问the online application
其代码将在本文中进行部分说明。 这个应用程序可以为您提供以下功能:
- 观看(四分之一决赛美国公开赛2013年)在现场模式的一个或几个网球比赛
- 点击您要实时访问的每个比赛
- 下注比赛的获胜者
- 每当用户下注相同的比赛时,计数器就会增加
您会说: “没什么特别的!” ,而您是对的:)
乍一看,这听起来像是当今许多应用程序中已经看到的东西,但是它所使用的技术确实很重要,因为,正如您将在下面看到的那样,所有内容都基于新的WebSocket协议(ws:// ou)的标准。 wss://),而不是在“ HTTP hacking”上。
用于开发此应用程序的技术是:
- 前端:
HTML5
,CSS
,带有Bootstrap CSS和AngularJS
Javascript (WebSocket API)
- 后端:
Java API for WebSocket
,EJB
,JSON-P
,JAX-RS
不! 该演示不是聊天应用程序:)很明显,“聊天演示”是首先想到的用于说明WebSocket技术使用的示例。 但是,还有许多其他用例,例如在线文本文档上的协作工作或JavaOne 2013主题演讲中展示的象棋这样的在线游戏。
借助RedHat的云计算PaaS产品OpenShift ,该应用程序可在云上使用。 它已部署在WildFly 8.0.0-Beta1(正常认证的Java EE 7到2013年底)上。 要在OpenShit上设置WildFly之类的应用服务器,您只需阅读Shekhar Gulati的博客文章
WebSocket(WS):不同于HTTP的新协议
HTTP是Web的标准协议,它在许多用例中非常有效,但是在交互式Web应用程序中却有一些缺点 :
- 半双工 :基于请求/响应模式,客户端发送请求,服务器在发送响应之前执行处理,客户端被迫等待服务器响应
- 详细 :在HTTP请求和HTTP响应中,与消息相关的HTTP标头中发送了大量信息
- 为了添加服务器推送模式,您需要使用解决方法(轮询,长轮询,Comet / Ajax),因为没有标准
该协议并未针对在实时双向通信方面有重大需求的大型应用进行优化。 这就是为什么新的WebSocket协议比HTTP提供更多高级功能的原因,因为它是:
- 基于
1 unique TCP connection between 2 peers
(而每个HTTP请求/响应都需要一个新的TCP连接) -
bidirectionnal
:客户端可以向服务器发送消息,服务器也可以向客户端发送消息 -
full-duplex
:客户端可以将多个消息发送到服务器,也可以将服务器发送到客户端,而无需等待彼此的响应
术语“ 客户端”仅用于定义启动连接的客户端 。 建立连接后,客户端和服务器将成为具有相同容量的对等端。
WebSocket协议原本打算成为HTML5规范的一部分,但是随着HTML5将于2014年正式发布,WebSocket协议以及HTTP协议最终都由IETF规范和RFC 6455设置 。
如下图所示, WebSocket协议在两个阶段起作用 :
-
handshake
(打开和关闭) -
data transfer
开幕握手
打开握手阶段是发起连接的一个请求 (对等客户端)和对等服务器之间的唯一HTTP请求/响应 。 此HTTP交换是特定的,因为它使用HTTP规范中定义的升级概念。 原理很简单: 升级HTTP允许客户端要求服务器更改通信协议,从而确保客户端和服务器可以使用HTTP以外的协议进行讨论。
例子1. HTTP握手示例请求
GET /usopen/matches/1234 HTTP/1.1
Host: wildfly-mgreau.rhcloud.com:8000
Upgrade: websocket
Connection: Upgrade
Origin: http://wildfly-mgreau.rhcloud.com
Sec-WebSocket-Key:0EK7XmpTZL341oOh7x1cDw==
Sec-WebSocket-Version:13
第1行:需要HTTP GET方法和HTTP 1.1版本
第2行:用于WebSocket连接的主机
第3行:请求升级到WebSocket协议 第4行:请求从HTTP升级到另一个协议
例子2. HTTP握手响应样本
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Sec-WebSocket-Accept:SuQ5/hh0kStSr6oIzDG6gRfTx2I=
Upgrade:websocket
第1行:HTTP响应代码101:服务器兼容并接受通过其他协议发送消息
第4行:接受升级到WebSocket协议
当端点服务器批准了从HTTP到WebSocket协议的升级请求时,就不再可以使用HTTP通信,所有交换都必须通过WebSocket协议进行。
数据传输
一旦握手被批准,就可以使用WebSocket协议。 在对等服务器端以及对等客户端端都有一个开放的连接,调用回调处理程序以启动通信。
现在可以开始数据传输 ,因此两个对等方可以双向和全双工通信方式交换消息。
如图3所示 ,对peer server
可以发送多条消息( 在此示例中:每个得分点1条消息,每次用户在此游戏下注时每条消息1条,比赛结束时每条消息1条消息 ),而无需发送任何消息peer client
响应,对等客户端也可以随时发送消息( 在此示例中:押注比赛获胜者 )。 每个对等方都可以发送特定的消息以关闭连接。
使用Java EE7平台,对peer server side
代码是用Java编写的,而对peer client side
代码是用Java或Javascript编写的。
关闭握手
这个阶段可以由双方发起 。 想要关闭通信的对等方需要发送一个关闭控制帧,并且它也会收到一个关闭控制帧作为响应。
WebSocket Javascript API(客户端)
要使用WebSocket协议从Web应用程序与服务器进行通信,必须使用客户端Javascript API 。 定义此API是W3C的角色。 JavaScript WebSocket API的W3C规范正在最终确定。 WebSocket接口提供以下功能:
- 一个属性,用于定义到服务器端点的连接URL(
url
) - 知道连接状态的属性(
readyState
:CONNECTING,OPEN,CLOSING,CLOSED) - 与WebSocket生命周期有关的一些事件处理程序 ,例如:
- 当新连接打开时,将调用事件处理程序
onopen
- 当新连接打开时,将调用事件处理程序
- 可以将不同类型的流(文本,二进制)发送到端点服务器的方法(
send(DOMString data)
,send(Blob data)
)
例子3.来自http://websocket.org的 JavaScript源代码例子
var wsUri = "ws://echo.websocket.org/";function testWebSocket() {websocket = new WebSocket(wsUri);websocket.onopen = function(evt) { onOpen(evt) };websocket.onclose = function(evt) { onClose(evt) };websocket.onmessage = function(evt) { onMessage(evt) };websocket.onerror = function(evt) { onError(evt) }; }
}function onOpen(evt) {writeToScreen("CONNECTED");doSend("WebSocket rocks");
}
function onClose(evt) {writeToScreen("DISCONNECTED");
}
function onMessage(evt) {writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');websocket.close();
}function onError(evt) {writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}
function doSend(message) {writeToScreen("SENT: " + message);websocket.send(message);
}
JSR 386:用于WebSocket协议的Java API
由于W3C定义了如何在Javascript中使用WebSocket,因此Java Communitee Process(JCP)通过JSR 386在Java世界中进行了相同的操作。JSR356 为WebSocket协议定义了Java API,该协议是Java EE Web Profile的一部分,并提供了有能力 :
- 创建一个
WebSocket Endpoint
(服务器或客户端),该名称是可以通过WebSocket协议进行通信的Java组件的名称 - 注释或程序化方法的选择
- 通过此协议发送和使用消息控件,文本或二进制
- 将消息作为完整消息或部分消息序列进行管理
- 配置和管理WebSocket会话 (超时,Cookie…)
开源JSR-356 RI(参考实现)是Tyrus项目
WebSocket服务器端点
将普通旧Java对象(POJO)转换为服务器WebSocket端点 (即能够处理来自同一URI的不同客户的请求) 非常容易,因为您只需用@ServerEndpoint注释Java类,并用@注释一种方法即可。 OnMessage :
import javax.websocket.OnMessage;
import javax.websocket.ServerEndpoint;@ServerEndpoint("/echo")
public class EchoServer {@OnMessage public String handleMessage(String message){return "Thanks for the message: " + message;}}
第04行:@ServerEndpoint将此POJO转换为WebSocket端点,为将访问URI设置为此端点,必须使用value属性
第07行:将为每个收到的消息调用handleMessage方法
注解
此Java API提供了几种与WebSocket协议完全兼容的注释:
注解 | 角色 |
---|---|
@ServerEndpoint | 声明服务器端点 |
@ClientEndpoint | 声明客户端端点 |
@OnOpen | 声明此方法处理打开事件 |
@OnMessage | 声明此方法处理Websocket消息 |
@OnError | 声明此方法处理错误 |
@OnClose | 声明此方法处理WebSocket关闭事件 |
@ServerEndpoint
属性在下面列出:
- 值
- 相对URI或模板URI(例如:“ / echo”,“ / matches / {match-id}”)
- 解码器
- 消息解码器类名列表
- 编码器
- 消息编码器类名列表
- 子协议
- 支持的子协议的名称列表(例如:http://wamp.ws)
编码器和解码器
如本文前面所述,端点服务器可以接收消息中不同类型的内容:文本格式(JSON,XML…)或二进制格式的数据。
为了有效地管理来自对等客户端或应用程序业务代码中的对等客户端的消息,可以创建Encoders和Decoders Java类。
无论采用哪种转换算法,都可以进行转换:
- 业务POJO以所需的通信格式(JSON,XML,Binary…)流动
- 以特定格式(JSON,XML ..)流入业务POJO
因此,应用程序代码的结构使得业务逻辑不受对等服务器和对等客户端流之间交换的消息的类型和格式的影响。
本文后面将提供一个具体示例。
WebSocket客户端端点
该Java API还提供了对创建客户端Java端点的支持。
例子4. Java客户端端点样本
@ClientEndpoint
public class HelloClient {@OnMessagepublic String message(String message){// code}
}WebSocketContainer c = ContainerProvider.getWebSocketContainer();
c.connectToServer(HelloClient.class, "hello");
美国公开赛
该示例应用程序被部署为使用Apache Maven构建的WAR结果。 除了传统的管理WebSocket生命周期,发送消息的工作流程还如下:
- 每个对等客户端可以连接1或4个实时比赛
- 每个对等客户端都可以断开匹配
- 在比赛的每个点,连接到该比赛的客户端将接收数据(得分,服务…)
- 对等客户可能会发送一条消息,押注比赛的获胜者
- 每次一个对等客户下注一场比赛,所有其他在相同比赛中下注的对等客户都会收到一条消息,其中包含投注者总数
- 在比赛结束时, 对等客户押注这场比赛,会收到一条消息,其中包含获胜者的姓名和一条特定消息
所有消息均以JSON格式交换
项目布局如下:
例子5. Maven项目结构
+ src/main/java|+ com.mgreau.wildfly.websocket|+ decoders|- MessageDecoder.java |+ encoders |- BetMessageEncoder.java|- MatchMessageEncoder.java|+ messages |- BetMessage.java|- MatchMessage.java|- Message.java|+ rest |- RestApplication.java|- TournamentREST.java|- MatchEndpoint.java |- StarterService.java |- TennisMatch.java
+ src/main/resources
+ scr/main/webapp|+ css|+ images|+ js|+ live |- app.js|- controllers.js|- directives.js|- services.js|- websocket.js |+ templates|- bet.html|- match.html|- msg.html|- index.html|- live.html
pom.xml
第04行:解码从对等客户端 (关于获胜者的赌注)发送到POJO( BetMessage )的JSON消息
第05行:以JSON格式编码(通过JSON-P),有关获胜者的所有消息以及对等客户端的比赛详细信息
第08行:POJO处理对等体之间发送的消息 第12行:REST端点,列出所有比赛的比赛 第15行:应用程序WebSocket服务器端点( 对等服务器 ) 第16行:EJB @Startup,以便在部署时初始化此应用程序 第17行:POJO处理有关比赛的信息 第23行:AngularJS文件通过REST和WebSocket调用处理多个匹配项(live.html) 第28行:包含用于WebSocket协议的Javascript API实现的文件,用于处理简单情况下的客户端通信(index.html)
Java EE 7 API的Maven依赖关系
例子6.带有Java EE 7依赖关系的pom.xml
<project>
...
<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><!-- Java EE 7 --><javaee.api.version>7.0</javaee.api.version>
</properties<dependencies><dependency><groupId>javax</groupId> <artifactId>javaee-api</artifactId><version>${javaee.api.version}</version><scope>provided</scope></dependency>
</dependencies>
...
</project>
第11行:使用Java EE 7依赖关系以便能够在多个Java EE应用程序服务器(WildFly,Glassfish…)中部署同一应用程序而无需更改代码,这一点很重要 。
添加服务器端点
该端点可以接收有关下注比赛获胜者的消息(由match-id标识),并且还可以向对等客户端发送有关比赛过程和下注的所有信息。
例子7.服务器端点:MatchEndpoint.java
@ServerEndpoint(value = "/matches/{match-id}", decoders = { MessageDecoder.class }, encoders = { MatchMessageEncoder.class, BetMessageEncoder.class } )
public class MatchEndpoint {private static final Logger logger = Logger.getLogger("MatchEndpoint");/** All open WebSocket sessions */static Set<Session> peers = Collections.synchronizedSet(new HashSet<Session>());/** Handle number of bets by match */static Map<String, AtomicInteger> nbBetsByMatch = new ConcurrentHashMap<>();@Inject StarterService ejbService;@OnOpenpublic void openConnection(Session session,@PathParam("match-id") String matchId) { session.getUserProperties().put(matchId, true);peers.add(session);//Send live result for this matchsend(new MatchMessage(ejbService.getMatches().get(matchId)), matchId);}public static void send(MatchMessage msg, String matchId) {try {/* Send updates to all open WebSocket sessions for this match */for (Session session : queue) {if (Boolean.TRUE.equals(session.getUserProperties().get(matchId))){if (session.isOpen()){session.getBasicRemote().sendObject(msg); }}}} catch (IOException | EncodeException e) {logger.log(Level.INFO, e.toString());}}public static void sendBetMessage(Session session, BetMessage betMsg, String matchId){try {betMsg.setNbBets(nbBetsByMatch.get(matchId).get());session.getBasicRemote().sendObject(betMsg);logger.log(Level.INFO, "BetMsg Sent: {0}", betMsg.toString());} catch (IOException | EncodeException e) {logger.log(Level.SEVERE, e.toString());}}@OnMessagepublic void message(final Session session, BetMessage msg, @PathParam("match-id") String matchId) {session.getUserProperties().put("bet", msg.getWinner());//Send betMsg with bet countif (!nbBetsByMatch.containsKey(matchId)){nbBetsByMatch.put(matchId, new AtomicInteger());}nbBetsByMatch.get(matchId).incrementAndGet();sendBetMessages(null, matchId, false);}@OnClosepublic void closedConnection(Session session,@PathParam("match-id") String matchId) {if (session.getUserProperties().containsKey("bet")){nbBetsByMatch.get(matchId).decrementAndGet();sendBetMessages(null, matchId, false);}/* Remove this connection from the queue */peers.remove(session);}
...
}
第02行:由于应用程序上下文根为/ usopen ,因此访问此端点的URI的最终URL如下所示: ws://<host>:<port>/usopen/matches/1234
第03行: MessageDecoder将传入的JSON流(约等于赢家的赌注)转换为POJO BetMessage
第4行:这2个编码器增加了将MatchMessage POJO和BetMessage POJO转换为JSON格式消息的功能 第20行: @PathParam
批注允许提取WebSocket请求的一部分并将值(id匹配)作为方法的参数传递,对于每个匹配,可以管理多个客户端进行的多个匹配。 第34行:向连接的对等方发送有关比赛过程的消息。 多亏了MatchMessageEncoder对象,只需传递MatchMessage对象即可。 第55行:由于使用了MessageDecoder对象,因此可以处理接收到的有关赢家押注的消息 ,该方法的参数之一是BetMessage对象。
编码和解码消息
要对同位体之间交换的消息进行编码或解码,只需根据消息类型(文本,二进制)和处理方向(编码,解码)实现适当的接口,然后重新定义关联的方法。
在下面的示例中,它是MatchMessage POJO到JSON格式的编码器 。 用于执行此处理的API也是Java EE 7发行的新API: 用于JSON Processiong(JSON-P)的Java API
例子8.文本编码器:MatchMessageEncoder.java
public class MatchMessageEncoder implements Encoder.Text<MatchMessage> {@Overridepublic String encode(MatchMessage m) throws EncodeException {StringWriter swriter = new StringWriter();try (JsonWriter jsonWrite = Json.createWriter(swriter)) {JsonObjectBuilder builder = Json.createObjectBuilder();builder.add("match",Json.createObjectBuilder().add("serve", m.getMatch().getServe()).add("title", m.getMatch().getTitle())...}jsonWrite.writeObject(builder.build());}return swriter.toString();}
}
HTML5 Web客户端(单个匹配– index.html)
此应用程序的index.html页面加载websocket.js文件以实现Javascript WebSocket API,从而与Java Server Endpoint进行交互。 此页面仅处理一次匹配。
例子9.在websocket.js中实现的API Javascript
var wsUrl;
if (window.location.protocol == 'https:') { wsUrl = 'wss://' + window.location.host + ':8443/usopen/matches/1234';
} else {wsUrl = 'ws://' + window.location.host + ':8000/usopen/matches/1234';
}function createWebSocket(host) {if (!window.WebSocket) { ...} else {socket = new WebSocket(host); socket.onopen = function() {document.getElementById("m1-status").innerHTML = 'CONNECTED...';};socket.onclose = function() {document.getElementById("m1-status").innerHTML = 'FINISHED';};...socket.onmessage = function(msg) {try {console.log(data);var obj = JSON.parse(msg.data); if (obj.hasOwnProperty("match")){ //titlem1title.innerHTML = obj.match.title;// commentsm1comments.value = obj.match.comments;// serveif (obj.match.serve === "player1") {m1p1serve.innerHTML = "S";m1p2serve.innerHTML = "";} else {m1p1serve.innerHTML = "";m1p2serve.innerHTML = "S";}..}...} catch (exception) {data = msg.data;console.log(data);}}}
}
第02行:根据当前使用的HTTP协议(是否安全)选择适当的WebSocket协议
第09行:检查浏览器是否支持WebSocket API
第12行:创建WebSocket对象 第23行:尝试将对等服务器发送的JSON消息解析为onmessage
Event Handler调用的函数 第24行:检查接收到的对象类型(MatchMessage或BetMessage)以对DOM进行适当处理
要了解哪些浏览器与WebSocket API兼容, 请访问网站caniuse.com 。 如今,除了Android和Opera Mini浏览器外,所有最新版本的浏览器都兼容,二者仅占Web流量的3%。
AngularJS客户端(几个匹配项– live.html)
正如我们在本文开头所看到的,用于处理多次匹配的版本是在客户端使用AngularJS开发的。 因此,有4个JS文件包含:
-
app.js
:仅定义TennisApp角度应用程序 -
controllers.js
:TournamentCtrl控制器 -
directives.js
:- bet指令和bet.html模板以显示投注者数量和当前投注
-
services.js
:- WebSocketService:通过回调处理WebSocket生命周期
这篇文章不是有关AngularJS的教程,因为这是我第一个使用此框架的应用程序。 但这是在客户端处理多个匹配项的快速解决方案。 您可以阅读以下3篇文章,以更好地理解JS代码: 法语的 指令以及AngularJS 指令的重构和AngularJS WebSocket服务示例
Github上的源代码
您可以在https://github.com/mgreau/javaee7-websocket
Github上分叉此项目
该示例应用程序是基本的,可能会有很多改进,例如在其他条件上下注...
从技术上讲,一个有趣的功能是根据每个获胜点的坐标创建一种新的下注类型。 只需通过HTML5 Canvas API绘制背景并管理用户选择的坐标(例如获胜点),然后与获胜者的实际坐标进行比较即可。
构建和部署WAR
先决条件:
- JDK 7
- Apache Maven 3.0.4以上
- Java EE 7应用服务器(WildFly 8)
为了构建WAR,您只需执行以下Maven命令即可;
mvn clean package
如果您的应用服务器是WildFly,则可以使用以下命令快速部署WAR(必须启动WildFly):
mvn jboss-as:deploy
然后可以在以下位置找到usopen应用程序:
- http:// localhost:8080 / usopen /仅使用本机Javascript的简单情况
- http:// localhost:8080 / usopen / matches用于具有多次匹配和AngularJS的版本
WildFly 8使用其Web Server called Undertow
新Web Server called Undertow
来代替Tomcat。
我没有在Glassfish 4上测试此应用程序,但是由于我仅使用Java EE 7 API依赖项,因此它可以与相同的代码一起使用而不会出现问题。 让我知道您是否对GlassFish有麻烦。
基准测试:WebSocket VS REST
为了获得有关此新协议性能的一些指标,Arun Gupta开发了一个应用程序,该应用程序可以比较 WebSocket代码和REST代码执行的相同处理的执行时间 。
每个终结点(REST终结点和WebSocket终结点)都只是执行“回显”,因此它们仅返回接收到的流。 应用程序的Web界面允许您定义消息的大小以及在测试结束之前必须发送消息的次数。
如下所示的基准测试结果非常雄辩:
请求 | 总执行时间 REST端点 | 总执行时间 WebSocket端点 |
---|---|---|
发送10个1字节的消息 | 220毫秒(63毫秒)* | 7毫秒(29毫秒)* |
发送10个字节的100条消息 | 986毫秒(587毫秒)* | 57毫秒(74毫秒)* |
发送100个字节的1000条消息 | 10210毫秒(4 636毫秒)* | 179毫秒(288毫秒)* |
发送1000个字节的5000条消息 | 54449毫秒(18049毫秒)* | 1202毫秒(2862毫秒)* |
()*之间的值表示在具有默认配置的WildFly 8.0.0-beta1的笔记本电脑(MacBook Pro 2013 i5 – 8Go – 128SSD)上进行的相同测试。
关于WebSocket的参考
我特别推荐Arun Gupta的会议,这些会议使您可以在不到1小时的时间内发现并了解WebSocket技术以及适用于WebSocket的Java API。
有关更多高级信息,理想的是IETF,W3C和Java规范。
- RFC 6455:WebSocket协议 – IETF规范
- W3C:WebSocket API – W3C规范 (候选推荐)
- JSR 356:用于WebSocket协议的Java API – Java规范
- 采用JSR – JSR 356
- Java EE 7和WebSocket API – Arun Gupta的会议@ SF (从第46分钟开始)
- WebSocket和SSE入门 – 2013年Devoxx UK的Arun Gupta会议
本文的结构是基于英国2013 Devoxx会议。
结论
本文通过一个具体示例介绍了随Java EE 7一起发布的WebSocket协议,HTML5 WebSocket API和用于WebSocket的Java API 。 已经可以将WebSocket与Java框架(如Atmosphere)一起使用,但缺乏标准。
如今,所有标准都已完成或即将完成 ,这项新技术满足了特定需求,并且在性能方面很有前途。 要大量使用该协议,将需要在通常仅允许HTTP协议的企业中使用该协议。
翻译自: https://www.javacodegeeks.com/2014/01/java-ee-7-and-websocket-api-for-java-jsr-356-with-angularjs-on-wildfly.html