简介
在上一篇文章 中,我演示了 Proto.Actor(适用于 .NET 的 Actor 模型框架)的基础用法。本文将构建一个更复杂的示例:使用三个 Actor 实现 TCP 服务器,分别处理连接、字节接收和数据处理。
项目概述
核心 Actor
1. WaitForTcpConnectionActor
- 监听新的 TCP 连接。
- 为每个连接生成
ReceiveBytesActor
实例。
2. ReceiveBytesActor
- 从套接字接收字节流。
- 生成
ProcessActor
实例以反序列化并记录数据;若失败最多重启 3 次。
3. ProcessActor
- 将传入字节反序列化为
Sample
对象。 - 将对象内容输出到控制台。
要求
- .NET 8 或更高版本
- NuGet 包 :
启动 Actor 系统
配置 Actor 系统以生成 WaitForTcpConnectionActor
,并在按下 Ctrl+C 时停止它:
using Proto;
using TcpServer;
var system = new ActorSystem();
var cancellationTokenSource = new CancellationTokenSource();
Console.CancelKeyPress += (_, _) =>
{
cancellationTokenSource.Cancel();
};
system.Root.Spawn(Props.FromProducer(() => new WaitForTcpConnectionActor(9091)));
while (!cancellationTokenSource.IsCancellationRequested)
{
await Task.Delay(1_000);
}
await system.ShutdownAsync();
等待 TCP 连接的 Actor
Actor 模型通过 Actor 之间传递消息实现通信。我们将使用内置消息(如 Started 和 Terminated)以及自定义消息 WaitForNextConnection。
如上一篇文章 所示,Actor 的定义方式如下:
public class WaitForTcpConnectionActor(int port) : IActor
{
public async Task ReceiveAsync(IContext context)
{
}
}
启动 TCP 监听器
第一步是启动 TCP 服务器。为此,我们使用内置的 Started
消息:
public class WaitForTcpConnectionActor(int port) : IActor
{
private TcpListener? _listener;
public async Task ReceiveAsync(IContext context)
{
if (context.Message is Started)
{
Console.WriteLine("正在 9091 端口监听");
_listener = TcpListener.Create(port);
_listener.Start();
}
}
}
随后,等待客户端连接。为此,我们需要向自身发送一条 WaitForNextConnection
消息:
public class WaitForTcpConnectionActor(int port) : IActor
{
private TcpListener? _listener;
public async Task ReceiveAsync(IContext context)
{
if(context.Message is Started)
{
_listener = TcpListener.Create(port);
_listener.Start();
context.Send(context.Self, new WaitForNextConnection());
}
}
}
等待 TCP 连接
现在我们已开始监听连接,下一步是接收它们,并为每个连接生成新的 ReceiveBytesActor
实例:
public class WaitForTcpConnectionActor(int port) : IActor
{
...
public async Task ReceiveAsync(IContext context)
{
if(context.Message is Started)
{
...
}
else if(context.Message is WaitForNextConnection)
{
var socket = await _listener!.AcceptSocketAsync(cancellationToken);
var actor = context.Spawn(Props.FromProducer(() => new ReceiveBytesActor()))
.WithChildSupervisorStrategy(new OneForOneStrategy(
(_, exception) =>
{
Console.WriteLine("Error: {0}", exception);
return SupervisorDirective.Restart;
},
3,
TimeSpan.FromSeconds(1)));;
context.Send(actor, new SocketAccepted(socket));
context.Send(context.Self, new WaitForNextConnection());
}
}
}
Actor 监督机制
我们通过 OneForOneStrategy 配置对 ReceiveBytesActor
实例的监督策略:
- 若子 Actor 发生故障 ,它将在 1 秒内最多重启 3 次 。
- 此策略确保临时性错误 (如消息格式错误)不会导致整个系统崩溃 。
通知完成
当处理完成后,子 Actor 会向父 Actor 发送 ProcessCompleted
消息。这提示父 Actor 显式停止子 Actor ,确保资源清理并避免内存泄漏。
public class WaitForTcpConnectionActor(int port) : IActor
{
...
public async Task ReceiveAsync(IContext context)
{
if(context.Message is Started)
{
...
}
else if(context.Message is { Message: Terminated, Sender: not null }))
{
_listener?.Dispose();
}
else if(context.Message is ProcessCompleted)
{
await context.StopAsync(Sender);
}
else if(context.Message is WaitForNextConnection)
{
...
}
}
}
资源清理
当连接处理完成后:
ProcessCompleted
消息 通知任务完成。- 父 Actor 停止子 Actor 并通过终止 Actor 触发资源清理。
完整的 WaitForTcpConnectionActor 代码
using System.Net.Sockets;
using Proto;
namespace TcpServer;
public class WaitForTcpConnectionActor(int port) : IActor
{
private static readonly Props Props = Props.FromProducer(() => new ReceiveBytesActor())
.WithChildSupervisorStrategy(new OneForOneStrategy(
(_, exception) =>
{
Console.WriteLine("Error: {0}", exception);
return SupervisorDirective.Restart;
},
3,
TimeSpan.FromSeconds(1)));
private TcpListener? _listener;
public async Task ReceiveAsync(IContext context)
{
if (context is { Message: Terminated, Sender: not null })
{
Close();
}
else if (context.Message is Started)
{
Open();
context.Send(context.Self, new WaitForNextConnection());
}
else if (context.Message is ProcessCompleted)
{
Console.WriteLine("stopping actor: {0}", context.Sender);
await context.StopAsync(context.Sender!);
}
else if(context.Message is WaitForNextConnection)
{
var socket = await AcceptTcpClientAsync(context.CancellationToken);
Console.WriteLine("Accepted connection from {0}", socket.RemoteEndPoint);
var actor = context.Spawn(Props);
context.Send(actor, new SocketAccepted(socket));
context.Send(context.Self, new WaitForNextConnection());
}
}
private void Open()
{
Close();
Console.WriteLine("Listening on port 9091");
_listener = TcpListener.Create(port);
_listener.Start();
}
private async Task<Socket> AcceptTcpClientAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Waiting for connection...");
return await _listener!.AcceptSocketAsync(cancellationToken);
}
private void Close()
{
Console.WriteLine("Closing listener");
_listener?.Dispose();
}
}
优雅关闭
当 Actor 系统关闭时,必须正确释放 TCP 监听器 ,以避免资源泄漏:
public class WaitForTcpConnectionActor(int port) : IActor
{
...
public async Task ReceiveAsync(IContext context)
{
if(context.Message is Started)
{
...
}
else if(context.Message is { Message: Terminated, Sender: not null }))
{
_listener?.Dispose();
}
else if(context.Message is ProcessCompleted)
{
....
}
else if(context.Message is WaitForNextConnection)
{
...
}
}
}
接收字节
下一步是从套接字接收字节流:
处理 SocketAccepted
事件
当新连接被接受时,Actor 会存储套接字实例并读取可用字节:
public class ReceiveBytesActor : IActor
{
private Socket? _socket;
private byte[]? _buffer;
public async Task ReceiveAsync(IContext context)
{
if(context.Message is SocketAccepted socket)
{
_socket = socket;
_buffer = new byte[_socket.Available];
await _socket.ReceiveAsync(_buffer);
var props = Props.FromProducer(() => new ProcessActor());
var actor = context.SpawnNamed(props, "json-serializer");
context.Send(actor, new SocketReceived(_buffer!));
}
}
}
通知完成
处理完成后,当前 Actor 停止子级 ProcessActor
并 通知其父级释放资源 :
public class ReceiveBytesActor : IActor
{
...
public async Task ReceiveAsync(IContext context)
{
if(context.Message is SocketAccepted socket)
{
...
}
else if(context.Message is ProcessCompleted)
{
await context.StopAsync(Sender);
context.Send(context.Parent!, new ProcessCompleted());
}
}
}
优雅关闭套接字
当 Actor 终止时,释放套接字资源 并停止所有子级 Actor 以防止资源泄漏:
public class ReceiveBytesActor : IActor
{
...
public async Task ReceiveAsync(IContext context)
{
if(context.Message is Terminated)
{
_buffer = null;
_socket?.Dispose();
await context.Children.StopMany(context);
}
else if(context.Message is SocketAccepted socket)
{
...
}
else if(context.Message is ProcessCompleted)
{
...
}
}
}
重新发送缓冲数据
若 ProcessActor
发生故障并重启,ReceiveBytesActor
会重新发送其缓冲区中的数据以重新处理:
public class ReceiveBytesActor : IActor
{
...
public async Task ReceiveAsync(IContext context)
{
if(context.Message is Terminated)
{
...
}
else if(context.Message is SocketAccepted socket)
{
...
}
else if(context.Message is ProcessCompleted)
{
...
}
else if(context.Message is ResendBufferReceived)
{
context.Send(Sender, new ResendBufferReceived(_buffer!));
}
}
}
完整的 ReceiveBytesActor 代码
using System.Net.Sockets;
using Proto;
namespace TcpServer;
public class ReceiveBytesActor : IActor
{
private Socket? _socket;
private byte[]? _buffer;
public async Task ReceiveAsync(IContext context)
{
if (context.Message is Terminated)
{
Console.WriteLine("Terminating actor");
_buffer = null;
_socket?.Dispose();
await context.Children.StopMany(context);
}
else if (context.Message is ProcessCompleted)
{
Console.WriteLine("Process completed");
await context.StopAsync(context.Sender!);
context.Send(context.Parent!, new ProcessCompleted());
}
else if (context.Message is SocketAccepted socketAccepted)
{
await SocketAccepted(socketAccepted.Socket);
var props = Props.FromProducer(() => new ProcessActor());
var actor = context.SpawnNamed(props, "json-serializer");
context.Send(actor, new BufferReceived(_buffer!));
}
else if (context.Message is ResendBufferReceived)
{
context.Send(context.Sender!, new BufferReceived(_buffer!));
}
}
private async Task SocketAccepted(Socket socket)
{
if (_socket != null)
{
return;
}
_socket = socket;
_buffer = new byte[_socket.Available];
await _socket.ReceiveAsync(_buffer);
}
}
ProcessActor
最终的 Actor 负责反序列化数据并记录日志:
BufferReceived 消息
BufferReceived
消息包含从套接字接收的原始字节流。该 Actor 将数据反序列化为 Sample
对象并输出到控制台。处理完成后,通过 ProcessCompleted
消息通知父级 Actor(ReceiveBytesActor
)清理资源:
public class ProcessActor : IActor
{
public Task ReceiveAsync(IContext context)
{
if (context.Message is BufferReceived socketReceived)
{
var json = JsonSerializer.Deserialize<Sample>(socketReceived.Data)!;
Console.WriteLine("Received sample with id: {0} and name: {1}", json.Id, json.Name);
context.Send(context.Parent!, new ProcessCompleted(context.Self));
}
return Task.CompletedTask;
}
}
重启机制
当 Actor 发生故障并重启时,Proto.Actor 会向该 Actor 发送 Restarting
消息。这使得重启的 Actor 能够通知其父级重新传输原始消息(或状态),以便重启后的 Actor 可以重新处理这些数据
public class ProcessActor : IActor
{
public Task ReceiveAsync(IContext context)
{
if (context.Message is Restarting)
{
context.Send(context.Parent!, new ResendBufferReceived());
}
else if (context.Message is BufferReceived socketReceived)
{
var json = JsonSerializer.Deserialize<Sample>(socketReceived.Data)!;
Console.WriteLine("Received sample with id: {0} and name: {1}", json.Id, json.Name);
context.Send(context.Parent!, new ProcessCompleted(context.Self));
}
return Task.CompletedTask;
}
}
完整的 ProcessActor 代码
using System.Text.Json;
using Proto;
namespace TcpServer;
public class ProcessActor : IActor
{
public Task ReceiveAsync(IContext context)
{
if (context.Message is Restarting)
{
context.Send(context.Parent!, new ResendBufferReceived());
}
else if (context.Message is BufferReceived socketReceived)
{
var json = JsonSerializer.Deserialize<Sample>(socketReceived.Data)!;
Console.WriteLine("Received sample with id: {0} and name: {1}", json.Id, json.Name);
context.Send(context.Parent!, new ProcessCompleted());
}
return Task.CompletedTask;
}
}
TCP 客户端
最后,我们来实现一个简单的 TCP 客户端,它会将用户输入转换为 JSON 格式并发送到服务器:
using System.Net.Sockets;
using System.Text.Json;
using TcpServer.Client;
var id = 0;
while (true)
{
Console.Write("Type a name (q to quit/f to non json): ");
var name = Console.ReadLine();
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
if (name == "q")
{
break;
}
try
{
var connection = new TcpClient();
await connection.ConnectAsync("localhost", 9091);
var stream = connection.GetStream();
if (name == "f")
{
await stream.WriteAsync(new[] { (byte)'f' });
}
else
{
await JsonSerializer.SerializeAsync(stream, new Sample
{
Id = id++,
Name = name,
});
}
connection.Close();
}
catch (Exception e)
{
Console.WriteLine("Error: {0}",e);
}
}
结论
Proto.Actor 库是构建容错系统的强大工具,它展示了 Actor 模型如何简化并发处理、资源管理和错误恢复。通过使用三个 Actor(WaitForTcpConnectionActor
、ReceiveBytesActor
和 ProcessActor
)实现 TCP 服务器,我们探索了以下核心概念:
核心实践
- 监督策略 :通过
OneForOneStrategy
配置子 Actor 在失败时最多重启 3 次,确保临时性错误(如消息解析失败)不会导致系统崩溃。 - 生命周期管理 :通过
Started
、Terminated
和Restarting
等消息安全地管理 Actor 状态和资源释放。 - 消息弹性 :通过
ResendSocketAccepted
和ProcessCompleted
等机制重试失败操作,保障数据完整性。
学习与生产环境的权衡
本示例为教学目的简化了复杂场景。例如:
-
_socket.Available
的局限性- 演示用途 :直接使用
Available
属性读取数据长度在演示中便捷,但在生产环境中不可靠(可能读取到不完整数据)。 - 生产建议 :采用动态缓冲(如
MemoryStream
)处理变长数据流,并结合消息边界标识(如前缀长度字段)。
- 演示用途 :直接使用
-
错误处理的改进
- 当前实现 :缺乏对边缘情况(如畸形输入)的健壮异常处理。
- 生产建议 :将套接字操作包裹在
try-catch
块中,并系统化记录错误日志(如通过 Serilog 等工具)。
-
资源清理的增强
- 基础保障 :
ProcessCompleted
消息确保套接字和 Actor 被正确释放。 - 生产优化 :添加超时机制或心跳检测,避免孤儿连接(如客户端异常断开后资源未释放)。
- 基础保障 :