Всем привет! Меня зовут Григорий Дядиченко, и я технический продюсер. Сегодня хотелось бы поговорить про протокол HTTP, про сервера, и про простенькую реализацию Http сервера вшитого в Unity. Если вам это интересно – добро пожаловать под кат!

Чтож, начнём пожалуй с задачи на примере которой мы будем всё разбирать. В данной статье мы будем разбирать только Unity часть. Предположим вы делаете какой-то интерактивный стенд или на какой-нибудь крупной конференции демонстрируете свою игру скажем издателю. Если игра на пк, можно это обыграть управлением с геймпада и подключить его или же джойстиком. Чтобы человеку было удобно играть, и он не закрывал собой экран ноутбука играя в вашу самую лучшую игру. Но есть любопытная альтернатива. Сделать геймпад из телефона пользователя! В этом случае при наличии какой никакой экспертизе в веб разработке можно там же удобно собирать контакты. Всё что вам понадобится это ноутбук, wi fi роутер, геймпад в виде веб приложения и http сервер зашитый в unity.

Важно: в условиях реальной выставки что с bluetooth геймпадом, что с подобным решением, нужно помнить о том, что там может быть много устройств и шума. Поэтому это может работать нестабильно, но обычно такое бывает, когда таких роутеров очень много. В целом при наличии своего вайфая на выставке проще подключиться к нему, так как это локальная сеть, очень маленькие запросы. И во-первых, это не даст особой нагрузки на сеть, во-вторых, не будет создавать радио шум, в-третьих, потенциальный игрок вполне возможно что будет уже к нему подключён и не нужно будет давать ему логин и пароль. Итак, мы определили “зачем”, хотя кейс не единственный, теперь поговорим про “что”.

Протокол HTTP

В современном мире этот протокол должен знать вообще любой разработчик, в идеале со множеством знаний построенных поверх него. REST API, GraphQL, GRPC и так далее. Я в целом не хочу повторяться и тратить время на описание самого протокола, простым языком отлично это и так сделали в этой статье. Но важно было про него упомянуть.

Http Server на Unity

Если брать на вооружение самую простую и низкоуровневую реализацию HTTP сервера на Unity, то можно сразу же вспомнить про класс HttpListener. И реализовать всё с помощью него. Итак, для начала всё начинается с коннекта и определения порта. Если что в любом браузере любой существующий сайт по умолчанию стучится в 80 порт. То есть если сервер развёрнут на 80 порту, а айпи вашего ПК (можно посмотреть через ipconfg в cmd) 192.168.1.24 к примеру. То ссылка в браузере будет вида http://192.168.1.24, если же развернуть сервер на скажем порте 10021, то тогда уже она будет вида http://192.168.1.24:10021. Для того, чтобы начать “слушать порт”, создаём такой Monobehavior класс:

Пример
public class HttpServer : MonoBehaviour
{
	[SerializeField] private int _Port = 10021;
  private HttpListener _httpListener;
	private void Start()
	{
		_httpListener = new HttpListener();
		_httpListener.Prefixes.Add($"http://*:{_Port}/");
		_httpListener.Start();
		_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
	}

	private async void OnGetCallback (IAsyncResult result)
	{
	}
}

Важный нюанс. Порт – это условное число в диапазоне от 0 до 65535. Но лучше опасаться портов от 0 до 1023, когда вы не понимаете что вы делаете. Так как эти порты называются системными, и используются многими популярными программами. Соответственно открывать соединение на 80 порту можно, если вы знаете, что на вашем ПК ничего больше не запущено на этом порту. Там часто по умолчанию развёрнут IIS, NGINX и так далее. Если кому интересно подробнее можно посмотреть тут.

Так что если у вас возникает ошибка, что порт уже занят, возможно вы открываете соединение на порту, который уже зарезервировала какая-то программа. То что программы между собой общаются посредством протокола http в целом не такая редкая практика.

Итак соединение мы открыли, теперь нам надо бы что-то возвращать, как ответ сервера и реагировать на запросы. Допишем реализацию нашего метода OnGetCallback:

