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

How to process Websocket messages from client in Java?

我正在 Java 使用 Websocket 开发客户端-服务器应用程序。目前,所有客户端消息都使用 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 中可能吗?或者,有没有更好的解决方案?

谢谢。

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

您的游戏看起来像拼字游戏。我不会玩拼字游戏,所以让我们以纸牌游戏为例。假设您有三种类型的操作:

  1. 全球行动(加入table,离开table ...)
  2. 金钱行动(下注、分注……)
  3. 卡片行动(抽牌等)

那么您的消息可以看起来像

public class AbstractAction{
    // not relevant for global action but let's put that aside for the example
    public abstract void endTurn();
}

public class GlobalAction{
    // ...
}

public class MoneyAction{

    enum Action{
        PLACE_BET, PLACE_MAX_BET, SPLIT_BET, ...;
    }

    private MoneyAction.Action action;
    // ...
}

public class CardAction{
    // ...
}

正确定义解码器和编码器后,您的开关将更易于阅读和维护。在我的项目中,代码如下所示:

@ServerEndPoint(value = ..., encoders = {...}, decoders = {...})
public class ServerEndPoint{

    @OnOpen
    public void onOpen(Session session){
        // ...
    }

    @OnClose
    public void onClose(Session session){
        // ...
    }

    @OnMessage
    public void onMessage(Session session, AbstractAction action){

        // I'm checking the class here but you
        // can use different check such as a 
        // specific attribute 

        if(action instanceof GlobalAction){
            // do some stuff
        }

        else if (action instanceof CardAction){
            // do some stuff
        }

        else if (action instance of MoneyAction){
            MoneyAction moneyAction = (MoneyAction) action;
            switch(moneyAction.getAction()){
                case PLACE_BET:
                    double betValue = moneyAction.getValue();
                    // do some stuff here
                    break;
                case SPLIT_BET:
                    doSomeVeryComplexStuff(moneyAction);
                    break;
            }
        }

    }


    private void doSomeVeryComplexStuff(MoneyAction moneyAction){
        // ... do something very complex ...
    }

}

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

  1. 消息设计可以利用您的实体设计(如果您在后面使用 JPA)
  2. 由于消息不再是纯文本而是对象,因此可以使用枚举,并且枚举在这种 switch-case 情况下非常强大。使用相同的逻辑,但在较小的扩展中,class 抽象也很有用
  3. ServerEndPoint class 仅处理通信。业务逻辑由此 class 直接在 Messages classes 中或在某些 EJB 中处理。因为这样拆分,代码维护就容易多了
  4. 奖励:@OnMessage 方法可以作为协议的摘要阅读,但不应在此处显示详细信息。每个 case 只能包含几行。
  5. 我更喜欢避免使用反射:它会破坏你的代码可读性,在 websocket 的特定场景中

为了进一步超越代码的可读性、维护性和效率,如果这可以改进您的代码,您可以使用 SessionHandler 来拦截一些 CDI 事件。我在. If you need a more advanced example, Oracle provides a great tutorial about it中举了一个例子。它可能会帮助您改进代码。

经过一些测试和研究,我找到了两个替代方案来避免长 switch case 场景。

  1. 匿名class方法(策略模式)
  2. 带注释的反射

使用匿名 Class

匿名class 方法是规范,下面的代码显示了如何实现它。我在这个例子中使用了 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 项不同的任务要执行。这种匿名 class 方法将导致为单个客户端创建 100 个对象。我的应用程序无法承受创建过多的对象,其中将有 5,000 多个活动并发连接。看看这篇文章http://blogs.microsoft.co.il/gilf/2009/11/22/applying-strategy-pattern-instead-of-using-switch-statements/

带注释的反射

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

执行的

注释

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)

public @interface TaskAnnotation {
    public String value();
}

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

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 class 如下所示

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
}

这种方法的主要缺点是性能下降。与直接方法调用方法相比,这种方法速度较慢。而且,很多文章都建议远离Reflection,除非我们处理的是动态规划。

阅读这些答案以了解有关反射的更多信息What is reflection and why is it useful?

反射性能相关文章

Faster alternatives to Java's reflection

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

最终结果

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