Java游戏服务器入门04 - 游戏项目重构

一、将用户字典_userMap和信道组_channelGroup进行简单封装

1.封装用户字典
需要注意的是:在hashMap在并发环境下可能出现的问题,这里使用ConcurrentHashMap作为用户的用户字典的容器

package com.tk.tinygame.herostory;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 用户管理器
 */
public final class UserManager {

    /**
     * 用户字典
     */
    static private final Map<Integer, User> _userMap = new ConcurrentHashMap<>();

    /**
     * 私有化类默认构造器
     */
    private UserManager() {
    }

    /**
     * 添加用户
     *
     * @param user
     */
    static public void addUser(User user){
        if(null != user){
            _userMap.putIfAbsent(user.userId,user);
        }
    }

    /**
     * 移除用户
     *
     * @param userId
     */
    static public void removeByUserId(int userId) {
        _userMap.remove(userId);
    }

    /**
     * 列表用户
     *
     * @return
     */
    static public Collection<User> listUser() {
        return _userMap.values();
    }
}

2.封装信道组

package com.tk.tinygame.herostory;

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * 广播员
 */
public final class Broadcaster {
    /**
     * 信道组, 注意这里一定要用 static,
     * 否则无法实现群发
     */
    static private final ChannelGroup _channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 私有化类默认构造器
     */
    private Broadcaster() {
    }

    /**
     * 添加信道
     *
     * @param ch
     */
    static public void addChannel(Channel ch) {
        if (null != ch) {
            _channelGroup.add(ch);
        }
    }

    /**
     * 移除信道
     *
     * @param ch
     */
    static public void removeChannel(Channel ch) {
        if (null != ch) {
            _channelGroup.remove(ch);
        }
    }

    /**
     * 广播消息,广播给所有用户
     *
     * @param msg
     */
    static public void broadcast(Object msg) {
        if (null != msg) {
            _channelGroup.writeAndFlush(msg);
        }
    }
}

3.把应用到用户字典和信道组的地方替换成封装类,主要在GameMsgHandler类中

package com.tk.tinygame.herostory;

import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 自定义消息处理器
 */
public class GameMsgHandler extends SimpleChannelInboundHandler<Object> {
    /**
     * 日志对象
     */
    static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgHandler.class);


    /**
     * 信道组
     * @param ctx
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        if (null == ctx) {
            return;
        }

        try {
            super.channelActive(ctx);
            //建立长连接后,将信道添加到信道组
            Broadcaster.addChannel(ctx.channel());
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }


    /**
     * @param ctx
     * @param msg
     * @throws Exception
     * @deprecated 处理用户消息
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (null == ctx || null == msg) {
            return;
        }

        LOGGER.info("收到客户端消息, msgClzz={},msgBody = {}",
                msg.getClass().getName(),msg);

        /**
         * 根据消息类型作对应处理
         */
        try {
            if (msg instanceof GameMsgProtocol.UserEntryCmd) {
                //
                // 处理用户入场消息
                //
                GameMsgProtocol.UserEntryCmd cmd = (GameMsgProtocol.UserEntryCmd) msg;
                int userId = cmd.getUserId();               //用户id
                String heroAvatar = cmd.getHeroAvatar();    //英雄形象

                //将登录的用户加入用户字典
                User newUser = new User();
                newUser.userId = userId;
                newUser.heroAvatar = heroAvatar;
                UserManager.addUser(newUser);

                GameMsgProtocol.UserEntryResult.Builder resultBuilder = GameMsgProtocol.UserEntryResult.newBuilder();
                resultBuilder.setUserId(userId);
                resultBuilder.setHeroAvatar(heroAvatar);

                // 构建结果并广播
                GameMsgProtocol.UserEntryResult newResult = resultBuilder.build();
                Broadcaster.broadcast(newResult);
            } else if (msg instanceof GameMsgProtocol.WhoElseIsHereCmd) {
                //
                // 处理还有谁在场消息
                //
                GameMsgProtocol.WhoElseIsHereResult.Builder resultBuilder = GameMsgProtocol.WhoElseIsHereResult.newBuilder();

                //遍历用户字典
                for (User currUser : UserManager.listUser()) {
                    if (null == currUser) {
                        continue;
                    }

                    GameMsgProtocol.WhoElseIsHereResult.UserInfo.Builder userInfoBuilder = GameMsgProtocol.WhoElseIsHereResult.UserInfo.newBuilder();
                    userInfoBuilder.setUserId(currUser.userId);
                    userInfoBuilder.setHeroAvatar(currUser.heroAvatar);
                    resultBuilder.addUserInfo(userInfoBuilder);
                }

                //把用户字典用的用户广播
                GameMsgProtocol.WhoElseIsHereResult newResult = resultBuilder.build();
                ctx.writeAndFlush(newResult);
            }
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }
}

二、使用工厂模式,使消息的处理更加易懂,尽量满足开闭原则,设计模式实战!

在我们之前的处理中,使用了大量的if..else处理各种不同的消息,这也是我们在进行到这步进行重构的原因,在有更多消息,比如移动消息,攻击消息加入时,需要使用更多的类似代码,使我们的消息处理类变得臃肿。
1.创建一个新的package:cmdhandler,专门处理各种消息



2.创建interface:ICmdHandler,并且只需要提供一个方法来处理各种消息,由于消息的类型不清楚,但是他们都是GeneratedMessageV3下的子类,我们可以使用泛型来做统一处理

package com.tk.tinygame.herostory.cmdhandler;

import com.google.protobuf.GeneratedMessageV3;
import io.netty.channel.ChannelHandlerContext;

/**
 * 命令处理器接口
 *
 * @param <TCmd>
 */
public interface ICmdHandler<TCmd extends GeneratedMessageV3>{
    /**
     * 处理命令
     *
     * @param ctx
     * @param cmd
     */
    void handle(ChannelHandlerContext ctx, TCmd cmd);
}

3.创建class:UserEntryCmdHandler并实现接口ICmdHandler处理用户入场消息,具体的内容只需要把之前消息处理类的入场消息代码复制过来即可

package com.tk.tinygame.herostory.cmdhandler;

import com.tk.tinygame.herostory.Broadcaster;
import com.tk.tinygame.herostory.User;
import com.tk.tinygame.herostory.UserManager;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;

/**
 * 处理用户入场
 */
public class UserEntryCmdHandler implements ICmdHandler<GameMsgProtocol.UserEntryCmd>{
    @Override
    public void handle(ChannelHandlerContext ctx, GameMsgProtocol.UserEntryCmd cmd) {
        //空值判断
        if (null == ctx || null == cmd) {
            return;
        }
        //
        // 处理用户入场消息
        //
        int userId = cmd.getUserId();               //用户id
        String heroAvatar = cmd.getHeroAvatar();    //英雄形象

        //将登录的用户加入用户字典
        User newUser = new User();
        newUser.userId = userId;
        newUser.heroAvatar = heroAvatar;
        UserManager.addUser(newUser);


        GameMsgProtocol.UserEntryResult.Builder resultBuilder = GameMsgProtocol.UserEntryResult.newBuilder();
        resultBuilder.setUserId(userId);
        resultBuilder.setHeroAvatar(heroAvatar);

        // 构建结果并广播
        GameMsgProtocol.UserEntryResult newResult = resultBuilder.build();
        Broadcaster.broadcast(newResult);
    }
}

4.创建class:WhoElseIsHereCmdHandler并实现接口ICmdHandler处理谁在场,具体的内容只需要把之前消息处理类的谁在场消息代码复制过来即可

package com.tk.tinygame.herostory.cmdhandler;

import com.tk.tinygame.herostory.User;
import com.tk.tinygame.herostory.UserManager;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;

/**
 * 还有谁在场
 */
public class WhoElseIsHereCmdHandler implements ICmdHandler<GameMsgProtocol.WhoElseIsHereCmd>{
    @Override
    public void handle(ChannelHandlerContext ctx, GameMsgProtocol.WhoElseIsHereCmd cmd) {
        if (null == ctx || null == cmd) {
            return;
        }
        //
        // 处理还有谁在场消息
        //
        GameMsgProtocol.WhoElseIsHereResult.Builder resultBuilder = GameMsgProtocol.WhoElseIsHereResult.newBuilder();

        //遍历用户字典
        for (User currUser : UserManager.listUser()) {
            if (null == currUser) {
                continue;
            }

            GameMsgProtocol.WhoElseIsHereResult.UserInfo.Builder userInfoBuilder = GameMsgProtocol.WhoElseIsHereResult.UserInfo.newBuilder();
            userInfoBuilder.setUserId(currUser.userId);
            userInfoBuilder.setHeroAvatar(currUser.heroAvatar);
            resultBuilder.addUserInfo(userInfoBuilder);
        }

        //把用户字典用的用户广播
        GameMsgProtocol.WhoElseIsHereResult newResult = resultBuilder.build();
        ctx.writeAndFlush(newResult);
    }
}

5.创建消息处理工厂类:CmdHandlerFactory,来根据不同消息类型生成对应的消息处理实例,这里使用了hashMap并对map进行初始化,key为消息类型,value为对应的消息处理实例

package com.tk.tinygame.herostory.cmdhandler;

import com.google.protobuf.GeneratedMessageV3;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;

import java.util.HashMap;
import java.util.Map;

/**
 * 命令处理器工厂类
 */
public final class CmdHandlerFactory {
    /**
     * 命令处理器字典
     */
    static private Map<Class<?>, ICmdHandler<? extends GeneratedMessageV3>> _handlerMap = new HashMap<>();

    /**
     * 私有化类默认构造器
     */
    private CmdHandlerFactory() {
    }

    /**
     * 初始化
     */
    static public void init() {
        _handlerMap.put(GameMsgProtocol.UserEntryCmd.class, new UserEntryCmdHandler());
        _handlerMap.put(GameMsgProtocol.WhoElseIsHereCmd.class, new WhoElseIsHereCmdHandler());
    }

