Files
youlegames/codes/games/server/docs/guides/framework/03-RPC处理机制.md
2026-02-04 23:47:45 +08:00

26 KiB
Raw Blame History

RPC处理机制详解

文档目标详细说明进贤麻将的RPC请求处理流程包括RpcHandler、OperationEnumerator、AIRpcHandler的功能和使用方法。

📚 目录

  1. RPC机制概述
  2. RpcHandler - RPC请求处理器
  3. OperationEnumerator - 操作列举器
  4. AIRpcHandler - AI玩家处理器
  5. RPC处理标准流程
  6. 数据包构建规范
  7. 实现示例

1. RPC机制概述

1.1 什么是RPC

RPC (Remote Procedure Call) 是远程过程调用,允许客户端通过网络调用服务端的方法。

在友乐游戏框架中:

客户端(浏览器)              服务端Node.js
      │                            │
      │ WebSocket/HTTP              │
      │ ────────────────────────>  │
      │  {app, route, rpc, data}   │
      │                            │
      │                         packet.js
      │                            ↓
      │                         youle_app
      │                            ↓
      │                      mod_jinxianmahjong
      │                            ↓
      │                    mod_jinxianmahjong.player_discard(pack)
      │                            ↓
      │                        处理业务逻辑
      │                            ↓
      │  <────────────────────────│
      │    响应数据包               │

1.2 三层路由机制

【第1层】packet.js - 网络层
    ↓ 根据app字段路由
【第2层】youle_app - 应用层  
    ↓ 根据route字段路由
【第3层】mod_jinxianmahjong - 模块层
    ↓ 根据rpc字段调用
【执行】RPC方法执行

示例数据包

{
    "app": "youle",              // 应用标识 → 路由到youle_app
    "route": "jinxianmahjong",   // 模块标识 → 路由到mod_jinxianmahjong
    "rpc": "player_discard",     // 方法名 → 调用mod_jinxianmahjong.player_discard()
    "data": {                    // 业务数据
        "agentid": "agent001",
        "playerid": 12345,
        "gameid": "jinxianmahjong",
        "roomcode": 100001,
        "seat": 0,
        "cardUniqueId": 45
    }
}

1.3 进贤麻将RPC架构

┌────────────────────────────────────────┐
│           mod.js (RPC定义)              │
│  - 定义所有RPC方法                      │
│  - mod_jinxianmahjong.player_discard   │
│  - mod_jinxianmahjong.player_peng      │
│  - mod_jinxianmahjong.player_gang      │
│  - ...                                 │
└───────────────┬────────────────────────┘
                │ 调用
┌───────────────▼────────────────────────┐
│         RpcHandler.js                  │
│  - 处理RPC请求                         │
│  - 参数提取和验证                      │
│  - 调用业戏控制器                      │
│  - 构建响应数据包                      │
└───────────────┬────────────────────────┘
                │ 委托
┌───────────────▼────────────────────────┐
│      GameController / OperationManager │
│  - 游戏逻辑控制                        │
│  - 状态管理                            │
│  - 操作验证                            │
└───────────────┬────────────────────────┘
                │ 发包
┌───────────────▼────────────────────────┐
│         o_room.method.sendpack_*       │
│  - sendpack_toall()                    │
│  - sendpack_toseat()                   │
│  - sendpack_toother()                  │
└────────────────────────────────────────┘

2. RpcHandler - RPC请求处理器

2.1 RpcHandler的职责

RpcHandler是进贤麻将的核心RPC处理模块,负责:

  1. 接收和解析RPC请求:提取参数、验证格式
  2. 玩家身份验证:调用check_player验证玩家
  3. 委托业务逻辑调用GameController或OperationManager
  4. 构建响应数据:按"一包多信息"原则构建响应
  5. 发送响应:调用发包接口推送给客户端
  6. 错误处理:统一的错误处理和日志记录

2.2 主要RPC方法

进贤麻将实现的核心RPC方法