Пример
public class HttpServer : MonoBehaviour
{
	[SerializeField] private int _Port = 10021;
  private HttpListener _httpListener;
	private void Start()
	{	
		_httpListener = new HttpListener();
		_httpListener.Prefixes.Add($"http://*:{_Port}/");
		_httpListener.Start();
		_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
	}

	private async void OnGetCallback (IAsyncResult result)
	{
		HttpListenerContext context = _httpListener.EndGetContext(result);
		var response = context.Response;
		var request = context.Request;
		context.Response.Headers.Clear();
		try
		{
			CreateResponse(response, new NetworkAnswer(){ status = 200 });
		}
		catch (Exception e)
		{
			CreateErrorResponse(response, e.Message);
		}
		if (_httpListener.IsListening)
		{
			_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
		}
	}
	private async void CreateResponse(HttpListenerResponse response, NetworkAnswer data = default)
	{
		response.SendChunked = false;
		response.StatusCode = data.status;
		response.StatusDescription = data.status == 200 ? "OK" : "Internal Server Error";
		using (var writer = new StreamWriter(response.OutputStream, response.ContentEncoding))
		{
			await writer.WriteAsync(JsonConvert.SerializeObject(data));
		}
		response.Close();
	}
	private async void CreateErrorResponse(HttpListenerResponse response, string error)
	{
		response.SendChunked = false;
		response.StatusCode = 500;
		response.StatusDescription = "Internal Server Error";
		using (var writer = new StreamWriter(response.OutputStream, response.ContentEncoding))
		{
			await writer.WriteAsync(JsonConvert.SerializeObject(new NetworkAnswer()
			{
				status = 500,
				errorMessage =  error
			}));
		}
		response.Close();
	}
}

public class NetworkAnswer
{
	public int status;
	public string errorMessage;
	public object data;
}

Теперь можно пойти в браузер и сделать запрос на localhost. И вуаля, It’s Alive! Правда? Наш хттп сервер готов? Ну не совсем.

Вездесущий CORS

На localhost так то оно работает, но вот при попытке сделать запрос скажем с телефона в браузере вылетит ошибка CORS. Что такое CORS и как оно работает? Если коротко, это технология которая ограничивает возможности запросов с одного домена на другой. Она была когда-то сделана во избежание проблем с фишингом, да и в целом это много для чего довольно удобно. Подробнее можно почитать тут.

Нас же интересует, а как это обработать. Так как если его не обработать, то запросы просто не пропустит CORS политика браузера. Сделать это довольно просто. Нужно прописать необходимые хедеры в наш метод OnGetCallback:

Пример
public class HttpServer : MonoBehaviour
{
	[SerializeField] private int _Port = 10021;
  private HttpListener _httpListener;
	private void Start()
	{	
		_httpListener = new HttpListener();
		_httpListener.Prefixes.Add($"http://*:{_Port}/");
		_httpListener.Start();
		_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
	}

	private async void OnGetCallback (IAsyncResult result)
	{
		HttpListenerContext context = _httpListener.EndGetContext(result);
		var response = context.Response;
		var request = context.Request;
		context.Response.Headers.Clear();
    
		response.AppendHeader("Access-Control-Allow-Origin", "*");
		response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
		response.AddHeader("Access-Control-Allow-Methods", "GET, POST");
		response.AddHeader("Access-Control-Max-Age", "1728000");
		
    try
		{
			CreateResponse(response, new NetworkAnswer(){ status = 200 });
		}
		catch (Exception e)
		{
			CreateErrorResponse(response, e.Message);
		}
		if (_httpListener.IsListening)
		{
			_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
		}
	}
	private async void CreateResponse(HttpListenerResponse response, NetworkAnswer data = default)
	{
		response.SendChunked = false;
		response.StatusCode = data.status;
		response.StatusDescription = data.status == 200 ? "OK" : "Internal Server Error";
		using (var writer = new StreamWriter(response.OutputStream, response.ContentEncoding))
		{
			await writer.WriteAsync(JsonConvert.SerializeObject(data));
		}
		response.Close();
	}
	private async void CreateErrorResponse(HttpListenerResponse response, string error)
	{
		response.SendChunked = false;
		response.StatusCode = 500;
		response.StatusDescription = "Internal Server Error";
		using (var writer = new StreamWriter(response.OutputStream, response.ContentEncoding))
		{
			await writer.WriteAsync(JsonConvert.SerializeObject(new NetworkAnswer()
			{
				status = 500,
				errorMessage =  error
			}));
		}
		response.Close();
	}
}

