• Akka边学边写(4)-- MiniRPG


    前面几篇文章用Akka写了HelloWorldEchoServer,为了更进一步学习Akka,本文将会实现一个非常小的RPG游戏server:MiniRPG

    游戏逻辑

    由于是迷你RPG,所以逻辑非常easy。server能够处理四种操作:创建玩家、给玩家加经验、升级、查询玩家信息。以下是Player类的代码(Getters和Setters省略):

    public class Player {
        
        private int id;
        private String name;
        private int exp;
        private int level;
    
        // Getters & Setters ...
        
        public void addExp(int val) {
            exp += val;
        }
        
        public void levelUp() {
            if (exp > 100) {
                exp -= 100;
                level++;
            }
        }
        
    }
    

    消息协议

    MiniRPG底层使用TCP协议,消息使用JSON格式。完整的消息格式例如以下图所看到的:


    前八个字节能够觉得是消息头。当中前四个字节是消息ID。后四个字节是JSON字符串长度。其余字节是消息体,也就是UTF8格式编码的JSON字符串。

    消息接口

    MiniRPG设计了三个接口来表示游戏消息,这三个接口都是Marker接口。里面未定义不论什么方法。例如以下图所看到的:

    MsgRegistry

    MiniRPG使用GSON来编码和解码JSON字符串,为了把JSON解析为对应的消息对象。须要一个消息ID和class之间的映射关系。MsgRegistry类便是要建立起这样一个映射关系,以下是它的完整代码:

    public class MsgRegistry {
        
        private static final Map<Integer, Class<?

    >> msgById = new HashMap<>(); private static final Map<Class<?>, Integer> idByMsg = new HashMap<>(); static { register(1, CreatePlayerRequest.class); register(2, CreatePlayerResponse.class); register(3, AddExpRequest.class); register(4, AddExpResponse.class); register(5, LevelUpRequest.class); register(6, LevelUpResponse.class); register(7, GetPlayerInfoRequest.class); register(8, GetPlayerInfoResponse.class); } private static void register(int msgId, Class<?> msgClass) { msgById.put(msgId, msgClass); idByMsg.put(msgClass, msgId); } public static Class<?> getMsgClass(int msgId) { return msgById.get(msgId); } public static int getMsgId(Class<?> msgClass) { return idByMsg.get(msgClass); } public static int getMsgId(Object msg) { return getMsgId(msg.getClass()); } }

    serverActor系统设计

    MiniRPGserver的Actor系统例如以下图所看到的:

    TcpServer负责监听TCP连接。连接建立之后。交给Codec处理。Codec将收到的字节编码成消息对象。然后交给MsgHandler处理。对于每条请求消息。MsgHandler都会产生一条响应消息。响应消息被Codec编码之后发送到client。

    以下具体介绍整个Actor系统是怎样实现的。

    TcpServer

    TcpServer是一个UntypedActor,实例化TcpServer时,我们把MsgHandler引用传给它:

    public class TcpServer extends UntypedActor {
        
        private final ActorRef msgHandler;
        
        public TcpServer(ActorRef msgHandler) {
            this.msgHandler = msgHandler;
        }
    
    }
    TcpServer仅仅关心四种消息。以下是onReceive()方法实现:

        @Override
        public void onReceive(Object msg) throws Exception {
            if (msg instanceof Integer) {
                final int port = (Integer) msg;
                startServer(port);
            } else if (msg instanceof Bound) {
                getSender().tell(msg, getSelf());
            } else if (msg instanceof CommandFailed) {
                getContext().stop(getSelf());
            } else if (msg instanceof Connected) {
                final Connected conn = (Connected) msg;
                getSender().tell(conn, getSelf());
                registerCodec(getSender());
            }
        }

    Integer消息通知TcpServer绑定到某个port,准备接收client连接。假设收到Bound消息,则port绑定成功。server正常启动。假设是CommandFailed消息。则server启动失败:

        private void startServer(int port) {
            final InetSocketAddress endpoint = new InetSocketAddress("localhost", port);
            final Object bindCmd = TcpMessage.bind(getSelf(), endpoint, 100);
            Tcp.get(getContext().system()).getManager()
                    .tell(bindCmd, getSelf());
        }
    

    假设是Connected消息。说明有client连接已经建立,TcpServer创建一个子Actor(也就是Codec)来处理client连接:

        private void registerCodec(ActorRef connection) {
            final Props codecProps = Props.create(MsgCodec.class, connection, msgHandler);
            final ActorRef codec = getContext().actorOf(codecProps);
            connection.tell(TcpMessage.register(codec), getSelf());
        }

    MsgCodec

    MsgCodec主要负责消息的编码和解码。为此,MsgCodec内部使用了一个ByteString来缓存接收到的字节:

    public class MsgCodec extends UntypedActor {
        
        private static final Gson GSON = new Gson();
        
        private final ActorRef connection;
        private final ActorRef msgHandler;
        private ByteString buf = ByteString.empty();
    
        public MsgCodec(ActorRef connection, ActorRef msgHandler) {
            this.connection = connection;
            this.msgHandler = msgHandler;
        }
    
    }
    
    假设MsgCodec收到的是Received消息,说明有数据到达。MsgCodec尝试解码出一个消息对象。假设收到的是GameMessage消息。MsgCodec将其编码为byte[]然后发送给client。

    假设收到的是ConnectionClosed,说明连接已经断开了:

        @Override
        public void onReceive(Object msg) throws Exception {
            if (msg instanceof Received) {
                final ByteString data = ((Received) msg).data();
                buf = buf.concat(data);
                decodeMsg();
            } else if (msg instanceof ConnectionClosed) {
                getContext().stop(getSelf());
            } else if (msg instanceof GameMessage) {
                final ByteString data = encodeMsg(msg);
                connection.tell(TcpMessage.write(data), getSelf());
            }
        }
    每当有数据到达时。decodeMsg()方法都会被调用。decodeMsg()先确定能否够把消息头解码出来,假设不能,就继续等待很多其它的字节到达。

    假设消息头完整到达,decodeMsg()就能够知道消息体的长度,然后等到消息体完整到达。之后依据消息ID和JSON字符串解码消息对象。然后通知msgHandler:

        private void decodeMsg() {
            while (buf.length() > 8) {
                final ByteIterator it = buf.iterator();
                final int msgId = it.getInt(ByteOrder.BIG_ENDIAN);
                final int jsonLength = it.getInt(ByteOrder.BIG_ENDIAN);
                
                if (buf.length() >= 8 + jsonLength) {
                    final Object msg = decodeMsg(msgId, buf.slice(8, 8 + jsonLength));
                    buf = buf.drop(8 + jsonLength);
                    msgHandler.tell(msg, getSelf());
                }
            }
        }
    
        private Object decodeMsg(int msgId, ByteString jsonData) {
            final Class<?

    > msgClass = MsgRegistry.getMsgClass(msgId);         final Reader reader = new InputStreamReader(                 jsonData.iterator().asInputStream(),                 StandardCharsets.UTF_8);         return GSON.fromJson(reader, msgClass);     }

    消息的编码就简单多了,代码例如以下所看到的:

        private ByteString encodeMsg(Object msg) {
            final int msgId = MsgRegistry.getMsgId(msg);
            final byte[] jsonBytes = GSON.toJson(msg)
                    .getBytes(StandardCharsets.UTF_8);
            
            final ByteStringBuilder bsb = new ByteStringBuilder();
            bsb.putInt(msgId, ByteOrder.BIG_ENDIAN);
            bsb.putInt(jsonBytes.length, ByteOrder.BIG_ENDIAN);
            bsb.putBytes(jsonBytes);
            
            return bsb.result();
        }

    MsgHandler

    游戏逻辑由MsgHandler来处理。

    由于仅仅是个demo,所以MsgHandler内部使用HashMap来模拟数据库。以下是MsgHandler的完整代码:

    public class MsgHandler extends UntypedActor {
        
        private final List<Player> players = new ArrayList<>();
    
        @Override
        public void onReceive(Object msg) throws Exception {
            if (msg instanceof CreatePlayerRequest) {
                int newPlayerId = createPlayer((CreatePlayerRequest) msg);
                getSender().tell(new CreatePlayerResponse(newPlayerId), getSelf());
            } else if (msg instanceof AddExpRequest) {
                int newExp = addExpToPlayer((AddExpRequest) msg);
                getSender().tell(new AddExpResponse(newExp), getSelf());
            } else if (msg instanceof LevelUpRequest) {
                int newLevel = levelUpPlayer((LevelUpRequest) msg);
                getSender().tell(new LevelUpResponse(newLevel), getSelf());
            } else if (msg instanceof GetPlayerInfoRequest) {
                PlayerInfo playerInfo = getPlayerInfo((GetPlayerInfoRequest) msg);
                getSender().tell(new GetPlayerInfoResponse(playerInfo), getSelf());
            }
        }
        
        private int createPlayer(CreatePlayerRequest req) {
            int playerId = players.size() + 1;
            Player newPlayer = new Player();
            newPlayer.setId(playerId);
            newPlayer.setLevel(1);
            newPlayer.setName(req.getPlayerName());
            players.add(newPlayer);
            return playerId;
        }
        
        private int addExpToPlayer(AddExpRequest req) {
            Player player = players.get(req.getPlayerId());
            player.addExp(req.getExp());
            return player.getExp();
        }
        
        private int levelUpPlayer(LevelUpRequest req) {
            Player player = players.get(req.getPlayerId());
            player.levelUp();
            return player.getLevel();
        }
        
        private PlayerInfo getPlayerInfo(GetPlayerInfoRequest req) {
            Player player = players.get(req.getPlayerId());
            return new PlayerInfo(player.getId(), player.getName(),
                    player.getExp(), player.getLevel());
        }
        
    }
    

    ServerApp

    ServerApp是MiniRPG游戏server主类,main()方法建立好整个Actor系统,然后通知tcpServer绑定到port12345。让server运转起来:

    public class ServerApp {
        
        public static void main(String[] args) {
            ActorSystem mySystem = ActorSystem.create("rpgServer");
            ActorRef msgHandler = mySystem.actorOf(Props.create(MsgHandler.class));
            ActorRef tcpServer = mySystem.actorOf(Props.create(TcpServer.class, msgHandler));
            tcpServer.tell(12345, ActorRef.noSender());
        }
        
    }
    

    client

    为了測试MiniServer。我写了个简单的客户端程序。详细实现就不在这里介绍了。



  • 相关阅读:
    纸牌游戏
    万圣节派对
    士兵杀敌(三)简单线段树
    百度之星2016资格赛之部分题解
    hdu Simpsons’Hidden Talents(kmp)
    滑梯理论
    PAP认证方式原理和实现
    Google的Protobuf协议分析
    HMac基本介绍
    为Tcl编写C的扩展库
  • 原文地址:https://www.cnblogs.com/mfmdaoyou/p/6956722.html
Copyright © 2020-2023  润新知