美文网首页Java游戏服务器编程
Java游戏跨服实现(Hessian+Jetty)

Java游戏跨服实现(Hessian+Jetty)

作者: 小圣996 | 来源:发表于2019-12-30 00:15 被阅读0次

    倘若你迷失在黑暗之中,除了前行别无他法。 --深海泰坦

    现在的游戏基本都有跨服玩法,跨服玩法是一个游戏的核心玩法,承载着游戏趣味、人气及盈利的重要使命,因此每个游戏基本都有跨服实现。

    跨服实现需根据游戏的类型去选择跨服方案,假如你的游戏是回合制的,即实时性要求不是很高的,一入场就秒算战斗流程的,那跨服协议可以选择用Http,框架可以用Hessian;假如你的游戏是多人实时PK的,这种对时延和处理效率要求过高的,那用Http协议可能不合适了,因为Http协议是一种短连接的协议(虽然HTTP1.1加入了Keep-Alive,能使连接保持一段时间,但仍有时间限制),每建立一次连接,都要重新发送鉴别信息,因此会消耗多余无用的传输和处理的时间,在这种情况下,可以选择长连接的协议,如TCP协议,框架可以用Netty或Mina。

    更多网络知识,请参考
    游戏之网络初篇
    游戏之网络进阶
    Netty的可以参考
    使用Netty+Protobuf实现游戏TCP通信
    使用Netty+Protobuf实现游戏WebSocket通信

    跨服方案除了协议的选择外,还有很多事情要考虑。
    比如到底是用跨服直连好还是消息转发好,跨服直连是指平常游戏时连接游戏服,但是跨服玩法时,转而连接跨服(服务器),但是你的游戏数据却还在游戏服,因此要想办法把你的游戏数据推送过去;消息转发就是跨服玩法的数据不是在跨服中直接发给玩家的,而是必须发送玩家所在游戏服,再由游戏服维护的玩家session发给玩家,这样消息就可能多经过了一个跨服。直连跨服时,其实跨服和游戏服之间的数据交互也还是蛮多的,战前需把很多数据发往跨服,战后又需把很多数据返回游戏服处理。
    还有一个就是服务发现问题,所有的跨服和游戏服,乃至中心服交叉形成了一个服务器网络,那么跨服玩法参与的那些服务器,彼此应该是透明的,因此要维护好所有服务器的ip、端口和状态信息,这可以用zookeeper或Redis做到(后续会有一篇做服务发现的博文,敬请期待)。也可以自己手写实现服务发现,即所有服务器启动时,表明此服务器的类型是中心服还是跨服还是游戏服,跨服和游戏服启动时,都需要把自己的ip和端口信息注册到中心服,在中心服或跨服上维护跨服开启规则,玩法规则,玩法开启时,从中心服上获取参与玩法的游戏服信息,给游戏服分配跨服,即告诉游戏服它们的跨服玩法届时应连哪个跨服,此后,交互变为跨服和游戏服之间的通信,并由跨服给相应的游戏服广播。
    另外,在跨服中广播协议时,最好不要一个玩家一个玩家的广播协议,最好根据游戏服id为单位,一个游戏服一个游戏服的广播。

    本文着重讲解一下使用Hessian+Jetty实现回合制卡牌类游戏的跨服。
    Hessian是由caucho提供的一个基于binary-RPC实现的远程通讯library,支持多种语言,包括c++,java,c#等。Jetty 是一个开源的servlet容器,它是作为一个可以嵌入到其他的Java代码中的servlet容器而设计的。通过jetty和hessian结合,就可以使一个普通的java工程提供远程通信服务,而不需要建立一个web工程。

    通俗来说,Hessian是一个RPC框架,Jetty是一个servlet容器。我们的游戏服都是用基础的java代码书写的,当要做跨服通信时,可选的协议通常为TCP或HTTP,当采用HTTP时(延时要求低的游戏可用),我们如何才能把一些数据或对象通过http协议发给跨服呢? 这时就可以使用Hessian+Jetty来做。Hessian在内部把java对象使用Http协议发送出去,当游戏服、跨服两端都支持Jetty时,便可以方便的把通信中的java对象再解析出来,中间节省了我们自己把java对象转为http内容的过程,和平常书写java代码相差无几。

    dubbo文档中,有对hessian的一段描述:
    hessian是一个轻量级的RPC服务,是基于Binary-RPC协议实现的
    连接个数:多连接
    连接方式:短连接
    传输协议:HTTP
    传输方式:同步传输
    序列化:Hessian二进制序列化
    适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
    适用场景:页面传输,文件传输,或与原生hessian服务互操作
    约束:
    参数及返回值需实现Serializable接口
    参数及返回值不能自定义实现List, Map, Number, Date, Calendar等接口,只能用JDK自带的实现,因为hessian会做特殊处理,自定义实现类中的属性值都会丢失。

    假设A为游戏服,B为跨服,那么在跨服B上需建立和启动Jetty Server,建立和启动方式参考《java游戏服引入jetty
    java游戏服引入jetty 一文中可知,处理web请求的servlet都继承了HttpServlet;因为我们是采用Hessian+Jetty结合的框架形式,所以在处理跨服请求的servlet时它的实现与纯Jetty的HttpServlet不同,集成Hessian的需要继承HessianServlet类,如下,如果AB需要互调,那么A服和B服都需要有这个文件。

    /**
     * 接受远程调用的底层实体,这里将会根据请求的消息头做分发
     */
    @CrossServlet
    @WebServlet(urlPatterns = "HessianService", description="服务器远程端口调用")
    public class HessianServiceImpl<Request extends Serializable, Response extends Serializable> extends HessianServlet
            implements HessianService<Request, Response> {
        private static final long serialVersionUID = -4636257555865679839L;
    
        @Override
        public Response reply(int cmd, Request request) throws Exception {
            return CrossHanderlManager.invoke(cmd, request);
        }
    
    }
    

    在CrossHanderlManager.java中,利用反射就可以调用B服上各跨服模块方法了,由协议号cmd可以获得该cmd所在的handler类,由cmd也可知道处理该cmd的方法Method,知道具体方法和handle类了,利用反射就可以调用相应方法了。

    Hessian是使用代理模式 实现远程调用的,比如,A服请求B服的10001协议,在A服上是如此调用的:

    IReqCrossServerHandler handler = CrossHandlerProxy.getProxy(IReqCrossServerHandler.class, "http://192.168.1.5:33222/HessianService");
    RespVo respVo = handler.fight(new ReqVo(1001, 2001));
    System.out.println("respVo:" + respVo);
    

    IReqCrossServerHandler为B服上的跨服协议接口,如下:

    public interface IReqCrossServerHandler {
        @CrossCmd(cmd = 10001)
        public RespVo fight(ReqVo reqVo);
    }
    

    B服上还需有此接口的实现类(其中AbstractCrossHandler类在spring扫描时会把接口对应的协议cmd及方法注册到CrossHanderlManager中,以供CrossHanderlManager根据cmd获取方法及handler类反射调用):

    @Component
    public class ReqCrossServerHandler extends AbstractCrossHandler implements IReqCrossServerHandler{
    
        @Override
        public RespVo fight(ReqVo reqVo) {
            System.out.println("reqVo:" + reqVo);
            return new RespVo(1001, "xiaosheng996", 32);
        }
    }
    

    代理类CrossHandlerProxy实现为(如果AB需要互调,那么A服和B服都需要有这个文件):

    public class CrossHandlerProxy {
        private static final Logger log = LoggerFactory.getLogger(CrossHandlerProxy.class);
        // 所有代理对象
        private static Map<String, Object> proxyMap = new ConcurrentHashMap<>();
    
        @SuppressWarnings("unchecked")
        public static <T> T getProxy(Class<T> clazz, String url) {
            String proxyKey = clazz.getSimpleName() + "_" + url;
            Object proxy = proxyMap.get(proxyKey);
            if (proxy != null) {
                return (T) proxy;
            }
            synchronized (proxyMap) {
                proxy = proxyMap.get(proxyKey);
                if (proxy != null) {
                    return (T) proxy;
                }
                try {
                    proxy = Proxy.newProxyInstance(CrossHandlerProxy.class.getClassLoader(),
                            new Class[] { clazz }, new CrossHandlerInvoke(url));
                    proxyMap.put(proxyKey, proxy);
                    return (T) proxy;
                } catch (Exception e) {
                    log.error("创建代理错误[{}]", clazz.getName(), e);
                    return null;
                }
            }
        }
    }
    

    Proxy.newProxyInstance方法会创建一个动态的代理对象,该代理对象能够调用new Class[] { clazz }中方法,当调用这些方法时,会关联到CrossHandlerInvoke中的invoke调用。

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
    loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载;
    interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了;
    h:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上。

    因为我们需要跨服调用,跨服的实现正是体现在CrossHandlerInvoke中的:

          public CrossHandlerInvoke(String url) {
            this.url = url;
        }
        
        @SuppressWarnings("unchecked")
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
            String methodName = method.getName();
            Class<?> returnType = method.getReturnType();
            if (methodName.equals("toString") || methodName.equals("hashCode") || method.equals("equals")) {
                return getDefaultValue(returnType);
            }
            CrossCmd crossCmd = method.getAnnotation(CrossCmd.class);
            if (crossCmd == null) {
                log.error("函数[{}]未定义注解", method.getName());
                return null;
            }
            //判断方法参数数量
            if (method.getParameterCount() < 1) {
                return null;
            }
            //判断是同步还是异步
            boolean sync = method.getParameterCount() == 1;
            if (sync) {
                return RemoteAsker.syncAsk(url, crossCmd.cmd(), (Serializable) args[0]);
            }
            RemoteAsker.asyncAsk(url, crossCmd.cmd(), (Serializable) args[0], (AsyncResult<? extends Serializable>) args[1]);
            return getDefaultValue(returnType);
        }
    

    进而在RemoteAsker中,交由Hessian发起远程调用,注意当中的hessianService.reply,即调用了reply方法,而reply方法是实现在上述HessianServiceImpl的HessianServlet中的

          /** 同步向远程发消息 */
        @SuppressWarnings("unchecked")
        public static <Request extends Serializable, Response extends Serializable> Response syncAsk//
        (String url, int cmd, Request request){
            if (url == null) {
                logger.error("cross cmd url为null, cmd:"+ cmd);
                return null;
            }
            HessianService<Request, Response> hessianService = null;
            Response resp = null;
            for (int i = 0; i < 2; i++) {
                try {
                    hessianService = HessianFactory.getHessianService(HessianService.class, url);
                    return hessianService.reply(cmd, request);
                } catch (Exception e) {
                    if (i < 1) {
                        await();
                    } else {
                        logger.error("向远程发起请求失败:" + url + "," + cmd + "," + request + "," + resp, e);
                    }
                }
            }
            return resp;
        }
    

    HessionFactory实现为:

        private static final ConcurrentHashMap<String, HessianService<? extends Serializable, ? extends Serializable>> CACHE = new ConcurrentHashMap<>();
    
        @SuppressWarnings("unchecked")
        public static <T extends HessianService<? extends Serializable, ? extends Serializable>> T getHessianService(
                Class<T> api, String urlName) {
            if (CACHE.containsKey(urlName)) {
                return (T) CACHE.get(urlName);
            } else {
                synchronized (HessianFactory.class) {
                    if (CACHE.containsKey(urlName))
                        return (T) CACHE.get(urlName);
                    HessianProxyFactory hessianProxyFactory = new HessianProxyFactory();
                    //setOverloadEnabled 如果为false,Hessian调用的时候获取接口仅根据方法名;反之,Hessian调用时决定调用哪个方法是通过方法名和参数类型一起决定
                    //setOverloadEnabled 如果为false,存在重载的方法时可能报错
                    hessianProxyFactory.setOverloadEnabled(true); 
                    //True if the proxy should send Hessian 2 requests.
                    hessianProxyFactory.setHessian2Request(true);
                    //True if the proxy can read Hessian 2 responses.
                    hessianProxyFactory.setHessian2Reply(true);
                    try {
                        HessianService<?, ?> hessianService = (HessianService<?, ?>) hessianProxyFactory.create(api,
                                urlName);
                        CACHE.put(urlName, hessianService);
                        return (T) hessianService;
                    } catch (MalformedURLException e) {
                        logger.error("getHessianService from  [" + api + "," + urlName + "]", e);
                        return null;
                    }
                }
            }
        }
    

    如此这般,便实现了跨服数据传输。

    相关文章

      网友评论

        本文标题:Java游戏跨服实现(Hessian+Jetty)

        本文链接:https://www.haomeiwen.com/subject/wreeoctx.html