public class NetworkAnswer
{
	public int status;
	public string errorMessage;
	public object data;
}

Всё, корс пройден? Не совсем. Лучше заранее так же обработать такую штуку, которая называется предварительный запрос. Так как мы не знаем в каком контексте будет использоваться наш простенький Http Server, то мы заранее обработаем тот случай, когда CORS считает запрос сложным. Дело в том, что если современный браузер считает запрос сложным, то перед отправкой основного запроса он посылает предварительный запрос с методом OPTIONS. Например признаки сложных запросов:

  • Использующие методы кроме GET, POST, или HEAD

  • Включающие заголовки кроме Accept, Accept-Language, Content-Type или Content-Language

  • Со значением Content-Type отличнным от application/x-www-form-urlencoded, multipart/form-data, или text/plain

Так как в всё это значит для нас то, что в наш сервер просто придёт лишний запрос с методом OPTIONS, который нам надо обработать и вернуть правильный респонс. Мы просто добавляем в наш метод OnGetCallback:

Пример
public class HttpServer : MonoBehaviour
{
	[SerializeField] private int _Port = 10021;
  private HttpListener _httpListener;
	private void Start()
	{	
		_httpListener = new HttpListener();
		_httpListener.Prefixes.Add($"http://*:{_Port}/");
		_httpListener.Start();
		_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
	}

	private async void OnGetCallback (IAsyncResult result)
	{
		HttpListenerContext context = _httpListener.EndGetContext(result);
		var response = context.Response;
		var request = context.Request;
		context.Response.Headers.Clear();
		response.AppendHeader("Access-Control-Allow-Origin", "*");
		response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
		response.AddHeader("Access-Control-Allow-Methods", "GET, POST");
		response.AddHeader("Access-Control-Max-Age", "1728000");
		if (request.HttpMethod == "OPTIONS")
		{
			CreateResponse(response, new NetworkAnswer(){ status = 200});
			if (_httpListener.IsListening)
			{
				_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
			}
			return;
		}
		try
		{
			CreateResponse(response, new NetworkAnswer(){ status = 200 });
		}
		catch (Exception e)
		{
			CreateErrorResponse(response, e.Message);
		}
		if (_httpListener.IsListening)
		{
			_httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
		}
	}
	private async void CreateResponse(HttpListenerResponse response, NetworkAnswer data = default)
	{
		response.SendChunked = false;
		response.StatusCode = data.status;
		response.StatusDescription = data.status == 200 ? "OK" : "Internal Server Error";
		using (var writer = new StreamWriter(response.OutputStream, response.ContentEncoding))
		{
			await writer.WriteAsync(JsonConvert.SerializeObject(data));
		}
		response.Close();
	}
	private async void CreateErrorResponse(HttpListenerResponse response, string error)
	{
		response.SendChunked = false;
		response.StatusCode = 500;
		response.StatusDescription = "Internal Server Error";
		using (var writer = new StreamWriter(response.OutputStream, response.ContentEncoding))
		{
			await writer.WriteAsync(JsonConvert.SerializeObject(new NetworkAnswer()
			{
				status = 500,
				errorMessage =  error
			}));
		}
		response.Close();
	}
}

public class NetworkAnswer
{
	public int status;
	public string errorMessage;
	public object data;
}

Вух! CORS прошли, самое сложное можно сказать позади. Теперь добавим немного удобства. Теперь наш http server готов?

Вездесущий фаерволл

