我有一个小型游戏服务器我将会有数十个连接不断发送玩家数据。虽然我终于完成了一些基础知识并且现在有数据发送/接收,但我现在面临着服务器和客户端充斥着太多数据的问题。我试图将其限制回来,但即便如此,我只是因为接收和处理运行CPU的数据而达到了90-100%的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;
}
答案 0 :(得分:2)
根据显示的代码,我不能说为什么CPU利用率很高。循环将等待数据,等待不应消耗CPU。也就是说,它仍然在检查DataAvailable
属性时轮询连接,这是低效的,并且可能导致您忽略收到的数据(在显示的实现中......这不是DataAvailable
的固有问题)。
我会比另一个答案更进一步说明你应该简单地重写代码。轮询套接字无法处理网络I / O.在任何情况下都是如此,但如果您正在尝试编写游戏服务器,则会出现问题,因为您将不必要地耗尽大量CPU带宽,将其从游戏逻辑中移除。
你应该在这里做出两个最大的改变:
请勿使用DataAvailable
属性。永远。相反,使用其中一个异步API来处理网络I / O.我最喜欢使用最新.NET的方法是将Socket
包裹在NetworkStream
中(或者从NetworkStream
获取TcpClient
,就像在代码中一样)然后使用Stream.ReadAsync()
以及async
和await
。但Socket
的旧异步API也可以正常工作。
将您的网络I / O代码与游戏逻辑代码分开。您在此处显示的Receive()
方法在同一方法中具有相对于游戏状态的I / O和实际数据处理。这两个功能实际上属于两个单独的类。保持这两个类,特别是它们之间的接口,非常简单,代码编写和维护起来会更容易。
如果你决定忽略上述所有内容,你至少应该知道你的GetStreamByteBuffer()
方法中有一个错误:如果你在读取之前到达流的末尾,那么请求的字节数就是你的仍然返回一个尽可能大的缓冲区,调用者无法知道缓冲区是否不完整。
最后,恕我直言,你应该更加小心你如何关闭和关闭连接。阅读TCP协议的“优雅闭包”。重要的是,在任一端实际关闭连接之前,它们完成发送的每个结束信号以及每个端接收另一端的信号。这将允许底层网络协议尽可能有效和快速地释放资源。请注意,TcpClient
会将套接字公开为Client
属性,您可以使用该属性来调用Shutdown()
。
答案 1 :(得分:1)
轮询很少是一种很好的通信方法,除非您正在编程16位微控制器(即便如此,可能不是最好的解决方案)。
您需要做的是切换到生产者 - 消费者模式,其中您的输入端口(串行端口,输入文件或TCP套接字)将充当填充FIFO缓冲区(字节队列)的生产者),程序的其他一些部分将能够异步使用排队数据。
在C#中,有几种方法可以实现:you can simply write a couple of methods using a ConcurrentQueue<byte>
, or a BlockingCollection
,或者您可以尝试像TPL Dataflow Library这样的库,IMO不会为.NET 4中的现有结构增加太多的价值。在.NET 4之前,您只需使用Queue<byte>
,锁定和AutoResetEvent
来完成相同的工作。
所以一般的想法是:
答案 2 :(得分:1)
您想使用Task-based Asynchronous Pattern。可能会自由使用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.");
}
答案 3 :(得分:0)
你需要在你的while循环中放一个Thread.Sleep(10)。这也是接收tcp数据的一种非常脆弱的方式,因为它假定另一方在调用此接收之前已发送所有数据。如果另一方只发送了一半数据,则该方法失败。这可以通过发送固定大小的包或首先发送包的长度来抵消。
答案 4 :(得分:0)
您的播放器位置更新类似于VNC协议中的帧缓冲更新,其中客户端请求屏幕框架&amp;服务器使用更新的屏幕数据响应它。但有一个例外,VNC服务器不会盲目地发送新屏幕它只发送更改(如果有的话)。因此,您需要将逻辑从发送所有请求的对象列表更改为仅在上次发送后更改的对象。另外,除了它之外,你应该只发送一次整个对象,之后只发送更改的属性,这将大大减少发送的数据的大小。在客户和客户处理服务器