繁体   English   中英

如何使用Java处理来自客户端的Websocket消息?

[英]How to process Websocket messages from client in Java?

我正在使用Websocket用Java开发客户端服务器应用程序。 当前,所有客户端消息都使用切换条件进行处理,如下所示。

@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,巨大的开关是最好的选择。 为了提高代码的可读性和维护性,我建议使用编码器和解码器。 然后,您的问题变成:我应该如何设计我的消息?

您的游戏看起来像拼字游戏。 我不知道如何玩Scrabble,所以让我们以带钱的纸牌游戏为例。 假设您有三种类型的动作:

  1. 全局动作(联接表,离开表...)
  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. 由于消息不再是纯文本,而是对象,因此可以使用枚举,并且在这种切换情况下,枚举非常强大。 使用相同的逻辑,但扩展程度较小,类抽象也可能有用
  3. ServerEndPoint类仅处理通信。 业务逻辑是在此类之外直接在Messages类或某些EJB中处理的。 由于此拆分,代码维护更加容易
  4. 奖励: @OnMessage方法可以作为协议摘要来阅读,但详细信息不应在此处显示。 每个case只能包含几行。
  5. 我更喜欢避免使用Reflection:在websocket的特定情况下,它将破坏代码的可读性

为了超越代码的可读性,维护性和效率,如果可以改善代码,则可以使用SessionHandler来拦截某些CDI事件。 我在这个答案中举了一个例子。 如果您需要一个更高级的示例,Oracle会提供一个很棒的教程 它可能会帮助您改善代码。

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

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

使用匿名类

匿名类方法是规范,下面的代码显示了如何实现它。 在此示例中,我使用了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个不同的任务要执行。 这种匿名类方法将导致为单个客户端创建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();
}

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

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
}

这种方法的主要缺点是性能下降。 与直接方法调用方法相比,此方法速度较慢。 此外,除非我们正在处理动态编程,否则许多文章建议不要使用反射。

阅读这些答案,以了解有关反射的更多信息。 什么是反射,为什么有用?

反射性能相关文章

Java反射的更快替代品

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

最后结果

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM