文章目录
Netty异常处理这块
在io线程中,入站时,发生的一切异常,都可以最后一个handler的ExceptCatched 回调中处理
而出站方向的异常,你必须在writeAsync中处理,这个函数是异步的,只负责将msg写到该chanel的写任务队伍中;需要在它返回的异常Task对象中,用t.continuewith()来处理完成以后的逻辑;
public override Task WriteAsync(IChannelHandlerContext context, object message)
{
Task t = base.WriteAsync(context, message);
t.ContinueWith((o) =>
{
// Console.WriteLine(t.IsFaulted);
if (t.IsFaulted)
{
if (t.Exception is IOException)
{
return;
}
LogHelper.logger.Error("在出站时,发生错误:{},消息为:{}",t.Exception,message.ToString());
return t;
}
这里,t.IsFaulted可以用来判断该写任务是否失败,并打印t.Exception;
小心自己线程中的异常
注意,如果自己另开辟线程处理逻辑,一定要自己处理这里面的异常,这中间的异常是不会让Netty的入站和出站捕获的,因为它只能捕获IO线程中的异常;
对于Task线程池的异常,自己用异常的方式来处理;其它线程,直接try
小心byteBuffer
Netty使用线程threadlocal的方式来辨别 bufferpool,如果你在一个线程中使用了它的池,给另一个在其它io线程中的chanel发送buffer写数据,这是异步的,可能 ,另一个chanel在发送数据时,在它自己的IO线程中,去回收这个byteBUffer,这就相当于一个不断生产,一个不断添加,而失去了池的作用;
结果就是内存泄漏
ByteBuffer写出后,会被自动Release(),所以,如果一个IO线程中使用其它channel去写数据,一定要每个channel都让ByteBufffer的引用加1;否则,第二个chanel真到写入socket时,会被判定使用了一个引用为0的ByteBuffer;总之,小心引用
关于配置数据,实例数据和逻辑这方面
我犯了一个错误,用一个Role来映射配置文件中的配置数据;
而Role又是我应用中使用的实例数据,这就造成了数据混乱 ,一个实体身上带着很多配置文件中才使用的一次性数据;
所以,实例Role就是实例,而配置数据,最好再使用一个类,RoleConfig来得到;这样,逻辑清晰,数据也不会冗余;
第三,最好还是使用代理方式将数据和逻辑分开,逻辑中涉及到的方面比较多,经常牵扯到其它模块和程序集的引用;
配置文件放到公用目录中最好,还有数据实例类,最好不要加命名空间,方便其它项目添加;
关于UserInfo这类的数据
一般一个连接,总要绑定一个实例用来和连接关联;我这里范了个错误;不同的项目,最好单独使用自己的类,不要牵扯;
我这里的UserInfo和Player竟然是同一个父类,这样就很不好了,逻辑上虽然有相通的地方,但这样很不灵活;
不要为了省劲不面向接口编程
前期业务一些问题想不到,懒的用接口,后期发现有麻烦
前期一切面向接口,逻辑清晰后,用的抽象类来做个统一实现,需要的功能,直接给抽象类上接口就好了;真的很给力;
要利用父类的构造方法来约束来控制子类的实例必须给一些参数赋值
在本项目中,所有的战士,都应该有阵营和房间的引用;可是我没在父类中约束,导致,运行期间出现了大量的空指针异常;
然后,再一个一个找出来,赋值,所以要利用父类的构造方法来强制子类传参
关于mongodb
关于抽象类
将抽象类的子类写和数据库,没问题,但是读取时,就会出问题,因为你的泛型一定用的是抽象 类,可是数据库中放的是子类,那么会出现抽象 类匹配不上子类的子段而报错的问题
//解决方案,注册子类
BsonClassMap.RegisterClassMap<TaskWenHou>(cm =>
{
cm.MapCreator(p => new TaskWenHou(p.Id, p.TaskType));
});
映射 对象时构造函数的执行时机
其实,是默认用无参构造函数,,先实例化一个对象,然后把数据库中的字段一个一个映射;
这就造成了,之前我们在构造函数中的一些初始化动作没法用了,解决方案:
//解决方案,注册子类时,利用映射的对象调用指定的构造函数,此时的P是赋值以后的
BsonClassMap.RegisterClassMap<TaskWenHou>(cm =>
{
cm.MapCreator(p => new TaskWenHou(p.Id, p.TaskType));
});
对外提供统一的遍历 手段
之前用列表保存任务实例对象,在多个函数中,都直接使用遍历线性表的方式遍历这个列表;
后来,数据结构 有问题,需要换字典,导致使用这个列表的地方,都得修改,造成了很大的麻烦
所以,应该用接口的方式对外提供统一的遍历 方式,如返回一个可换代对象 Ienumable<TaskBase>
这种方式
对外提供统一的Get和Set手段
比如,当前用一个字典管理当前支线任务,不考虑并发问题,后期可能考虑到多线程,换成线程安全的字典;
如果,直接暴露给调用者字典,那么将来更换容器时,要进行大量的修改,所以对方提供统一的GetModel和添加 元素删除元素的接口
数据与逻辑分开
从数据库充序列化出来的对象,要给一些初始化的接口;最好不要只在构造中初始化,因为反序列化 时,不会调用它的
数据包大小的限制
自己做了一个缓冲区类,用来放收到的服务器的消息,设置了2048的大小;结果导致客户端拉取数据时,出现大小超出范围的情况;这个bug很奇怪;不容易发现,而我加的主动抛异常的机制,不知道为何,客户端也不会报错;
总之,小心这个包大小的限制
游戏逻辑注意的问题
角色天生拥有一些属性,装备自带一些属性,还有战斗时,buff会自带一些属性;
要注意,这些属性持久化带来的危险,比如,一个buff是加力量,这个加成是暂时的,不能持久化的,要是这时,玩家掉线或者退出了;不应该把这个加成写到数据库中,要做一些移除的处理;
测试数据和DEBUG数据
还是那句话,为了省劲,有些数据要用临时的测试,直接就在代码中,赋值临时数据;想着就这一点代码,将来不用了,也好改;
可是随着项目的深入,发现,越来越多的地方使用了临时数据测试;将来这些数据肯定要去掉的,结果 改起来,相当麻烦
所以啊,还是用预编译命令,区分开哪些是DEBUG下的代码,哪些是测试用的代码,将来直接修改宏命令就好了
不要着急动代码,先和产品把游戏逻辑整理好,
把公式等数据和字段名字之类的东西分类整理,逻辑清晰后,再开始架构你的项目,首先自己心里要对大部分逻辑清楚才行;
不要因为产品一句话,这个后面才考虑吧,以后再说,导致你的项目写起来很乱,很多代码重复,结构不合理,所以,还是要按面向对象的思想,面向接口,面向抽象,不要耦合,单一职责 ,这些要时刻记在心里,不要想当然认为项目不大,后面会发现无法收拾;