UE 网络笔记1

概述

https://www.cwlgame.cn/2024/06/27/ue-%e7%bd%91%e7%bb%9c%e5%88%9d%e5%a7%8b%e5%8c%96%e6%b5%81%e7%a8%8b/

以前写的

本文主要由AI编写

核心分层

┌────────────────────────────────────────────────────────────────────┐
│                        游戏逻辑层                                   │
│  Actor / Component / GameMode / PlayerController / GameState       │
├────────────────────────────────────────────────────────────────────┤
│                        复制层(Replication)                        │
│  属性复制 / RPC / 预测 / 校正                                       │
│  FObjectReplicator / FRepLayout / FRepState                        │
├────────────────────────────────────────────────────────────────────┤
│                        通道层(Channel)                            │
│  UActorChannel / UControlChannel / UVoiceChannel                   │
├────────────────────────────────────────────────────────────────────┤
│                        连接层(Connection)                         │
│  UNetConnection → SendBuffer / ReceiveBuffer                       │
├────────────────────────────────────────────────────────────────────┤
│                        驱动层(Driver)                             │
│  UNetDriver (UIpNetDriver) → Socket / Packet 收发                  │
├────────────────────────────────────────────────────────────────────┤
│                        传输层(Transport)                          │
│  UDP Socket → Internet                                             │
└────────────────────────────────────────────────────────────────────┘

数据流向

属性复制(服务器→客户端)

Actor 属性变化
  → FObjectReplicator 检测属性差异(FRepLayout 比较)
  → 序列化变化的属性到 FOutBunch
  → UActorChannel 打包 Bunch
  → UNetConnection 合并到 SendBuffer
  → UNetDriver 通过 UDP Socket 发送 Packet
  → 客户端 UNetDriver 接收
  → UNetConnection 解包
  → UActorChannel 派发 Bunch
  → FObjectReplicator 反序列化并应用属性

RPC(双向)

调用 UFUNCTION(Server/Client/NetMulticast)
  → AActor::CallRemoteFunction()
  → UActorChannel::QueueRemoteFunctionBunch() 或直接发送
  → 序列化函数参数到 Bunch
  → 通过 Channel → Connection → Driver 发送
  → 接收端 FObjectReplicator::ReceivedBunch() 
  → ProcessRemoteFunction() 反序列化参数并调用函数

网络模式(NetMode)

// EngineBaseTypes.h
enum ENetMode
{
    NM_Standalone,      // 单机模式,无网络。仍被视为服务器(拥有全部服务器功能)
    NM_DedicatedServer, // 专用服务器,无本地玩家,无渲染
    NM_ListenServer,    // 监听服务器,本地玩家同时是Host
    NM_Client,          // 网络客户端,连接到远程服务器
};

各模式关键区别

特性 Standalone DedicatedServer ListenServer Client
有服务器逻辑
有本地玩家
渲染/显示
GameMode 存在
Actor 复制方向 Server→Client Server→Client 接收

常用接口

// 在 Actor/Component 中
GetNetMode()                        // 返回 ENetMode
HasAuthority()                      // 等价于 Role == ROLE_Authority
IsNetMode(NM_DedicatedServer)       // 快速判断

// 实用宏/模式
if (GetLocalRole() == ROLE_Authority)       // 当前进程对此 Actor 有权威
if (GetRemoteRole() == ROLE_SimulatedProxy) // 远端是模拟代理

网络角色(NetRole)

// EngineTypes.h
enum ENetRole : int
{
    ROLE_None,            // 无角色(不参与网络)
    ROLE_SimulatedProxy,  // 模拟代理:服务器复制状态,客户端模拟插值
    ROLE_AutonomousProxy, // 自主代理:客户端本地控制,拥有输入权
    ROLE_Authority,       // 权威:拥有最终决定权的服务器端
};

每个 Actor 有两个角色属性:

  • RoleGetLocalRole()):此 Actor 在当前进程中的角色
  • RemoteRoleGetRemoteRole()):此 Actor 在远端进程中的角色
服务器端的 PlayerCharacter:
  Role = ROLE_Authority          ← 服务器是权威
  RemoteRole = ROLE_AutonomousProxy  ← 客户端是自主代理(因为玩家自己控制)

