解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
28.74 KiB
X
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using NewLife.Data;
using NewLife.Log;
using NewLife.Messaging;
using NewLife.Model;

namespace NewLife.Net;

/// <summary>Udp会话</summary>
/// <remarks>
/// <para>仅用于服务端与某一固定远程地址通信。</para>
/// <para>特性:</para>
/// <list type="bullet">
/// <item>绑定到固定的远程地址</item>
/// <item>共享UdpServer的底层Socket</item>
/// <item>收到空数据包时自动结束会话。判定以收到的原始数据报为准,在进入事件链之前定下;业务在事件内释放或置空 <see cref="ReceivedEventArgs.Packet"/> 不影响判定,要主动结束会话请 <see cref="DisposeBase.Dispose()"/></item>
/// </list>
/// </remarks>
public class UdpSession : DisposeBase, ISocketSession, ITransport, ILogFeature
{
    #region 属性
    /// <summary>会话编号</summary>
    /// <remarks>用于在多会话环境中唯一标识当前会话</remarks>
    public Int32 ID { get; set; }

    /// <summary>名称</summary>
    /// <remarks>主要用于日志输出,默认继承自服务器名称</remarks>
    public String Name { get; set; }

    /// <summary>服务器</summary>
    /// <remarks>所属的UdpServer实例</remarks>
    public UdpServer Server { get; set; }

    /// <summary>底层Socket</summary>
    /// <remarks>返回UdpServer的Socket</remarks>
    Socket? ISocket.Client => Server?.Client;

    /// <summary>本地地址</summary>
    /// <remarks>接收数据的本地网络地址</remarks>
    public NetUri Local { get; set; }

    /// <summary>端口</summary>
    /// <remarks>本地端口号</remarks>
    public Int32 Port { get => Local.Port; set => Local.Port = value; }

    /// <summary>远程地址</summary>
    /// <remarks>通信的目标远程地址</remarks>
    public NetUri Remote { get; set; }

    private Int32 _timeout;
    /// <summary>超时时间(毫秒)</summary>
    /// <remarks>接收操作的超时时间,默认3000ms</remarks>
    public Int32 Timeout
    {
        get => _timeout;
        set
        {
            _timeout = value;
            if (Server?.Client is { } sock)
                sock.ReceiveTimeout = _timeout;
        }
    }

    /// <summary>协议编解码器。非空时为协议模式:数据报按完整帧定界并构造消息分发(无粘包/半包处理,无需泵任务)</summary>
    public IMessageCodec? Protocol { get; set; }

    /// <summary>最大并发处理数。协议模式下消息处理并发度(随服务器下发):1=串行(默认),大于1=并行派发(兼作并发上限)</summary>
    /// <remarks>并行下同一会话的多个消息处理顺序不定(SRMP 按序列号配对);并行要求业务处理器线程安全。</remarks>
    public Int32 MaxConcurrency { get; set; } = 1;

    /// <summary>请求-响应匹配队列。协议模式下等待响应时使用,首次等待时自动创建,可注入共享或自定义实现</summary>
    /// <remarks>与流式会话共用同一匹配实现:数据报每包即完整帧,交付时直接配对,无粘包处理</remarks>
    public IMatchQueue? MatchQueue
    {
        get => _matcher.Queue;
        set => _matcher.Queue = value;
    }

    /// <summary>请求-响应匹配等待超时(毫秒)。默认30_000</summary>
    public Int32 MatchTimeout
    {
        get => _matcher.Timeout;
        set => _matcher.Timeout = value;
    }

    /// <summary>响应匹配器。与流式会话共用:请求-响应配对在报文交付阶段完成</summary>
    private readonly ResponseMatcher _matcher = new();

    /// <summary>Socket服务器</summary>
    /// <remarks>当前通讯所在的Socket服务器</remarks>
    ISocketServer ISocketSession.Server => Server;

