真正的大师,永远都怀着一颗学徒的心。--剑圣
在前面的《Java游戏跨服实现(Hessian+Jetty)》一文中,讲述了用Hessian+Jetty方式实现跨服,其中也讲到了这是基于Http协议的,大家知道,Http协议是一种短连接,无状态的协议,每次请求都会有3次握手,因为无状态,每次包头会带上重复信息,显得冗余占用带宽且影响效率。但是,这在卡牌类的游戏,即对延迟要求不高的场景中它是行得通的,且好用又方便集成到java项目中。
如果游戏需要使用长连接且实时性高,那么TCP协议将有很大可能纳入考虑之中,本文即以Netty+Protobuf实现游戏跨服TCP通信。在之前的另一篇文章中《使用Netty+Protobuf实现游戏TCP通信》仅讲述了客户端到服务端的通信,即客户端A使用Netty客户端启动类,服务端B使用Netty服务端启动类。但是在游戏跨服中,它是如下的一种通信情形:
客户端A -> 游戏服B -> 跨服C
另:游戏服架构方案可参考文章《游戏架构方案》
这三个服都以Netty作为网络框架,A为Netty客户端,C为Netty服务端,但是B既要作为A的服务端,又需要是C的客户端,因此B中既需要Netty的服务端启动类,也需要Netty的客户端启动类。
据说最佳实践是在服务器B的handler中的channelactive方法中,创建一个客户端的引导类,该引导类复用服务器B的worker eventLoop,然后连接C服就差不多了。今天在家办公没什么事,于是自己动手实现了下。
源码用的是《使用Netty+Protobuf实现游戏TCP通信》中客户端和服务端源码,把服务端项目再复制一个出来,作为中间服(游戏服B),然后在其ServerHandler中,添加如下代码,再做些协议测试,发现是可行的。
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
System.out.println("【B服】 A->B客户端channel:"+ctx.channel().id().asLongText()+"激活!");
aTobClientsMap.put(ctx.channel(), ctx.channel());
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(ctx.channel().eventLoop()) //这里不能用new NioEventLoopGroup(1),否则会多很多线程出来,就不是eventLoop复用了
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();//为每个连接新建ChannelPipeline
pipeline.addLast("decoder", new ProtoDecoder(5120));
pipeline.addLast("encoder", new ProtoEncoder(2048));
pipeline.addLast("serverHandler", new MidClientHandler());
}
});
bootstrap.connect(new InetSocketAddress("192.168.1.2", 38997))
.addListener(new ChannelFutureListener(){
@Override
public void operationComplete(ChannelFuture future)
throws Exception {
aTobClientsMap.put(ctx.channel(), future.channel());
System.out.println("【B服】 A->B客户端channel:" + ctx.channel().id().asLongText()
+ ",【B服】 B->C客户端channel:"+future.channel().id().asLongText());
}
});
}
每个客户端激活时都会进来这里,这里执行时还处于worker线程组的IO线程中,利用每个IO线程所在的EventLoop与跨服C新建了一个连接通道ChanneBC,因此每个客户端A除了和游戏服B之间维持了一个ChannelAB之外,还和跨服C维持了另一个ChanneBC,这个ChanneBC和ChannelAB是一一对应的,它们与跨服C的通信便在ChanneBC中进行的。因此,这些ChannelBC通道也需在游戏服B上维护起来。这里的每个IO线程请求与跨服C连接时,在跨服C上,仍然会有一个boss线程组去处理这些连接请求,连接成功后,便会分配跨服C上的worker线程组管理这些新建的Channel。
注意,使用bootstrap.connect(new InetSocketAddress("192.168.1.2", 38997))时,不能像客户端那样使用bootstrap.connect(new InetSocketAddress("192.168.1.2", 38997)).sync();否则容易发生死锁报错:
io.netty.util.concurrent.BlockingOperationException
请见《分析 Netty 死锁异常 BlockingOperationException》分析。
原因及建议如下:
其余的测试协议的过程略,感兴趣的请见源码。
最终A服请求协议及输出为:
A服协议及请求B服数据.png
B服协议及输出为(AB服需共有一份协议文件AProto.proto):
B服转发A服请求数据1001给C服,并将C服返回的数据3001给A服
C服协议及输出为(BC服需共有一份协议文件BProto.proto):
C服收到B服转发的数据,并将数据3001再让B服转发给A服
最终的线程关系为:
最终线程显示.png
由此采用Netty+Protobuf实现了跨服游戏TCP通信。
其中
跨服C项目为NettyProtobufTcpServer(先启);
游戏服B项目为NettyProtobufMidTcpServer(次启);
客户端A项目为NettyProtobufTcpClient(最后启)。
因为代码中在A服Channel激活后,便会发协议请求B服数据,因此项目的启动顺序如上,如果要下载源码运行的话。
网友评论