You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

applyTo
**/Net/**

网络编程指令

适用于基于 NewLife.Net 的网络服务器(NetServer)和客户端(ISocketClient)开发任务。


1. 架构概览

NewLife 网络框架分为两层:

层级 服务端 客户端 说明
应用层 NetServer / NetServer<TSession> 管理监听、会话生命周期、管道
传输层 TcpServer / UdpServer TcpSession / UdpServer(客户端模式) 底层 Socket 收发
会话 NetSession / NetSession<TServer> 每个连接对应一个会话,业务逻辑入口
管道 IPipeline + IPipelineHandler 同左 编解码、粘包拆包、消息匹配

关键接口

  • ISocketClient — 客户端连接接口Open/Close/Send/Receive
  • ISocketRemote — 远程通信接口Send/Receive/SendMessageAsync
  • INetSession — 网络会话接口(服务端每个连接的业务处理单元)
  • INetHandler — 网络数据处理器接口Init/Process

2. 服务端开发规范

2.1 基本模式

推荐使用泛型 NetServer<TSession> + 自定义 NetSession 子类:

/// <summary>自定义网络服务器</summary>
class MyServer : NetServer<MySession> { }

/// <summary>自定义会话,每个客户端连接对应一个实例</summary>
class MySession : NetSession<MyServer>
{
    /// <summary>客户端连接</summary>
    protected override void OnConnected()
    {
        base.OnConnected();
        WriteLog("客户端已连接 {0}", Remote);
    }

    /// <summary>收到客户端数据</summary>
    protected override void OnReceive(ReceivedEventArgs e)
    {
        base.OnReceive(e);
        // 业务处理
    }

    /// <summary>客户端断开</summary>
    protected override void OnDisconnected(String reason)
    {
        base.OnDisconnected(reason);
    }
}

2.2 服务器启动配置

var server = new MyServer
{
    Port = 8080,                              // 监听端口0 表示随机
    ProtocolType = NetType.Tcp,               // Tcp/Udp/Unknown同时监听
    // AddressFamily = AddressFamily.InterNetwork, // 仅IPv4默认同时IPv4+IPv6
    ServiceProvider = provider,               // 依赖注入
    Log = XTrace.Log,                         // 应用日志
    SessionLog = XTrace.Log,                  // 会话日志
    Tracer = tracer,                          // APM 追踪
#if DEBUG
    SocketLog = XTrace.Log,                   // Socket 层日志(仅调试)
    LogSend = true,
    LogReceive = true,
#endif
};
server.Start();

2.3 会话生命周期

连接建立 → OnConnected() → OnReceive()... → OnDisconnected(reason) → Dispose()
  • OnConnected:初始化会话状态、发送欢迎消息
  • OnReceive:核心业务处理入口,e.Packet 为原始数据,e.Message 为管道解码后的消息
  • OnDisconnected:清理资源、记录日志,reason 包含断开原因
  • 会话内可通过 ServiceProvider 获取 Scoped 服务

2.4 服务端发送数据

方法 说明
Send(IPacket) 直接发送原始数据,不经过管道
Send(String) 发送字符串,默认 UTF-8
Send(ReadOnlySpan<Byte>) 高性能发送
SendMessage(Object) 通过管道编码后发送,不等待响应
SendReply(Object, ReceivedEventArgs) 发送响应消息,与请求关联(用于 StandardCodec 等协议)
SendMessageAsync(Object) 通过管道发送并等待响应

2.5 群发

// 群发数据给所有在线客户端
await server.SendAllAsync(data);

// 带过滤条件群发
await server.SendAllAsync(data, session => session.ID > 100);

// 群发管道消息
server.SendAllMessage(message, session => session["VIP"] is true);

群发要求 UseSession = true(默认开启)。

2.6 事件模式(简单场景)

不需要自定义会话时,可直接使用事件:

var server = new NetServer { Port = 8080 };
server.Received += (sender, e) =>
{
    if (sender is INetSession session)
        session.Send(e.Packet);  // Echo
};
server.Start();

3. 客户端开发规范

3.1 创建客户端

通过 NetUri.CreateRemote() 扩展方法创建:

// TCP 客户端
var client = new NetUri("tcp://127.0.0.1:8080").CreateRemote();

// UDP 客户端
var client = new NetUri("udp://127.0.0.1:8080").CreateRemote();

// WebSocket 客户端
var client = new NetUri("ws://127.0.0.1:8080/path").CreateRemote();

CreateRemote 根据协议自动返回 TcpSession / UdpServer / WebSocketClient

3.2 客户端使用

var uri = new NetUri("tcp://127.0.0.1:8080");
var client = uri.CreateRemote();
client.Log = XTrace.Log;
client.Open();

// 发送原始数据(不经过管道)
client.Send("Hello");

// 事件驱动接收
client.Received += (sender, e) =>
{
    // e.Packet 原始数据e.Message 管道解码后的消息
};

// 或同步/异步接收
using var pk = client.Receive();
using var pk = await client.ReceiveAsync(cancellationToken);

client.Close("完成");  // 或 client.Dispose()

3.3 请求-响应模式(需要管道编解码器)

var client = new NetUri("tcp://127.0.0.1:8080").CreateRemote();
client.Add<StandardCodec>();
client.Open();

var response = await client.SendMessageAsync(payload, cancellationToken);  // 等待响应
client.SendMessage(message);  // 不等待响应

3.4 SSL/TLS

// 服务端 SSL
var server = new NetServer
{
    Port = 443,
    SslProtocol = SslProtocols.Tls12,
    Certificate = new X509Certificate2("server.pfx", "password"),
};

// 客户端 SSL自动根据端口判断或手动指定
var client = new NetUri("tcp://host:443").CreateRemote();
if (client is TcpSession tcp)
{
    tcp.SslProtocol = SslProtocols.Tls12;
    // tcp.Certificate = cert;  // 客户端证书(如果服务端要求)
}

4. 管道与编解码器

4.1 管道机制

管道(IPipeline是处理器链Read/Write 返回值作为下一个处理器的输入,返回 null 截断管道:

接收Socket → [Codec1.Read] → [Codec2.Read] → FireRead → OnReceive
发送SendMessage → [Codec2.Write] → [Codec1.Write] → FireWrite → Socket

Open 正序传播Close 逆序传播。先添加的在底层(靠近 Socket后添加的在上层靠近业务

4.2 内置编解码器

编解码器 基类 说明 典型场景
StandardCodec MessageCodec<IMessage> 4字节头部Flag+Seq+Length支持请求-响应匹配 自定义 RPC 协议
LengthFieldCodec MessageCodec<IPacket> 长度字段头部,可配置偏移和大小 MQTT、通用二进制协议
JsonCodec Handler JSON 文本编解码,不处理粘包 文本协议(通常与 StandardCodec 级联)
SplitDataCodec Handler 分隔符拆包(默认 \r\n 文本行协议
WebSocketCodec Handler WebSocket 帧编解码 WebSocket 通信

4.3 添加编解码器

// 服务端添加
server.Add<StandardCodec>();

// 客户端添加
client.Add<StandardCodec>();

// 多层管道级联(按添加顺序组成链)
server.Add<StandardCodec>();  // 底层:粘包拆包 + 请求响应匹配
server.Add<JsonCodec>();      // 上层JSON 编解码

4.4 StandardCodec 请求-响应

StandardCodec 使用 DefaultMessage,包含 Flag1字节、Sequence1字节、Length2字节 支持自动序列号分配和请求-响应匹配。

// 服务端 Echo 示例
server.Add<StandardCodec>();
server.Received += (sender, e) =>
{
    if (sender is INetSession session && e.Message is IPacket pk)
        session.SendReply(pk, e);  // 使用 SendReply 关联请求上下文
};

// 客户端请求-响应
client.Add<StandardCodec>();
var response = await client.SendMessageAsync(payload);

4.5 基类选择

基类 适用场景 典型代表
MessageCodec<T> 需要粘包拆包和/或请求-响应匹配(内置 IMatchQueueEncode/Decode StandardCodecLengthFieldCodec
Handler 简单转换、帧协议、文本协议(轻量,仅 Read/Write/Open/Close JsonCodecSplitDataCodecWebSocketCodec

4.6 编解码器设计规范

4.6.1 粘包拆包PacketCodec 模式)

TCP 是字节流协议,必须处理粘包拆包。统一模式(完整实现见 4.7 模板):

  1. 每个连接独立的 PacketCodec 实例,存储在 ss["Codec"]
  2. 通过 GetLength2 委托告诉 PacketCodec 如何计算完整帧长度
  3. PacketCodec.Parse() 返回完整帧列表,自动缓存不完整数据

GetLength2 规范(签名 Int32 GetLength(ReadOnlySpan<Byte> span)):返回帧完整长度(含头部),数据不足时返回 0

public static Int32 GetLength(ReadOnlySpan<Byte> span)
{
    if (span.Length < 4) return 0;
    var reader = new SpanReader(span) { IsLittleEndian = true };
    reader.Advance(2);
    return 4 + reader.ReadUInt16();  // 头部4字节 + 负载长度
}

4.6.2 编码与内存管理

  • ExpandHeader(size):编码时优先复用负载缓冲区前置空间写入头部,零拷贝;空间不足时创建 OwnerPacket,原包作为 Next 链节点
  • SpanWriter:配合 ExpandHeader 写入头部字段,注意 IsLittleEndian 大小端
  • 兜底释放MessageCodec<T>.Write 基类自动 TryDisposeHandler 子类需在 Writefinally 中手动调用
  • 对象池DefaultMessage.Rent() / DefaultMessage.Return() 减少 GC 压力

4.6.3 请求-响应匹配

MessageCodec<T> 内置 IMatchQueue,流程:WriteAddToQueue 入队 → Decode 解码 → Queue.MatchIsMatch 匹配 → 唤醒 SendMessageAsyncTask

  • 重载 AddToQueue:控制哪些消息入队(通常只有请求消息)
  • 重载 IsMatch:根据序列号等字段匹配请求和响应(见 4.7 模板)
  • QueueSize:匹配队列大小,默认 256
  • Timeout:等待响应超时,默认 30_000ms
  • UserPacket:为 true 时向上层传递 Payload 而非整个 IMessage,用于编码器级联

4.6.4 Close 清理

必须Close 中执行 ss["Codec"] = null 清理 PacketCodec,否则 MemoryStream 缓存泄漏(见 4.7 模板)。

4.6.5 上下文扩展IExtend

管道处理器通过 IExtend 在会话/上下文上传递元数据:

用途 示例
"Codec" 每连接的 PacketCodec 实例 编解码器的 Decode/Close 中读写
"Flag" 数据类型标记 DataKinds JsonCodec.Write 设置 → StandardCodec.Write 消费
"_raw_message" 原始请求消息 MessageCodec.Read 设置 → Write 中创建响应时消费
"TaskSource" TaskCompletionSource 框架内部,AddToQueue 消费

4.6.6 多层管道级联

  • 底层编解码器处理粘包拆包和请求-响应匹配,上层处理数据格式转换
  • UserPacket = true 让底层向上层传递 Payload 而非整个 IMessage
  • 上层通过 ext["Flag"] 向底层传递数据类型标记

4.7 自定义编解码器模板

方式一:继承 MessageCodec需要粘包/请求响应匹配)

/// <summary>自定义协议编解码器</summary>
public class MyCodec : MessageCodec<MyMessage>
{
    /// <summary>编码消息为数据包</summary>
    protected override Object? Encode(IHandlerContext context, MyMessage msg)
    {
        return msg.ToPacket();
    }

    /// <summary>解码数据包为消息</summary>
    protected override IEnumerable<MyMessage>? Decode(IHandlerContext context, IPacket pk)
    {
        if (context.Owner is not IExtend ss) yield break;

        if (ss["Codec"] is not PacketCodec pc)
        {
            ss["Codec"] = pc = new PacketCodec
            {
                GetLength2 = MyMessage.GetLength,
                MaxCache = MaxCache,
                Tracer = (context.Owner as ISocket)?.Tracer
            };
        }

        foreach (var item in pc.Parse(pk))
        {
            var msg = new MyMessage();
            if (msg.Read(item)) yield return msg;
        }
    }

    /// <summary>是否匹配响应</summary>
    protected override Boolean IsMatch(Object? request, Object? response) =>
        request is MyMessage req && response is MyMessage res
        && req.Sequence == res.Sequence;

    /// <summary>连接关闭时清理</summary>
    public override Boolean Close(IHandlerContext context, String reason)
    {
        if (context.Owner is IExtend ss) ss["Codec"] = null;

        return base.Close(context, reason);
    }
}

方式二:继承 Handler简单转换/帧协议)

/// <summary>自定义帧编解码器</summary>
public class MyFrameCodec : Handler
{
    /// <summary>读取数据(接收时)</summary>
    public override Object? Read(IHandlerContext context, Object message)
    {
        if (message is IPacket pk)
        {
            // 解码:二进制 → 业务对象
            var frame = MyFrame.Parse(pk);
            message = frame;
        }

        return base.Read(context, message);
    }

    /// <summary>写入数据(发送时)</summary>
    public override Object? Write(IHandlerContext context, Object message)
    {
        IPacket? owner = null;
        if (message is MyFrame frame)
        {
            // 编码:业务对象 → 二进制
            message = owner = frame.ToPacket();
        }

        try
        {
            return base.Write(context, message);
        }
        finally
        {
            owner.TryDispose();  // 兜底释放
        }
    }

    /// <summary>连接关闭时清理缓存</summary>
    public override Boolean Close(IHandlerContext context, String reason)
    {
        if (context.Owner is IExtend ss) ss["Codec"] = null;

        return base.Close(context, reason);
    }
}

5. 常见模式与最佳实践

5.1 端口选择

  • 测试代码使用端口 0(系统自动分配随机端口),避免端口冲突
  • 正式服务指定固定端口
  • 启动后可通过 server.Port 获取实际监听端口

5.2 协议选择

场景 推荐
可靠传输、长连接 NetType.Tcp
低延迟、广播、允许丢包 NetType.Udp
同时支持(默认) NetType.Unknown
Web 浏览器通信 NetType.WebSocket

5.3 会话管理

  • UseSession = true(默认):维护会话集合,支持群发、按 ID 查找
  • UseSession = false:不维护会话集合,减少内存开销,适合海量短连接
  • SessionTimeout:设置会话超时时间(秒),超时无数据自动断开
  • 会话中通过 Items 字典存储自定义数据

5.4 日志分层

属性 用途 建议
Log 服务器应用层日志 始终设置
SessionLog 会话级别日志 调试时设置
SocketLog 底层 Socket 日志 仅 DEBUG 时设置
LogSend / LogReceive 收发数据内容日志 仅 DEBUG 时开启
Tracer 应用层 APM 生产环境追踪
SocketTracer Socket 层 APM 排查底层问题

5.5 资源释放

  • 服务端:调用 server.Stop(reason)server.Dispose()
  • 客户端:调用 client.Close(reason)client.Dispose()
  • 会话自动随连接断开释放,无需手动管理
  • ISocketClient 实现 IDisposable,推荐 using 模式

5.6 INetHandler 业务处理器

通过重载 NetServer.CreateHandler 注入自定义业务处理器:

class MyServer : NetServer<MySession>
{
    /// <summary>为会话创建网络数据处理器</summary>
    public override INetHandler? CreateHandler(INetSession session) => new MyHandler();
}

处理器在会话 Start 时初始化,OnReceive 前调用 Process,适合前置协议解析。


6. 常见错误

  • OnReceive 中执行长时间阻塞操作(会影响其他连接的数据接收)
  • 不加管道编解码器直接调用 SendMessageAsync(无法匹配响应)
  • 混淆 SendSendMessage:前者直接发原始数据,后者经过管道编码
  • 混淆 SendMessageSendReply:响应消息必须用 SendReply 关联请求上下文
  • 忘记调用 base.OnConnected() / base.OnDisconnected(reason) / base.OnReceive(e)
  • 在会话中使用 Task.ResultTask.Wait()(导致死锁和线程池饥饿)
  • 使用固定端口编写测试(端口冲突),应使用 Port = 0
  • 服务端 SSL 未指定证书

7. 完整示例

7.1 带 StandardCodec 的 Echo 服务

// 服务端
var server = new NetServer
{
    Port = 8080,
    ProtocolType = NetType.Tcp,
    Log = XTrace.Log,
};
server.Add<StandardCodec>();
server.Received += (sender, e) =>
{
    if (sender is INetSession session && e.Message is IPacket pk)
        session.SendReply(pk, e);
};
server.Start();

// 客户端
var client = new NetUri($"tcp://127.0.0.1:{server.Port}").CreateRemote();
client.Add<StandardCodec>();
client.Open();

var response = await client.SendMessageAsync(new ArrayPacket("Hello".GetBytes()));

7.2 自定义会话服务器

class ChatServer : NetServer<ChatSession> { }

class ChatSession : NetSession<ChatServer>
{
    protected override void OnConnected()
    {
        base.OnConnected();
        Send($"欢迎 [{Remote}] 进入聊天室!\r\n");
    }

    protected override void OnReceive(ReceivedEventArgs e)
    {
        base.OnReceive(e);
        var msg = e.Packet?.ToStr();
        if (msg.IsNullOrEmpty()) return;

        // 广播给所有在线用户
        var host = (this as INetSession).Host;
        host.SendAllMessage($"[{ID}] {msg}");
    }

    protected override void OnDisconnected(String reason)
    {
        base.OnDisconnected(reason);
        WriteLog("用户离开:{0}", reason);
    }
}

(完)