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.

574 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
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` 子类:
```csharp
/// <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 服务器启动配置
```csharp
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 群发
```csharp
// 群发数据给所有在线客户端
await server.SendAllAsync(data);
// 带过滤条件群发
await server.SendAllAsync(data, session => session.ID > 100);
// 群发管道消息
server.SendAllMessage(message, session => session["VIP"] is true);
```
群发要求 `UseSession = true`(默认开启)。
### 2.6 事件模式(简单场景)
不需要自定义会话时,可直接使用事件:
```csharp
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()` 扩展方法创建:
```csharp
// 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 客户端使用
```csharp
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 请求-响应模式(需要管道编解码器)
```csharp
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
```csharp
// 服务端 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 添加编解码器
```csharp
// 服务端添加
server.Add<StandardCodec>();
// 客户端添加
client.Add<StandardCodec>();
// 多层管道级联(按添加顺序组成链)
server.Add<StandardCodec>(); // 底层:粘包拆包 + 请求响应匹配
server.Add<JsonCodec>(); // 上层JSON 编解码
```
### 4.4 StandardCodec 请求-响应
StandardCodec 使用 `DefaultMessage`,包含 Flag1字节、Sequence1字节、Length2字节
支持自动序列号分配和请求-响应匹配。
```csharp
// 服务端 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>` | 需要粘包拆包和/或请求-响应匹配(内置 `IMatchQueue`、`Encode`/`Decode` | `StandardCodec`、`LengthFieldCodec` |
| `Handler` | 简单转换、帧协议、文本协议(轻量,仅 `Read`/`Write`/`Open`/`Close` | `JsonCodec`、`SplitDataCodec`、`WebSocketCodec` |
### 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`
```csharp
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` 基类自动 `TryDispose``Handler` 子类需在 `Write``finally` 中手动调用
- **对象池**`DefaultMessage.Rent()` / `DefaultMessage.Return()` 减少 GC 压力
#### 4.6.3 请求-响应匹配
`MessageCodec<T>` 内置 `IMatchQueue`,流程:`Write` → `AddToQueue` 入队 → `Decode` 解码 → `Queue.Match``IsMatch` 匹配 → 唤醒 `SendMessageAsync``Task`
- 重载 `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<T>(需要粘包/请求响应匹配)
```csharp
/// <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简单转换/帧协议)
```csharp
/// <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` 注入自定义业务处理器:
```csharp
class MyServer : NetServer<MySession>
{
/// <summary>为会话创建网络数据处理器</summary>
public override INetHandler? CreateHandler(INetSession session) => new MyHandler();
}
```
处理器在会话 `Start` 时初始化,`OnReceive` 前调用 `Process`,适合前置协议解析。
---
## 6. 常见错误
- ❌ 在 `OnReceive` 中执行长时间阻塞操作(会影响其他连接的数据接收)
- ❌ 不加管道编解码器直接调用 `SendMessageAsync`(无法匹配响应)
- ❌ 混淆 `Send``SendMessage`:前者直接发原始数据,后者经过管道编码
- ❌ 混淆 `SendMessage``SendReply`:响应消息必须用 `SendReply` 关联请求上下文
- ❌ 忘记调用 `base.OnConnected()` / `base.OnDisconnected(reason)` / `base.OnReceive(e)`
- ❌ 在会话中使用 `Task.Result``Task.Wait()`(导致死锁和线程池饥饿)
- ❌ 使用固定端口编写测试(端口冲突),应使用 `Port = 0`
- ❌ 服务端 SSL 未指定证书
---
## 7. 完整示例
### 7.1 带 StandardCodec 的 Echo 服务
```csharp
// 服务端
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 自定义会话服务器
```csharp
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);
}
}
```
---
(完)