该 PlayerCharacter 在控制它的客户端:
  Role = ROLE_AutonomousProxy    ← 本地是自主代理
  RemoteRole = ROLE_Authority    ← 远端是权威(服务器)

其他客户端看到的该 PlayerCharacter:
  Role = ROLE_SimulatedProxy     ← 仅模拟
  RemoteRole = ROLE_Authority    ← 远端是权威

Role 的决定因素

条件 服务器端 Role 控制端 Client Role 其他 Client Role
玩家控制的 Pawn Authority AutonomousProxy SimulatedProxy
PlayerController Authority AutonomousProxy 不复制
一般 replicated Actor
(如果不复制的actor只存在于spawn那一端)
Authority SimulatedProxy SimulatedProxy
bOnlyRelevantToOwner 的物品 Authority AutonomousProxy 不可见

核心类层次结构

UNetDriver — 网络驱动

Engine/Classes/Engine/NetDriver.h

网络系统的核心,负责 Socket 管理、Packet 收发、连接管理。

class UNetDriver : public UObject
{
    UWorld* World;                      // 所属世界
    UNetConnection* ServerConnection;   // [客户端] 到服务器的连接(服务器端为 null)
    TArray<UNetConnection*> ClientConnections; // [服务器] 所有客户端连接

    UReplicationDriver* ReplicationDriver;    // 可选:替代默认 ServerReplicateActors (Rep Graph使用)

    FNetworkObjectList& GetNetworkObjectList(); // 所有需要复制的 Actor 信息

    // ★ 核心:每帧服务器端复制主循环
    virtual int32 ServerReplicateActors(float DeltaSeconds);

    // 通道配置
    TArray<FChannelDefinition> ChannelDefinitions; // Actor/Control/Voice 等通道定义
};

关键子类:

  • UIpNetDriver — 基于 IP/UDP 的标准网络驱动
  • UIpConnection — IP 连接实现

挂在World上

UNetConnection — 网络连接

Engine/Classes/Engine/NetConnection.h

代表一个 端到端的网络连接。服务器为每个客户端维护一个。

class UNetConnection : public UPlayer
{
    TArray<UChannel*> OpenChannels;     // 所有打开的通道
    AActor* OwningActor;               // 拥有此连接的 Actor(通常是 PlayerController)
    AActor* ViewTarget;                // 视图目标(用于相关性计算)

    int32 MaxPacket;                   // 最大包大小
    FBitWriter SendBuffer;             // 发送缓冲区

    // 子连接(Split Screen / 多玩家连接)
    TArray<UChildConnection*> Children;

    // 状态
    EConnectionState GetConnectionState();

    // 通道管理
    UChannel* CreateChannelByName(FName ChName, EChannelCreateFlags Flags, int32 Index);
};

UChannel

Engine/Classes/Engine/Channel.h

通道是连接上的逻辑管道,不同类型的数据走不同通道。

// 通道类型
enum EChannelType
{
    CHTYPE_None    = 0,  // 无效
    CHTYPE_Control = 1,  // 控制通道(登录握手、NMT 消息)
    CHTYPE_Actor   = 2,  // Actor 通道(属性复制、RPC)
    CHTYPE_File    = 3,  // 文件传输(弃用中)
    CHTYPE_Voice   = 4,  // 语音
};

class UChannel : public UObject
{
    UNetConnection* Connection;   // 所属连接
    int32 ChIndex;                // 通道索引
    FName ChName;                 // 通道名称

    uint32 OpenAcked:1;           // 通道是否已 ACK 打开
    uint32 Closing:1;             // 是否正在关闭
    uint32 Dormant:1;             // 是否休眠
    uint32 OpenedLocally:1;       // 是否本地打开

    FInBunch* InRec;              // 收到但有依赖的包队列
    FOutBunch* OutRec;            // 已发送但未 ACK 的可靠包队列

    virtual void ReceivedBunch(FInBunch& Bunch);   // 接收数据
    virtual int64 Close(EChannelCloseReason);       // 关闭通道
};

UActorChannel — Actor 通道 ★核心

Engine/Classes/Engine/ActorChannel.h