RPC方法 对应操作 说明
handlePlayCard 出牌 玩家打出一张手牌
handleDeclarePeng 碰牌 玩家碰别人打出的牌
handleDeclareGang 杠牌 玩家杠牌(明杠/暗杠/加杠)
handleDeclareHu 胡牌 玩家胡牌(自摸/点炮)
handlePass 过牌 玩家放弃吃碰杠胡机会
handleReady 准备 玩家准备开始游戏
getGameState 获取状态 获取当前游戏状态

2.3 RPC方法标准结构

每个RPC方法遵循统一的处理流程

handlePlayCard: function(pack) {
    try {
        // ===== 第1步提取参数 =====
        var agentid = pack.data.agentid;
        var playerid = parseInt(pack.data.playerid);
        var gameid = pack.data.gameid;
        var roomcode = pack.data.roomcode;
        var seat = parseInt(pack.data.seat);
        var cardUniqueId = parseInt(pack.data.cardUniqueId);
        
        console.log('[RpcHandler.playCard] 开始处理出牌请求:', {
            playerid, seat, cardUniqueId
        });
        
        // ===== 第2步验证玩家 =====
        var o_room = mod_jinxianmahjong.import.check_player(
            agentid, gameid, roomcode, seat, playerid, 
            pack.conmode, pack.fromid
        );
        
        if (!o_room) {
            console.error('[RpcHandler.playCard] 玩家验证失败');
            return { success: false, error: "玩家验证失败" };
        }
        
        // ===== 第3步获取游戏对象 =====
        var o_desk = o_room.o_desk;
        if (!o_desk) {
            console.error('[RpcHandler.playCard] 游戏桌不存在');
            this.sendErrorResponse(o_room, seat, 500, "游戏桌不存在");
            return { success: false, error: "游戏桌不存在" };
        }
        
        // ===== 第4步记录收包调试 =====
        if (o_desk.debug && typeof o_desk.debug.save_receivepack === 'function') {
            o_desk.debug.save_receivepack(pack, seat, playerid);
        }
        
        // ===== 第5步委托业务逻辑 =====
        var operationRequest = {
            operation: "discard_card",
            playerSeat: seat,
            uniqueId: cardUniqueId,
            requestId: this._generateRequestId(),
            timestamp: Date.now()
        };
        
        var operationResult = OperationManager.handleOperation(o_room, operationRequest);
        
        // ===== 第6步处理结果 =====
        if (operationResult.success) {
            // 构建响应数据包
            var responseData = this._buildPlayCardResponse(o_room, seat, cardUniqueId, operationResult);
            
            // 发送响应
            this._sendResponse(o_room, seat, responseData);
            
            return { success: true };
        } else {
            // 操作失败,发送错误
            this.sendErrorResponse(o_room, seat, 400, operationResult.error);
            return { success: false, error: operationResult.error };
        }
        
    } catch (error) {
        console.error('[RpcHandler.playCard] 处理失败:', error);
        this.sendErrorResponse(o_room, seat, 500, "服务器内部错误");
        return { success: false, error: error.message };
    }
}

2.4 "一包多信息"原则

RpcHandler遵循友乐平台的**"一包多信息"设计原则**

原则说明

  • 单个响应包包含完整的状态更新信息
  • 减少网络请求次数
  • 确保客户端状态同步

示例:出牌响应包包含

{
    status: 200,
    seat: 0,                         // 出牌玩家
    discardedCard: {...},            // 打出的牌
    gameState: {                     // 游戏状态更新
        currentPlayer: 1,
        remainingCards: 52,
        phase: "playing"
    },
    playerActions: {                 // 其他玩家可执行操作
        1: {
            availableActions: ["peng", "gang", "hu"],
            pengResult: {...},       // 碰牌详情
            gangResult: {...},       // 杠牌详情
            huResult: {...},         // 胡牌详情
            timeout: 10000
        },
        2: {
            availableActions: ["hu"],
            huResult: {...},
            timeout: 10000
        }
    },
    autoDrawCard: null,              // 是否自动摸牌
    waitingForResponse: true         // 是否等待响应
}

2.5 分层推送机制

RpcHandler实现分层推送,为不同玩家定制不同的信息:

