如何在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中可能吗?或者,有没有更好的解决方案?
谢谢。
经过一番测试和研究后,我发现了两种方法来避免长时间切换的情况。
- 匿名类方法(策略模式)
- 思考与注解
使用匿名类
匿名类方法是常态和下面的代码显示如何实现它。在这个例子中我使用了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,巨大的开关是最好的选择。为了提高代码的可读性和维护性,我建议使用编码器和解码器。然后,你的问题就变成了:我应该如何设计我的信息?
你的游戏看起来像拼字游戏。我不知道如何玩拼字游戏,所以让我们以钱打卡片游戏的例子。让我们假设你有三种类型的动作:
- 全球行动(连接表,离开表...)
- 货币措施(下赌注,赌裂,...)
- 卡操作(画卡等)
那么你的消息可能看起来像
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 ...
}
}
我更喜欢这种做法,因为:
- 这些消息的设计可以充分利用你的实体设计(如果你使用JPA后面)
- 作为消息不再是纯文本,但可以使用对象,枚举,枚举在这种开关情况下非常强大。使用相同的逻辑,但较少扩展,类抽象也可以有用
- ServerEndPoint类只处理通信。业务逻辑是从这个类中处理出来的,可以直接在Messages类或某些EJB中处理。由于这种拆分,代码维护要容易得多
- 红利:
@OnMessage
方法可以作为协议摘要读取,但不应在此处显示细节。每个case
只能包含几行。 - 我宁愿避免使用反思:它会毁了你的代码的可读性,在WebSocket的具体情况
为了进一步超越代码的可读性,维护和效率,可以使用SessionHandler拦截一些CDI事件如果这可以改善你的代码。我在this answer举了一个例子。如果您需要更高级的示例,则Oracle提供great tutorial about it。它可以帮助你改进你的代码。
我不认为这种方法将有助于我的情况。即使我这样做,每个动作都会有switch语句。我正在寻找一些方法来彻底消除开关和if语句。我将尝试获取有关散列表的一些信息,并查看是否有任何方法可以避免长时间切换场景。谢谢。 –
感谢您的反馈。如果您找到摆脱开关的方法,请随时在此分享。它可以帮助其他用户寻找相同的东西 – Al1
我已经发布了一个答案。你有什么意见?任何改进或建议? –
关于'@endpoint ...',你在谈论javax.xml.ws.Endpoint吗?这看起来很web服务,而不是websocket。 AFAIK,websocket是Java EE环境(JSR 356)中的唯一标准。其他方面,指的是可以通过[Reflection](https://stackoverflow.com/a/161005/4906586)完成方法,但恕我直言,长开关更容易处理 – Al1
它的websocket只。如果我没有错,像Spring这样的框架使用@endpoint注释来将API请求定向到特定的方法/类。我想,如果我能得到这个注解的底层实现,我可以建立类似的东西。例如,当客户端发送请求登录时,我可以将请求转发到特定的方法来执行任务,而不使用任何条件语句。 –
这是我正在谈论的注释。 @ServerEndpoint是如何工作的?http://docs.oracle.com/javaee/7/api/javax/websocket/server/ServerEndpoint.html –