前面几篇文章用Akka写了HelloWorld和EchoServer,为了更进一步学习Akka,本文将会实现一个很小的RPG游戏服务器:MiniRPG。
因为是迷你RPG,所以逻辑很简单。服务器可以处理四种操作:创建玩家、给玩家加经验、升级、查询玩家信息。下面是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接口,里面没有定义任何方法,如下图所示:
MiniRPG使用GSON来编码和解码JSON字符串,为了把JSON解析为相应的消息对象,需要一个消息ID和class之间的映射关系。MsgRegistry类便是要建立起这样一个映射关系,下面是它的完整代码:
public class MsgRegistry {
private static final Map> msgById = new HashMap<>();
private static final Map, 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());
}
}
MiniRPG服务器的Actor系统如下图所示:
TcpServer负责监听TCP连接,连接建立之后,交给Codec处理。Codec将收到的字节编码成消息对象,然后交给MsgHandler处理。对于每条请求消息,MsgHandler都会产生一条响应消息,响应消息被Codec编码之后发送到客户端。下面详细介绍整个Actor系统是如何实现的。
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绑定到某个端口,准备接收客户端连接。如果收到Bound消息,则端口绑定成功,服务器正常启动。如果是CommandFailed消息,则服务器启动失败:
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消息,说明有客户端连接已经建立,TcpServer创建一个子Actor(也就是Codec)来处理客户端连接:
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内部使用了一个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[]然后发送给客户端。如果收到的是
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来处理。因为只是个demo,所以MsgHandler内部使用HashMap来模拟数据库。下面是MsgHandler的完整代码:
public class MsgHandler extends UntypedActor {
private final List 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是MiniRPG游戏服务器主类,main()方法建立好整个Actor系统,然后通知tcpServer绑定到端口12345,让服务器运转起来:
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());
}
}
为了测试MiniServer,我写了个简单的客户端程序,具体实现就不在这里介绍了。