终端斗地主--看看源码


最近看了一个蛮有意思的项目,是终端的斗地主,适合编程人群上班摸鱼,这里简单说一下整体的设计和客户端和服务端交互的流程

项目介绍

先说一下项目地址:
https://github.com/ainilili/ratel
具体的使用和介绍可以在Github中查看
这个项目的通信框架使用的是Netty,序列化框架是Protobuf,Netty对于Protobuf支持还是蛮好的,提供相应的处理器。

设计

整体的源码并不多,看下来,还是很佩服作者的整体设计。
整体是一个Netty经典的通信方式,先看客户端的,在pipipeline的链式handler中,主要又三部分:

  • 心跳检测
  • protobuf编解码器
  • 逻辑处理

前两个都是netty自带的,不做详解,主要看逻辑处理的类TransferHandler.
在内部重写了三个方法,一个是心跳处理的,一个是异常捕获的,剩下的就是最重要的channelRead,用于接受服务端传来的数据并做处理。
这里作者使用了一个顶层的抽象类ClientEventListener,来抽象出客户端的所有行动,内部的call()抽象方法是主要的逻辑处理,各个行动类继承这个类,然后实现call()方法。
内部还有一个get()方法,来通过传来的String code 来映射出相应的处理类,通过反射的方式拿到对应的Class类,然后实例化。
这里的实例化的类都是单例的,并不是通过类本身来限制单例,而是通过get()方法内部的判断,将实例化的类放进map,可以保证单例,因为这些具体的处理类都是没有状态的。

具体的处理类下面再详细说,有一个我较为疑惑的点,作者封装了一个ChannelUtils的类,用于向客户端和服务端推消息,前面也说了,使用的是protobuf序列化框架,所以这里的类都是由protobuf生成的,客户端和服务端的一样,如下:

    string code = 1;

    string data = 2;

    string info = 3;