Всё, можно слать запросы с телефона? Не тут то было, а точнее не совсем. Если в билде всё уже работает, то в редакторе нужно сделать последний шаг. И это “пройти фаервол”. Если у вас отключен фаервол то “вы ходите по *** тонкому льду”, но ваше право. А если же он всё-таки включен, то в юнити есть небольшая странность. Возможно очень топорная защита от уязвимостей, что странно в инструменте для разработчиков. Но юнити про многое по моему опыту считало в своё время “да вам это не надо”. При установке по умолчанию он прописывает правило запрещающее паблик коннекты к редактору Unity.

Я уже как-то подсвечивал этот момент в этой статье. Но тогда я был молодой неопытный и не мог объяснить всё нормальным языком. Firewall, который многие не заслужено не любят, это ваш хороший друг. Как и вообще любая политика безопасности, так как не дураки его придумывали. И лучше уметь его настраивать, а не отключать. Собственно чтобы редактор начал отвечать нужно изменить в Advanced Settings в Inbound Rules редактора с запрета паблик соединений на разрешение.

Пример картинками

Фух! Справились, теперь наш http server отвечает и в Unity.

Рефлексию в каждый дом

Многие не любят рефлексию. Но не я. Я не люблю лишние, беспощадные и бессмысленные оптимизации. У рефлексии есть свои плюсы и минусы, она не везде работает, не всегда применима, но как же она иногда упрощает жизнь. Рефлексия (или механизм отражения) в С# позволяет творить очень много очень хитрой магии. Но нам понадобится конкретная её часть. Возможность “заглянуть в ассембли” и посмотреть, а какие наследники базового класса есть в нашей программе. Собственно это один из моих любимых паттернов для реализации консольных комманд, собственных скриптовых языков и т.п. Для начала создадим базовый абстрактный класс:

Класс HTTPServerHandler
public abstract class HTTPServerHandler
{
  protected abstract string _route { get; }
  protected string[] _params;
  public bool IsThisRoute (string url)
  {
    return url.ToLower().Contains(_route.ToLower());
  }
  private void ParseParams (string url)
  {
    _params = url.Replace(_route, string.Empty).Split('/');
  }
  public abstract NetworkAnswer GetAnswerData ();
  public virtual void ProcessParams (string url)
  {
    ParseParams(url);   
  }
}

Параметры в http запросе мы будем писать в путь запроса через “/” для простоты реализации. А дальше соберём наших наследников в список хендлеров через рефлексию, дописав метод Start нашего http сервера и введя поле _httpServerHandlers:

Пример
 private List<HTTPServerHandler> _httpServerHandlers;
 private void Start()
 {
   _httpServerHandlers = new List<HTTPServerHandler>();
   var subclassTypes = Assembly
     .Load("Assembly-CSharp")
     .GetTypes()
     .Where(t => t.IsSubclassOf(typeof(HTTPServerHandler)));
   foreach (var subclassType in subclassTypes)
   {
     _httpServerHandlers.Add(Activator.CreateInstance(subclassType) as HTTPServerHandler);
   }

   _httpListener = new HttpListener();
   _httpListener.Prefixes.Add($"http://*:{_Port}/");
   _httpListener.Start();
   _httpListener.BeginGetContext(new AsyncCallback(OnGetCallback), null);
 }

В Unity “Assembly-CSharp” – это основное ассембли проекта. Сделано в данном случае это так, а не запрашивая ассембли класса для того, чтобы не запутаться при использовании в проекте механизма Assebly Definitions. Так как можно создать наследника класса вне ассембли с классом HttpServerHandler и тогда рефлексия его не увидит. Не очень оптимально, так как по сути мы запросили все типы основного ассембли программы, и отфильтровали нужные нам, что может быть достаточно медленно. Но это уже каждый может оптимизировать под себя. И переделать этот блок хоть под регистрацию хенделров в явном виде. Класс же активатор позволяет нам создать объект типа, которые мы нашли. И вуаля наши хендлеры зарегистрированы. Можно сделать более красивый вариант с реализацией через атрибуты, но мы разберём самый простой способ упростить себе жизнь.

