Реализация TCP асинхронного подключения на игровом движке Unity 3D
Всем привет. Не могу правильно реализовать систему подключения. Задача следующая: Нужно создать асинхронное подключение, чтобы срабатывал метод только в том случае, когда был ответ от сервера NetworkStream.ReadAsync. этот метод приостанавливает выполнения вторичного потока и ожидает приход от сервера пакета(ов). Проблема в том, что Unity не многопоточная. И когда я открываю вторичный поток на чтение, мне даёт ошибку как раз о том, что я пытаюсь использовать API Unity не в главном его потоке. Думал реализовать собственный сигнализированный метод, с помощью: ManualResetEvent. Но понятия не имею, как проверить пришли ли данные или нет. Через IEnumerator не подходит. При задержке в 1 секунду. Происходят артефакты сети. К примеру пакет пришел, и он должен сразу же выполнится. А IEnumerator будет ещё ждать секунду. Как лучше поступить? Как с помощью Task.Factory.StartNew синхронизировать поток с главным потоком Unity? Для не кастыльного общения с API Unity
Ответы (1 шт):
Рабочий клиент-сервер я представляю примерно так.
Но у вас не одна проблема, для начала разберитесь с сетью. TCP - протокол с гарантированной доставной, но нет гарантии, что принимаемый пакет прилетит за раз. То есть пакет может быть фрагментирован. Решается это только передачей и проверкой длины пакета (реализовано в примере по ссылке выше).
Далее, чтобы передавать и принимать данные из рабочего потока, вам нужна реализация Producre/Consumer, то есть потокобезопасная коллекция, исполняющая роль буфера. В .NET Framework 4.x я знаю только одну такую - BlockingCollection, возможно в самой Unity есть и другие средства, я с движком не работал, знаю только сам .NET. И не используйте Task.Factory.StartNew, используйте Task.Run.
Есть еще например для приема данных неблокирующая ConcurrentQueue, в рабочем потоке вы например можете класть в нее принятые пакеты, а в каком-нибудь FixedUpdate() проверять, есть ли там что-нибудь, и если есть - забирать.
Но можно и без async/await обойтись, как говорится, старыми дедовским способами.
Отправка
using System.Collections.Concurrent;
class SendExample : MonoBehavior
{
// byte[] просто для примера, здесь может быть любой тип.
private readonly BlockingCollection<byte[]> _sendQueue = new BlockingCollection<byte[]>();
void Start()
{
Task.Run(SendLoop);
}
private void SendLoop()
{
foreach (byte[] data in _sendQueue.GetConsumingEnumerable())
{
SendToServer(data); // вызов метода вашей сетевой части из рабочего потока
}
}
private void StopSend() // этот метод надо вызвать перед остановкой работы скрипта
{
_sendQueue.Complete();
}
private void Send(byte[] data)
{
_sendQueue.Add(data);
}
}
Получение
class ReceiveExample : MonoBehavior
{
private readonly ConcurrentQueue<byte[]> _receiveQueue = new ConcurrentQueue<byte[]>();
void FixedUpdate()
{
if (_receiveQueue.TryDequeue(out byte[] data))
{
// получены новые данные
}
}
private void Receive(byte[] data) // этот метод надо вызвать из рабочего потока, получающего данные из сети
{
_receiveQueue.Enqueue(data);
}
}
Это по сути самая простая Poducer/Consumer развязка между потоками. К примеру по ссылке в начале этого ответа ее должно быть не сложно прикрутить. И цель этой развязки - не допустить тормозорв игры, пока работает сеть.