如何在Java中处理来自客户端的Websocket消息?

时间:2017-06-05 16:26:49

标签: java websocket annotations switch-statement netty

我正在使用Websocket在Java中开发客户端 - 服务器应用程序。目前,所有客户端消息都使用switch-case处理,如下所示。

@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);
}

服务器必须处理来自客户端的50多个不同请求,并导致超过50个case语句。在未来,我预计它会增长。有没有更好的方法来处理来自客户端的Websocket消息?或者,这通常是怎么做的?

我在某处读到了哈希表的使用,以避免通过映射到函数指针来实现长切换情况。这在Java中可行吗?或者,有没有更好的解决方案?

感谢。

2 个答案:

答案 0 :(得分:0)

如评论中所述,websockets缺点之一是您将自己指定通信协议。 AFAIK,巨大的开关是最好的选择。为了提高代码的可读性和维护性,我建议使用编码器和解码器。然后,您的问题变成:我应该如何设计我的消息?

你的游戏看起来像拼字游戏。我不知道如何玩拼字游戏,所以让我们以金钱卡片游戏为例。我们假设您有三种类型的操作:

  1. 全局行动(加入表,离开表......)
  2. 金钱行动(下注,分拆赌注......)
  3. 卡片动作(抽奖卡等)
  4. 然后您的消息可能看起来像

    @OnMessage

    正确定义解码器和编码器后,您的开关将更易于阅读并更易于维护。在我的项目中,代码看起来像这样:

    case

    我更喜欢这种方法,因为:

    1. 消息设计可以利用您的实体设计(如果您使用的是JPA)
    2. 由于消息不再是纯文本,而是对象,因此可以使用枚举,并且在这种切换情况下枚举非常强大。使用相同的逻辑但在较小的范围内,类抽象也很有用
    3. ServerEndPoint类仅处理通信。业务逻辑直接在Messages类或某些EJB中由此类处理。由于这种分裂,代码维护更容易
    4. Bonus:{{1}}方法可以作为协议的摘要阅读,但不应在此处显示详细信息。每个{{1}}只能包含几行。
    5. 我更喜欢避免使用Reflection:它会破坏你的代码可读性,在websocket的特定场景中
    6. 为了进一步超越代码可读性,维护和效率,如果可以改进代码,可以使用SessionHandler拦截某些CDI事件。我在this answer中举了一个例子。如果您需要更高级的示例,Oracle会提供great tutorial about it。它可能会帮助您改进代码。

答案 1 :(得分:0)

经过一些测试和研究后,我找到了两种避免长时间切换案例的方案。

  1. 匿名类方法(策略模式)
  2. 带注释的反思
  3. 使用匿名类

    匿名类方法是常态,下面的代码显示了如何实现它。我在这个例子中使用了Runnable。如果需要更多控制,请创建自定义界面。

    public class ClientMessageHandler {
    
        private final HashMap<String, Runnable> taskList = new HashMap<>();
    
        ClientMessageHandler() {
    
            this.populateTaskList();
        }
    
        private void populateTaskList() {
    
            // Populate the map with client request as key
           // and the task performing objects as value
    
            taskList.put("action1", new Runnable() {
                @Override
                public void run() {
                    // define the action to perform.
                }
            });
    
           //Populate map with all the tasks
        }
    
        public void onMessageReceived(JSONObject clientRequest) throws JSONException {
    
            Runnable taskToExecute = taskList.get(clientRequest.getString("task"));
    
            if (taskToExecute == null)
                return;
    
            taskToExecute.run();
        }
    }
    

    此方法的主要缺点是对象创建。比如说,我们有100个不同的任务要执行。这种Anonymous类方法将导致为单个客户端创建100个对象。对于我的应用程序来说,太多的对象创建是不可承受的,其中将有超过5,000个活动并发连接。看看这篇文章http://blogs.microsoft.co.il/gilf/2009/11/22/applying-strategy-pattern-instead-of-using-switch-statements/

    使用注释进行反射

    我真的很喜欢这种方法。我创建了一个自定义注释来表示方法执行的任务。对象创建没有任何开销,例如在策略模式方法中,因为任务由单个类执行。

    注释

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    
    public @interface TaskAnnotation {
        public String value();
    }
    

    下面给出的代码将客户端请求键映射到处理任务的方法。在这里,map被实例化并仅填充一次。

    public static final HashMap<String, Method> taskList = new HashMap<>();
    
    public static void main(String[] args) throws Exception {
    
        // Retrieves declared methods from ClientMessageHandler class 
    
        Method[] classMethods = ClientMessageHandler.class.getDeclaredMethods();
    
        for (Method method : classMethods) {            
            // We will iterate through the declared methods and look for
            // the methods annotated with our TaskAnnotation
    
            TaskAnnotation annot = method.getAnnotation(TaskAnnotation.class);
    
            if (annot != null) {                
                // if a method with TaskAnnotation is found, its annotation
                // value is mapped to that method.
    
                taskList.put(annot.value(), method);
            }
        }
    
        // Start server
    }
    

    最后,我们的ClientMessageHandler类如下所示

    public class ClientMessageHandler {
    
        public void onMessageReceived(JSONObject clientRequest) throws JSONException {
    
            // Retrieve the Method corresponding to the task from map
            Method method = taskList.get(clientRequest.getString("task"));
    
            if (method == null)
                return;
    
            try {
                // Invoke the Method for this object, if Method corresponding
                // to client request is found 
    
                method.invoke(this);
            } catch (IllegalAccessException | IllegalArgumentException
                    | InvocationTargetException e) {
                logger.error(e);
            }
        }
    
        @TaskAnnotation("task1")
        public void processTaskOne() {
    
        }
    
        @TaskAnnotation("task2")
        public void processTaskTwo() {
    
        }
    
        // Methods for different tasks, annotated with the corresponding
        // clientRequest code
    }
    

    这种方法的主要缺点是性能受到影响。与直接方法调用方法相比,这种方法很慢。此外,许多文章都建议远离反思,除非我们正在处理动态编程。

    阅读这些答案,了解有关反思What is reflection and why is it useful?

    的更多信息

    反思表现相关文章

    Faster alternatives to Java's reflection

    https://dzone.com/articles/the-performance-cost-of-reflection

    最终结果

    我继续在我的应用程序中使用switch语句,以避免任何性能损失。