Что это позволяет нам сделать? Ну по сути теперь для того, чтобы наш хендлер подключился к системе сервера, нам достаточно реализовать его наследника, и он подтянется автоматически. Напишем например хендлер для нашего “удалённого геймпада”:

Класс GamepadHandler
public class GamepadHandler : HTTPServerHandler
{
	protected override string _route => "/gamepad/";
	private bool IsSuccess = true;
	private string ErrorMessage;
	public override void ProcessParams(string url)
	{
		base.ProcessParams(url);
		if(Enum.TryParse<KeyCode>(_params[1], true, out var key))
		{
			if (_params[0] == "down")
			{
				WebInput.SetKeyDown(key);
			}
			else
			{
				
				WebInput.SetKeyUp(key);
			}
		}
		else
		{
			ErrorMessage = $"Invalid keycode in param 1. There are no keycode {_params[0]}";
		}
	}
	public override NetworkAnswer GetAnswerData()
	{
		return new NetworkAnswer()
		{
			status = IsSuccess ? 200 : 500,
			errorMessage = IsSuccess ? null : ErrorMessage
		};
	}
}
public class WebInput : IKeyInput
{
	private static Dictionary<KeyCode, KeyState> _Keys = new Dictionary<KeyCode, KeyState>();
	public static void SetKeyDown(KeyCode key)
	{
		_Keys[key] = KeyState.Down;
	}
	public static void SetKeyUp(KeyCode key)
	{
		_Keys[key] = KeyState.Up;
	}

	public bool GetKeyDown(KeyCode key)
	{
		if (!_Keys.ContainsKey(key)) return false;
		return _Keys[key] == KeyState.Down;
	}
	public bool GetKeyUp(KeyCode key)
	{
		if (!_Keys.ContainsKey(key)) return false;
		return _Keys[key] == KeyState.Up;
	}

	public bool GetKey(KeyCode key)
	{
		if (!_Keys.ContainsKey(key)) return false;
		return _Keys[key] == KeyState.Pressed;
	}
	
	public void ProcessState()
	{
		foreach (var key in _Keys.Keys.ToList())
		{
			switch (_Keys[key])
			{
				case KeyState.Down:
					_Keys[key] = KeyState.Pressed;
					break;
				case KeyState.Up:
					_Keys[key] = KeyState.None;
					break;
			}
		}
	}


	private enum KeyState
	{
		None = 0,
		Up = 1,
		Down = 2,
		Pressed = 4
	}
}
public interface IKeyInput
{
	bool GetKey(KeyCode key);
	bool GetKeyDown(KeyCode key);
	bool GetKeyUp(KeyCode key);
	void ProcessState();
}

Плюс дополнительно описать обработчик WebInput и интерфейс IKeyInput. Всё. Наш хендлер подключен и работает. Можно проверить в браузере по урлу вида http://localhost:{yourPort}/gamepad/down/A/. (Лучше предварительно в статик методах вывести лог) Поэтому я обожаю этот паттерн для консольных команд, типа читов в игре. Так как не нужно думать “что и куда тут подключить”. Создаёшь наследника класса, прописываешь имя команды и её параметры (парсинг стандартизирован в основном классе) и логику его обработки. И всё, он автоматически подключен в систему.

Потоки и контексты

Но выше был довольно простой пример не учитывающий один небольшой нюанс. Юнити нормально работает только в Main Thread. Допустим задать текст в текстовое поле в интерфейсе. Для этого нам нужно сменить контекст исполнения. В шарпах появился вроде как более элегантный способ, но я предпочитаю по старинке. Создать клаcc ThreadDispatcher:

Класс ThreadDispatcher
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using UnityEngine;

public class ThreadDispatcher : MonoBehaviour
{
    private ConcurrentQueue<Action> _Events;
    public static ThreadDispatcher Instance;
    private void Awake()
    {
        Instance = this;
        _Events = new ConcurrentQueue<Action>();
    }

    private void Update()
    {
        while(_Events.Count > 0)
        {
            if (_Events.TryDequeue(out var action))
            {
                action?.Invoke();
            }
        }
    }