每个被复制的 Actor 在每个连接上有一个 ActorChannel。负责该 Actor 的属性复制和 RPC 传输。

+── ActorChannel Bunch 结构 ─────────────────────────────────────+
│ SpawnInfo (首次打开时)                                          │
│   - Actor Class / Spawn Location / Rotation                    │
│   - Actor NetGUID / Component NetGUIDs                         │
├────────────────────────────────────────────────────────────────┤
│ Content Block (每个需要复制的对象一个 Block)                     │
│   - NetGUID ObjRef (标识对象)                                   │
│   - Properties... (变化的属性)                                  │
│   - RPCs... (排队的RPC)                                        │
├────────────────────────────────────────────────────────────────┤
│ </End Tag>                                                     │
+────────────────────────────────────────────────────────────────+
class UActorChannel : public UChannel
{
    AActor* Actor;                            // 关联的 Actor
    FNetworkGUID ActorNetGUID;               // Actor 的网络 GUID

    TSharedPtr<FObjectReplicator> ActorReplicator;  // Actor 属性复制器
    TMap<UObject*, TSharedRef<FObjectReplicator>> ReplicationMap; // 子对象复制器

    // ★ 核心:复制此 Actor 的属性差异
    int64 ReplicateActor();

    // 子对象复制
    bool ReplicateSubobject(UObject* Obj, FOutBunch& Bunch, FReplicationFlags RepFlags);

    // RPC 排队
    void QueueRemoteFunctionBunch(UObject* CallTarget, UFunction* Func, FOutBunch& Bunch);

    // 休眠
    virtual bool ReadyForDormancy(bool debug=false);
    virtual void StartBecomingDormant();
};

FObjectReplicator — 对象复制器

每个需要复制的 UObject(Actor 或子对象)有一个 FObjectReplicator,负责:

  • 属性差异检测(通过 FRepLayout)
  • 属性序列化/反序列化
  • Properties 的 Recent 状态缓存
  • Custom Delta Serialization(如 FastArray)

FRepLayout — 属性布局

描述一个 UClass/UStruct 的所有 Replicated 属性的内存布局:

  • 属性偏移、大小、类型
  • 条件复制标记(COND_OwnerOnly 等)
  • 父级/子级关系
  • 用于快速内存比较(memcmp)判断属性是否变化

类关系图

UNetDriver
├── ServerConnection (UNetConnection)        // 客户端到服务器
├── ClientConnections[] (UNetConnection)     // 服务器到各客户端
│   └── OpenChannels[] (UChannel)
│       ├── UControlChannel [Index 0]        // 控制通道(仅一个)
│       ├── UActorChannel [Index 1~N]        // 每个 Actor 一个
│       │   ├── ActorReplicator (FObjectReplicator)
│       │   │   └── RepLayout (FRepLayout)
│       │   └── ReplicationMap (子对象 Replicators)
│       └── UVoiceChannel [Index 1]          // 语音
├── ReplicationDriver (UReplicationDriver)   // 可选替代
└── NetworkObjectList                        // 全部复制 Actor 元数据
UNetDriver
├── ServerConnection (UNetConnection)        // 客户端到服务器
├── ClientConnections[] (UNetConnection)     // 服务器到各客户端

客户端 UNetDriver 用 ServerConnection 连接了,服务器 UNetDriver 的 ClientConnections 中的一个。

客户端,服务器各自一个连接对象,连上了。

连接上每一个复制的Actor都有一个ActorChannel 对应

连接建立与握手流程

连接过程

客户端                              服务器
  │                                   │
  │──── NMT_Hello ──────────────────→│  1. 客户端发送 Hello(版本号、加密等)
  │                                   │
  │←─── NMT_Challenge ──────────────│  2. 服务器返回 Challenge(防伪造)
  │                                   │
  │──── NMT_Login ──────────────────→│  3. 客户端发送 Login(玩家名、凭证等)
  │                                   │     → AGameModeBase::PreLogin() 检查
  │                                   │     → 拒绝则 NMT_Failure
  │                                   │
  │←─── NMT_Welcome ────────────────│  4. 服务器发送 Welcome(地图名等)
  │                                   │
  │──── NMT_Netspeed ───────────────→│  5. 客户端发送网络速度设置
  │                                   │
  │                                   │  6. 服务器执行:
  │                                   │     AGameModeBase::Login() → 创建 PlayerController
  │                                   │     AGameModeBase::PostLogin()
  │                                   │
  │←─── 复制 PlayerController ───────│  7. 服务器开始复制 PC 给客户端
  │                                   │
  │──── NMT_Join ───────────────────→│  8. 客户端确认加入
  │                                   │     → AGameModeBase::HandleStartingNewPlayer()
  │                                   │     → RestartPlayer() → 生成 Pawn