// 1. 给操作玩家:包含完整信息
var dataForPlayer = {
    status: 200,
    seat: 0,
    myHandCards: [...],        // 我的手牌(完整)
    discardedCard: {...},
    gameState: {...}
};
o_room.method.sendpack_toseat(dataForPlayer, 0);

// 2. 给其他玩家:隐藏私密信息
var dataForOthers = {
    status: 200,
    seat: 0,
    handCardCount: 13,         // 只显示数量,不显示具体牌
    discardedCard: {...},
    gameState: {...}
};
o_room.method.sendpack_toother(dataForOthers, 0);

3. OperationEnumerator - 操作列举器

3.1 OperationEnumerator的职责

OperationEnumerator负责生成玩家可执行的所有操作选项

  1. 枚举所有可能操作:出牌、吃、碰、杠、胡、过
  2. 计算操作参数:每个操作需要的牌、组合等
  3. 生成choiceIndex:为每个操作分配索引
  4. 验证操作合法性:确保操作符合规则

3.2 操作类型

var operationTypes = {
    discard: [],    // 出牌操作
    chi: [],        // 吃牌操作
    peng: [],       // 碰牌操作
    gang: [],       // 杠牌操作(明杠、暗杠、加杠)
    hu: [],         // 胡牌操作
    pass: []        // 过牌操作
};

3.3 核心方法

generateAvailableOperations

生成完整的可执行操作列表:

/**
 * 为指定玩家生成完整的可执行操作列表
 * @param {Object} gameState - 当前游戏状态
 * @param {number} seat - 玩家座位号
 * @param {Object} context - 上下文信息
 * @returns {Object} 完整的操作列表
 */
generateAvailableOperations(gameState, seat, context = {}) {
    var operations = {
        discard: [],
        chi: [],
        peng: [],
        gang: [],
        hu: [],
        pass: []
    };

    try {
        // 1. 生成出牌操作
        operations.discard = this.generateDiscardActions(gameState, seat);
        
        // 2. 如果有刚出的牌,检查吃碰杠胡操作
        if (context.lastDiscardCard) {
            operations.chi = this.generateChiActions(gameState, seat, context.lastDiscardCard);
            operations.peng = this.generatePengActions(gameState, seat, context.lastDiscardCard);
            operations.gang = this.generateMingGangActions(gameState, seat, context.lastDiscardCard);
            operations.hu = this.generateHuActions(gameState, seat, context.lastDiscardCard, "discard");
        }
        
        // 3. 检查自摸杠牌和胡牌
        if (context.justDrawCard) {
            operations.gang = operations.gang.concat(this.generateAnGangActions(gameState, seat));
            operations.gang = operations.gang.concat(this.generateJiaGangActions(gameState, seat));
            operations.hu = operations.hu.concat(this.generateHuActions(gameState, seat, context.justDrawCard, "draw"));
        }
        
        // 4. 生成过牌操作
        if (this.hasNonDiscardOperations(operations)) {
            operations.pass = this.generatePassAction();
        }

    } catch (error) {
        console.error('[OperationEnumerator] 生成操作列表时出错:', error);
    }

    return operations;
}

generateDiscardActions

生成出牌操作:

generateDiscardActions(gameState, seat) {
    if (!gameState.playerHands || !gameState.playerHands[seat]) {
        return [];
    }

    var handCards = gameState.playerHands[seat];
    var allowedCards = [];

    // 获取所有手牌的uniqueId
    for (var i = 0; i < handCards.length; i++) {
        if (handCards[i] && handCards[i].uniqueId) {
            allowedCards.push(handCards[i].uniqueId);
        }
    }

    return [{
        choiceIndex: 0,
        operationType: "discard",
        allowedCards: allowedCards,
        description: "出牌操作"
    }];
}

generatePengActions

生成碰牌操作:

generatePengActions(gameState, seat, sourceCard, fromSeat) {
    var pengActions = [];
    
    if (!gameState.playerHands || !gameState.playerHands[seat] || !sourceCard) {
        return pengActions;
    }

    var handCards = gameState.playerHands[seat];
    var sourceCode = sourceCard.code || sourceCard;

    // 检查手牌中是否有至少2张相同的牌
    var sameCards = this.findSameCards(handCards, sourceCode, 2);
    
    if (sameCards.length >= 2) {
        pengActions.push({
            choiceIndex: 0,
            operationType: "peng",
            sourceCard: {
                uniqueId: sourceCard.uniqueId,
                code: sourceCode,
                fromSeat: fromSeat
            },
            requiredCards: sameCards.slice(0, 2),
            description: "碰" + this.getCardName(sourceCode)
        });
    }

    return pengActions;
}