然后是生成对应的Java代码,生成的代码是自带builder的,我的疑惑就是这里ChannelUtils中的pushToServerpushToClient中的writeAndFlush方法,是向另一端发消息,但是这里发送的是两个封装的消息的builder类,后面的编解码框架中会对builder类进行`build(),但是我并没有很理解这样做的原因,我猜想是为了方便在之后进行一些修改,但是暂时还没有发现,可能是为了扩展吧。

服务端的类似,没有很大的区别。

处理类和交互流程

  1. 客户端发送请求,第一次到达服务端会触发channelRegistered方法,这个方法内部,首先拿到ctx中的channel,然后构造一个客户端侧的类ClientSide,然后注册入服务端map,然后向客户端发送两个命令,

    • CODE_CLIENT_CONNECT
    • CODE_CLIENT_NICKNAME_SET
      第一个是向一个已经连接的通知,客户端并不做反应。
      第二个是一个昵称的设置,这是客户端会做反应。
  2. CODE_CLIENT_NICKNAME_SET中,对应的是处于客户端的处理类ClientEventListener_CODE_CLIENT_NICKNAME_SET,内部,首先是对服务端传来的数据进行验证(第一次服务端传来为null,后续会传来对客户端昵称的检验结果,暂时就是长度检测,),如果为null,会在客户端本地进行长度检测,长度限制是10,如果不符合,就再次调用该方法,如果符合,会将昵称发送给服务端,命令为CODE_CLIENT_NICKNAME_SET.

  3. 服务端对应的类为ServerEventListener_CODE_CLIENT_NICKNAME_SET,这个类比较简单,就是对昵称长度进行检测,长度也是10(这里又一个建议,这个长度常量写在common模块中的常量类中比较好)。如果不符合,就发送验证结果到客户端,客户端的处理在上面。如果符合,会修改前面存储的客户端侧类中的昵称值,然后发送命令CODE_SHOW_OPTIONS

  4. 客户端的选项类是ClientEventListener_CODE_SHOW_OPTIONS,内部有三个选型,下面会对三个选项分别介绍。

    • PVP
      pvp模式是主要的逻辑,客户端选择pvp模式,会向客户端发送命令CODE_SHOW_OPTIONS_PVP到本地,对应的类是ClientEventListener_CODE_SHOW_OPTIONS_PVP,然后是再次提供四个选项,分别是:

      • 创建房间
        创建房间会向服务端发送命令CODE_ROOM_CREATE,在服务端的处理类是ServerEventListener_CODE_ROOM_CREATE,内部,主要是创建一个Room类,类的具体字段先不介绍,其中Roomid是一个全局增长的原子变量,然后讲用户侧类中的房间id设为该房间id,然后将Room对象序列化,发送到客户端,命令是CODE_ROOM_CREATE_SUCCESS。客户端不对这个命令反应,只是在客户端显示加入的房间id,然后初始化对局属性。创建房间就这样完类,主要是需要等待其他人进入房间。

      • 房间列表
        房间列表顾名思义,就是获取线上的所有房间列表,这里的发向服务端的命令是CODE_GET_ROOMS,在服务端的类是ServerEventListener_CODE_GET_ROOMS,然后从服务端存储的RoomMap中获取所有房间,构造新的map-RoomList,属性为
        roomIdroomOwnerroomClientCountroomType四种.
        然后序列化整个roomList,发往客户端,命令是CODE_SHOW_ROOMS,回到客户端,对应的类为ClientEventListener_CODE_SHOW_ROOMS,内部,就是对房间列表的格式化展示,然后调用本地CODE_SHOW_OPTIONS_PVP命令,供再次选择房间。

      • 加入房间
        加入房间会调用服务端,命令CODE_ROOM_JOIN,服务端对应类ServerEventListener_CODE_ROOM_JOIN,内部,首先根据客户端传来的roomid获取房间,如果房间不存在,返回一个CODE_ROOM_JOIN_FAIL_BY_INEXIST的客户端命令,客户端进行展示结果并返回到选项界面。
        如果房间存在,首先查看房间人是否已经满了,如果满了,就调用命令CODE_ROOM_JOIN_FAIL_BY_FULL,然后客户端还是进行展示,然后返回选项界面。
        如果房间未满,则将该用户加入房间,并修改相关存储信息,如果加入后,房间有3个人,开始游戏,调用服务端本地CODE_GAME_STARTING命令,在方法内部,先为房间里的三人分发扑克,扑克的生成方法自行查看,在PokerHelper里面,然后发送房间信息和该玩家的牌到玩家方。
        如果客户端是玩家,则发送CODE_GAME_STARTING命令到客户端,对应类是ClientEventListener_CODE_GAME_STARTING,内部向玩家展示扑克,然后本地调用抢地主类。
        如果客户端侧是机器人,且轮到该机器人抢地主,调用服务端机器人抢地主方法。
        抢地主,有三个选项,分别是

        • 退出房间
          退出房间调用服务端命令CODE_CLIENT_EXIT,服务端修改房间数据,并发送相同命令到客户端,客户端做提示退出房间,然后返回到选项界面

        • 抢地主
          调用服务端CODE_GAME_LANDLORD_ELECT命令,内部先判断是否有房间,然后将地主牌添加到地主牌里,设置房间地主相关属性,然后将更新后的房间属性和地主额外牌告诉所有人。

        • 不抢地主,还是调用CODE_GAME_LANDLORD_ELECT命令,先判断该玩家是否已经是地主,如果是,就进入游戏,如果不是,就更新index,下个玩家决定是否抢地主。
          如果没有人抢地主,就重新洗牌,开始游戏

          下面说一下游戏正式开始。决定地主后,由地主确认后,进入CODE_GAME_POKER_PLAY_REDIRECT方法,服务端进行出牌重定向,然后发向客户端,
          先停在这

      • 返回
        返回选项界面

    • PVE
      pve的玩法类似,就是服务端有机器人玩家做操作,处理过程类似。

    • 设置

对局中还没有分析,是斗地主的逻辑了,下次再分析,总之,这是一个很好的项目,很适合作为Netty的学习项目,很简单,也不长。希望可以去GitHub支持作者。


  目录