WebSocket 支持

    这个简单的聊天服务应用 展示了如何使用 ActFramework 实现一个群聊服务:

    前端代码大致是:

    1. var socket;
    2. if (window.WebSocket) {
    3. socket = new WebSocket("ws://localhost:5460/msg");
    4. socket.onmessage = function (event) {
    5. var home = document.getElementById('chat');
    6. home.innerHTML = home.innerHTML + event.data + "<br />";
    7. };
    8. } else {
    9. alert("Your browser does not support Websockets. (Use Chrome)");
    10. }
    11. function send(message) {
    12. if (!window.WebSocket) {
    13. return false;
    14. }
    15. if (socket.readyState == WebSocket.OPEN) {
    16. socket.send(message);
    17. } else {
    18. alert("The socket is not open.");
    19. }
    20. }
    21. </script>

    介绍 II - 一个 Echo 服务

    使用 ActFramework 提供的 WebSocketContext.sendToSelf(String) API 来实现一个 Echo 服务:

    1. @WsAction("echo")
    2. public void onMessage(String message, WebSocketContext context) {
    3. context.sendToSelf(message);
    4. }

    就像普通的 HTTP 消息响应器使用 @GetAction, @PostAction 等, 我们使用 @WsAction 注解来标识一个 WebSocket 的消息服务端点:

    1. @WsAction("/ws/msg")
    2. context.sendToPeers(messageText);
    3. }
    • String messsageText - 任何通过 websocket 连接发送的文字消息
    • WebSocketContext context - 框架注入的 WebSocketContext 对象

    该方法使用 WebSocketContext::sendToPeers(String) API 向所有连接到 /ws/msg 的 websocket 发送消息。很明显这是一个聊天室应用。

    绑定复杂类型

    浏览器可以向服务器发送 JSON 编码的复杂类型,比如:

    在服务器端可以定义一个 Message 类:

    1. @Data
    2. public class Message implements SimpleBean {
    3. public String text;
    4. public String room;
    5. public String from;
    6. public String nickname;
    7. }

    然后我们可以直接在消息响应器中使用 Message 类作为参数:

    1. @WsAction("/chat")
    2. public void handlePojoMessage(Message pojo, WebSocketContext context) {
    3. }

    当浏览器中执行 new websocket('ws://myhost/myurl') 语句的时候,将发出一个 HTTP GET 请求到 /myurl,undertow 会升级 HTTP 协议并建立一个 websocket 连接. ActFramework 通过事件分发机制支持应用设置连接建立时的逻辑:

    1. private static final AtomicInteger CONN_COUNTER = new AtomicInteger(0);
    2. @OnEvent
    3. public static void handleConnection(WebSocketConnectEvent event) {
    4. CONN_COUNTER.incrementAndGet();
    5. final WebSocketContext context = event.source();
    6. context.tag(Room.MAIN);
    7. }

    上面的事件响应代码会在任何一个 websocket 连接建立时触发。应用会增加连接计数器,然后通过 WebSocketContext.tag(String label) API 将新连接打上 main room 的标签。这样所有发送到 main room 的消息会被分发到新的连接。按照我们上面的代码,客户端发送到 main room 的消息可以是:

    开发人员也可以针对连接断开事件编码:

    1. public static void handleConnectionClose(WebSocketCloseEvent event) {
    2. final WebSocketContext context = event.source();
    3. CONN_COUNTER.decrementAndGet();
    4. }

    注意 任何 websocket 连接在建立或者断开的时候都会触发相应的 WebSocketConnectEventWebSocketCloseEvent 事件,而和具体的 websocket 服务 URL 无关。假如应用有多个 websocket 服务端点, 需要处理特定 URL 连接建立断开事件, 应用必须检查连接的 URL:

    1. public static void handleConnectionClose(WebSocketCloseEvent event) {
    2. final WebSocketContext context = event.source();
    3. if ("/ws/endpoint1".equals(context.url())) {
    4. System.out.println("endpoint1 closed");
    5. }
    6. }

    发送消息到特定用户

    1. @OnEvent
    2. public static void handleNewsUpdate(NewsUpdateEvent event, User.Dao userDao, WebSocketConnectionManager connectionManager) {
    3. NewsUpdate update = event.source();
    4. List<User> users = userDao.findBySubscription(update.topic());
    5. for (User user: users) {
    6. connectionManager.sendJsonToUser(update, user.username());