3.4 choiceIndex机制

choiceIndex是操作选择的索引,用于客户端选择和服务端执行:

// 服务端生成操作列表
var operations = OperationEnumerator.generateAvailableOperations(gameState, seat, context);

// 示例输出
{
    peng: [
        { choiceIndex: 0, operationType: "peng", ... },  // 碰1万
    ],
    gang: [
        { choiceIndex: 0, operationType: "gang", gangType: "angang", ... },  // 暗杠2万
        { choiceIndex: 1, operationType: "gang", gangType: "jiagang", ... }  // 加杠3万
    ],
    hu: [
        { choiceIndex: 0, operationType: "hu", huType: "zimo", ... }
    ]
}

// 客户端选择操作发送choiceIndex
// 例如选择加杠操作choiceIndex=1
var request = {
    operation: "gang",
    choiceIndex: 1,    // 选择第2个杠牌操作
    // ...
};

// 服务端根据choiceIndex执行对应操作
var selectedOperation = operations.gang[choiceIndex];

4. AIRpcHandler - AI玩家处理器

4.1 AIRpcHandler的职责

AIRpcHandler处理AI玩家的自动操作

  1. AI决策:根据游戏状态做出决策
  2. 自动操作:自动出牌、吃碰杠胡
  3. 延时模拟:模拟人类玩家的思考时间
  4. 策略选择:根据难度选择不同策略

4.2 AI决策流程

// AI玩家轮到操作
AIRpcHandler.handleAITurn(o_room, aiSeat) {
    // 1. 获取可执行操作
    var operations = OperationEnumerator.generateAvailableOperations(
        gameState, aiSeat, context
    );
    
    // 2. AI决策
    var decision = AIStrategy.makeDecision(gameState, aiSeat, operations);
    
    // 3. 延时模拟思考
    setTimeout(function() {
        // 4. 执行AI操作
        if (decision.operation === "discard") {
            AIRpcHandler.executeAIDiscard(o_room, aiSeat, decision.cardUniqueId);
        } else if (decision.operation === "peng") {
            AIRpcHandler.executeAIPeng(o_room, aiSeat, decision);
        }
        // ...
    }, decision.thinkingTime);
}

4.3 AI策略

// 简单策略示例
AIStrategy.makeDecision = function(gameState, seat, operations) {
    // 优先级:胡 > 杠 > 碰 > 吃 > 出牌
    
    if (operations.hu.length > 0) {
        return { operation: "hu", choiceIndex: 0 };
    }
    
    if (operations.gang.length > 0) {
        return { operation: "gang", choiceIndex: 0 };
    }
    
    if (operations.peng.length > 0) {
        return { operation: "peng", choiceIndex: 0 };
    }
    
    // 默认出牌:出最不需要的牌
    var cardToDiscard = this.selectCardToDiscard(gameState, seat);
    return { 
        operation: "discard", 
        cardUniqueId: cardToDiscard.uniqueId 
    };
};

5. RPC处理标准流程

5.1 完整RPC处理流程图

客户端发送请求
    ↓
【1】packet.js接收
    ↓
【2】youle_app路由
    ↓
【3】mod_jinxianmahjong路由
    ↓
【4】RPC方法如player_discard
    ↓
【5】RpcHandler.handlePlayCard
    ├─ 提取参数
    ├─ check_player验证
    ├─ 记录收包
    └─ 委托OperationManager
        ↓
【6】OperationManager.handleOperation
    ├─ 验证操作合法性
    ├─ 执行游戏逻辑
    ├─ 更新游戏状态
    └─ 返回结果
        ↓
【7】RpcHandler构建响应
    ├─ 基础响应数据
    ├─ 检查其他玩家操作机会
    ├─ 添加操作提示
    └─ 构建完整响应包
        ↓
