本文来源于我在InfoQ中文站原创的文章,原文地址是:http://www.infoq.com/cn/news/2013/12/websocket-and-java
Bozhidar Bozhanov是Ontotext AD的高级软件工程师,拥有多年的从业经验,也是stackoverflow上的活跃用户。他精通于Java与Java技术栈,如Spring、JPA、JavaEE等,同时还是http://computoser.com与http://welshare.com的创始人。曾开发过爱立信的项目、保加利亚电子政务项目以及大型招聘平台等。近日Bozhidar撰文谈到了WebSocket与Java,并给出了相应的代码示例。Bozhidar在文中详细分析了WebSocket的原理、适用范围,以及如何通过Java来使用WebSocket。
WebSocket是一个很酷的新技术,可以实现浏览器与服务器之间实时、双向的通信,几乎没有任何额外的代价。我这里要做的事情就是提供一个非常简洁,但却内容丰富的概览,介绍如何开始使用这门技术。首先读者需要了解如下一些事情:
在给出具体的示例代码前,我首先来介绍一下Socket的生命周期,包括客户端的与服务器端的:
目前,还没有任何一个API或框架能够支持基于注解的路由。Java API支持基于注解的端点处理器,不过每个连接URL需要一个类来处理,通常情况下,你希望在单个连接上执行多个操作。也就是说,你连接到ws://yourserver.com/game/,然后想要传递“joinGame”和“leaveGame”等消息。类似地,服务器需要发送回多种类型的消息。我使用了枚举来实现这一点,枚举中包含了所有可能的动作与事件类型,然后使用switch来确定该调用哪一个。
因此,我决定为我的算法音乐作曲家开发一个简单的游戏。它使用了Spring API,感兴趣的读者可以看看这个介绍,这是我在公司所做的一次演讲。下面是一些示例代码:
@Component public class GameHandler extends WebSocketHandlerAdapter { private Map players = new ConcurrentHashMap<>(); private Map playerGames = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Player player = new Player(session); players.put(session.getId(), player); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { leaveGame(session.getId()); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception { try { GameMessage message = getMessage(textMessage); //deserializes the JSON payload switch(message.getAction()) { case INITIALIZE: initialize(message, session); break; case JOIN: join(message.getGameId(), message.getPlayerName(), session); break; case LEAVE: leave(session.getId()); break; case START: startGame(message); break; case ANSWER: answer(message, session.getId()); break; } } catch (Exception ex) { logger.error("Exception occurred while handling message", ex); } }
下面来看一个示例场景,其中服务器需要向客户端发送消息。这就好比一个玩家加入了游戏一样,这时其他所有玩家都会收到有新人加入的通知。系统中的中心类是Game,它拥有一个玩家列表。如你所见,一个Player包含了一个对WebSocket Session的引用。这样,当新的玩家加入时,下面的Game中的方法就会得到调用:
public boolean playerJoined(Player player) { for (Player otherPlayer : players.values()) { otherPlayer.playerJoined(player); } players.put(player.getSession().getId(), player); return true; }
player.playerJoined(..)会在连接之上发送一条消息,通知浏览器有新的玩家加入了:
public void playerJoined(Player player) { GameEvent event = new GameEvent(GameEventType.PLAYER_JOINED); event.setPlayerId(player.getSession().getId()); event.setPlayerName(player.getName()); try { session.sendMessage(new TextMessage(event.toJson())); } catch (IOException e) { new IllegalStateException(e); } }
从服务器向浏览器发送消息可能还需要一个调度job进行触发。
关键在于你维护了一个所有已连接的浏览器列表,这样就可以向回发送信息了。这个列表可以是个静态属性,不过对于单例的Spring Bean来说就没必要这么做了。
现在,有两个重要的方面需要我们注意——安全与认证。这是来自于Heroku的一篇很不错的文章,对安全与认证进行了详细的介绍。如果还有其他敏感信息,你就应该使用wss(Websocket over TLS)了。你还应该在服务器端与客户端验证输入,而不应该依赖于Origin头,因为攻击者可以轻而易举地骗过浏览器。
认证可以依赖于HTTP Session cookie,不过显而易见的是,有些人更喜欢实现自己的类cookie工作流,从而获取一个短暂的令牌,它可以用于执行认证操作。
WebSocket使得DDD变得更加自然。你不必再处理贫血对象了,你的对象有各自的状态以及对这些状态的操作。与之相关的是,WebSocket应用的测试变得更加容易了。
在开发WebSocket应用时还有不少需要注意的事情。值得注意的是,你没有必要在任何地方都使用WebSocket,我将其限定在需要“推送”的场景下。
总的来说,WebSocket是一个很棒、很有趣的技术,它非常有希望灭掉所有采用hack手段实现的推送模拟技术。