[英]NetworkStream Receive, how to processing data without using 100% CPU?
我有一个小型的游戏服务器,它将有数十个连接不断发送玩家数据。 虽然我终于完成了一些基础工作,现在可以进行数据发送/接收了,但现在我面临着服务器和客户端数据过多的问题。 我试图限制它的运行速度,但是即使那样,我仍然达到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
简单地编写几种方法 ,或者可以尝试使用IMO不会增加太多价值的TPL Dataflow Library之类的库。在.NET 4中的现有结构上,您只需使用Queue<byte>
,锁和AutoResetEvent
来完成相同的工作。
因此,总体思路是:
您要使用基于任务的异步模式 。 大概自由地使用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服务器不会盲目发送新屏幕,只有存在更改时才发送更改。 因此,您需要将逻辑从发送所有请求的对象列表更改为仅发送到最后发送之后更改的对象。 除此之外,您只应发送整个对象一次,然后仅发送更改的属性,这将大大减少在客户端和服务器上发送和处理的数据量。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.