【8】发送响应
    ├─ sendpack_toseat给操作玩家
    ├─ sendpack_toother给其他玩家
    └─ sendpack_toall广播给所有人
        ↓
客户端接收响应
    ├─ 更新本地状态
    ├─ 播放动画
    └─ 显示界面

5.2 标准RPC方法实现模板

// 在mod.js中定义RPC方法
mod_jinxianmahjong.player_discard = function(pack) {
    return RpcHandler.handlePlayCard(pack);
};

mod_jinxianmahjong.player_peng = function(pack) {
    return RpcHandler.handleDeclarePeng(pack);
};

mod_jinxianmahjong.player_gang = function(pack) {
    return RpcHandler.handleDeclareGang(pack);
};

mod_jinxianmahjong.player_hu = function(pack) {
    return RpcHandler.handleDeclareHu(pack);
};

mod_jinxianmahjong.player_pass = function(pack) {
    return RpcHandler.handlePass(pack);
};

6. 数据包构建规范

6.1 响应包标准结构

{
    status: 200,                    // HTTP状态码风格
    message: "操作成功",            // 消息说明
    data: {                         // 业务数据
        // 具体业务数据
    },
    timestamp: 1234567890           // 时间戳
}

6.2 错误响应结构

{
    status: 400,                    // 错误状态码
    error: "操作失败",              // 错误消息
    code: "INVALID_OPERATION",      // 错误码
    details: "详细错误信息",        // 详细说明
    timestamp: 1234567890
}

6.3 状态码规范

状态码 含义 使用场景
200 成功 操作执行成功
400 请求错误 参数错误、操作不合法
401 未授权 玩家验证失败
403 禁止操作 不是当前玩家的回合
404 未找到 房间或玩家不存在
500 服务器错误 内部错误

7. 实现示例

7.1 完整的出牌RPC实现

// mod.js - 定义RPC方法
mod_jinxianmahjong.player_discard = function(pack) {
    return RpcHandler.handlePlayCard(pack);
};

// RpcHandler.js - 实现处理逻辑
RpcHandler.handlePlayCard = function(pack) {
    try {
        // 1. 提取和验证参数
        var params = this._extractParams(pack);
        if (!params.valid) {
            return { success: false, error: params.error };
        }
        
        // 2. 验证玩家
        var o_room = mod_jinxianmahjong.import.check_player(
            params.agentid, params.gameid, params.roomcode, 
            params.seat, params.playerid, pack.conmode, pack.fromid
        );
        
        if (!o_room) {
            return { success: false, error: "玩家验证失败" };
        }
        
        // 3. 执行出牌操作
        var result = OperationManager.handleOperation(o_room, {
            operation: "discard_card",
            playerSeat: params.seat,
            uniqueId: params.cardUniqueId
        });
        
        if (!result.success) {
            this.sendErrorResponse(o_room, params.seat, 400, result.error);
            return result;
        }
        
        // 4. 构建响应数据
        var responseData = this._buildPlayCardResponse(o_room, params.seat, result);
        
        // 5. 发送响应
        this._sendLayeredResponse(o_room, params.seat, responseData);
        
        return { success: true };
        
    } catch (error) {
        console.error('[RpcHandler.playCard] 错误:', error);
        return { success: false, error: error.message };
    }
};

// 辅助方法:构建响应数据
RpcHandler._buildPlayCardResponse = function(o_room, seat, result) {
    var gameState = o_room.o_desk.gameState;
    
    return {
        status: 200,
        seat: seat,
        discardedCard: this._serializeCard(result.discardedCard),
        gameState: {
            phase: gameState.phase,
            currentPlayer: gameState.currentPlayer,
            remainingCards: gameState.gameData.deck.length
        },
        playerActions: this._buildPlayerActions(o_room, result),
        waitingForResponse: result.hasResponse,
        timestamp: Date.now()
    };
};

