Netty游戏服务器开发——利用Channel绑定机制 共享聊天服务器与逻辑服务器信息

因为工作原因,之前做的网游项目一直没有时间对其技术点做一个统一的整理,今天正好朋友问到了这个问题,所以顺便把之前项目中的方案贴出来供大家参考。
首先我们需要明白一些概念,我们所玩的网络游戏的服务器是一台大规模的收发兼处理工厂,与客户端交互的数据流如同一个个快递。
Netty的NIO机制保证了服务器与每一个客户端之间都有一条特快专线,即Channel,游戏中的各种交互性数据即是通过这个Channel进行收发的。

现在问题来了,我们假设玩家的战斗行走等信息都是一个个包裹,聊天喊话信息都是一封封信件,客户端与服务器交互时,这些数据都是在一条该客户端独有的Channel里的,无论服务器还是客户端对数据的读取都是顺序执行的,如果此时客户端在接受一个角色战斗信息(包裹)时,聊天窗口有大量的聊天信息(信件)发来,那么客户端需要先读取完排在角色战斗信息(包裹)之前的聊天信息(信件)然后再读取战斗信息,接着才进行相应动画处理,这样就会产生一个delay,影响战斗的流畅感。反之服务器的接收也是如此。

所以为了保证在聊天信息大量收发的同时,战斗及其他游戏信息能够快速被相应,我们需要为每一个客户端再开辟一个收发聊天信息的Channel,把聊天信息通道分离出来。因此需要另外开始一个Netty服务器,即聊天服务器。
架设完成之后你将会看到,每个客户端与服务器之间都产生了2个Channel,分别为包裹专线,和信件专线。并且这两个服务器的Port是不一样的,需要客户端分别进行2次的连接。

这个时候又产生了一个问题,如果此时玩家要发送消息给周围的玩家或是自己的好友,而这些人物的列表并不在聊天服务器上,而是在逻辑服务器上。那么怎样将信息传达到对应人的Channel上呢。
传统的方法可能是在聊天服务器上再复制一份好友列表,甚至周围玩家列表,并且两个服务器之间时刻进行通信,以保证数据的同步性;或者每次发送聊天信息时,调用缓存区的数据,通过目标ID遍历寻找到目标的Channel。

我们来简单分析下这两种方案的运算开销:

方案一中,同步包的心跳最长不得超过1s,否则可能会出现玩家体验上的不同步。那么每1s同步一次,OK,我们设1000个玩家,每1s需要同步1000个玩家的各种信息,当然这显然不现实。所以继续优化方案一,改为触发型同步,即当逻辑服务器上的数据更新后,立即发送该更新数据的同步包给聊天服务器。此时1000个玩家如果聚集在主城附近来回跑动,最频繁的更新当属周围玩家列表的更新,或者采用的是九宫格灯塔AOI(此区域管理算法另外开篇说明)机制不需要周围玩家列表,但仍旧需要不停的更新自己的区域坐标——仍旧是大量的同步包。

在方案二中,假设每分钟单个玩家发送10条Chat信息,同时在线1000个玩家,那么服务器每分钟需要遍历10000次玩家列表来寻找Channel。当然服务器没这么弱小,这些运算量还是可以承受的。但是能承受并不代表我们就能滥用,如果数据量再一次增长呢。

所以以上两个方案可行但并不是目前最优方案,我们应当以最小的代价去获得最大的效益。
事实上我们可以通过Netty的一些特性来进行聊天服务器与逻辑服务器的数据同步,或者准确的说应该是共享。

设玩家类为Player.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.jboss.netty.channel.Channel;
/**
 * 玩家基础类
 * @author BeiTown
 */

public class Player{

    private Channel _mainChannel;// 逻辑服务器频道
    private Channel _chatChannel;// 聊天服务器频道
    private Player[] _mapPlayerArr;//周围玩家数组
    ......
    public Player(Channel ch) {
    this._mainChannel = ch;
    }

    public setChatChannel(Channel ch){
        this._chatChannel = ch;
    }
    ......

}

客户端首先对逻辑服务器进行Socket连接,在连接成功后即将逻辑服务器的Channel绑定在Player上

MainServerHandler.java

1
2
3
4
5
6
7
8
9
private Player _player;

@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {

     _player = new Player(e.getChannel());

}
......

连接完成之后,客户端发送登录信息,将玩家ID登记进服务器缓存Global.GlobalPlayerHashMap中去
ID写入成功后客户端发起对聊天服务器的Socket连接,并最终进行Channel绑定,Chat服务端代码如下

ChatServerHandler.java

1
2
3
4
5
6
7
8
9
private Player _player;

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {

    ......
    _player = Global.GlobalPlayerHashMap.get(packet.getInt());// 获取对象ID
    _player.setChatChannel(e.getChannel());// 绑定聊天端口
    ......

这里说明一下,在申请绑定时客户端需要同时发来玩家的ID,以此在全局玩家HashMap中找到对应的Player对象,并最终进行绑定操作。
一旦绑定成功后,聊天服务器即与逻辑服务器共享了Player对象,并可以通过任意Player对象的_chatChannel进行聊天交互而不影响到_mainChannel的信息吞吐。

当然这个方案还是有一个弊端的,就是必须得将两个服务端放在同一个Main中加载,无法相互独立开启甚至是分出主机。但就目前来说,平台的服务器通常都是8核16G以上,如此高配通常都是好几组游戏在一台主机上跑,更不可能因为资源问题需要将聊天服务器独立到另一台主机上去。

以上是关于Netty游戏服务器开发的一些个人经验,如果大家有更好的处理方案,欢迎交流讨论。

BeiTown
2012.12.18

Tags: , , , , ,

4 Responses to Netty游戏服务器开发——利用Channel绑定机制 共享聊天服务器与逻辑服务器信息

  1. Hi丨猫先生 说道:

    文章字体忽大忽小,什么情况?

  2. JiaoMing42 说道:

    讲得的非常棒,刚好就是我现在在游戏服务器开发中遇到的问题, 非常受益

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>