Akka边学边写(4)-- MiniRPG
前面几篇文章用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系统例如以下图所看到的:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenhob28=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
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。我写了个简单的客户端程序。详细实现就不在这里介绍了。
Akka边学边写(4)-- MiniRPG的更多相关文章
- Akka边学边写(2)-- Echo Server
EchoServer 上篇文章里,我们用Akka写了一个简单的HelloWorld样例,对Akka(以及Actor模式)有了初步的认识.本文将用Akka写一个EchoServer,看看在Actor的世 ...
- Akka边学边写(1)-- Hello, World!
Akka Akka是什么呢?直接引用Akka站点上面的描写叙述吧: Akka is a toolkit and runtime for building highly concurrent, dist ...
- Akka边学边写(3)-- ByteString介绍
Akka的IO层设计能够參考这篇文档,本文简介一下ByteString的设计. Immutable消息 Actor之间是通过消息沟通的.但为了避免同步问题,消息必须是Immutable. 因此.Akk ...
- Django学习笔记(现学现写,实时更新)
说明:我是先上手做一些简单的例子,然后在尝试的过程中理解Django的原理,笔记也是按这个思路来的. 一.Django结构与基本文件介绍 1. django-admin.py 工程管理工具,主要用于创 ...
- [HTML] 学HTML写的第一第二个网页
①第一个网页 <h2>英雄联盟(电子竞技类游戏)</h2> <p><b>(英雄联盟)</b>(简称lol)是由美国<i>Roit ...
- JavaScript中的作用域和作用域链(边学边写)[看着别人的博客纯手敲]
作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域的工作原理.今天这篇文章对JavaScript作用域和作用域链简单的介绍,希望能帮 ...
- 苦涩的技术我该怎么学?Akka 实战
上次我们在“懵 B”的状态下,聊了聊 Actor 模型的理论知识.稍微再补充两句,如上图所示在 Actor 模型系统中,主要有互不依赖的 Actor 组成(图中圆圈),Actor 之间的通信是通过消息 ...
- 关于自己写C++的一点风格
现在,我学了很长时间的C++,但是自己就是无法精通.许多知识是入门书上没有的.现在写C++最重要的就是风格问题. 我现在的C++风格: 把自己所有的东西都放在一个名称空间下. 没有全局的函数,有的函数 ...
- 我也要学iOS逆向工程--函数
大家好,这篇我开始学习函数了.先学 C 函数,然后再 OC 的吧.OC 应该复杂点的吧. 然后看看汇编情况哦! 学习函数呢,肯定要弄清楚几个事情. 1.跳转地址. 2.返回地址 3.参数 4.函数获取 ...
随机推荐
- .Net中字典的使用
/// <summary> /// 获取用户市信息 /// </summary> /// <param name="CustomerId">&l ...
- BZOJ 3544 treap (set)
我只是想找个treap的练习题-- 每回找到lower_bound 就好啦 //By SiriusRen #include <cstdio> #include <cstring> ...
- Git 操作笔记
¥先放个地址:这个视频超详细 https://www.bilibili.com/video/av10475153 分了2部分: 1是GitHub的功能介绍:2是pc端+doc命令的操作+发布个人网站 ...
- 【转】一篇关于32位Linux内核使用大内存的文章——Hugemem Kernel Explained &nb
红旗DC系列Linux操作系统(x86平台)中带有四类核心: UP (支持单内核) SMP (支持多内核) hugemem Icc* (用intel C编译器编译的核心) 其中hugemem核心往往引 ...
- whereis---定位指令的二进制程序、源代码文件和man手册页等相关文件的路径。
whereis命令用来定位指令的二进制程序.源代码文件和man手册页等相关文件的路径. whereis命令只能用于程序名的搜索,而且只搜索二进制文件(参数-b).man说明文件(参数-m)和源代码文件 ...
- Mysql学习总结(7)——MySql索引原理与使用大全
一.索引介绍 索引是对数据库表中一列或多列的值进行排序的一种结构.在关系数据库中,索引是一种与表有关的数据库结构,它可以使对应于表的SQL语句执行得更快.索引的作用相当于图书的目录,可以根据目录中的页 ...
- Nginx模型 & 惊群问题
这篇写的不错 http://www.cnblogs.com/linguoguo/p/5511293.html Nginx为啥性能高-多进程异步IO模型 1. 对于每个worker进程来说,独立的进程, ...
- Android学习笔记(23):列表项的容器—AdapterView的子类们
AdapterView的子类的子类ListView.GridView.Spinner.Gallery.AdapterViewFlipper和StackView都是作为容器使用,Adapter负责提供各 ...
- maven项目运行没问题,但是项目名上有一把×
原因有以下:你先要自己进行检查,看是出现的什么错误 有红叉,并不代表编译和运行就出错.到Windows—Show view—Problems下看看到底报了什么错.像一些validation出错,项目虽 ...
- 项目: 更新(二) python 实现大概FTP的功能
服务器利用 socketserver 模块 构造, 实现了 多进程. 客户端仍然利用的是底层的 socket模块. 只不过进行了更深度的 解耦, 新加或者删除 某些功能 更方便 在上一个版本的基础上, ...