You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

游戏服务器TCP客户端线程安全架构:单线程队列实现问询

解决TCP游戏服务器的线程安全问题:单线程顺序处理数据包

你的思路完全正确——保持异步接收的高效性,同时把数据包处理和发送放到单独的顺序执行线程里,就能从根本上避免并发操作共享资源带来的线程安全问题。下面给你两种落地实现方案,以及配套的发送线程架构:

方案一:ConcurrentQueue + 后台工作线程(兼容所有.NET版本)

这是经典的生产者-消费者模式实现,用线程安全队列存储待处理数据,配合信号量避免线程空转:

1. 定义全局队列和信号量

// 线程安全的数据包队列
private readonly ConcurrentQueue<byte[]> _processingQueue = new ConcurrentQueue<byte[]>();
// 用于通知处理线程有新数据的信号量
private readonly AutoResetEvent _newDataSignal = new AutoResetEvent(false);
// 服务器运行状态标识
private bool ListenerStarted = false;

2. 启动处理线程

在服务器初始化时,启动一个后台线程专门处理队列中的数据包:

// 启动处理线程
Task.Run(() =>
{
    while (ListenerStarted)
    {
        // 等待新数据信号,100ms超时用于检查是否停止服务
        _newDataSignal.WaitOne(100);
        
        // 一次性处理队列中所有待处理数据
        while (_processingQueue.TryDequeue(out var data))
        {
            RequestHandling(data);
        }
    }
});

3. 接收数据后入队

在异步接收数据的代码中,把有效数据复制后加入队列,并触发信号:

byte[] Buffer = new byte[8192];
int bytesRead = await Stream.ReadAsync(Buffer, 0, 8192);
if (bytesRead > 0)
{
    // 必须复制有效数据,避免Buffer被后续读取覆盖
    byte[] processedData = new byte[bytesRead];
    Array.Copy(Buffer, processedData, bytesRead);
    
    _processingQueue.Enqueue(processedData);
    _newDataSignal.Set(); // 通知处理线程有新数据
}

方案二:使用.NET Channel(推荐,异步友好)

.NET Core 3.0+引入的Channel是专门为异步生产者-消费者场景设计的,比手动维护队列和信号量更简洁高效,还支持背压机制:

1. 创建处理用的Channel

private readonly Channel<byte[]> _processingChannel = Channel.CreateUnbounded<byte[]>();
private bool ListenerStarted = false;

2. 启动异步处理循环

// 启动异步处理任务
_ = Task.Run(async () =>
{
    // 按顺序读取Channel中的所有数据
    await foreach (var data in _processingChannel.Reader.ReadAllAsync())
    {
        RequestHandling(data);
    }
});

3. 接收数据写入Channel

byte[] Buffer = new byte[8192];
int bytesRead = await Stream.ReadAsync(Buffer, 0, 8192);
if (bytesRead > 0)
{
    byte[] processedData = new byte[bytesRead];
    Array.Copy(Buffer, processedData, bytesRead);
    
    // 异步写入Channel,无需手动发信号
    await _processingChannel.Writer.WriteAsync(processedData);
}

配套:单线程处理客户端发送

按照你的架构思路,发送数据也可以用同样的模式,避免并发发送导致的TCP粘包或资源冲突:

1. 定义发送消息结构体

// 包含目标客户端和要发送的数据
public record ClientSendMessage(TcpClient TargetClient, byte[] SendData);

2. 创建发送Channel和处理线程

private readonly Channel<ClientSendMessage> _sendChannel = Channel.CreateUnbounded<ClientSendMessage>();

// 启动发送线程
_ = Task.Run(async () =>
{
    await foreach (var msg in _sendChannel.Reader.ReadAllAsync())
    {
        try
        {
            var stream = msg.TargetClient.GetStream();
            await stream.WriteAsync(msg.SendData, 0, msg.SendData.Length);
        }
        catch (Exception ex)
        {
            // 处理发送错误,比如断开异常客户端
            Console.WriteLine($"发送数据失败: {ex.Message}");
        }
    }
});

3. 发送数据时写入Channel

当需要给客户端发响应时,只需要把消息写入发送Channel即可:

// 假设responseData是要发送的响应字节数组
await _sendChannel.Writer.WriteAsync(new ClientSendMessage(tcpClient, responseData));

关键注意事项

  • 复制接收缓冲区:一定要把ReadAsync读取到的有效数据复制出来再入队,否则后续的读取操作会覆盖缓冲区里的旧数据。
  • 优雅停止:当服务器要关闭时,除了设置ListenerStarted = false,还要关闭Channel的Writer(如果用方案二),或者触发信号让处理线程退出循环。
  • 异常隔离:在处理和发送的循环中一定要捕获异常,避免单个数据包的处理错误导致整个服务线程崩溃。

内容的提问来源于stack exchange,提问作者Sindeev Victor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 10:18:10