使用 Proto.Actor 构建 TCP 服务器:探索 .NET 中的 Actor 模型

简介

在上一篇文章 中,我演示了 Proto.Actor(适用于 .NET 的 Actor 模型框架)的基础用法。本文将构建一个更复杂的示例:使用三个 Actor 实现 TCP 服务器,分别处理连接、字节接收和数据处理。

项目概述

核心 Actor

1. WaitForTcpConnectionActor

  •  监听新的 TCP 连接。
  • 为每个连接生成 ReceiveBytesActor 实例。

2. ReceiveBytesActor

  • 从套接字接收字节流。
  • 生成 ProcessActor 实例以反序列化并记录数据;若失败最多重启 3 次。

3. ProcessActor

  • 将传入字节反序列化为 Sample 对象。
  • 将对象内容输出到控制台。

要求

启动 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(WaitForTcpConnectionActorReceiveBytesActorProcessActor)实现 TCP 服务器,我们探索了以下核心概念:

核心实践

  • 监督策略 :通过 OneForOneStrategy 配置子 Actor 在失败时最多重启 3 次,确保临时性错误(如消息解析失败)不会导致系统崩溃。
  • 生命周期管理 :通过 StartedTerminatedRestarting 等消息安全地管理 Actor 状态和资源释放。
  • 消息弹性 :通过 ResendSocketAcceptedProcessCompleted 等机制重试失败操作,保障数据完整性。

学习与生产环境的权衡

本示例为教学目的简化了复杂场景。例如:

  1. _socket.Available 的局限性

    • 演示用途 :直接使用 Available 属性读取数据长度在演示中便捷,但在生产环境中不可靠(可能读取到不完整数据)。
    • 生产建议 :采用动态缓冲(如 MemoryStream)处理变长数据流,并结合消息边界标识(如前缀长度字段)。
  2. 错误处理的改进

    • 当前实现 :缺乏对边缘情况(如畸形输入)的健壮异常处理。
    • 生产建议 :将套接字操作包裹在 try-catch 块中,并系统化记录错误日志(如通过 Serilog 等工具)。
  3. 资源清理的增强

    • 基础保障 :ProcessCompleted 消息确保套接字和 Actor 被正确释放。
    • 生产优化 :添加超时机制或心跳检测,避免孤儿连接(如客户端异常断开后资源未释放)。

完整代码

GitHub 仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值