Async Sockets и неявный Multithreading...

Общие вопросы о Unity3D

Async Sockets и неявный Multithreading...

Сообщение LemanRass 28 июл 2015, 20:03

Всем привет. Не уверен что мне помогут конечно, потому как по идеи тут проблема спрятана довольно глубоко, и придется предоставлять весомую часть кода которую читать скорее всего будет всем влом. Но как говориться надежда умирает последней. Постараюсь разложить все по полочкам, что бы как можно детальнее описать суть.

Идея:
Написать подобную систему постройки своего сервера как реализовано в PhotonServer на асинхронных сокетах.

Чего я уже добился:
Почти все уже реализовано (как для упрощенного, но рабочего, макета), а именно:
1)Отправка структурированного пакета наполненого пользовательскими данными произвольного типа.
2) Получение данных на стороне сервера, формирование и отправка ответа.
3) Получение ответа на клиентах.

Сама проблема:
Для упрощения я сейчас пишу авторизацию используя свою новоиспеченную сетевую библиотечку которую я назвал банально SocketServer.
И вот появились основания добавить некоторое окно, которое будет подобием MessageBox из Windows Forms, оно должно выводить сообщения об ошибке если сервер ответил что авторизация не удалась и вдобавок это окно что бы можно было вызвать из любой сцены.
Вот код класса MessageBox который я накатал пока как временный вариант:
Синтаксис:
Используется csharp
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class MessageBox : MonoBehaviour {

    public static CanvasGroup errorFrame;
    public static Text caption;
    public static Text message;
    public static Button submit;

    public CanvasGroup ErrorFrame;
    public Text CaptionField;
    public Text MessageField;
    public Button SubmitButton;

    void Start()
    {
        errorFrame = ErrorFrame;
        caption = CaptionField;
        message = MessageField;
        submit = SubmitButton;
        submit.onClick.AddListener(() => SubmitButtonHandler());
    }

    public static void Show(string _caption, string _message)
    {
        caption.text = _caption;
        message.text = _message;
        EnableCG(errorFrame, true);
    }

    public void SubmitButtonHandler()
    {
        EnableCG(errorFrame, false);
    }

    static void EnableCG(CanvasGroup cg, bool visible)
    {
        cg.alpha = visible ? 1 : 0;
        cg.blocksRaycasts = visible;
        cg.interactable = visible;
    }
}


Кратко о скрипте, я создал некое визуальное оформление окна в канвасе, закинул скрипт на родительский елемент этого окна в канвасе и закинул в паблик переменные нужные данные. Дальше вы по скрипту можете увидеть что я единажды заношу все данные из простых публичных переменных в статические, что бы иметь возможность использовать их в статической функции Show отвечающей собственно за показ окна. Может реализация не суперская, чесно признаться я слабоват в статичности в языках программирования. Но проблема скорее всего не в этом скрипте, так как убирание всего статика из скрипта и вызов функции Show через объект класса выдал тот же результат.

А вот скрипт SocketServer на клиентской стороне (аналог PhotonServer):
Синтаксис:
Используется csharp
using UnityEngine;
using System.Net.Sockets;
using System.Net;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using CommonTemplate;

public class SocketNetwork : MonoBehaviour {

    public string ServerAddress = "178.74.209.179";
    public int ServerPort = 3333;

    private Socket clientSocket;
    private byte[] buffer = new byte[2048];
    private BinaryFormatter binFormatter;

    private static SocketNetwork _instance;
    public static SocketNetwork Instance
    {
        get { return _instance; }
    }

    private SocketNetwork PhotonPeer { get; set; }

    void Awake()
    {
        if (Instance != null)
            DestroyObject(gameObject);

        DontDestroyOnLoad(gameObject);
        Application.runInBackground = true;
        _instance = this;
    }