    /// <summary>最后一次通信时间</summary>
    /// <remarks>主要表示活跃时间,包括收发操作</remarks>
    public DateTime LastTime { get; private set; } = DateTime.Now;

    /// <summary>APM性能追踪器</summary>
    /// <remarks>用于记录关键操作的性能追踪</remarks>
    public ITracer? Tracer { get; set; }
    #endregion

    #region 构造
    /// <summary>实例化Udp会话</summary>
    /// <param name="server">所属服务器</param>
    /// <param name="local">接收数据的本地地址</param>
    /// <param name="remote">远程终结点</param>
    public UdpSession(UdpServer server, IPAddress? local, IPEndPoint remote)
    {
        Name = server.Name;

        Server = server;
        Remote = new NetUri(NetType.Udp, remote);
        Tracer = server.Tracer;

        Local = server.Local.Clone();
        if (local != null) Local.Address = local;

        // 检查并开启广播
        server.Client?.CheckBroadcast(remote.Address);
    }

    /// <summary>开始数据交换</summary>
    public void Start()
    {
        if (Disposed || Server == null) return;

        Protocol = Server.Protocol;
        MaxConcurrency = Server.MaxConcurrency;
        MatchTimeout = Server.MatchTimeout;

        Server.Open();

        WriteLog("New {0}", Remote.EndPoint);
    }

    private void Stop(String reason)
    {
        if (Server == null) return;

        WriteLog("Close {0} {1}", Remote.EndPoint, reason);

        Server = null!;
    }

