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

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

问题描述:

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

@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中可能吗?或者,有没有更好的解决方案?

谢谢。

+0

关于'@endpoint ...',你在谈论javax.xml.ws.Endpoint吗?这看起来很web服务,而不是websocket。 AFAIK,websocket是Java EE环境(JSR 356)中的唯一标准。其他方面,指的是可以通过[Reflection](https://stackoverflow.com/a/161005/4906586)完成方法,但恕我直言,长开关更容易处理 – Al1

+0

它的websocket只。如果我没有错,像Spring这样的框架使用@endpoint注释来将API请求定向到特定的方法/类。我想,如果我能得到这个注解的底层实现,我可以建立类似的东西。例如,当客户端发送请求登录时,我可以将请求转发到特定的方法来执行任务,而不使用任何条件语句。 –

+0

这是我正在谈论的注释。 @ServerEndpoint是如何工作的?http://docs.oracle.com/javaee/7/api/javax/websocket/server/ServerEndpoint.html –

经过一番测试和研究后,我发现了两种方法来避免长时间切换的情况。

  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/

反思与注释

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

注释

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

主要缺点是对性能的影响。与直接方法调用方法相比,此方法速度较慢。此外,许多文章都建议远离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语句以避免任何性能问题。

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

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

  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{ 
    // ... 
} 

一旦你的解码器和编码器正确定义,您SWI tch会更容易阅读和更容易维护。在我的项目,代码应该是这样的:

@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. 我宁愿避免使用反思:它会毁了你的代码的可读性,在WebSocket的具体情况

为了进一步超越代码的可读性,维护和效率,可以使用SessionHandler拦截一些CDI事件如果这可以改善你的代码。我在this answer举了一个例子。如果您需要更高级的示例,则Oracle提供great tutorial about it。它可以帮助你改进你的代码。

+0

我不认为这种方法将有助于我的情况。即使我这样做,每个动作都会有switch语句。我正在寻找一些方法来彻底消除开关和if语句。我将尝试获取有关散列表的一些信息,并查看是否有任何方法可以避免长时间切换场景。谢谢。 –

+0

感谢您的反馈。如果您找到摆脱开关的方法,请随时在此分享。它可以帮助其他用户寻找相同的东西 – Al1

+0

我已经发布了一个答案。你有什么意见?任何改进或建议? –