    private string debugMessage = "";
    void OnGUI()
    {
        GUI.skin.box.fontSize = 16;
        GUI.Box(new Rect(Screen.width / 2 - 200, 0, 400, 100), debugMessage);
        GUI.skin.box.fontSize = 12;
    }

    void Start()
    {
        clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        clientSocket.BeginConnect(new IPEndPoint(IPAddress.Parse(ServerAddress), ServerPort), new AsyncCallback(ConnectCallback), null);
        binFormatter = new BinaryFormatter();
    }


    //Callbacks
    private void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            clientSocket.EndConnect(ar);
            clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), null);
            Debug.Log("Client was connected to the server!");
        }
        catch (Exception ex)
        {
            debugMessage += ex.Message;
            Debug.LogError(ex.Message);
        }
    }


    private void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            clientSocket.EndReceive(ar);
            OperationResponse response = ServerUtills.DeserializeResponse(buffer);

            switch (response.opCode)
            {
                case OperationCode.OperationLogin:
                    LoginHandler(response);
                    break;
            }

            clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), null);
        }
        catch (SocketException socketEx)
        {
            debugMessage += socketEx.Message;
            Debug.LogError(socketEx.Message);
        }
        catch (Exception ex)
        {
            debugMessage += ex.Message;
            Debug.LogError(ex.Message);
        }
    }


    //Operations
    public void LoginOperation(string Login, string Pass)
    {
        OperationData operationData = new OperationData((byte)OperationCode.OperationLogin);
        operationData.Parameters = new Dictionary<byte, object>
        {
            { (byte)ParameterCode.CharacterLogin, Login },
            { (byte)ParameterCode.CharacterPasswd, Pass }
        };

        SendPacket(operationData);
    }



    //Handlers
    private  void LoginHandler(OperationResponse response)
    {
        if(response.errCode != ErrorCode.Ok)
        {
            Debug.Log("FAILURE!");
            MessageBox.Show("FAILURE!", "Something is wrong!");
            return;
        }

        Debug.Log("SUCCESS! Name: " + response.Parameters[(byte)ParameterCode.CharacterName] + " ID: " +
            response.Parameters[(byte)ParameterCode.CharacterId]);

        MessageBox.Show("SUCCESS!", "SUCCESS! Name: " + response.Parameters[(byte)ParameterCode.CharacterName] + " ID: " +
            response.Parameters[(byte)ParameterCode.CharacterId]);
    }



    //Tools
    private void SendPacket(OperationData operationData)
    {
        byte[] data = ServerUtills.SerializePacket(operationData);
        debugMessage = "SendingDataLength: " + data.Length;
        clientSocket.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback(SendCallback), clientSocket);
    }

    private void SendCallback(IAsyncResult ar)
    {
        clientSocket.EndSend(ar);
    }
}


И в функции LoginHandler при попытке обратиться к MessageBox я получаю ошибку:
Изображение

При этом если я комментирую строку с вызовом MessageBox (остается еще обычный Debug.Log) ошибки нет, и все выводит в консоль.
Еще, когда учился работать с асинхронными сокетами я видел что-то подобное, и это решали через MethodInvoker но он вроде как только для WindowsForms.
Собственно ошибка говорит о том, что оперировать с елементами окна (Text, Image, CanvasGroup) как с елементами формы в Windows Forms нужно строго с главного потока, но я что то не припоминаю что бы я создавал несколько потоков на стороне клиента :-o . Хотя в теме с многопоточностью я тоже пока новичек.
Собственно довольно большая тема получилась по размерам. Я надеюсь кому-то будет все же не лень это все читать #:-s
Разработчик SpaceBall
Скрытый текст:
LemanRass
UNIверсал
 
Сообщения: 385
Зарегистрирован: 23 фев 2014, 12:00
Skype: coder.dev

Re: Async Sockets и неявный Multithreading...

Сообщение LemanRass 29 июл 2015, 15:05

UP
Разработчик SpaceBall
Скрытый текст:
LemanRass
UNIверсал
 
