websocket api
建议为支持Java [3]中的WebSocket协议而最近发布的JSR 356 Java API for WebSocket [3],已经随Servlet容器的最新版本一起提供,例如Tomcat,Wildfly,Jetty和GlassFish。 尽管这是朝着正确方向迈出的真正良好一步,但在许多领域中,API要么功能不足,要么可以从重大改进中受益。 在最初发表在《 JAX杂志》上的这篇文章中,让·弗朗西斯·阿坎德(Jean Francis Arcand)向我们介绍了其中的一些。在使用API之前,想要使用WebSocket协议的应用程序需要使用专有的WebSocket API,以牺牲可移植性,或者使用诸如Atmosphere [2]之类的框架,该框架抽象了一个API层,允许跨Servlet容器的WebSocket应用程序,以及诸如Vert.x,播放! 和Netty。
另一个重要的区别是,只有在浏览器支持WebSocket协议时,JSR 356 WebSocket应用程序才能运行。 如果没有浏览器,则该应用程序将无法运行。 这对于Atmosphere来说不是问题-框架能够透明地回退到HTTP等其他传输方式,而无需对应用程序进行任何更改。 请注意,如果您打算使用纯JSR 356 websocket应用程序进行生产,在Web上直播,则许多浏览器将无法与您的应用程序进行通信。 让我们使用一个聊天应用程序来比较JSR 356和Atmosphere的使用,并查看JSR从哪些改进中可以受益。 我们将通过编写最简单的端点开始比较。 在这里假设您对JSR 356注释ṡ有所了解。 首先,让我们编写我们的骨架类。 使用JSR 356,我们的端点将看起来像:
Listing 1
@ServerEndpoint(value = "/chat")
public class ChatRoom {
@OnOpen
public void ready(Session s) {
s.getAsyncRemote().sendText("You are connected");
}
@OnMessage
public void message(String message, Session session) {
// Sent the message back to all connected users.
for(Session s: session.getOpenSessions()) {
if (s.isOpen()) s.getAsyncRemote().sendText(message);
}
}
@OnClose
public void close(CloseReason reason) {
}
}
End
使用Atmosphere,我们的端点将类似于:
Listing 2
@ManagedService(path="/chat")
public class ChatRoom {
@Ready
public String ready(AtmosphereResource r) {
return "You are connected";
}
@Message
public String message(String message) {
// Sent the message back to all connected users.
return message;
}
@Disconnect
public void close(AtmosphereResourceEvent event) {
}
}
End
在编码方面,JSR 356和Atmosphere之间没有太大区别,不同之处在于,对于Atmosphere,您不需要实现写操作,只需要做的就是设置带注释的方法的返回值。
但是就服务质量而言,@ ManagedService注释比@ServerEndpoint具有更多的功能。
@ManagedService批注透明地启动,尽管在后台运行网络上的应用程序需要一些功能。
WebSockets的最大问题之一是代理。
如果不支持该协议的代理或配置了超时的代理,则最终将终止websocket连接。
在这种情况下,您需要确保客户端和服务器代码都可以透明地重新握手其状态,以便您的应用程序在断开连接后仍然可以生存。
对于JSR 356,这意味着我们需要在服务器端进行更多工作以防止恶意代理。
一种幼稚的方法是添加:
Listing 3
@ServerEndpoint(
value = "/chat"
)
public class ChatRoom {
// Warning, the Scheduler will never be shutdown. Add a ShutdownHook.
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
private final AtomicBoolean isStated = new AtomicBoolean();
@OnOpen
public void ready(final Session session) {
session.getAsyncRemote().sendText("You are connected");
aliveSession.offer(session);
if (!isStated.getAndSet(true)) {
scheduler.schedule(new Runnable() {
@Override
public void run() {
// Send heartbeat every 10 seconds
for(Session s: session.getOpenSessions()) {
if (s.isOpen()) s.getAsyncRemote().sendText(" ");
}
}
}, 10, TimeUnit.SECONDS);
}
}
@OnMessage
public void message(String message, Session session) {
for(Session s: session.getOpenSessions()) {
if (s.isOpen()) s.getAsyncRemote().sendText(message);
}
}
@OnClose
public void close(Session session, CloseReason reason) {
}
}
End
缺少的另一个关键功能是当Websocket连接突然被代理或浏览器与服务器之间的任何内容突然关闭时。
例如,移动浏览器可能经常遭受意外断开的困扰。
到浏览器重新连接时,消息可能已经发布,丢失的消息将永远不会发送到浏览器。
@ManagedService批注透明地处理这种情况,确保缓存消息并在浏览器重新连接时将其发送回。
使用JSR 356,缺少对这种情况的支持,因此您需要编写自己的脚本。
这远非易事,因为您需要跟踪浏览器并确保最终将传递所有消息。
一个非常本地化的实现可能是:
Listing 4
@ServerEndpoint(
value = "/chat"
)
public class ChatRoom {
private final ConcurrentLinkedQueue<Session> aliveSession = new ConcurrentLinkedQueue<Session>();
// Warning, the Scheduler will never be shutdown
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
private final AtomicBoolean isStated = new AtomicBoolean();
private final ConcurrentHashMap<String, List<String>> messages = new ConcurrentHashMap<String, List<String>>();
@OnOpen
public void ready(final Session session) {
session.getAsyncRemote().sendText("You are connected");
aliveSession.offer(session);
checkForCachedMessage(session);
if (!isStated.getAndSet(true)) {
scheduler.schedule(new Runnable() {
@Override
public void run() {
// Send heartbeat every 10 seconds
for(Session s: session.getOpenSessions()) {
if (s.isOpen()) s.getAsyncRemote().sendText(" ");
}
}
}, 10, TimeUnit.SECONDS);
}
}
private void checkForCachedMessage(Session session) {
// Unique Token
String clientUniqueToken = parseQueryStringForUniqueId(session.getQueryString());
List<String> cachedMessages = messages.get(clientUniqueToken);
for (String m : cachedMessages) {
if (s.isOpen()) session.getAsyncRemote().sendText(m);
}
}
private String parseQueryStringForUniqueId(String queryString) {
...
}
@OnMessage
public void message(String message, Session session) {
for(Session s: session.getOpenSessions()) {
if (s.isOpen()) s.getAsyncRemote().sendText(message);
}
}
@OnClose
public void close(Session session, CloseReason reason) {
aliveSession.remove(session);
startCachingMessageFor(session);
}
private void startCachingMessageFor(Session session) {
...
}
}
End
并非不可能实现,但是请考虑使用@ManagedService批注时透明地获得该功能。
JSR 356缺少的另一个功能是,如果应用程序发送的消息大于基础服务器的I / O缓冲区,则有时可以将消息分成两个块传递给客户端。
使用文本消息时,这可能不是问题,但是例如,如果使用JSON,则尝试在客户端使用以下方式解码时,如果接收到两个大块的JSON消息,则会产生错误:
Listing 5
websocket.onmessage(message) {
// Will fail if the browser receives incomplete message.
window.JSON.parse(message);
}
End
您可以在该函数周围添加难看的try / catch,将下一条消息追加到已接收的消息中,直到解析操作成功为止,或者可以在消息本身中编码预期消息的长度。
它需要客户端和服务器组件上的代码。
使用Atmosphere,您可以免费透明地内置该功能。
这意味着您在定义时一定会收到完整的消息(有关更多详细信息,请参见客户端的下一部分)。
Listing 6
atmosphereSocket.onMessage(response) {
var message = response.responseBody;
// Will always work
window.JSON.parse(message);
}
End
谈到JSON,让我们停止使用String,而是添加一些Encoder和Decoder。
例如,让我们定义以下Message类:
Listing 7
public class Message {
private String message;
public Message(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
End
JSR 356定义了消息的编码器和解码器的概念。
例如,假设浏览器正在发送JSON消息。
为了处理这些消息,让我们使用Jackson编写一个JSR 356解码器:
Listing 8
public class MessageDecoder implements Decoder.Text<Message> {
private ObjectMapper mapper = new ObjectMapper();
@Override
public Message decode(String s) {
return mapper.readValue(s, Message.class);;
}
@Override
public boolean willDecode(String s) {
return true;
}
@Override
public void init(EndpointConfig config) {
}
@Override
public void destroy() {
}
}
with Atmosphere,
public class ChatDecoder implements Decoder<String, Message> {
private ObjectMapper mapper = new ObjectMapper();
@Override
public Message decode(String s) {
return mapper.readValue(s, Message.class);
}
}
End
这里的主要区别在于,与JSR 356相比,您可以基于对象的String内容决定不对其进行解码。
在Atmosphere中,我们改为支持对解码器的链接,例如,将解码器解码的对象作为另一个解码器的输入。
这样,就不必像对JSR 356一样多次解析消息。要对Message类进行编码,请使用JSR 356公开Encoder:
Listing 9
public class ChatEncoder implements Encoder.Text<Message> {
private ObjectMapper mapper = new ObjectMapper();
@Override
public String encode(Message s) {
return mapper.writeValueAsString(s);
}
@Override
public void init(EndpointConfig config) {
}
@Override
public void destroy() {
}
}
with Atmosphere,
public class ChatEncoder implements Encoder<Message, String> {
private ObjectMapper mapper = new ObjectMapper();
@Override
public String encode(Message s) {
return mapper.writeValueAsString(s);
}
}
End
这里的主要区别在于,与Atmosphere的Decoder一样,您可以链接Encoder并将编码的对象传递给下一个Encoder。
使用JSR 356,您只能编码为String或PrintWriter。
有了Atmosphere,您就没有这种限制。
放在一起,JSR 356实现看起来像:
Listing 10
@ServerEndpoint(
value = "/chat",
encoders = {ChatEncoder.class},
decoders = {ChatDecoder.class}
)
public class ChatRoom {
@OnOpen
public void ready(final Session session) {
session.getAsyncRemote().sendText("You are connected");
}
@OnMessage
public void message(Message message, Session session) {
for(Session s: session.getOpenSessions()) {
if (s.isOpen()) s.getAsyncRemote().sendText(message);
}
}
@OnClose
public void close(Session session, CloseReason reason) {
}
}
End
有了大气,
Listing 11
@ManagedService(path="/chat")
public class BasicChatRoom {
@Ready
public String ready(AtmosphereResource r) {
return "You are connected";
}
@Message(encoders = {ChatEncoder.class}, decoders = {ChatDecoder.class})
public String message(String message) {
return message;
}
@Disconnect
public void close(AtmosphereResourceEvent event) {
}
}
End
这里的主要区别在于Atmosphere,编码器/解码器可以应用于方法级别,而对于JSR 356,则仅适用于类级别。
浏览器客户端API现在让我们探索WebSocket应用程序的客户端。
JSR 356不附带Javascript客户端库,因此我们将使用大多数浏览器支持的W3C接口。
对于我们的简单应用程序,我们使用:
Listing 11
var websocket = new WebSocket(uri);
websocket.onopen = function(evt) {
...
};
websocket.onmessage = function(evt) {
...
};
websocket.onclose = function(evt) {
...
};
websocket.onerror = function(evt) {
...
};
websocket.send(...);
End
大气:
Listing 12
var request = { url: url
transport: 'websocket',
fallbacktransport: 'long-polling'};
request.onOpen = function (response) {
...
};
request.onMessage = function (response) {
...
};
request.onClose = function (response) {
...
};
request.onError = function (response) {
...
};
socket = atmosphere.subscribe(request);
socket.push(...);
End
该API看起来很相似,但有一个主要区别:如果浏览器不支持WebSocket API,则Atmosphere将透明地回退,而改用长轮询。
但是,并不是所有的应用程序都需要使用后备传输,因此我们不要专注于此。
Atmosphere客户端Javascript具有不错的功能,例如,与服务器一样,如果连接中断,则大多数情况下客户端需要重新连接。
使用W3C API,您必须在onclose或onerror函数内部实现逻辑。
没什么复杂的,但是您必须照顾好它。
由于服务器可能无法自动使用,因此您可能需要暂停五秒钟后尝试重新连接。
在尝试重新连接时设置限制,等等。所有这些代码都需要实现。
有了Atmosphere,它已经内置在Javascript中,因此您所需要做的就是:
Listing 13
var request = { url: url
reconnectInterval : 5000 // 5 seconds before reconnecting
maxReconnectOnClose : 5 // Stop after 5 unsuccesful reconnect
transport: 'websocket',
fallbacktransport: 'long-polling'};
request.onOpen = function (response) {
...
};
request.onReOpen = function (response) {
// Invoked when the client successfully reconnect
};
request.onReconnect = function (response) {
// Invoked before trying to reconnect.
};
request.onMessage = function (response) {
...
};
request.onClose = function (response) {
...
};
request.onError = function (response) {
...
};
socket = atmosphere.subscribe(request);
socket.push(...);
End
同样,您可以使用W3C API来执行此操作,但这将需要大量代码。
如上一节所述,在尝试使用JSON解析器解析之前,您可能还必须确保websocket.onmessage(data)包含服务器发送的完整响应。
JSR 356也附带了一个客户端API,但是我不会在本文中进行详细介绍。
Atmosphere还具有一个名为wAsync [4]的客户端API,它支持websocket以及其他传输方式,例如服务器端事件,长轮询等。
截至2013年11月,Tomcat 7/8(测试版),Jetty 9.1.0(测试版),GlassFish 4,Wildfly / Undertow(测试版)和Resin 4支持JSR356。Atmosphere支持本地的WebSocket Tomcat 7/8,Jetty 7 / 8/9,GlassFish 3/4,WebLogic 12,JBoss 7.1.x,Netty 3.x,Vert.x 2.x和Play!
Framework 2.x及更高版本。
使用Atmosphere,您的应用程序是可移植的,并且您可以使用已经准备就绪的服务器,而无需等待支持JSR 356的更新版本。
JSR 356是在Java领域采用WebSocket的重要一步,但缺少重要功能,例如不支持websocket协议的浏览器的回退传输,消息缓存以防止消息丢失,适当的生命周期重新连接以及在生产环境中部署服务器得到广泛采用。
大气已经准备好进行生产,几乎可以部署在任何地方。
它已经支持消息缓存,适当的重新连接生命周期,回退传输等功能。
Atmosphere可以在支持JSR 356的服务器以及完善的服务器和框架上运行。
更重要的是,Atmosphere附带了可以解决许多问题的客户端Javascript。
最后,大多数现有框架都支持Atmosphere,并且可以向PrimeFaces,RestEasy,Wicket,Jersey等框架透明地添加WebSocket支持。这意味着您可以编写一个良好的旧Servlet应用程序,该应用程序透明地在WebSocket协议之上运行。
[1] http://jcp.org/en/jsr/detail?id=356 [2] http://github.com/Atmosphere/atmosphere [3] http://tools.ietf.org/html/rfc6455 [4] http://github.com/Atmosphere/wasync
翻译自: https://jaxenter.com/jsr-356-java-api-for-websocket-or-atmosphere-107316.html
websocket api