// 辅助方法:分层发送响应
RpcHandler._sendLayeredResponse = function(o_room, seat, responseData) {
    // 给操作玩家:包含完整信息
    var dataForPlayer = Object.assign({}, responseData, {
        myHandCards: this._serializeHandCards(o_room, seat)
    });
    o_room.method.sendpack_toseat(dataForPlayer, seat);
    
    // 给其他玩家:隐藏私密信息
    var dataForOthers = Object.assign({}, responseData, {
        handCardCount: this._getHandCardCount(o_room, seat)
    });
    o_room.method.sendpack_toother(dataForOthers, seat);
};

7.2 操作枚举示例

// 使用OperationEnumerator
var operations = OperationEnumerator.generateAvailableOperations(
    gameState, 
    seat, 
    {
        lastDiscardCard: { uniqueId: 45, code: 13 },  // 刚出的3万
        fromSeat: 1
    }
);

// 输出示例
{
    discard: [],
    chi: [],
    peng: [
        {
            choiceIndex: 0,
            operationType: "peng",
            sourceCard: { uniqueId: 45, code: 13, fromSeat: 1 },
            requiredCards: [
                { uniqueId: 12, code: 13 },
                { uniqueId: 78, code: 13 }
            ],
            description: "碰3万"
        }
    ],
    gang: [],
    hu: [
        {
            choiceIndex: 0,
            operationType: "hu",
            huType: "dianpao",
            winCards: [...],
            score: 8,
            description: "胡牌 - 8分"
        }
    ],
    pass: [
        {
            choiceIndex: 0,
            operationType: "pass",
            description: "过"
        }
    ]
}

8. 最佳实践

8.1 参数提取

// ✅ 统一的参数提取方法
_extractParams: function(pack) {
    try {
        return {
            valid: true,
            agentid: pack.data.agentid,
            playerid: parseInt(pack.data.playerid),
            gameid: pack.data.gameid,
            roomcode: pack.data.roomcode,
            seat: parseInt(pack.data.seat),
            cardUniqueId: parseInt(pack.data.cardUniqueId)
        };
    } catch (error) {
        return {
            valid: false,
            error: "参数解析失败: " + error.message
        };
    }
}

8.2 错误处理

// ✅ 统一的错误响应
sendErrorResponse: function(o_room, seat, statusCode, message) {
    var errorMsg = {
        app: "youle",
        route: "jinxianmahjong",
        rpc: "error",
        data: {
            status: statusCode,
            error: message,
            timestamp: Date.now()
        }
    };
    o_room.method.sendpack_toseat(errorMsg, seat);
}

8.3 日志记录

// ✅ 详细的日志记录
console.log('[RpcHandler.playCard] 开始处理:', {
    playerid: params.playerid,
    seat: params.seat,
    cardUniqueId: params.cardUniqueId
});

console.log('[RpcHandler.playCard] 操作结果:', {
    success: result.success,
    error: result.error
});

9. 常见问题

Q1: RpcHandler和OperationManager的区别

A:

  • RpcHandler处理RPC请求负责参数提取、验证、响应构建
  • OperationManager:执行游戏逻辑,负责状态管理、规则验证

Q2: 为什么需要OperationEnumerator

A: 因为需要提前告诉客户端有哪些操作可以执行客户端根据choiceIndex选择操作。

Q3: choiceIndex有什么用

A: choiceIndex是操作选择的索引

  • 服务端生成所有可能操作并分配索引
  • 客户端选择后发送choiceIndex
  • 服务端根据索引执行对应操作

Q4: 如何实现分层推送?

A: 使用不同的发包接口:

  • sendpack_toseat:发给操作玩家(完整信息)
  • sendpack_toother:发给其他玩家(隐藏私密信息)

Q5: AI玩家如何处理

A: AI玩家通过AIRpcHandler自动处理

  • 监听游戏状态变化
  • 自动调用AI决策
  • 模拟延时后执行操作

10. 下一步

阅读以下文档继续学习:


相关代码文件

  • server/games2/jinxianmahjong/mod.js - RPC方法定义
  • server/games2/jinxianmahjong/rpc/RpcHandler.js - RPC处理器
  • server/games2/jinxianmahjong/rpc/OperationEnumerator.js - 操作列举器
  • server/games2/jinxianmahjong/rpc/AIRpcHandler.js - AI处理器