Сообщения: 385
Зарегистрирован: 23 фев 2014, 12:00
Skype: coder.dev

Re: Async Sockets и неявный Multithreading...

Сообщение MF_Andreich 29 июл 2015, 15:08

Сеть всегда работает в другом потоке. Не вызывайте напрямую, спамьте в очередь, в главном потоке дергайте из очереди и вызывайте. Данная тема поднималась не раз и решается с нуля за пару вечеров.
Holly Shovel Team
Аватара пользователя
MF_Andreich
Старожил
 
Сообщения: 924
Зарегистрирован: 20 июн 2013, 10:09
Откуда: Барнаул
Skype: mf_andreich
  • ICQ

Re: Async Sockets и неявный Multithreading...

Сообщение LemanRass 30 июл 2015, 10:31

Индексировать все функции некоторым Function ID затем создать очередь ( List<int>) и создать скрипт которые будет в апдейте проверять есть ли что то в очереди, и если есть - сравнивать FunctionID с возможными вариациями FunctionID в enum`е и таким образом вызывать предписанную функцию которая соответствует FunctionID? Но как тогда быть с разным количеством параметров под каждую функцию? Создавать не List<int> под FunctionID а Dictionary<int, object[]> и в целом работать с ним как описал выше? Или все-таки есть более правильный "накатанный" метод, которым уже давно все решают эту задачу и он считается самым верным? Просто мой способ у меня не внушает доверия. Хотя конечно я им воспользуюсь если ничего лучше не найдется, ведь он рабочий, хоть и не очень элегантный..
Разработчик SpaceBall
Скрытый текст:
LemanRass
UNIверсал
 
Сообщения: 385
Зарегистрирован: 23 фев 2014, 12:00
Skype: coder.dev

Re: Async Sockets и неявный Multithreading...

Сообщение MF_Andreich 30 июл 2015, 10:49

Эм... я конечно понимаю, что иногда люди выдумывают велосипеды... но чтоб такое...

Даю вам свою заметку, без правок и ссылок на то, где она была выложена. Это не итоговый вариант (потому в продакшене его без допилки юзать ни в коем случае нельзя), а только пояснение принципа, не более.

Суть проблемы
А вся суть в том, что Unity - движок потоко не безопасный. Основную часть его апи не получится вызвать из других потоков, хотя с запуском стандартных C# потоков проблем не наблюдается. И ассинхронная загрузка данных, даже самостоятельная, вполне реальна. Но стоит попытаться вызвать создание объекта, пересчитать вершины меша или другие многие стандартные действия Unity - мы получаем отказ в виде варнинга или даже эксепшна.
Мне в моем проекте было необходимо запускать треды, так как важно было запускать тяжелый по времени цикл, не разбивая его на кадры. Да и сам факт его укладывания в кадр, два или три по времени тоже не был важен... запуск его в Update выдавал весьма плачевный результат... а именно лютую просадку FPS, что логично.
В чем собственно суть, почему в юнити это закрыто? Ну все просто. Представьте, что у вас в игре сталкивается 5 шариков, обсчитывает все физический движок после исполнения FixedUpdate ... и тут, левый поток меняет координаты одного из них, и что мы получаем? Физика уже считается, но шарика на нужном месте нет, он есть в другом, возникают новые колизии, которые, возможно движок и разрулит, но вряд ли. Таким образом мы в лучшем случае получим бардак с физикой на экране, в худшем мы получим вылет билда. И так почти со всем юнити апи, каждая функция, метод имеют место для вызова в общем пайплайне и не должны вызываться вне него.
Авторы движка вполне могли сделать юнити потокобезопасным. Но для этого им пришлось бы повозится с собственной реализацией мультитред апи. К примеру они давали бы людям свою реализацию тредов и приостанавливали бы их в критичных местах... что по сути возможно, но сложно и идеалогически неверно...
Как можно это решить
Тем кто напишет "юзай куротины" - читать дальше не советую, вы не понимаете сути мультитрединга и работы куротин юнити впринципе. Тем кому стало интересно, расскажу суть своего подхода, не претендующего на лавры лучшего, имеющего узкие места, но тем не менее дающего нам почти мультитрединг (во всем, где не юзаются юнити апи, где они используются, приходится немного терять во времени). Я не буду давать готовые классы, дам только куски кода (опишусь сразу, в системе у меня все намного сложнее, идею я пишу на лету и просто для примера. Так как компиляцию это все не проходило, могут быть ошибки... воспринимайте как псевдокод.).
Создаем свою компоненту вот с таким полями:
Синтаксис:
Используется csharp
        object sync = new object();
        List<Action> actions = new List<Action>();    

в Update вписываем следующее:
Синтаксис:
Используется csharp
void Update(){
        lock(sync){ //обеспечиваем потокобезопасность чтения листа
            while(actions.Count!=0){ //и исполняем все действия
                actions[0].Invoke();
                actions.RemoveAt(0);
            }
        }
}

Так же создадим вот такой метод в этом же компоненте:
Синтаксис:
Используется csharp
public void Execute(Action action){
        lock(sync){ //обеспечиваем потокобезопасность записи в лист
            actions.Add(action);
        }
        try{
          Thread.Sleep ();//усыпляем вызвавший поток
        }catch(ThreadInterruptedException){
        }finally{}
       
}

Данный метод нельзя вызывать из основного потока, ни в коем случае.
По сути автомат для исполнения у нас есть, как же нам нужно запускать треды? А вот так:
Синтаксис:
Используется csharp
//customComponent далее это экземпляр вашего компонента

Thread testThread = null;

Action threadAction = () => {
    //действия, которые могут быть исполнены в потоке просто так,
    //например циклы с расчетами, загрузками и прочим
    Thread.Sleep(10000); //мы же для примера просто усыпим наш тред на 10 секунд
    //если нужно выполнить какой то код из юнити апи, например создать кубик  
    GameObject cube = null;
    customComponent.Execute(() => { cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        testThread.Interrupt(); //данное действие очень важно в конце всей последовательности
    });
    //действия, которые могут быть исполнены в потоке просто так, в cube кстати будет лежать наш кубик
}

testThread = new Thread(new ThreadStart(threadAction));
testThread.Start();

Как работает идея
Да очень просто. Что делает компонент? Он хранит очередь действий, которые нужно сделать в основном потоке. Что делает метод Execute? Он вызывается из побочного треда, и складывает в очередь исполнения действие и усыпляет поток. В самом действии в конце, есть код, который разбудит тред (та самая важная строчка). Таким образом из треда вы скармливаете небезопасный для вызова код главному треду и ждете его исполнения.
Идеи для развития
все они реализованы по сути, тут же я их просто опишу
- Разделение очередей. Вывести отдельные очереди для Update, LateUpdate и FixedUpdate.
- Автоматизация вызова Interrupt() для действий, дабы не приходилось скармливать его самому действию.
- Добавить возможность выполнения ассинхронных (для вызывающего потока, а не для главного) действий с колбеком.
- Добавить контроль времени, на случай если заданий в очереди много, как то динамически останавливать обработку и откладывать до следующего вызова.
За сим все.

З.Ы. Итоговую систему выкладывать не буду в виду заточенности онной "под меня", а сделать юзабельной для всех можно, но если честно лень.
Holly Shovel Team
Аватара пользователя
MF_Andreich
Старожил
 
Сообщения: 924
Зарегистрирован: 20 июн 2013, 10:09
Откуда: Барнаул
Skype: mf_andreich
  • ICQ

Re: Async Sockets и неявный Multithreading...

Сообщение LemanRass 30 июл 2015, 11:29

Вроде как тип данных Action это делегат анонимного метода который не принимает параметров и не возвращает ничего.
И вся ваша конструкция вроде как построена на этом Action. Что если мне придется вызывать функцию Show из моего класса MessageBox которая принимает 2 параметра типа string да и вообще это кассается всех других функций которые могут иметь произвольное количество разных параметров?
Разработчик SpaceBall
Скрытый текст:
LemanRass
UNIверсал
 
Сообщения: 385
Зарегистрирован: 23 фев 2014, 12:00
Skype: coder.dev

Re: Async Sockets и неявный Multithreading...

Сообщение MF_Andreich 30 июл 2015, 11:33

LemanRass писал(а):Вроде как тип данных Action это делегат анонимного метода который не принимает параметров и не возвращает ничего.
И вся ваша конструкция вроде как построена на этом Action. Что если мне придется вызывать функцию Show из моего класса MessageBox которая принимает 2 параметра типа string да и вообще это кассается всех других функций которые могут иметь произвольное количество разных параметров?


Синтаксис:
Используется csharp
Action act = () => {Show(param1,param2);}


Кушайте.
З.Ы. Это кстати одно из немногих поистине полезных применений лямбда выражений. Присваивать переменной не обязательно, в лист сразу можно складывать лямбду.
Holly Shovel Team
Аватара пользователя
MF_Andreich
Старожил
 
Сообщения: 924
Зарегистрирован: 20 июн 2013, 10:09
Откуда: Барнаул
Skype: mf_andreich
  • ICQ

Re: Async Sockets и неявный Multithreading...

Сообщение Dewa1s 30 июл 2015, 12:45

Ребят, ну этот же велосипед давно изобретен:
http://answers.unity3d.com/questions/20 ... aging.html

хотя теперь и на русском пусть будет, хотя вопрос этот реже задавать не станут...
Аватара пользователя
Dewa1s
Старожил
 
Сообщения: 564
Зарегистрирован: 26 дек 2011, 02:12

Re: Async Sockets и неявный Multithreading...

Сообщение MF_Andreich 30 июл 2015, 13:07

Dewa1s писал(а):Ребят, ну этот же велосипед давно изобретен:
http://answers.unity3d.com/questions/20 ... aging.html

хотя теперь и на русском пусть будет, хотя вопрос этот реже задавать не станут...

Ну я для себя писал когда то сам, мне ассинхронные колбэки нужны были, вот и написал заметульку на другой сайт, тут же вопрос выползает раз в два дня точно. Вот и выложил саму заметку.
Holly Shovel Team
Аватара пользователя
MF_Andreich
Старожил
 
Сообщения: 924
Зарегистрирован: 20 июн 2013, 10:09
Откуда: Барнаул
Skype: mf_andreich
  • ICQ

Re: Async Sockets и неявный Multithreading...

Сообщение LemanRass 31 июл 2015, 11:45

Всем большое спасибо за ответы.
Способ предоставленный вами очень элегантен по сравнению с моим велосипедом. (прямо как я и хотел).
Все работает.
Разработчик SpaceBall
Скрытый текст:
LemanRass
UNIверсал
 
Сообщения: 385
Зарегистрирован: 23 фев 2014, 12:00
Skype: coder.dev

Re: Async Sockets и неявный Multithreading...

Сообщение MF_Andreich 31 июл 2015, 13:44

LemanRass писал(а):Всем большое спасибо за ответы.
Способ предоставленный вами очень элегантен по сравнению с моим велосипедом. (прямо как я и хотел).
Все работает.

О майн гад... это для продакшна... как бы сказать, слегка небезопасно =)
Если сил дописать до нормального уровня нет, стучитесь в скайп, обсудим.
Holly Shovel Team
Аватара пользователя
MF_Andreich
Старожил
 
Сообщения: 924
Зарегистрирован: 20 июн 2013, 10:09
Откуда: Барнаул
Skype: mf_andreich
  • ICQ


Вернуться в Общие вопросы

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 9