Netty游戏服务器开发实战(14):游戏推送的设计

时间:2024-05-19 21:49:02

导读- 本篇主要介绍如何实现游戏服务器推送消息到客户端或者服务器和服务器之间进行消息推送,结合Netty组件,设计一个具有推送功能的高性能游戏服务器框架。

什么是推送?为何需要推送?
首先,我们要明白什么是推送?推送,就是把服务器消息或者某个客户端的消息发送给另外一个客户端或者是服务器,一般对于客户端来说是一个被动接受消息的过程。
推送功能,一般分为在线推送和离线推送,本文主要介绍在线推送功能的实现原理已经常用的游戏场景。
在强联网游戏服务器过程中,一般客户端和服务器保持一条通讯信道,主要用于传输数据进行交互。因此我们主要介绍强联网服务器游戏推送功能。

场景一:slg游戏中的定时任务
知道slg玩法的小伙伴可能会明白,在slg游戏中很多时候我们需要升级建筑,收取资源,行军攻击等操作。当没有游戏中的消耗资源时候我们选择采用时间来换取资源。如我们升级主城建筑,可能需要20分钟,20分钟后服务器需要通知客户端升级建筑已经完成的一条推送消息。等等引用场景在slg游戏中最为常见。如下图:

Netty游戏服务器开发实战(14):游戏推送的设计
一个来回的请求流程主要通过一条信道上完成,上图主要是请求之后立即返回结果信息,但是有时候我们没有资源采用时间来进行消耗时,结构大致如下:

Netty游戏服务器开发实战(14):游戏推送的设计
服务器处理完成后,推送升级完成的消息给客户端,客户端刷新当前数据。

场景二:大世界聊天系统
几乎所有的网络游戏都有大世家聊天功能,主要为了玩家的互动和活跃性,增加游戏的活跃度和用户量,玩家可以通过大世界聊天获得游戏玩法信息等。
在slg游戏中,很多时候采用分区分服模式,也就是一个服务器是一个世界,但是为了游戏活跃,通常让所有的玩家都能够通过大世界进行交互。下面我们来简单的通过代码来实现一个游戏推送功能。

通过前面几篇文章介绍的基础框架基础上。利用客户端和服务器连接对象。实现一个推送系统。

实现原理
长链接网络实时通信。
每一条推送都有自己的消息id,通过不同的消息id来携带不同的数据,给每一条推送消息标记一个id


/**
 * @author twjitm 2019/4/6/21:06
 */
public final class MessageCmd {
    /**
     * 请求消息号
     */
    public static final int REQUEST_BASE = 1000;
    public static final int GET_USER_INFO = REQUEST_BASE + 1;
    public static final int AUTH_ACCOUNT = REQUEST_BASE + 2;
    public static final int LOGIN = REQUEST_BASE + 3;
    public static final int SEND_CHAT = REQUEST_BASE + 4;
    public static int GET_CHAT_RECORD = REQUEST_BASE + 5;


    /**
     * 推送消息号
     */
    public final static class PUSH_MESSAGE_CMD {
        static final int PUSH_BASE = 9999;
        public static final short PUSH_CHAT = PUSH_BASE + 1;
    }

    /**
     * rpc 消息号
     */
    public final static class RCP_MESSAGE_CMD {
        static final int RPC_BASE = 19999;


    }
}


例如定义不同的消息id。

通过定义proto文件如下消息体

// 聊天消息体
message PChatInfo {
    optional int64 uid=1;
    optional int64 targetUid=2;
    optional int64 mid=3;
    optional int32 type=4;//1文字,2表情,3图片,。。。。后面再添加
    optional string context=5;
    optional int64 isRead=6;
    optional int64 cTime=7;
}


当用户登陆的时候,保存用户链接信息;

public class IoClientManager {
    private static final AttributeKey<Long> SESSION_CLIENT_ID = AttributeKey.valueOf("SESSION_CLIENT_ID");
    private static Map<Long, UserInfo> onLineSession = new ConcurrentHashMap<>();


    public static void put(UserInfo userInfo) {

        userInfo.getChannel().attr(SESSION_CLIENT_ID).set(userInfo.getUid());
        onLineSession.putIfAbsent(userInfo.getUid(), userInfo);
    }

    public static UserInfo getOnlineUser(long uid) {
        return onLineSession.get(uid);
    }

    public static UserInfo getUserInfo(IoClient client) {
        long uid = client.getChannel().attr(SESSION_CLIENT_ID).get();
        return getUserInfo(uid);
    }

    public static UserInfo getUserInfo(long uid) {
        UserInfo userInfo = getOnlineUser(uid);
        if (userInfo == null) {
            //db
        }
        return userInfo;
    }


}



最后利用保存的链接对象channel进行数据发送;

/**
 * @author twjitm 2019/4/16/23:49
 */
@MessageAnnotation(cmd = MessageCmd.SEND_CHAT)
public class SendChat extends AbstractTcpHandler {
    @Override
    public void handler(IoClient client, short cmd, byte[] bytes) {
        //服务器收到客户端发送过来的消息
        try {
            PSendChat pSendChat = PSendChat.parseFrom(bytes);
            UserInfo userInfo = IoClientManager.getUserInfo(client);
            long uid = userInfo.getUid();
            PChatInfo pChat = pSendChat.getChat();
            long toUid = pChat.getTargetUid();
            int chatType = pChat.getType();

            UserInfo targetUser = IoClientManager.getOnlineUser(toUid);
            if (targetUser != null) {
                PushChatMessage.Builder pushChatMessage = PushChatMessage.newBuilder();
                pushChatMessage.setChatInfo(pChat);
                //往别的客户端推送消息,暂时不考虑分布式
                targetUser.writeData(MessageCmd.PUSH_MESSAGE_CMD.PUSH_CHAT, pushChatMessage.build().toByteArray());
            }
            //保存消息
            //Todo


        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }

    }
}