    public void AddEvent(Action action)
    {
        _Events.Enqueue(action);
    }
}

Пример использования:

ThreadDispatcher.Instance.AddEvent(() =>
	{
  	//your require main thread code here
  });

В заключении

А где же геймпад? Собственно для реализации геймпада нам нужно сделать веб приложение. Я сделал его на связке React/Redux + PIXI.js. Может базово разберу его в отдельной статье, а пока пример можно найти тут и просто взять as-is если кому-то будет полезно. Полный разбор, особенно понятный Unity разработчикам, требует погружения в js и его технологии. А не все в них любят погружаться. Хотя чтобы быть настоящим кроссплатформенным разработчиком вы должны знать нюансы всех платформ, и веба в том числе. Спасибо за внимание! 

Комментарии (16)


  1. shok96
    21.02.2022 08:37

    а почему не держать socket соединение?


    1. DyadichenkoGA Автор
      21.02.2022 08:42

      Есть конечно же веб-сокеты. Но в первую очередь сложнее тестировать. Веб сокеты на уровне транспорта это всё равно TCP, так что не даст выигрыша особого в скорости (тем более в локальной сети вообще плевать). А UDP в браузере по-моему (особенно в мобильных) пока открыть нельзя, не уверен что там с поддержкой HTTP QUIC, но вот это HTTP на UDP.

      Типа как бы можно, основной вопрос "зачем?" Есть задачи по принципу "послать команду", там персистент коннект не нужен. А как показывает практика у джунов веб-сокеты вызывают какие-то странные непонятки и проблемы. Да и клиентские разработчики чаще умеют работать с http нежели с сокетами. Если нужен персистент коннект зачем-то - сокеты лучше. А в целом можно и так, и так, так как ну не будет в пакете хедера от http, по сути почти вся разница в данном контексте.


      1. thedrnic
        21.02.2022 11:02
        +2

        А на уровне железа это и вовсе по одним каналам работает. Вот только для создания и обработки HTTP запроса потребуется кратно больше ресурсов и времени.
        Так как сокет инициализирует соединение, а потом засылает небольшие пакеты данных. В то время как каждый HTTP запрос ,будет слать всё тело с кучей всевозможной информации.

        Ваш подход вполне будет работать, пока не появятся некоторые "если":
        - Плохое соединени (например в сети кто-то качает торрент, а роутер слаб)
        - Слабый сервер (запуск игры съест все ресурсы и обработка HTTP запросов станет дольше)

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


        1. DyadichenkoGA Автор
          21.02.2022 11:05
          -1

          Да вы правы. Это можно немного решить, если поддержать Keep-Alive, но в общем да. Тут скорее "нет смысла в контексте рассматриваемых условий". А я рассматриваю условия "хорошие". В среднем сейчас слабый сервер, как контекст, скорее редкость, а вот плохое соединение может сыграть роль


          1. DyadichenkoGA Автор
            21.02.2022 11:16
            -1

            Ну я даже немного распространю. Плохое соединение на выставках - это скорее не перегружен траффик, а радиошум. Так как много железа, телефонов и другого оборудования. Можно решить 100% - дорогим оборудованием, но с обычным тут могут быть риски задержек. Причём весьма высоких. Но эту проблему можно решить только шнуром. Потому что там и персистент коннект не спасёт, так как оборудование банально пингуется долго. Поэтому обычно когда такая штука разворачивается, ставятся специфичные условия. Хотя бы приличный роутер :)

            Но геймпад это один пример применения вшитого http сервера. Есть геймплеи где задержка нажатия в 1 секунду не играет вообще никакой роли. Иногда нужно администрировать Unity приложения, которые вообще не игры. И удобнее это делать с планшетов. Но при этом с точки зрения апдейта и деплоя в разы проще просто обновлять контейнер на сервере в котором и веб лежит.


    1. DyadichenkoGA Автор
      21.02.2022 08:45

      https://habr.com/ru/post/321278/
      По сокетам в локальной сети, если мы не говорим про веб клиенты - у меня есть статья. Она конечно на сегодняшний день немного кривовата, так как писал я её 5 лет назад. Но там реализация на сокетах, если в репу залезть. Точнее если память не изменяет на юнити обёртке над сокетами. Там вроде не чистые сокеты


  1. Fen1kz
    21.02.2022 09:27

    Погодите, то есть геймпад работает так: "Юзер жмет кнопку А, отправляется запрос на http://.../gamepad/down/A/"?


    И это быстро работает? И как вы отличаете разных юзеров?


    1. DyadichenkoGA Автор
      21.02.2022 09:41
      +1

      Вполне. Ну можно скачать репозиторий да протестировать. В локальной сети - да. Ну в этом решении никак, в кастомных решениях для заказчиков - по-разному. Это сингл плеер упрощённый пример. Может потом допишу, как делается мультиплеер и аутентификация пользователей в подобных системах. А также система очередей. Так как на реальном ивенте может быть проблема, что пользователей коннектится сразу очень много, и там нужно выстраивать очередь игроков. Но в целом всё реализуемо


    1. DyadichenkoGA Автор
      21.02.2022 09:46
      +1

      А ну да по работе. Ага. Ну считай

      http://${window.location.hostname}:10021/gamepad/down/A/;

      Если у нас порт стоит 10021 на серваке. Если в термины веба переводить. Ну там ещё есть запрос UP. Можно почитать, как реализовано уже в репах. При этом почему веб на пикси? Мультитач работает нормально, события работают нормально. Только с вёрсткой конечно без css грустно и с рендером текстов там не всё гладко. Но зато быстро. Я изначально пробовал на реакте в чистую написать, но там на айос ниже 15.2 очень мешал баг с "magnifying glass" плюс мультитач там работал странно мягко говоря. Вебгл и пикси в этом плане в разы по приятнее. Хотя тоже пришлось повозиться


  1. OptimumOption
    21.02.2022 10:32

    "...Порт – это условное число в диапазоне от 0 до 65353..." - шта?!


    1. DyadichenkoGA Автор
      21.02.2022 10:35

      А что это по вашему? Это неотрицательное число от 0 до 65353 записываемое в заголовках в транспортной модели OSI. То есть просто условное число в заголовке пакета


      1. OptimumOption
        21.02.2022 11:52

        Вы откуда цифру 65353 взяли? С потолка?


        1. DyadichenkoGA Автор
          21.02.2022 11:59

          В статье просто небольшая опечатка. Сейчас поправлю 65535 конечно же. Откуда? Достаточно посмотреть структуру пакета TCP. И порт там занимает 16 бит. 2^16-1 = 65535. Но всё ещё не понимаю реакцию, так как это явно опечатка


          1. OptimumOption
            21.02.2022 12:05

            Ну так вопрос был не про то, что такое ПОРТ, а про указанный вами диапазон, причем вы его так уверенно повторно продублировали в своём ответе :)


            1. DyadichenkoGA Автор
              21.02.2022 12:08

              Дак этож копипаста) Порядок 65353 или 65535 не так сильно бросается в глаза :) Я просто не увидел)


    1. DyadichenkoGA Автор
      21.02.2022 10:38

      Вот тут неплохо зарисована структура TCP пакета. https://webhamster.ru/mytetrashare/index/mtb0/1501768410v2kmovcru6#:~:text=Структура пакета TCP (формат заголовка сегмента)&text=Эти 16-битные поля содержат,клиенту на основании этого номера. Собственно там есть порт. А на уровне уже транспорта в модели OSI это всё резолвится, то есть на уровне протокола. Но в моём тезисе нет ничего неправильного. Так как HTTP это у нас пока настройка над TCP и там это собственно так и работает. Так что не совсем понял вопрос

      Просто в статье не вижу смысла углубляться в детали структуры пакетов и того, как работает весь стек TCP/IP. Во-первых, по этой теме есть целые книги. Во-вторых, это не является топиком статьи. А так в модели OSI на транспортном уровне всё это описано. Можно почитать специализированную литературу по этой теме, если вдруг интересно