倘若你迷失在黑暗之中,除了前行别无他法。 --深海泰坦
现在的游戏基本都有跨服玩法,跨服玩法是一个游戏的核心玩法,承载着游戏趣味、人气及盈利的重要使命,因此每个游戏基本都有跨服实现。
跨服实现需根据游戏的类型去选择跨服方案,假如你的游戏是回合制的,即实时性要求不是很高的,一入场就秒算战斗流程的,那跨服协议可以选择用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;
}
}
}
}
如此这般,便实现了跨服数据传输。