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

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

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

跨服实现需根据游戏的类型去选择跨服方案,假如你的游戏是回合制的,即实时性要求不是很高的,一入场就秒算战斗流程的,那跨服协议可以选择用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;
                }
            }
        }
    }

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容