NMT(Net Message Type)消息

客户端连接服务器时,双方需要先进行一系列"协商"(版本验证、登录、地图同步等),这个协商过程用的就是 NMT 消息。它是引擎内部的握手协议,不是游戏逻辑层的 RPC。

ControlChannel 就是上面 Connection 里面一堆Channel的第0个。

关键 GameMode 回调

回调 时机 典型用途
PreLogin(Options, Address, ErrorMessage) Login 消息到达时 验证、版本检查、Ban 检查
Login(NewPlayer, Portal, Options, UniqueId, ErrorMessage) PreLogin 通过后 创建 PlayerController
PostLogin(NewPlayer) PC 完全初始化后 初始化 PlayerState、加入 GameState
HandleStartingNewPlayer(NewPlayer) 客户端 Join 后 RestartPlayer()、选择出生点
Logout(Exiting) 断开连接时 清理

Actor 复制(Replication)机制

Actor 如何进入复制系统

AMyActor::AMyActor()
{
    bReplicates = true;    // ★ 必须:启用复制
    bNetLoadOnClient = true; // 关卡放置的 Actor 客户端是否加载
}

bReplicates = true 的 Actor 被 Spawn 时(服务器端):

  1. 引擎将其注册到 UNetDriver::NetworkObjectList
  2. 每帧 ServerReplicateActors 会考虑是否需要复制
  3. 首次对某个连接复制时,打开一个 UActorChannel

默认复制主循环 — ServerReplicateActors

ServerReplicateActors(DeltaSeconds)
│
├── 1. PrepConnections()
│   └── 准备所有活跃的客户端连接
│
├── 2. BuildConsiderList()
│   └── 遍历 NetworkObjectList,构建"可能需要复制"的 Actor 列表
│       过滤条件:bReplicates, bActorInitialized, NetUpdateTime 到期
│
├── 3. 对每个 Connection:
│   │
│   ├── a. PrioritizeActors()
│   │   └── 对 ConsiderList 中每个 Actor:
│   │       ├── IsNetRelevantFor() → 相关性判断
│   │       ├── GetNetPriority() → 优先级计算
│   │       └── 构建 FActorPriority 列表
│   │
│   ├── b. Sort
│   │   └── 按优先级降序排列
│   │
│   └── c. ProcessPrioritizedActors()
│       └── 按优先级顺序复制:
│           ├── Channel 是否存在?不存在则创建
│           ├── UActorChannel::ReplicateActor()
│           │   ├── FObjectReplicator::ReplicateProperties()
│           │   │   └── 比较属性差异 → 序列化变化的属性
│           │   ├── ReplicateSubobjects()
│           │   └── 发送排队的 RPC
│           └── 带宽耗尽 → 停止,推迟到下帧
│
└── 4. 发送 DestructionInfo(Actor 销毁通知)

函数找不到啊,让AI找一下:

NetDriver.cpp 实际的函数名
ServerReplicateActors= UNetDriver::ServerReplicateActors L6057
PrepConnections= ServerReplicateActors_PrepConnections L5077
BuildConsiderList= ServerReplicateActors_BuildConsiderList L5182
PrioritizeActors= ServerReplicateActors_PrioritizeActors L5407
ProcessPrioritizedActors ServerReplicateActors_ProcessPrioritizedActorsRange L5558

IsNetRelevantFor 实际叫 IsActorRelevantToConnection

// 没有 Channel 的 Actor 才需要判断相关性
if (!Channel)
{
    if (!IsLevelInitializedForActor(Actor, Connection))
        continue;  // 地图没加载

    if (!IsActorRelevantToConnection(Actor, ConnectionViewers))
        continue;  // 不相关,跳过,不建 Channel
}
// 已经有 Channel 的 Actor → 无论相关性如何都继续(会在后面超时后关 Channel)

已经有 Channel 的 Actor 不会被相关性过滤掉,而是在 Channel 超时后才关闭,这样防止 Actor 因为短暂离开视野就立刻消失

Actor 生命周期与复制

服务器 SpawnActor
  → 注册到 NetworkObjectList
  → 下一帧 ServerReplicateActors 发现它
  → 对每个相关连接打开 ActorChannel
  → 发送 SpawnInfo(Class, Transform, 初始属性)
  → 客户端 SpawnActor(远端实例化)
  → 后续帧只同步属性差异

服务器 DestroyActor
  → 关闭所有 ActorChannel
  → 发送 DestructionInfo
  → 客户端 DestroyActor

FNetworkGUID — 网络唯一标识

每个需要网络通信的 UObject 都需要一个 FNetworkGUID 来在不同进程中标识同一个对象。

  • 静态 Actor(关卡放置):基于路径生成稳定 GUID
  • 动态 Actor(SpawnActor):服务器分配递增 GUID,复制给客户端
  • UPackageMap 负责 GUID ↔ UObject 的映射

属性复制(Property Replication)

声明可复制属性

// 头文件
UPROPERTY(Replicated)
float Health;

UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;

// 源文件
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AMyActor, Health);
}

属性复制的底层流程

UActorChannel::ReplicateActor()
  → FObjectReplicator::ReplicateProperties(Bunch, RepFlags)
    → FRepLayout::ReplicateProperties(RepState, ChangelistState, Data, ...)
      │
      ├── 1. CompareProperties(属性比较)
      │   └── 遍历所有 Replicated Property
      │       ├── 使用 FRepLayout 中记录的属性 Offset 和 Size
      │       ├── 与 ShadowBuffer(上次发送的值缓存)做 memcmp
      │       ├── 如果启用 Push Model:只比较标记为 Dirty 的属性
      │       └── 产出 Changed Property Handles 列表
      │
      ├── 2. 条件过滤
      │   └── 对每个 Changed Property 检查 COND_OwnerOnly / COND_InitialOnly 等
      │
      └── 3. SendProperties(序列化发送)
          └── 对每个通过过滤的 Changed Property
              ├── 写入 Property Handle(标识)
              └── NetSerialize 或 NetDeltaSerialize 写入值

Shadow Buffer(影子缓冲区)

  • 每个 FObjectReplicator 维护一个 Shadow Buffer
  • 记录上次成功发送给客户端的属性值
  • 用于下次比较时判断哪些属性发生了变化
  • 只有在 Bunch 被 ACK 后才更新 Shadow Buffer

RepNotify — 属性复制回调

UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;

UFUNCTION()
void OnRep_Health()
{
    // 客户端收到 Health 变化后自动调用
    UpdateHealthBar();
}

// 带旧值参数版本(UE5)
UFUNCTION()
void OnRep_Health(float OldHealth)
{
    float Delta = Health - OldHealth;
    ShowDamageNumber(Delta);
}

RepNotify 触发时机:

  • 客户端收到属性新值并应用后立即调用
  • 服务器端不会调用 RepNotify(服务器不接收自己的属性复制)
  • 多个属性同时变化时,RepNotify 按属性声明顺序依次调用

条件复制

void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME_CONDITION(AMyActor, Ammo,     COND_OwnerOnly);     // 只给 Owner
    DOREPLIFETIME_CONDITION(AMyActor, TeamId,   COND_InitialOnly);   // 只首次
    DOREPLIFETIME_CONDITION(AMyActor, Stealth,  COND_Custom);        // 运行时动态
    DOREPLIFETIME_CONDITION(AMyActor, MoveData, COND_SimulatedOnly); // 只给模拟端
}

完整条件列表

条件 说明 场景
COND_None 无条件,始终复制 默认
COND_InitialOnly 只在首次复制时发送 阵营 ID、角色名称
COND_OwnerOnly 只发给 Owner 连接 弹药数、技能冷却
COND_SkipOwner 除 Owner 外所有人 第三人称动画状态
COND_SimulatedOnly 只发给 SimulatedProxy 移动模拟数据
COND_AutonomousOnly 只发给 AutonomousProxy 本地玩家特有数据
COND_SimulatedOrPhysics Simulated 或物理模拟 物理数据
COND_InitialOrOwner 首次或 Owner -
COND_Custom 运行时动态开关 隐身、可见性
COND_ReplayOrOwner Replay 或 Owner -
COND_ReplayOnly 仅 Replay -
COND_SkipReplay 跳过 Replay -
COND_Never 永不复制 占位/手动控制
COND_NetGroup 基于 NetGroup 过滤 Iris 系统

COND_Custom 动态条件

DOREPLIFETIME_CONDITION(AMyActor, StealthState, COND_Custom);

void AMyActor::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
    Super::PreReplication(ChangedPropertyTracker);
    // 只在存活时复制隐身状态
    DOREPLIFETIME_ACTIVE_OVERRIDE(AMyActor, StealthState, bIsAlive);
}

RPC 远程过程调用

RPC 类型

// Server RPC:客户端 → 服务器
UFUNCTION(Server, Reliable)
void ServerFireWeapon(FVector_NetQuantize AimLocation);

// Client RPC:服务器 → 拥有此 Actor 的客户端
UFUNCTION(Client, Reliable)
void ClientShowDamageNumber(float Damage, AActor* DamagedActor);

// NetMulticast RPC:服务器 → 所有已打开 Channel 的客户端
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayHitEffect(FVector HitLocation, FVector HitNormal);

RPC 类型对比

属性 Server Client NetMulticast
方向 Client → Server Server → Client Server → All Clients
调用者 AutonomousProxy Authority Authority
目标 Authority OwningClient 所有相关 Client
需要 Ownership ❌*
常见用途 输入/操作请求 UI 通知、特定反馈 特效、音效

*NetMulticast 不严格需要 Ownership,但只发给已有 ActorChannel 的连接。

Reliable vs Unreliable

特性 Reliable Unreliable
送达保证 ✅ 保证送达 ❌ 可能丢包
顺序保证 ✅ 有序 ❌ 可能乱序
丢包处理 重传 丢弃
带宽开销 较大(缓存、重传) 较小
适用场景 关键操作(技能释放、状态变更) 高频低价值(特效、位置微调)
潜在风险 大量 Reliable → 队列满 → 断连 视觉不同步

RPC 验证(Validation)

UFUNCTION(Server, Reliable, WithValidation)
void ServerUseItem(int32 SlotIndex);

bool AMyActor::ServerUseItem_Validate(int32 SlotIndex)
{
    // 返回 false → 断开该客户端连接(作弊检测)
    return SlotIndex >= 0 && SlotIndex < MaxSlots;
}

void AMyActor::ServerUseItem_Implementation(int32 SlotIndex)
{
    // 实际逻辑
}

RPC 的执行规则矩阵

和直觉还是比较契合的

Server RPC 调用 ServerFunction()

调用位置 Actor Owner 结果
Client(AutonomousProxy) 本连接 ✅ 发送到服务器执行
Client(SimulatedProxy) 其他连接 ❌ 丢弃
Server(Authority) - ✅ 本地直接执行

Client RPC 调用 ClientFunction()

调用位置 Actor Owner 结果
Server(Authority) 有 Owner ✅ 发送到 Owner 客户端执行
Server(Authority) 无 Owner ❌ 丢弃
Client - ✅ 本地直接执行(已到达目标)

NetMulticast 调用 MulticastFunction()

调用位置 结果
Server(Authority) ✅ 服务器本地执行 + 发送到所有相关客户端
Client ✅ 仅本地执行,不发送

总结

★ Server RPC 必须由 AutonomousProxy(PlayerController/Pawn 的 Owner)发起
★ Reliable RPC 不要高频调用(>10次/秒),否则缓冲区溢出断连
★ Unreliable RPC 在带宽不足时会被跳过
★ 蓝图中 "Run on Server" = Server RPC
★ 蓝图中 "Run on Owning Client" = Client RPC
★ 蓝图中 "Multicast" = NetMulticast RPC
上一篇
下一篇