NetworkStream Receive,如何在不使用100%CPU的情况下处理数据
本文关键字:100%CPU 情况下 数据 处理 Receive NetworkStream | 更新日期: 2023-09-27 18:30:46
我正在制作一个小型游戏服务器,它将有数十个连接不断发送玩家数据。虽然我终于完成了一些基础知识,现在有了数据发送/接收,但现在我面临着用太多数据淹没服务器和客户端的问题。我试图限制它,但即便如此,我还是达到了 90-100% 的 cpu,仅仅是因为接收和处理在 CPU 上运行的数据。
下面的方法是从服务器接收数据的裸版本。服务器发送玩家要接收的数据列表,然后通过该列表。我想也许只是使用带有基于类型的键的字典而不是用于循环,但我认为这不会显着改善它,问题是它不间断地处理数据,因为玩家位置不断更新,发送到服务器,然后发送给其他玩家。
下面的代码显示了客户端的接收,服务器接收看起来非常相似。我该如何开始克服这个问题?请善待,我对网络编程还是新手。
private void Receive(System.Object client)
{
MemoryStream memStream = null;
TcpClient thisClient = (TcpClient)client;
List<System.Object> objects = new List<System.Object>();
while (thisClient.Connected && playerConnected == true)
{
try
{
do
{
//when receiving data, first comes length then comes the data
byte[] buffer = GetStreamByteBuffer(netStream, 4); //blocks while waiting for data
int msgLenth = BitConverter.ToInt32(buffer, 0);
if (msgLenth <= 0)
{
playerConnected = false;
thisClient.Close();
break;
}
if (msgLenth > 0)
{
buffer = GetStreamByteBuffer(netStream, msgLenth);
memStream = new MemoryStream(buffer);
}
} while (netStream.DataAvailable);
if (memStream != null)
{
BinaryFormatter formatter = new BinaryFormatter();
memStream.Position = 0;
objects = new List<System.Object>((List<System.Object>)formatter.Deserialize(memStream));
}
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.ToString());
if (thisClient.Connected == false)
{
playerConnected = false;
netStream.Close();
thisClient.Close();
break;
}
}
try
{
if (objects != null)
{
for (int i = 0; i < objects.Count; i++)
{
if(objects[i] != null)
{
if (objects[i].GetType() == typeof(GameObject))
{
GameObject p = (GameObject)objects[i];
GameObject item;
if (mapGameObjects.TryGetValue(p.objectID, out item))
{
mapGameObjects[p.objectID] = p;;
}
else
{
mapGameObjects.Add(p.objectID, p);
}
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("Exception " + ex.ToString());
if (thisClient.Connected == false)
{
playerConnected = false;
netStream.Close();
break;
}
}
}
Console.WriteLine("Receive thread closed for client.");
}
public static byte[] GetStreamByteBuffer(NetworkStream stream, int n)
{
byte[] buffer = new byte[n];
int bytesRead = 0;
int chunk = 0;
while (bytesRead < n)
{
chunk = stream.Read(buffer, (int)bytesRead, buffer.Length - (int)bytesRead);
if (chunk == 0)
{
break;
}
bytesRead += chunk;
}
return buffer;
}
根据显示的代码,我无法说出为什么CPU利用率很高。循环将等待数据,等待不应消耗 CPU。也就是说,它仍然在检查 DataAvailable
属性时轮询连接,这是低效的,可能会导致您忽略接收的数据(在所示的实现中......这不是DataAvailable
的固有问题)。
我将比另一个答案更进一步,并指出您应该简单地重写代码。轮询套接字只是无法处理网络 I/O。这在任何情况下都是正确的,但如果您尝试编写游戏服务器,则尤其成问题,因为您将不必要地耗尽大量 CPU 带宽,使其远离游戏逻辑。
您应该在此处进行的两个最大更改是:
-
不要使用
DataAvailable
属性。曾。请改用异步 API 之一来处理网络 I/O。我对最新 .NET 最喜欢的方法是将Socket
包装在NetworkStream
中(或像在代码中一样从TcpClient
获取NetworkStream
),然后将Stream.ReadAsync()
与async
和await
一起使用。但是用于Socket
的旧异步 API 也可以很好地工作。 -
将网络 I/O 代码与游戏逻辑代码分开。此处显示的
Receive()
方法在同一方法中同时具有 I/O 和相对于游戏状态的数据的实际处理。这两个功能实际上属于两个单独的类。保持这两个类,尤其是它们之间的接口,非常简单,代码将更容易编写和维护。
如果您决定忽略上述所有内容,则至少应该意识到您的GetStreamByteBuffer()
方法中存在一个错误:如果您在读取请求的字节数之前到达流的末尾,您仍然返回一个与请求一样大的缓冲区,调用方无法知道缓冲区不完整。
最后,恕我直言,您应该更加小心如何关闭和关闭连接。阅读有关 TCP 协议的"优雅闭包"的信息。重要的是,在任何一端实际关闭连接之前,每一端都发出信号,表明它们已完成发送,并且每一端都接收到另一端的信号。这将允许底层网络协议尽可能高效、快速地释放资源。请注意,TcpClient
将套接字公开为 Client
属性,您可以使用该属性调用 Shutdown()
。
轮询很少是一种好的通信方法,除非你正在编写16位微控制器(即使这样,可能也不是最好的解决方案)。
您需要做的是切换到生产者-消费者模式,其中您的输入端口(串行端口、输入文件或 TCP 套接字)将充当填充 FIFO 缓冲区(字节队列)的生产者,而程序的其他部分将能够异步使用排队的数据。
在 C# 中,有几种方法可以做到这一点:您可以简单地使用 ConcurrentQueue<byte>
或 BlockingCollection
编写几个方法,或者您可以尝试像 TPL 数据流库这样的库,IMO 不会在 .NET 4 中的现有结构上增加太多价值。在 .NET 4 之前,您只需使用Queue<byte>
、锁和AutoResetEvent
即可完成相同的工作。
所以一般的想法是:
- 当输入端口触发"数据已接收"事件时,将所有接收的数据排队到 FIFO 缓冲区中,并设置同步事件以通知使用者,
- 在使用者线程中,等待同步事件。收到信号后,检查队列中是否有足够的数据。如果是,请处理它,如果不是,请继续等待下一个信号。
- 为了可靠起见,请使用额外的看门狗计时器(或简称"自上次接收数据以来的时间")以便在超时时失效。
您希望使用基于任务的异步模式。可能自由使用 async
函数修饰符和 await
关键字。
您最好用直接呼叫ReadAsync
代替GetStreamByteBuffer
。
例如,您可以从这样的流中异步读取。
private static async Task<T> ReadAsync<T>(
Stream source,
CancellationToken token)
{
int requestLength;
{
var initialBuffer = new byte[sizeof(int)];
var readCount = await source.ReadAsync(
initialBuffer,
0,
sizeof(int),
token);
if (readCount != sizeof(int))
{
throw new InvalidOperationException(
"Not enough bytes in stream to read request length.");
}
requestLength = BitConvertor.ToInt32(initialBuffer, 0);
}
var requestBuffer = new byte[requestLength];
var bytesRead = await source.ReadAsync(
requestBuffer,
0,
requestLength,
token);
if (bytesRead != requestLength)
{
throw new InvalidDataException(
string.Format(
"Not enough bytes in stream to match request length." +
" Expected:{0}, Actual:{1}",
requestLength,
bytesRead));
}
var serializer = new BinaryFormatter();
using (var requestData = new MemoryStream(requestBuffer))
{
return (T)serializer.Deserialize(requestData);
}
}
与您的代码一样,这会从流中读取一个int
以获取长度,然后读取该字节数并使用BinaryFormatter
将数据反序列化为指定的泛型类型。
使用此通用函数,您可以简化逻辑,
private Task Receive(
TcpClient thisClient,
CancellationToken token)
{
IList<object> objects;
while (thisClient.Connected && playerConnected == true)
{
try
{
objects = ReadAsync<List<object>>(netStream, token);
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.ToString());
if (thisClient.Connected == false)
{
playerConnected = false;
netStream.Close();
thisClient.Close();
break;
}
}
try
{
foreach (var p in objects.OfType<GameObject>())
{
if (p != null)
{
mapGameObjects[p.objectID] = p;
}
}
}
catch (Exception ex)
{
Console.WriteLine("Exception " + ex.ToString());
if (thisClient.Connected == false)
{
playerConnected = false;
netStream.Close();
break;
}
}
}
Console.WriteLine("Receive thread closed for client.");
}
你需要在 while 循环中放置一个 Thread.Sleep(10)。这也是接收 tcp 数据的一种非常脆弱的方式,因为它假定在您调用此接收之前另一端已发送所有数据。如果另一端只发送了一半的数据,则此方法将失败。这可以通过发送固定大小的包或先发送包的长度来应对。
您的播放器位置更新类似于VNC协议中的帧缓冲更新,其中客户端请求屏幕帧和服务器使用更新的屏幕数据对其进行响应。但有一个例外,VNC服务器不会盲目发送新屏幕,它只会发送更改。因此,您需要将逻辑从发送所有请求的对象列表更改为仅发送到上次发送后更改的对象。此外,除了它之外,你应该只发送一次整个对象,之后只发送更改的属性,这将大大减少在客户端和服务器上发送和处理的数据的大小。