GDNet
Это сеть, разработанная специально для игр. Она представляет собой фреймворк и систему состояний на основе действий. GDNet разработана с использованием C# и поддерживает .NET Framework и Core версии. В настоящее время она в основном используется в Unity3D, а также в проектах с формами и консольных приложениях.
Система обладает высокой расширяемостью и позволяет быстро расширять новые протоколы. На данный момент поддерживаются tcp, gcp, udx, kcp и веб-протоколы. GDNet проста в освоении, имеет подробные комментарии к API.
Версия GDNet 6.0: изменения
Схема модулей
К сожалению, без доступа к исходному изображению, невозможно предоставить перевод схемы модулей.
Использование
Скачайте GameDesigner и распакуйте его. Откройте Unity и перейдите в Window/PackageManager. Нажмите «+», чтобы добавить пакет из папки на диске.
Выберите путь к папке GameDesigner/GameDesigner/package.json для импорта пакета GDNet.
Если всё сделано правильно, вы увидите пакет в списке.
Установите ApiCompatibilityLevel* = .NET 4.x и AllowUnsafeCode в настройках проекта. Для версий после 2021 года установите ApiCompatibilityLevel* = .NET Framework.
Создайте серверный проект. Вы можете использовать консоль или приложение с формой, либо добавить новый серверный проект в Assembly-CSharp проект Unity.
Добавьте серверный проект, выбрав консольный проект.
Определите имя сервера и выберите путь к корневому каталогу вашего проекта Unity, на уровне с папкой Assets.
Добавьте существующий проект, выбрав файл GameDesigner.csproj из каталога GameDesigner.
Выберите каталог GameDesigner, где находится файл GameDesigner.csproj. 10. Щёлкните правой кнопкой мыши на ссылку «Server», появится меню, выберите в нём пункт «Добавить ссылку».
Выберите пункт «GameDesigner» в меню «Проекты», подтвердите свой выбор.
Создайте новый файл скрипта службы (Service), этот файл будет содержать ваш класс сервера.
internal class Client : NetPlayer // Ваш класс клиента
{
}
internal class Scene : NetScene<Client> // Ваш игровой класс сцены
{
}
class Service : TcpServer<Client, Scene> // Ваш класс сервера
{
protected override bool OnUnClientRequest(Client unClient, RPCModel model)
{
Console.WriteLine(model.pars[0]);
// Вы можете понимать это как возврат true, если введённые имя пользователя и пароль верны, и false, если имя пользователя или пароль неверны.
return true; // 100% необходимо понять это. Если вы вернёте false, то будете постоянно вызываться здесь и не сможете вызвать методы с атрибутом [Rpc]. Если вы возвращаете true, клиент может вызывать методы с [Rpc] после отправки команды SendRT("метод", "параметр").
}
[Rpc(cmd = NetCmd.SafeCall)] // После использования SafeCall первый параметр будет содержать объект клиента, который является клиентом, отправившим запрос, а этот параметр — соответствующий объект этого клиента.
void test(Client client, string str)
{
Console.WriteLine(str);
SendRT(client, "test", "Ответ сервера rpc"); // Ответ сервера
}
}
var server = new Service(); // Создаём объект сервера
server.Log += Console.WriteLine; // Регистрируем метод вывода сообщений сервера в консоль
server.Run(9543); // Запускаем сервер на порту 9543
while (true)
{
Console.ReadLine();
}
Создайте проект клиента для консоли, следуя тем же шагам, что и при создании проекта сервера.
Определите класс Test для тестирования процесса rpc:
class Test
{
[Rpc]
void test(string str)
{
Console.WriteLine(str);
}
}
TcpClient client = new TcpClient();
client.Log += Console.WriteLine;
Test test = new Test();
client.AddRpcHandle(test);
client.Connect("127.0.0.1", 9543).Wait();
client.SendRT("test", "Первый вызов метода OnUnClientRequest на сервере.");
client.SendRT("test", "Запрос клиента rpc.");
while (true)
{
Console.ReadLine();
}
На этом основное использование завершено.
Если вам нужен серверный шлюз, вы можете использовать Nginx для пересылки запросов на реальный сервер. После добавления Nginx запросы будут проходить следующим образом: клиент → Nginx → игровой сервер, сервер → Nginx → клиент.
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
# Количество подключений клиентов
worker_connections 10240;
}
stream{
upstream gameServer{
# Здесь находится реальный игровой сервер. Если он находится в облаке Alibaba Cloud, Tencent Cloud, укажите соответствующий IP-адрес.
server 127.0.0.1:6667;
}
server{
# Порт, на котором Nginx слушает запросы от клиентов.
listen 9543;
proxy_pass gameServer;
}
}
Подробности см. по ссылке: https://www.cnblogs.com/knowledgesea/p/6497783.html.
GDNet предоставляет два типа пулов объектов: BufferPool для двоичных данных и ObjectPool для классов. В сетевом коде используется BufferPool, который позволяет быстро читать и записывать данные, вместо того чтобы каждый раз создавать новые массивы byte[].
Пример использования BufferPool:
var seg = BufferPool.Take(65535); // Выделяем 65535 байт памяти
seg.WriteValue(123); // Пишем значение 123
BufferPool.Push(seg); // Возвращаем память в пул для повторного использования
var seg1 = BufferPool.Take(65535); // На этот раз берём из пула уже существующий объект seg
seg1.WriteValue(456);
BufferPool.Push(seg1); // Снова возвращаем память в пул
В GDNet реализована быстрая сериализация, которая работает в 5–10 раз быстрее, чем protobuff. В примере 1 используется адаптер быстрой сериализации, который позволяет синхронизировать до 10 000 кубов. При использовании protobuff можно синхронизировать только 2500 кубов.
Существует три версии сериализатора:
Для использования NetConvertFast2 необходимо создать привязки типов. Для этого можно использовать инструмент генерации привязок, доступный по ссылке: https://gitee.com/leng_yue/fast2-build-tool. ``` NetConvertFast2.SerializeObject(new Test()); // Сериализация класса Test var obj = NetConvertFast2.DeserializeObject(seg); // Десериализация класса Test
Fast2BuildToolMethod.Build(typeof(Test), AppDomain.CurrentDomain.BaseDirectory); // Генерация типа привязки Test Fast2BuildToolMethod.BuildArray(typeof(Test), AppDomain.CurrentDomain.BaseDomain); // Генерация массива типов привязки Test Fast2BuildToolMethod.BuildGeneric(typeof(Test), AppDomain.CurrentDomain.BaseDirectory); // Генерация универсального типа привязки Test
В новой версии появилась возможность динамической компиляции типов привязки во время выполнения.
Fast2BuildMethod.DynamicBuild(BindingEntry.GetBindTypes()); // Динамическая компиляция указанного списка типов
Подробная информация доступна в примере GameDesigner\Example\SerializeTest\Scenes\example3.unity.
## Самая быстрая сериализация в сети
> Версия 3 «Экстремально быстрой сериализации» — самая быстрая в сети, никто не возражает?
**Вопрос**: почему так быстро?
**Ответ**: версия 3 «Экстремально быстрой сериализации» использует метод копирования памяти по адресам, напрямую копируя весь блок памяти объекта без необходимости чтения и записи каждого поля по отдельности.
**Вопрос**: что делать, если класс или структура содержит строку или пользовательский класс?
**Ответ**: если в классе нет полей базовых классов (byte, int, long...), то для хранения данных будет использоваться 8 дополнительных байт памяти для указателя на данные. Затем записываются данные строки или другого пользовательского класса.
**Вопрос**: если класс содержит только поля базовых классов, то это очень быстро. А если класс имеет поля пользовательских классов и базовых классов, занимает ли он больше места, чем другие методы сериализации, такие как fastbuff или MemoryPack?
**Ответ**: да, если класс содержит поля пользовательских типов, то потребуется дополнительно 8 байт данных. Можно сжать данные перед выполнением других операций.
После анализа доступа к памяти можно сделать вывод, что для структур требуется только один уровень указателей, а для классов — два уровня указателей.
Для получения адреса экземпляра объекта можно использовать код:
```var address = Unsafe.AsPointer(ref value); // Для структуры нужен только один указатель
address = (void*)(Unsafe.Read<long>(address) + 8); // Для класса нужны два указателя, чтобы получить адрес данных
Все, кроме базовых типов (byte, int, long...) требуют двухуровневых указателей, таких как string, DateTime, decimal... и массивы. Длина массива записывается как значение типа long после него.
Используя полученный адрес объекта, можно выполнить копирование памяти с помощью метода Unsafe.CopyBlock.
{
int offset = 154; // Здесь вычисляется размер всех полей класса
Unsafe.CopyBlock(ptr, address, (uint)offset); // Полное копирование
}
Ниже приведён тестовый код для проверки различных типов полей. ECS модуль
ECS модуль похож на GameObject->component в Unity. В ECS, GameObject = entity, component = component, выполняются системы. Основной процесс ECS и GameObject аналогичен, но компоненты в ECS можно повторно использовать, а компоненты GameObject — нет. При создании более 10 000 объектов GameObject нужно заново создавать объекты и компоненты, в то время как ECS при вызове Destroy помещает entity или компонент в пул объектов, ожидая следующего повторного использования. На самом деле объект не освобождается, поэтому производительность выше, чем у GameObject.
//ecs время компонента
public class TimerComponent : Component, IUpdate //наследование интерфейса IUpdate приводит к тому, что метод Update вызывается каждый кадр
{
private DateTime dateTime;
public override void Awake()
{
dateTime = DateTime.Now.AddSeconds(5);//при инициализации текущее время переносится на 5 секунд вперёд
}
public void Update()
{
if (DateTime.Now >= dateTime)//когда 5 секунд прошло, удалить этот временной компонент, фактически поместить его в пул объектов
{
Destroy(this);
}
}
public override void OnDestroy()//вызывается перед помещением в пул объектов
{
}
}
static void Main(string[] args)
{
var entity = GSystem.Instance.Create<Entity>();//создать объект сущности, который будет искать в пуле объектов, если в пуле нет объекта, он будет создан, иначе будет извлечён из сущности
entity.AddComponent<TimerComponent>();//добавить компонент времени, также искать из пула объектов, если нет, создать, иначе извлечь TimerComponent
while (true)
{
Thread.Sleep(30);
GSystem.Instance.Run();//выполнять систему ECS каждый кадр
}
}
MVC модуль
MVC модуль: модель, контроль, вид разделены. MVC модуль подходит для синхронизированных игр. Модель определяет поля, атрибуты и события объекта. Контроллер выполняет бизнес-логику, а представление отображает результат. В синхронизированной среде MVC разделено, каждый обрабатывает своё, достигая независимости.
Использование компонента FieldCollection для горячего обновления: когда проект обновляется, поля не требуют использования Find для различных поисков, компонент FieldCollection автоматически помогает вам собрать и обработать ссылки на поля, одно нажатие генерирует код вашей функции.
HybridCRL горячее обновление
В клиентском фреймворке поддерживается полное горячее обновление, которое очень просто использовать.
[SyncVar] поле или атрибут синхронизации
Переменные синхронизируются между клиентом и сервером. Переменные могут быть синхронизированы между игроками. Если используется [SyncVar], необходимо включить OnReloadInvoke в меню Unity GameDesigner/Network/InvokeHepler.
Пример кода для [SyncVar]:
using UnityEngine;
internal partial class SyncVarHandlerGenerate
{
internal virtual bool Equals(Rect a, Rect b) //[syncvar] Rect тип не имеет сравнения, поэтому необходимо написать его самостоятельно
{
if (a.x != b.x) return false;
if (a.y != b.y) return false;
if (a.width != b.width) return false;
if (a.height != b.height) return false;
return true;
}
}
internal class SyncVarHandler : SyncVarHandlerGenerate
{
//выполняется только максимальный объект, то есть класс SyncVarHandler выполняется, класс SyncVarHandlerGenerate не будет выполняться
public override int SortingOrder => 100;
//если сгенерированный метод Equals неверен, вы можете переписать уже сгенерированный метод Equals ниже, чтобы выполнить собственное суждение
}
Тестирование RPC малых данных на уровне 100 000
Здесь мы протестировали 1 000 000 запросов и ответов от клиента к серверу, и потребовалось 4,67 секунды.
Необходимо импортировать следующие пространства имён:
using Net.Client;
using Net.Config;
using Net.Event;
using Net.Server;
using Net.Share;
using Net.System;
using System;
using System.Diagnostics;
using System.Threading;
Код теста:
internal class Program
{
static Stopwatch stopwatch;
static void Main(string[] args)
{
NDebug.BindLogAll(Console.WriteLine);
BufferStreamShare.Size = 1024 * 1024 * 100;//сервер может кэшировать данные размером до 10 МБ для каждого клиента
//это серверная часть, которую можно скопировать в другой проект консоли
var server = new TcpServer();
server.LimitQueueCount = 10000000;//тестирование производительности небольших данных, можно установить здесь, по умолчанию ограничено 65536
server.PackageLength = 10000000;//размер пакета данных для быстрой передачи, один раз передаётся количество пакетов небольших данных
server.PackageSize = 1024 * 1024 * 50;//максимальный размер принимаемых данных пакета, если превышен, они будут отброшены
server.AddAdapter(new Net.Adapter.SerializeAdapter3());//используйте высокоскоростную сериализацию для сериализации модели данных rpc
server.AddAdapter(new Net.Adapter.CallSiteRpcAdapter<NetPlayer>(server));//используйте высокоскоростной вызов rpc метода адаптера
server.Run();
//Это клиентская часть, которая может быть скопирована в другой консольный проект
var client = new TcpClient();
client.LimitQueueCount = 10000000;
client.PackageLength = 10000000;
client.PackageSize = 1024 * 1024 * 50;
``` **Текст запроса:**
client.AddAdapter(new Net.Adapter.SerializeAdapter3());
client.AddAdapter(new Net.Adapter.CallSiteRpcAdapter(client));
client.AddRpcHandle(new Program());
client.Connect().Wait();
client.SendRT(new byte[1]);//先进入服务器
Thread.Sleep(500);
stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
client.SendRT(NetCmd.LocalRT, 1, i);
if (i % 10000 == 0)
Thread.Sleep(50);
}
Console.ReadLine();
}
[Rpc(hash = 1)]
void test(int i)
{
if (i % 10000 == 0)
Console.WriteLine(i);
if (i >= 999999)
{
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed);
}
}
**Перевод текста на русский язык:**
Client.AddAdapter (новый Net.Adapter.SerializeAdapter3 ());
Client.AddAdapter (новый Net.Adapter.CallSiteRpcAdapter (клиент));
Клиент.AddRpcHandle (новая программа ());
клиент.Connect (). Подождите ();
клиент.SendRT (новый байт [1]); // сначала войти на сервер
Поток.Sleep (500);
секундомер = секундомер.StartNew ();
для (int я = 0; я <1000000; я ++)
{
клиент.SendRT (NetCmd.LocalRT, 1, я);
если (я% 10000 = 0)
Поток.Sleep (50);
}
Консоль.ReadLine ();
}
[Rpc (хэш = 1)]
пустота тест (int я)
{
если (я% 10000 = 0)
Консоль.WriteLine (я);
если (я> = 999999)
{
секундомер.Стоп ();
Консоль.WriteLine (секундомер.Прошедшее);
}
}
*Примечание: в запросе присутствуют фрагменты кода, которые не были переведены.* 13 Крис — 200 + 1000 + 200.
14 Мэйпл — 200 + 200.
15 Синие точки льда — 10.
Безымянные эксперты, общее пожертвование через WeChat — 653.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )