
后端微服务【免费下载链接】akka.netCanonical actor model implementation for .NET with local distributed actors in C# and F#.项目地址https://gitcode.com/gh_mirrors/ak/akka.net点击查看免费下载Akka.NET 在 1.6 版本周期内对Akka.IOTCP 层进行了一次彻底的内核重构删除与System.Memory生态不兼容的自定义ByteString类型用ReadOnlySequencebyte重构 TCP 消息契约并用StreamSystem.IO.Pipelines双泵架构替换基于SocketAsyncEventArgsSAEA的回调式 I/O。本文以仓库内 openspec/changes/modernize-akka-io-tcp/tasks.md 的任务清单为主线结合 design.md、proposal.md 与当前源码实现完整梳理这次改造的目标框架迁移、消息类型变化、传输抽象落地、管道化读写循环、所有权携带写入ownership-carrying write机制以及测试验证路线。读完本文你将理解 Akka.NET 现代 TCP 栈的内部数据流、流控与关闭语义以及它如何为 TLS、Artery TCP 和 SerializerV2 铺路。改造背景两个阻碍 1.6 传输目标的遗留原语改造的动机在 proposal.md 中表述得很清楚旧版 Akka.IO TCP 层依赖两个与 .NET 现代System.Memory/System.IO.Pipelines生态不兼容的构件ByteStringAkka.NET 自有的不可变字节包装类型。它无法直接参与IBufferWriterbyte/ReadOnlySequencebyte管线每次跨层传递都要复制它也阻塞了SerializerV2的IBufferWriterbyte/ReadOnlySequencebyte契约集成。SocketAsyncEventArgsSAEA基于回调的 socket I/O 模型无法被SslStream包装使用——而 TLS 必须建立在Stream之上。DotNetty 的ByteBuf同样与System.Memory不兼容是未来高吞吐路径的死胡同。设计文档 design.md 引用了 TurboMQTT 项目petabridge/TurboMqtt作为参考实现其StreamSystem.IO.Pipelines模式同时解决了上述两个问题并通过IStreamProvider抽象让 TLS 变成一次透明替换。值得注意的是改造的明确目标是保留 Akka.IO TCP actor 消息协议与监督层级完全不变即Bind、Connect、Connected、Register、Close、Abort、PeerClosed、CommandFailed、SuspendReading、ResumeReading等消息与TcpManager → TcpListener → TcpIncomingConnection、TcpManager → TcpOutgoingConnection的层级结构都不变——变化的是底层 I/O 机制与消息的载荷类型。目标框架迁移单一 net10.0ByteString与 SAEA 问题的另一面是目标框架TFM过旧。原设计指出netstandard2.0目标缺少Stream.ReadAsync(Memorybyte)、Socket.SendAsync(Memorybyte)、SpanTAPI 以及System.IO.Pipelines支持而这些是零拷贝 I/O 的前提。决策 4design.md决定移除netstandard2.0与net6.0所有库项目只面向net10.0。理由很直接没有任何 .NET Framework 版本支持netstandard2.1.NET Framework 兼容性无论如何都会丢失而netstandard2.1只能覆盖已 EOL 的 .NET Core 3.x 与 Mono/Xamarin相比直接面向当前 LTS 的net10.0几乎毫无收益。单目标还顺带消灭了全部条件编译块、polyfill 包与多 TFM 复杂度tasks.md §1.1–§1.3。当前仓库的 Directory.Build.props 验证了这一落地NetTestVersionnet10.0/NetTestVersion NetStandardLibVersionnet10.0/NetStandardLibVersionNetStandardLibVersion的值已是net10.0测试目标同样统一为net10.0。设计文档还特别注明System.IO.Pipelines已包含在 net10.0 共享框架中因此无需新增 NuGet 包引用tasks.md §4.1 的结论。消息契约变化Tcp.Write / Tcp.Received 改用 System.Memory 类型任务清单 §2 的核心是把 TCP 消息的载荷类型从ByteString迁走。需要注意 tasks.md 早期文字写的是ReadOnlyMemorybyte而设计在落地过程中演进为ReadOnlySequencebyte见 design.md §8 的 2026-07-07 机制修订当前 Tcp.cs 中实际生效的契约是Tcp.Write.DataReadOnlySequencebyteTcp.Received.DataReadOnlySequencebyteTcp.CompoundWrite/Tcp.SimpleWriteCommand基于ReadOnlySequencebyte聚合多个非连续缓冲Tcp.Write.Create()提供了从ReadOnlySequencebyte、ReadOnlyMemorybyte、byte[]三种入参构造的工厂重载空数据统一映射为静态Write.EmptyAck字段保留原有语义——NoAck表示无需确认否则连接 actor 在写入成功入队到内核后回送所附的Ack令牌。Tcp.Received的 XML 注释明确说明Data采用ReadOnlySequencebyte是为容纳底层传输可能出现的非连续缓冲虽然当前实现中 actor 会先把 pipe 的池化段复制成单个连续数组再投递实际是单段 sequence下游 framing 阶段仍应按多段 sequence 处理以兼容未来的零拷贝优化。tasks.md §2.5 要求硬删除src/core/Akka/Util/ByteString.csdesign.md 决策 5不留 shim、不做适配器强制所有消费者一次性迁移。当前仓库中该定义文件已不存在。从全仓库搜索看ByteString字样仍残留于少量位置例如 Akka.Remote/Transport/DotNetty/TcpTransport.cs、Akka.Streams/Serialization/StreamRefSerializer.cs 及若干测试文件这与 tasks.md §7测试套件迁移尚未勾选完成的状态一致——核心 IO 路径已完成迁移存量引用集中在 DotNetty 路径与测试套件中属于后续清理范围。传输抽象从 IStreamProvider 设想到 ITransportConnection 落地tasks.md §3 最初的设计是引入IStreamProvider接口ConnectAsync/Close由TcpStreamProvider返回NetworkStream未来TlsStreamProvider返回SslStream——TLS 握手发生在ConnectAsync内部连接 actor 自始至终不知道数据是否加密design.md 决策 2。不过从当前源码结构看落地时采用了更进一步、更彻底的抽象搜索整个src目录并无IStreamProvider/TcpStreamProvider类型取而代之的是 ITransportConnection.cs 与 TcpTransportConnection.cs。这一层把管道 泵循环 缓冲管理 flush 批处理整体封装进了传输对象actor 只写字节、读字节完全不接触 Stream 或 Socketpublic interface ITransportConnection : IAsyncDisposable { PipeReader Input { get; } // 入站数据 void Write(ReadOnlySequencebyte data); // 写入输出缓冲不拒绝字节 ValueTaskFlushResult FlushAsync(CancellationToken ct default); Task ShutdownAsync(); // 半关闭flush FIN继续读 Task CloseAsync(); // 全关闭flush 后关闭 void Abort(); // RST立即中止 Task ReadCompleted { get; } Task WriteCompleted { get; } bool HasReadError { get; } Exception? ReadError { get; } }TcpTransportConnection是明文 TCP 实现内部持有SocketNetworkStreamownsSocket: false 两个Pipe输入/输出构造函数即启动两条泵循环。它还提供了从已有 Stream 构造的重载TcpTransportConnection(Socket, Stream, ...)其注释明确指出这是为 TLS 或测试准备的入口——这正对应原IStreamProvider的设计意图未来的 TLS 连接可以先把SslStream握手完成再以流的形式交给连接。出站连接TcpOutgoingConnection.cs仍直接使用Socket.ConnectAsync完成建连连接建立后再包一层TcpTransportConnection。TcpConnection 内部重写Stream Pipe 双泵架构tasks.md §4 是本次改造最核心的部分把 SAEA 接收/发送基础设施整体替换为 Stream Pipe 模式。当前 TcpConnection.cs 顶部有一张 ASCII 相位图描述了连接的生命周期Connecting → AwaitReg → Open → (PeerClosed → EOF)以及Closing → Closed各相位通过Become(...)切换AwaitRegBehaviour、OpenBehaviour、PeerSentEofBehaviour、ClosingBehaviour。TcpConnection把所有 I/O 机制委托给ITransportConnection自身只负责两条 actor 驱动的协调路径。读路径Stream → Pipe → actor Tell → handler底层TcpTransportConnection.RunReadPumpAsyncTcpTransportConnection.cs是第一条泵循环writer.GetMemory()→_stream.ReadAsync(memory, ct)→writer.Advance(bytesRead)→writer.FlushAsync(ct)读到 0 字节即判定 EOF出错时先把错误写入HasReadError/ReadError字段用Volatile.Write保证在 pipe writer 完成前可见再完成 pipe writer最后抛出异常使ReadCompleted.IsFaulted为真。异常不随 pipe writer 传递以保留缓冲数据供 actor 排空。actor 侧是按需发起的读循环RequestPipeRead()通过AwaitPipeReadAsync异步执行ReadPipeChunkAsync完成或取消后通过self-tell把PipeReadCompleted/PipeReadCanceled结果送回邮箱ReadPipeChunkAsync位于 TcpConnection.cs。关键语义在HandlePipeRead中由于Tell是非阻塞的handler 可能在 pipe 段归还池之前尚未消费数据因此必须先把数据复制出 pipe 的池化段——buffer.CopyTo(array)复制到新数组再包装为ReadOnlySequencebyte单段。复制完成后reader.AdvanceTo(buffer.End)归还缓冲。读管道完成且带数据时会额外发起一次 drain 读避免CompleteAsync内部 flush 出的尾段被丢弃随后区分三种结束读泵出错 →HandleIoError并最终投递Tcp.ErrorClosed正常 EOF → self-tellStreamEof→ 投递Tcp.PeerClosed。这条复制后投递的路径正是 design.md 决策 3读侧复制、写侧池化的落地actor 模型无法告知消费者何时用完消息数据Tcp.Received可能被无限期保存在 actor 状态、转发或解析为字符串所以读缓冲不能归还池必须复制而写缓冲生命周期有界Stream.WriteAsync消费完即可释放。写路径actor → Pipe → Streamflush/ack 批处理写侧同样分两层。底层RunWritePumpAsyncTcpTransportConnection.cs是第二条泵reader.ReadAsync(ct)→ 对缓冲中每个连续段执行_stream.WriteAsync(segment, ct)→reader.AdvanceTo(buffer.End)writer 完成 pipe 即退出写泵错误同样会 rethrow 使WriteCompleted.IsFaulted为真并有专门的 self-tell 消息WritePumpFailed让 actor 即使在一向空闲的单向连接上也能及时感知管道断裂。actor 侧EnqueueWriteTcpConnection.cs先按WriteCommandsQueueMaxSize做单条写入容量检查超限即回CommandFailed随后调用_transport.Write(write.Data)把字节写入输出 pipe再进_pendingAcks队列等待 flush。StartFlush维护一次只有一个 pending flush的约束flush 覆盖_pendingAcks中最旧的_flushCovers个写入同步成功则AckCovered()立即确认异步完成则等FlushCompleted/FlushFailedself-tell 回邮箱再处理。这实现了写回压——输出 pipe 超过 pause 阈值时 flush 挂起ack 被扣住上游自然被压住高吞吐时缓冲在阈值处自动 flush低吞吐时显式FlushAsync兜底。流控SuspendReading / ResumeReading 与 pull modeSuspendReadingInternalTcpConnection.cs只置_readingAllowed false在途的读仍会完成投递但之后不再发起新读直到ResumeReading。HandlePipeRead的收尾逻辑据此分支pull mode拉模式每次Tcp.Received之后必须收到一次ResumeReading才继续读。这是 Akka.Streams TCP 桥接依赖的模式tasks.md §8.2 专门要求验证。非 pull mode只要未被挂起就持续RequestPipeRead()。Connect/Bind消息均携带pullMode参数见 TcpMessageTcpConnection构造函数从设置与消息中取得该标志。关闭序列Close / ConfirmedClose / Abort / EOF / 错误连接关闭的全部路径都经由 actor 邮箱串行化保证线程安全Tcp.CloseHandleClose完成输出 pipe → flush 排空 → 取消读 CTS → await 两条泵 → 关闭 Stream 与 Socket → 投递Tcp.Closed。对应TcpTransportConnection.CloseAsync的完整序列。Tcp.ConfirmedClose半关闭ShutdownAsync先完成输出 pipe 并等待写泵排空然后_socket.Shutdown(SocketShutdown.Send)发 FIN继续读直至收到对端 FIN投递Tcp.ConfirmedClosed。Tcp.AbortHandleAbort立即取消 CTS、完成两条 pipe、用LingerOption(true, 0)直接 RST 关闭 socket投递Tcp.Aborted。优雅 EOF读泵返回 0 字节 → 完成输入 pipe writer → actor 投递Tcp.PeerClosedRegister(keepOpenOnPeerClosed: true)时连接转入PeerSentEofBehaviour保持写侧打开否则按Closing流程收尾。I/O 错误读泵异常经ReadPumpFailed/HasReadError传递最终以Tcp.ErrorClosed(cause)呈现_readPumpError与_transport.HasReadError双保险确保 actor 处理完成消息时能同步区分错误 EOF与正常 EOF。此外连接 actor 在PostStop中做尽力而为的清理取消 CTS、关闭传输但不会同步等待DisposeAsync——因为泵可能阻塞在流 I/O 上只有关闭 socket 才能解阻塞同步等待会死锁。已启动但未Register的写入会进入_pendingRegistrationWrites队列FIFO受_pendingRegistrationBytes记账约束Register到达后统一回放。Akka.Streams TCP 桥接迁移Akka.Streams 的 TCP 层通过 TcpStages.cs 把 actor 消息桥接为流元素。tasks.md §5 把IncomingConnectionStage、OutgoingConnectionStage、ConnectionSourceStage的元素类型统一从ByteString换成内存类型当前源码中三个阶段的 Shape 均为FlowShapeReadOnlySequencebyte, ReadOnlySequencebyte。值得强调的是TcpStages.cs内还实现了零拷贝写合并TcpStreamLogic内部维护一个WriteBufferSegment继承ReadOnlySequenceSegmentbyte并实现IOwnedSequenceSegmentAppendToWriteBuffer把生产者段携带 owner 的段或借用段零拷贝链入写缓冲——携带 owner 的段通过DetachOwner()转移所有权借用段以nullowner 链入DrainWriteBuffer一次性产出覆盖头尾段的多段ReadOnlySequencebyte作为单个Tcp.Write交给连接 actor。这正是 tasks.md §9.5 的落地合并时不再做.ToArray()复制缓冲的 owner 在 abort /OnUpstreamFailure/ 未 flush 即关闭时统一释放。Akka.Remote 与 Akka.Cluster 迁移tasks.md §6 要求 Akka.Remote 的MessageSerializer.cs、AkkaPduCodec.cs及 Akka.Cluster、contrib 全部跟随迁移。当前 Akka.Remote 中MessageSerializer与AkkaPduCodec均已改为处理ReadOnlySequencebyte/ReadOnlyMemorybyte载荷Akka.Streams 侧的StreamRefSerializer也已完成字节类型切换。Artery 传输的编码路径是所有权机制的关键消费者。ArteryEncodeStageArteryEncodeStage.cs的 Shape 为FlowShapeIOutboundEnvelope, ReadOnlySequencebyte它用PooledPayloadWriter从ArrayPool租用缓冲编码消息帧并推送携带 owner 的单段 sequenceOwnedSequenceSegment.Create(owner)。在 Artery 出站图上ArteryRemoting.MaterializeOutboundStream见 ArteryRemoting.cs 附近的注释owner 从编码阶段铸造、经OutboundHandshakeStage、LaneWriteBatchStage、kill switch、Tcp.OutgoingConnection全程透传最终由TcpConnection在管道复制点释放。LaneWriteBatchStage.AppendFrameToBatchArteryRemoting.cs把多帧的 owner 段零拷贝合并成批其注释明确指出每个池化缓冲始终只有一个段负责最终释放——所有权转移而非复制。所有权携带 Tcp.WriteOwnedSequenceSegment 与处置矩阵这是 tasks.md §9 也是设计文档决策 8 的主题源于两个真实事故事故 (a)TcpConnection的注册前写队列按引用保留了调用方缓冲Register迟迟不来时复用缓冲的调用方可能已改写数据造成真实损坏PR #8323 以入队复制防御。事故 (b)Artery 的ArteryEncodeStage只能靠上游 ack/pull 信号猜测管道复制何时发生再决定何时归还缓冲。一次真实的毒池压力测试真实 ActorSystem、300 条背靠背消息、池数组归还即涂改证伪了静态审计——高负载下 pull 与上一帧字节已离开缓冲并非 1:1每次运行都有 1–4/300 条消息损坏。发货版用保留两代缓冲规避但隔空推断复制时机本质上不可靠。决策与机制修订2026-07-07所有权搭乘在ReadOnlySequencebyte的段上而不是给Tcp.Write增加公共字段/重载。落地类型是 OwnedSequenceSegment.csOwnedSequenceSegment : ReadOnlySequenceSegmentbyte, IOwnedSequenceSegment可选携带一个IMemoryOwnerbyteDisposeOwner()幂等先清引用再释放DetachOwner()转移所有权并清空自身引用Append支持零拷贝链式追加。构造陷阱有专门文档携带 owner 的单段 sequence必须用 segment-backed 构造new ReadOnlySequencebyte(segment, 0, segment, length)否则Data.Start.GetObject()取不到段处置遍历会静默漏掉 owner。扩展方法DisposeOwnedSegments()沿段链遍历释放每个携带 owner 的段并以data.End段为界防止越界释放属于其他写入的尾段HasOwnedSegments()用于决策如是否需要防御性复制。借用段用普通ReadOnlySequenceSegmentbyte表示不实现IOwnedSequenceSegment遍历自动跳过——借用/拥有段可在同一条链中共存。TcpConnection的处置矩阵覆盖每一条路径tasks.md §9.3源码见 TcpConnection.cs路径处置时机开放/已注册路径WriteAsync复制进 pipe 后同一 actor turnDisposeOwnedSegments注册前延迟 flush不急切处置入队原样保留FlushPendingRegistrationWrites → EnqueueWrite真正复制时处置Register永不到达则由PostStop/排空处置队列满拒绝 /CommandFailed信号失败之前处置字节未进 pipe无人会读PostStop/ 连接排空处置所有仍被排队命令持有的 owner借用无 owner写入语义完全不变包括 #8323 的注册前防御性复制。NoAck 拥有是天然安全的处置由复制已发生驱动而非 ack 驱动。PR1Akka.IO 基础additive与 PR2消费者采用ArteryEncodeStageTcpStages合并零拷贝按真实耦合边界拆分各自可独立回退其中 §9.5 与 §9.6 因合并零拷贝 编码段自处置会互相破坏而原子不可分。tasks.md 也如实标注§9.1specs 增量文件本轮未完成§9.4 的EnqueueWritecatch 块处置路径缺少专门的竞态测试已在 PR1 交接报告中说明理由。毒池测试验证src/core/Akka.Tests/IO/TcpConnectionOwnershipDisposalSpec.cs 实现了 tasks.md §9.4 要求的合成毒池损坏测试PoisonOwner在归还时涂改数组针对每条处置路径断言无提前处置在途写入不被破坏与无 owner 泄漏永不处置共 8 个测试全部通过还覆盖了多 owner 合并写一个Tcp.Write的Data链上 N 个携带 owner 的段与混合借用拥有段链的场景。这类测试正是设计文档风险清单中处置矩阵容易写错的应对逐路径枚举 毒池式损坏检测而不是只测开放路径的 happy case。测试套件与验证路线尚未收尾的部分与已完成打勾的 §1–§6 不同tasks.md §7测试套件迁移与 §8验证仍处于未完成状态这决定了下游关注点§7 测试套件迁移更新所有 Akka.IO TCP 测试、Akka.Streams TCP 测试与 Akka.Remote 传输测试验证不直接引用 DotNetty API 的 Akka.Remote specs 全部通过最后运行dotnet test -c Release。从全仓库搜索看若干测试文件仍残留ByteString字样属于本阶段的清理对象。§8 验证清单dotnet build -warnaserror全目标通过Directory.Build.props已设TreatWarningsAsErrorstrueAkka.IO TCP pull mode 正确Akka.Streams TCP 桥接依赖它Akka.StreamsTcp.Bind()/Tcp.OutgoingConnection()端到端可用Akka.Remote 在两个 ActorSystem 之间收发消息正常dotnet test -c Release src/core/Akka.API.Tests更新 API 审批基线breaking changes 的验收依据。仓库现有测试src/core/Akka.Tests/IO已包含TcpIntegrationSpec、TcpListenerSpec、TcpConfirmedCloseSpec、TcpConnectionBatchingSpec、TcpConnectionWriteBackpressureSpec、TcpConnectionPreRegisterWriteRetentionSpec、TcpConnectionOwnershipDisposalSpec、TcpMaximumFrameSizeSpec、TcpSettingsSpec等覆盖了新内核对写批处理、写回压、注册前写保留、所有权处置等关键行为。风险与权衡设计文档留下的明确边界design.md 的风险清单值得完整保留因为它划定了本次改造的非目标写批处理回退风险旧 SAEA 用BufferList做 scatter-gather一次系统调用多缓冲Stream.WriteAsync只接收单缓冲。缓解靠 Pipe 自然合并小写必要时在写任务内显式合并需基准验证。后台任务竞态沿用 TurboMQTT 已验证的协调模式——self-tell 先于 caller-tell防止邮箱排序竞态、Interlocked.CompareExchange保护 CTS、Task.WhenAll跟踪生命周期、所有退出路径正确完成 PipeWriter。MemoryPoolbyte.Shared碎片化其内部基于ArrayPool碎片管理良好基准异常时可换自定义 slab 池。明确非目标TLS 实现独立TlsStreamProvider工作、替换/绕过 DotNetty remoting未来 Artery TCP 依赖此方向、SerializerV2 集成独立原子基础工作流、性能基准依赖 SerializerV2 与 Artery TCP、Akka.IO.Udp 改造、序列化源生成器、Akka.Streams materializer 架构调整。管道复制本身的成本设计文档明确不在范围内消除管道暂存复制。基准9900Xbench/write-path-copy-costs分支显示 256B 时管道复制约 197ns/0 分配、4KB 约 561ns对比有界通道直写交接约 987ns/843ns120B——复制在 4KB–64KB 交叉点以下反而胜出。真正的零拷贝到 socket 推迟到未来大消息流工作并明确要求字节感知交接的证据负担。换句话说决策 8 解决的是生命周期语义谁在何时释放什么不是消除复制或提升吞吐。影响与后续方向从 proposal.md 的 Impact 清单看这次改造波及面是全局的Akka 核心Tcp.cs消息类型、TcpConnection.cs全量重写、TcpOutgoingConnection.cs/TcpIncomingConnection.cs/TcpListener.cs、Akka.StreamsDSL、stages、TcpStages.cs、Akka.RemoteMessageSerializer.cs、AkkaPduCodec.cs、全部触碰ByteString的传输代码、Akka.Cluster 与 contrib以及构建系统TFM、可能的 Polyfill 移除。它解锁的三条下游能力链清晰可见TLSTcpTransportConnection的 Stream 构造重载 未来TlsStreamProvider语义、Artery TCP未来高速 remoting 路径依赖零拷贝与所有权契约、SerializerV2IBufferWriterbyte/ReadOnlySequencebyte契约贯通。对于想要继续深入源码的读者建议的阅读顺序是先看 Tcp.cs 的消息契约 → ITransportConnection.cs 与 TcpTransportConnection.cs 的双泵实现 → TcpConnection.cs 的 actor 侧协调读循环、flush/ack、流控、关闭矩阵→ OwnedSequenceSegment.cs 的所有权机制 → 最后用 TcpConnectionOwnershipDisposalSpec.cs 的毒池测试验证对处置矩阵的理解。这份任务清单tasks.md §7/§8 未勾选项也正是仓库当前测试套件迁移工作的路线图。赞分享后端微服务【免费下载链接】akka.netCanonical actor model implementation for .NET with local distributed actors in C# and F#.项目地址https://gitcode.com/gh_mirrors/ak/akka.net点击查看免费下载相关推荐Akka.NET Akka.IO TCP 现代化改造用 System.Memory 与 Pipelines 替换 ByteString 与 SocketAsyncEventArgsAkka.NET Akka.IO TCP 现代化改造用 System.Memory 与 Pipelines 替换 ByteString 与 SocketAsy后端微服务Akka.NET Akka.IO TCP 现代化改造从 ByteString 与 SocketAsyncEventArgs 走向 System.Memory 与 StreamPipeAkka.NET Akka.IO TCP 现代化改造从 ByteString 与 SocketAsyncEventArgs 走向 System.Memory后端微服务Akka.NET TCP I/O 现代化改造以 System.Memory 与 System.IO.Pipelines 重塑 Akka.IO 传输层Akka.NET TCP I/O 现代化改造以 System.Memory 与 System.IO.Pipelines 重塑 Akka.IO 传输层 导读 本后端微服务创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考