前面几篇文章用Akka写了HelloWorld和EchoServer,为了更进一步学习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。我写了个简单的客户端程序。详细实现就不在这里介绍了。