今天看啥  ›  专栏  ›  小圣996

Java游戏跨服实现(Netty)

小圣996  · 简书  ·  · 2020-02-19 23:04

真正的大师,永远都怀着一颗学徒的心。--剑圣

在前面的《 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服数据,因此项目的启动顺序如上,如果要下载源码运行的话。

源码地址:
https://github.com/zhou-hj/NettyMidServer.git




原文地址:访问原文地址
快照地址: 访问文章快照