    /// <summary>销毁资源</summary>
    /// <param name="disposing">是否释放托管资源</param>
    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);

        Stop(disposing ? "Dispose" : "GC");

        // 取消挂起的请求-响应等待,避免调用方悬挂(与流式会话口径一致)
        _matcher.Clear();

        // 只释放、不置空:置空后 ??= 会为后续派发新建“满额”信号量,即便会话已销毁也放行消息进入处理链。
        // 已派发的消息持有自己等待到的实例,其 Release 命中已释放实例抛 ObjectDisposedException,由调用点忽略(基类同样只释放不置空)
        _concurrency?.Dispose();

        //// 释放对服务对象的引用,如果没有其它引用,服务对象将会被回收
        //Server = null;
    }
    #endregion

    #region 发送
    /// <summary>发送数据</summary>
    /// <param name="data">数据包</param>
    /// <returns>实际发送的字节数</returns>
    /// <exception cref="ObjectDisposedException">会话已销毁</exception>
    public Int32 Send(IPacket data)
    {
        if (Disposed) throw new ObjectDisposedException(GetType().Name);

        return Server.OnSend(data, Remote.EndPoint);
    }

    /// <summary>发送数据</summary>
    /// <param name="data">字节数组</param>
    /// <param name="offset">偏移</param>
    /// <param name="count">字节数</param>
    /// <returns>实际发送的字节数</returns>
    /// <exception cref="ObjectDisposedException">会话已销毁</exception>
    public Int32 Send(Byte[] data, Int32 offset = 0, Int32 count = -1)
    {
        if (Disposed) throw new ObjectDisposedException(GetType().Name);

        // 全部发送
        if (count < 0) count = data.Length - offset;

#if NET6_0_OR_GREATER
        return Server.OnSend(new ReadOnlySpan<Byte>(data, offset, count), Remote.EndPoint);
#else
        return Server.OnSend(new ArraySegment<Byte>(data, offset, count), Remote.EndPoint);
#endif
    }

    /// <summary>发送数据</summary>
    /// <param name="data">数组段</param>
    /// <returns>实际发送的字节数</returns>
    /// <exception cref="ObjectDisposedException">会话已销毁</exception>
    public Int32 Send(ArraySegment<Byte> data)
    {
        if (Disposed) throw new ObjectDisposedException(GetType().Name);

        return Server.OnSend(data, Remote.EndPoint);
    }

    /// <summary>发送数据</summary>
    /// <param name="data">只读内存段</param>
    /// <returns>实际发送的字节数</returns>
    /// <exception cref="ObjectDisposedException">会话已销毁</exception>
    public Int32 Send(ReadOnlySpan<Byte> data)
    {
        if (Disposed) throw new ObjectDisposedException(GetType().Name);

        return Server.OnSend(data, Remote.EndPoint);
    }

    /// <summary>发送消息,不等待响应。经协议构建整帧后发送</summary>
    /// <param name="message">消息对象(须实现 <see cref="IMessage"/>)</param>
    /// <returns>实际发送的字节数</returns>
    /// <exception cref="InvalidOperationException">未设置协议或消息类型不支持</exception>
    public virtual Int32 SendMessage(Object message)
    {
        // 协议模式:消息经协议构建整帧后发送
        if (Protocol is { } codec && message is IMessage msg)
        {
            var data = codec.Build(msg);
            if (data == null) return 0;

            try
            {
                return Send(data);
            }
            finally
            {
                data.TryDispose();
            }
        }

        throw new InvalidOperationException($"Protocol not set or message is not IMessage for session [{Name}]");
    }

    /// <summary>发送消息并等待匹配的响应(协议模式)。请求入匹配队列,响应数据报到达时完成</summary>
    /// <param name="request">请求消息</param>
    /// <param name="cancellationToken">取消通知</param>
    /// <returns>响应消息;调用方负责消费负载并释放(<see cref="IDisposable.Dispose"/> 或读满负载)</returns>
    /// <exception cref="InvalidOperationException">未设置协议</exception>
    /// <exception cref="NotSupportedException">协议未实现请求-响应配对(<see cref="IMessageMatcher"/>)</exception>
    /// <remarks>
    /// <para>与流式会话共用同一响应匹配实现(<see cref="ResponseMatcher"/>):数据报每包即完整帧,交付时直接配对,无粘包处理。
    /// 响应消息先进入 <see cref="Received"/> 事件链(可观测),随后命中配对的交付本等待方;配对语义由协议的 <see cref="IMessageMatcher.Match"/> 决定。</para>
    /// <para>等待超时为 <see cref="MatchTimeout"/>;UDP 不可靠,未收到响应即超时,重传与否由调用方决定。</para>
    /// </remarks>
    public virtual ValueTask<IMessage> SendMessageAsync(IMessage request, CancellationToken cancellationToken = default)
    {
        if (request == null) throw new ArgumentNullException(nameof(request));

        var codec = Protocol ?? throw new InvalidOperationException($"Protocol not set for session [{Name}]");
        if (ResponseMatcher.Resolve(codec) == null) throw new NotSupportedException($"协议 [{codec.GetType().Name}] 未实现请求-响应配对(IMessageMatcher),无法等待响应");

        var span = Tracer?.NewSpan($"net:{Name}:SendMessageAsync", request);
        var source = _matcher.Register(this, request, span);

        try
        {
            SendMessage(request);
        }
        catch (Exception ex)
        {
            // 请求未能入队或送出:异常交给等待方(await 时抛出),资源随 GetResult 归还
            source.TrySetException(ex);
        }

        source.RegisterCancellation(cancellationToken);
        return source.ValueTask;
    }

    /// <summary>发送消息并等待匹配的响应(协议模式)</summary>
    /// <param name="message">消息对象(须实现 <see cref="IMessage"/>)</param>
    /// <param name="cancellationToken">取消通知</param>
    /// <returns>响应消息</returns>
    /// <exception cref="InvalidOperationException">未设置协议或消息类型不支持</exception>
    public virtual ValueTask<Object> SendMessageAsync(Object message, CancellationToken cancellationToken = default)
    {
        if (Protocol != null && message is IMessage msg) return SendMessageAsyncForObject(msg, cancellationToken);

        throw new InvalidOperationException($"Protocol not set or message is not IMessage for session [{Name}]");
    }

    private async ValueTask<Object> SendMessageAsyncForObject(IMessage message, CancellationToken cancellationToken)
        => await SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
    #endregion

    #region 接收
    /// <summary>同步接收数据</summary>
    /// <returns>接收到的数据包</returns>
    /// <exception cref="ObjectDisposedException">会话已销毁</exception>
    /// <exception cref="InvalidOperationException">服务器未设置</exception>
    public IOwnerPacket Receive()
    {
        if (Disposed) throw new ObjectDisposedException(GetType().Name);
        var server = Server;
        if (server?.Client is not { } sock) throw new InvalidOperationException(nameof(Server));

        // 服务器接收环运行时禁止会话拉取:两条读路径会争抢同一Socket,数据被分流且不可预期
        if (server.IsReceiving) throw new InvalidOperationException(NoPullMessage);

        using var span = Tracer?.NewSpan($"net:{Name}:Receive");
        try
        {
            var remote = Remote.EndPoint;
            var ep = remote as EndPoint;
            var pk = new OwnerPacket(server.BufferSize);
            while (true)
            {
                var size = sock.ReceiveFrom(pk.Buffer, ref ep);
                // 共享Socket可能收到其他对端的数据报,丢弃非本会话来源的数据报继续等待(契约:Udp只接受来自所属远方的数据)
                if (!IsFromRemote(ep, remote)) continue;

                span?.Value = size;
                return pk.Resize(size);
            }
        }
        catch (Exception ex)
        {
            span?.SetError(ex, null);
            throw;
        }
    }

    /// <summary>异步接收数据</summary>
    /// <param name="cancellationToken">取消通知</param>
    /// <returns>接收到的数据包</returns>
    /// <exception cref="ObjectDisposedException">会话已销毁</exception>
    /// <exception cref="InvalidOperationException">服务器未设置</exception>
    public virtual async Task<IOwnerPacket?> ReceiveAsync(CancellationToken cancellationToken = default)
    {
        if (Disposed) throw new ObjectDisposedException(GetType().Name);
        var server = Server;
        if (server?.Client is not { } socket) throw new InvalidOperationException(nameof(Server));

        // 服务器接收环运行时禁止会话拉取:两条读路径会争抢同一Socket,数据被分流且不可预期
        if (server.IsReceiving) throw new InvalidOperationException(NoPullMessage);

        using var span = Tracer?.NewSpan($"net:{Name}:Receive");
        try
        {
            var remote = Remote.EndPoint;
            var ep = remote as EndPoint;
            var pk = new OwnerPacket(server.BufferSize);
            while (true)
            {
#if NETFRAMEWORK || NETSTANDARD2_0
                var ar = socket.BeginReceiveFrom(pk.Buffer, 0, pk.Length, SocketFlags.None, ref ep, null, socket);
                var size = ar.IsCompleted ?
                    socket.EndReceiveFrom(ar, ref ep) :
                    await Task.Factory.FromAsync(ar, e => socket.EndReceiveFrom(e, ref ep)).ConfigureAwait(false);
                // 共享Socket可能收到其他对端的数据报,丢弃非本会话来源的数据报继续等待
                if (!IsFromRemote(ep, remote)) continue;
#elif NET7_0_OR_GREATER
                var result = await socket.ReceiveFromAsync(pk.GetMemory(), ep, cancellationToken).ConfigureAwait(false);
                // 共享Socket可能收到其他对端的数据报,丢弃非本会话来源的数据报继续等待
                if (!IsFromRemote(result.RemoteEndPoint, remote)) continue;
                var size = result.ReceivedBytes;
#else
                var result = await socket.ReceiveFromAsync(pk.Buffer, SocketFlags.None, ep).ConfigureAwait(false);
                // 共享Socket可能收到其他对端的数据报,丢弃非本会话来源的数据报继续等待
                if (!IsFromRemote(result.RemoteEndPoint, remote)) continue;
                var size = result.ReceivedBytes;
#endif
                span?.Value = size;
                return pk.Resize(size);
            }
        }
        catch (Exception ex)
        {
            span?.SetError(ex, null);
            throw;
        }
    }

    /// <summary>数据接收事件</summary>
    public event EventHandler<ReceivedEventArgs>? Received;

    internal Boolean OnReceive(ReceivedEventArgs e)
    {
        LastTime = DateTime.Now;

        // 协议模式:数据报即完整帧,定界后逐条分发;空数据报仍按停会话约定处理
        if (Protocol is { } codec && e?.Packet is { Length: > 0 } pk)
        {
            ProcessDatagram(codec, pk);
            return true;
        }

        // 我们约定,UDP收到空数据包时,结束会话。判定以收到的原始数据报为准,在进入事件链之前定下:
        // 业务按契约在事件内消费/释放本轮句柄(交发送链路、或置空做标记)后 Length 已归零,
        // 事件之后再判会把正常数据报当成空包,误停会话并清掉会话级状态。要主动结束会话,请 Dispose 会话。
        var empty = e != null && e.Packet is not { Length: > 0 };

        if (e != null) Received?.Invoke(this, e);

        // 我们约定,UDP收到空数据包时,结束会话
        if (empty)
        {
            Stop("Finish");
            Dispose();
        }

        return false;
    }

    /// <summary>处理协议数据报。一个数据报可能包含多个消息帧,逐条定界分发;坏帧截断丢弃</summary>
    /// <param name="codec">协议编解码器</param>
    /// <param name="pk">数据报(接收环轮内有效,消息体为零拷贝共享切片)</param>
    private void ProcessDatagram(IMessageCodec codec, IPacket pk)
    {
        var seq = pk.AsReadOnlySequence();
        var pos = 0L;
        while (pos < seq.Length)
        {
            var rs = codec.TryParse(seq.Slice(pos));
            if (rs == null) break;

            // 损坏帧:数据报内已无法继续定界,丢弃本报文等下一包;
            // 不能按异常处理,否则一个坏报文就会下线整个 UDP 服务
            if (rs.Value.Invalid) break;

            // 已定界但需整帧(装饰协议,如压缩):数据报是原子单位,无法在包内补齐,丢弃本报文等下一包
            if (rs.Value.NeedFullFrame) break;

            // 无消息帧(心跳/空行/分隔符):跳过字节后继续解析
            if (rs.Value.Message == null)
            {
                var skip = rs.Value.HeaderSize;
                if (skip <= 0) break;
                pos += skip;
                continue;
            }

            var message = rs.Value.Message;
            var headerSize = rs.Value.HeaderSize;
            var bodyLength = rs.Value.BodyLength;

            // 声明长度超出数据报窗口:坏帧截断丢弃
            if (headerSize + bodyLength > seq.Length - pos)
            {
                message.Dispose();
                break;
            }

            // 体绑定:协议未预绑定体时才从数据报窗口切零拷贝共享切片。
            // 预绑定体(如压缩协议解压后重绑)必须保留,否则会把解压结果替换成线上压缩字节(静默交出错误数据)——与帧泵分支保持一致
            if (message.Payload == null && bodyLength > 0)
                message.SetBody(pk.Slice((Int32)pos + headerSize, (Int32)bodyLength));

            // 并行模式:数据报体为零拷贝共享切片(天然独立),信号量约束后派发;处理顺序不定(SRMP 按序列号配对)
            if (MaxConcurrency > 1)
            {
                // 等待信号量放到线程池上:接收环运行在 IOCP/线程池线程,就地阻塞会拖慢整条接收链并挤爆内核缓冲
                var concurrency = Concurrency;
                _ = Task.Run(async () =>
                {
                    try
                    {
                        await concurrency.WaitAsync().ConfigureAwait(false);
                    }
                    catch (ObjectDisposedException)
                    {
                        // 会话销毁:信号量已释放,消息无人处理,就地释放
                        message.TryDispose();
                        return;
                    }

                    ProcessMessage(message, concurrency);
                });

                pos += headerSize + bodyLength;
                continue;
            }

            var delivered = false;
            try
            {
                OnMessage(message);

                // 事件链(可观测)可能已在处理器内读空消息体;命中配对的等待方还要消费同一份体,
                // 交付前把内存模式体复位到起点(数据报体天然为内存模式,与流式会话口径一致)
                if (message.Body is { IsStreaming: false } body) body.Reset();

                // 交付阶段配对:命中则交付等待方(由等待方释放);未命中的按普通消息收尾
                delivered = _matcher.TryMatch(this, Protocol, message);
            }
            catch (Exception ex)
            {
                OnError("OnMessage", ex);
            }
            finally
            {
                if (!delivered) message.TryDispose();
            }

            pos += headerSize + bodyLength;
        }
    }

    /// <summary>并行处理单个消息:完成后释放并发信号量</summary>
    /// <param name="message">消息(体为数据报内零拷贝共享切片,天然独立)</param>
    /// <param name="concurrency">派发前等待到的信号量实例。归还必须用同一实例:会话首访并发时可能各建一个,重读属性会把槽位还到另一个满额实例并抛 <see cref="SemaphoreFullException"/></param>
    private void ProcessMessage(IMessage message, SemaphoreSlim concurrency)
    {
        var delivered = false;
        try
        {
            OnMessage(message);

            // 事件链(可观测)可能已在处理器内读空消息体;命中配对的等待方还要消费同一份体,
            // 交付前把内存模式体复位到起点(与流式会话口径一致)
            if (message.Body is { IsStreaming: false } body) body.Reset();

            // 交付阶段配对:命中则交付等待方(由等待方释放);未命中的按普通消息收尾
            delivered = _matcher.TryMatch(this, Protocol, message);
        }
        catch (Exception ex)
        {
            OnError("OnMessage", ex);
        }
        finally
        {
            if (!delivered) message.TryDispose();

            // 释放自己等待到的那个并发槽位(背压);会话销毁时该实例已被释放,忽略该异常
            try { concurrency.Release(); }
            catch (ObjectDisposedException) { }
        }
    }

    private SemaphoreSlim? _concurrency;

    /// <summary>并发信号量。并行模式(<see cref="MaxConcurrency"/> 大于1)下约束同会话并发处理数,等待时形成背压</summary>
    /// <remarks>
    /// 多条接收槽线程并发首访时可能各建一个满额实例,字段只留最后一个。该竞态无害:每个消息只归还自己等待到的实例,
    /// 不存在“等待在A、归还到B”的错位,最坏仅首访瞬间并发上限短暂放大。刻意不加 CAS/锁,不为极窄窗口引入原子操作。
    /// </remarks>
    private SemaphoreSlim Concurrency => _concurrency ??= new SemaphoreSlim(MaxConcurrency, MaxConcurrency);

    /// <summary>收到消息。构造接收事件参数并进入事件链(协议模式)</summary>
    /// <param name="message">消息(头部字段就位、体已绑定)</param>
    /// <remarks>事件参数的 <see cref="ReceivedEventArgs.Packet"/> 为消息负载视图;与消息同生命周期(处理器返回后失效)</remarks>
    private void OnMessage(IMessage message)
    {
        var e = ReceivedEventArgs.Rent();
        try
        {
            e.Local = Local.Address;
            e.Remote = Remote.EndPoint;
            e.Packet = message.Payload;
            e.Message = message;

            LastTime = DateTime.Now;
            Received?.Invoke(this, e);

            // 升格到服务器层:以服务器为接入点的场景(如 NetClient)经此收到消息
            Server?.RaiseReceiveInternal(this, e);
        }
        finally
        {
            ReceivedEventArgs.Return(e);
        }
    }

    /// <summary>处理数据帧</summary>
    /// <param name="data">数据帧</param>
    void ISocketRemote.Process(IData data) => (Server as ISocketRemote)?.Process(data);
    #endregion

    #region 异常处理
    /// <summary>错误发生/断开连接时</summary>
    public event EventHandler<ExceptionEventArgs>? Error;

    /// <summary>触发异常</summary>
    /// <param name="action">动作</param>
    /// <param name="ex">异常</param>
    protected virtual void OnError(String action, Exception ex)
    {
        Log?.Error(LogPrefix + "{0}Error {1} {2}", action, this, ex.Message);
        Error?.Invoke(this, new ExceptionEventArgs(action, ex));
    }
    #endregion

    #region 辅助
    /// <summary>已重载。返回会话的字符串表示</summary>
    /// <returns>本地地址和远程地址的组合</returns>
    public override String ToString()
    {
        if (Remote != null && !Remote.EndPoint.IsAny())
            return $"{Local}<={Remote.EndPoint}";
        else
            return Local.ToString();
    }

    /// <summary>拉取模式提示。服务器接收环已启动时拉取数据将被拒绝</summary>
    private String NoPullMessage => $"[{Name}] 服务器接收环已启动(AutoReceive=true),不允许从会话拉取数据;请改用 Received 事件接收,或在服务器打开前设置 AutoReceive=false 使用拉取模式";

    /// <summary>判断数据报是否来自本会话的远端</summary>
    /// <param name="source">数据报的实际来源地址</param>
    /// <param name="remote">本会话的远端地址</param>
    /// <returns></returns>
    private static Boolean IsFromRemote(EndPoint? source, IPEndPoint remote)
    {
        // 远端为通配地址时不做过滤(防御)
        if (remote.Address.IsAny()) return true;

        return source is IPEndPoint ep && ep.Port == remote.Port && ep.Address.Equals(remote.Address);
    }
    #endregion

    #region ITransport接口
    Boolean ITransport.Open() => true;

    Boolean ITransport.Close() => true;
    #endregion

    #region 扩展接口
    private ConcurrentDictionary<String, Object?>? _items;

    /// <summary>数据项。首次访问时创建</summary>
    /// <remarks>并发首用时以 CAS 保证只保留一份实例:直接 ??= 会各自新建,败者刚写入的数据随之不可达而丢失</remarks>
    public IDictionary<String, Object?> Items
    {
        get
        {
            var items = _items;
            if (items != null) return items;

            var created = new ConcurrentDictionary<String, Object?>();

            return Interlocked.CompareExchange(ref _items, created, null) ?? created;
        }
    }

    /// <summary>设置 或 获取 数据项</summary>
    /// <param name="key">键</param>
    /// <returns>值</returns>
    public Object? this[String key] { get => _items != null && _items.TryGetValue(key, out var obj) ? obj : null; set => Items[key] = value; }
    #endregion

    #region 日志
    /// <summary>日志提供者</summary>
    public ILog Log { get; set; } = Logger.Null;

    /// <summary>是否输出发送日志</summary>
    /// <remarks>默认false</remarks>
    public Boolean LogSend { get; set; }

    /// <summary>是否输出接收日志</summary>
    /// <remarks>默认false</remarks>
    public Boolean LogReceive { get; set; }

    private String? _LogPrefix;
    /// <summary>日志前缀</summary>
    public virtual String LogPrefix
    {
        get
        {
            if (_LogPrefix == null)
            {
                var name = Server == null ? "" : Server.Name;
                _LogPrefix = $"{name}[{ID}].";
            }
            return _LogPrefix;
        }
        set => _LogPrefix = value;
    }

    /// <summary>输出日志</summary>
    /// <param name="format">格式化字符串</param>
    /// <param name="args">参数</param>
    public void WriteLog(String format, params Object?[] args) => Log.Info(LogPrefix + format, args);
    #endregion
}