    /**
     * 创建命令处理器
     *
     * @param msgClazz
     * @return
     */
    static public ICmdHandler<? extends GeneratedMessageV3> create(Class<?> msgClazz) {
        if (null == msgClazz) {
            return null;
        }

        return _handlerMap.get(msgClazz);
    }
}

6.修改GameMsgHandler,使得消息代码进行简化,可以看出,之前冗长的消息处理类里面的代码得到了极大的简化

package com.tk.tinygame.herostory;

import com.google.protobuf.GeneratedMessageV3;
import com.tk.tinygame.herostory.cmdhandler.CmdHandlerFactory;
import com.tk.tinygame.herostory.cmdhandler.ICmdHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 自定义消息处理器
 */
public class GameMsgHandler extends SimpleChannelInboundHandler<Object> {
    /**
     * 日志对象
     */
    static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgHandler.class);


    /**
     * 信道组
     * @param ctx
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        if (null == ctx) {
            return;
        }

        try {
            super.channelActive(ctx);
            //建立长连接后,将信道添加到信道组
            Broadcaster.addChannel(ctx.channel());
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }


    /**
     * @param ctx
     * @param msg
     * @throws Exception
     * @deprecated 处理用户消息
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (null == ctx || null == msg) {
            return;
        }

        LOGGER.info("收到客户端消息, msgClzz={},msgBody = {}",
                msg.getClass().getName(),msg);

        /**
         * 根据消息类型作对应处理
         */
        try {
            ICmdHandler<? extends GeneratedMessageV3> cmdHandler = CmdHandlerFactory.create(msg.getClass());
            if (null != cmdHandler) {
                cmdHandler.handle(ctx, cast(msg));
            }
        } catch (Exception ex) {
            // 记录错误日志
            LOGGER.error(ex.getMessage(), ex);
        }
    }

    /**
     * 转型为命令对象
     *
     * @param msg
     * @param <TCmd>
     * @return
     */
    static private <TCmd extends GeneratedMessageV3> TCmd cast(Object msg) {
        if (null == msg) {
            return null;
        } else {
            return (TCmd) msg;
        }
    }
}

7.在ServerMain中初始化工厂类的消息处理器

CmdHandlerFactory.init();
package org.tinygame.herostory;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.apache.log4j.PropertyConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinygame.herostory.cmdhandler.CmdHandlerFactory;

/**
 * 服务器入口类
 */
public class ServerMain {
    /**
     * 日志对象
     */
    static private final Logger LOGGER = LoggerFactory.getLogger(ServerMain.class);

    /**
     * 服务器端口号
     */
    static private final int SERVER_PORT = 12345;

    /**
     * 应用主函数
     *
     * @param argvArray 命令行参数数组
     */
    static public void main(String[] argvArray) {
        // 设置 log4j 属性文件
        PropertyConfigurator.configure(ServerMain.class.getClassLoader().getResourceAsStream("log4j.properties"));

        // 初始化命令处理器工厂
        CmdHandlerFactory.init();
        // 初始化消息识别器
        GameMsgRecognizer.init();

        EventLoopGroup bossGroup = new NioEventLoopGroup();   // 拉客的, 也就是故事中的美女
        EventLoopGroup workerGroup = new NioEventLoopGroup(); // 干活的, 也就是故事中的服务生

        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup);
        b.channel(NioServerSocketChannel.class); // 服务器信道的处理方式
        b.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(
                    new HttpServerCodec(), // Http 服务器编解码器
                    new HttpObjectAggregator(65535), // 内容长度限制
                    new WebSocketServerProtocolHandler("/websocket"), // WebSocket 协议处理器, 在这里处理握手、ping、pong 等消息
                    new GameMsgDecoder(), // 自定义的消息解码器
                    new GameMsgEncoder(), // 自定义的消息编码器
                    new GameMsgHandler() // 自定义的消息处理器
                );
            }
        });

        try {
            // 绑定 12345 端口,
            // 注意: 实际项目中会使用 argvArray 中的参数来指定端口号
            ChannelFuture f = b.bind(SERVER_PORT).sync();

            if (f.isSuccess()) {
                LOGGER.info("服务器启动成功!");
            }

            // 等待服务器信道关闭,
            // 也就是不要立即退出应用程序, 让应用程序可以一直提供服务
            f.channel().closeFuture().sync();
        } catch (Exception ex) {
            // 如果遇到异常, 打印详细信息...
            LOGGER.error(ex.getMessage(), ex);
        } finally {
            // 关闭服务器, 大家都歇了吧
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

三:测试代码

测试地址:http://cdn0001.afrxvk.cn/hero_story/demo/step010/index.html?serverAddr=127.0.0.1:12345&userId=1
经测试后会发现,用户的入场消息,谁在场消息都会同之前一样的处理,说明我们重构的代码是可行的。

效果图

下一篇文章,我们会按照这个思路添加移动消息,并且之后会对代码进行进一步的重构

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。