• Из-за обновления GTA 5 (был добавлен новый патч) может временно не работать вход в RAGE Multiplayer.

    ERROR: Your game version is not supported by RAGE Multiplayer.

    Данная ошибка говорит о том, что GTA V обновилась до новой версии (GTA Online тоже). Вам необходимо обновить саму игру в главном меню вашего приложения (Steam / Epic Games / Rockstar Games).
    Если после этого RAGE:MP все равно не работает - вам нужно дождаться выхода патча для самого мультиплеера (обычно это занимает от нескольких часов до нескольких дней).

    Новости и апдейты Rockstar Games - https://www.rockstargames.com/ru/newswire/
    Статус всех служб для Rockstar Games Launcher и поддерживаемых игр: https://support.rockstargames.com/ru/servicestatus


    Grand Theft Auto 5 (+ GTA Online) последний раз были обновлены:

FAQ Как не стоит делать: база данных и работа с ней на примере RedAge

welaurs

Начинающий специалист
Автор темы
30 Ноя 2021
19
46
48
Дисклеймер. Сказанное ниже является личным мнением автора. Сам по себе автор является мастером спорта международного класса по продавливанию дивана и ничего полезного в своей жизни он, разумеется, не сделал. Урок не претендует на звание истины в последней инстанции, а автор готов признавать свои ошибки и публично приносить извинения, если заденет чьи-то чувства прекрасного своей графоманией с шизофреническими наклонностями.

В рамках данного FAQ я рассмотрю примеры, взятые из мода самая последняя сборка RedAge (от Golem) от 19.08.21 и на конкретных участках кода покажу места, где есть определённые проблемы, а также предложу своё решение.

1. Выполнение запросов синхронно. Вероятно, это решение планировалось использовать временно в целях упрощения разработки. Но, как мы все знаем, нет ничего более вечного, чем временное. В результате это решение осталось в сливе оригинального мода и, смею предположить, с того момента этот код не модифицировался.
Проблема: блокировка потока исполнения (простыми словами код не исполняется, пока база данных не обработает запрос), падение общей производительности как следствие.
Решение: использование асинхронных запросов. В моде уже реализовано всё для асинхронных запросов, необходимо лишь переписать часто вызываемые методы, которые используют MySQL.QueryRead с использованием MySQL.QueryReadAsync.
Пример:

C#:
[Command("offunwarn")] // Снять варн у игрока в оффлайне (3 лвл)
public static void CMD_offunwarn(Player player, string target)
{
    try
    {
        if (!Main.Players.ContainsKey(player)) return;
        if (!Group.CanUseCmd(player, "offunwarn")) return;

        if (!Main.PlayerNames.ContainsValue(target))
        {
            Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок не найден", 3000);
            return;
        }
        if (NAPI.Player.GetPlayerFromName(target) != null)
        {
            Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок онлайн", 3000);
            return;
        }

        var split = target.Split('_');
        var data = MySQL.QueryRead($"SELECT warns FROM characters WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
        var warns = 0;
        foreach (System.Data.DataRow Row in data.Rows)
        {
            warns = Convert.ToInt32(Row["warns"]);
        }

        if (warns <= 0)
        {
            Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"У игрока нет варнов", 3000);
            return;
        }

        warns--;
        GameLog.Admin($"{player.Name}", $"offUnwarn", $"{target}");
        MySQL.Query($"UPDATE characters SET warns={warns} WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
        Notify.Send(player, NotifyType.Success, NotifyPosition.BottomCenter, $"Вы сняли варн у игрока {target}, у него {warns} варнов", 3000);
    }
    catch (Exception e) { Log.Write("offunwarn: " + e.Message, nLog.Type.Error); }
}
C#:
[Command("offunwarn")] // Снять варн у игрока в оффлайне (3 лвл)
public static async void CMD_offunwarn(Player player, string target)
{
    try
    {
        if (!Main.Players.ContainsKey(player)) return;
        if (!Group.CanUseCmd(player, "offunwarn")) return;

        if (!Main.PlayerNames.ContainsValue(target))
        {
            Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок не найден", 3000);
            return;
        }
        if (NAPI.Player.GetPlayerFromName(target) != null)
        {
            Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок онлайн", 3000);
            return;
        }

        var split = target.Split('_');
        var data = await MySQL.QueryReadAsync($"SELECT warns FROM characters WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
        var warns = 0;
        foreach (System.Data.DataRow Row in data.Rows)
        {
            warns = Convert.ToInt32(Row["warns"]);
        }

        if (warns <= 0)
        {
            Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"У игрока нет варнов", 3000);
            return;
        }

        warns--;
        GameLog.Admin($"{player.Name}", $"offUnwarn", $"{target}");
        MySQL.QueryAsync($"UPDATE characters SET warns={warns} WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
        Notify.Send(player, NotifyType.Success, NotifyPosition.BottomCenter, $"Вы сняли варн у игрока {target}, у него {warns} варнов", 3000);
    }
    catch (Exception e) { Log.Write("offunwarn: " + e.Message, nLog.Type.Error); }
}
Также рекомендую прочитать дополнения от @Serafim:
Дополнение 1
Дополнение 2

2. Создание нового подключения для каждого запроса. В целом, это не такая плохая практика, учитывая использование disposable. Но есть одно "но": при массовых запросах в базу данных (например, при сохранении игроков, домов или бизнесов) происходит большой оверхед при создании новых соединений. При игре на сервере с онлайном 1 вы вряд ли это заметите, но при онлайне 500 человек и больше это может стать серьёзной проблемой, так как помимо создания множества соединений необходимо будет обрабатывать данные от других игроков.
Проблема: блокировка потока исполнения, создание и закрытие соединений, когда можно использовать только лишь одно.
Решение: множество различных вариантов. Один из самых очевидных и простых для реализации - создание метода, который выполнит все запросы в рамках одного соединения. Я бы хотел рассмотреть ещё вариант использования транзакций, но, смею предположить, в данном случае он только лишь нанесёт ущерб производительности.
P. S. Пока я подготавливал пример заметил также проблему с реализацией сохранения бизнесов: сохранение происходит синхронно, что влечёт за собой заморозку потока. Учитывая, что сохранение вызывается только лишь при выключении сервера, это не такой серьёзный удар по производительности, но при массовом сохранении во время работы (а это именно то, что стоит делать) возникнет лаг.

P. P. S. Пример предложенного мною метода не является превосходной реализацией, но это лучше, чем было. В связи с тем, что я не хочу усложнять FAQ, я предложил именно это решение. Я ставлю перед собой задачу дать толчок в сторону хорошей (или хотя бы неплохой) реализации, а вы, при должном желании, можете сделать ещё лучше.
C#:
public static void SaveAll(object state = null)
{
    try
    {
        Log.Write("Saving items...", nLog.Type.Info);
        if (Items.Count == 0) return;
        Dictionary<int, List<nItem>> cItems = new Dictionary<int, List<nItem>>(Items);

        foreach (KeyValuePair<int, List<nItem>> kvp in cItems)
        {
            int UUID = kvp.Key;
            string json = JsonConvert.SerializeObject(kvp.Value);
            MySQL.Query($"UPDATE `inventory` SET items='{json}' WHERE uuid={UUID}");
        }
        Log.Write("Items has been saved to DB.", nLog.Type.Success);
    }
    catch (Exception e)
    {
        Log.Write("EXCEPTION AT \"INVENTORY_SAVEALL\":\n" + e.ToString(), nLog.Type.Error);
    }
}
C#:
public static async void SaveAll(object state = null)
{
    try
    {
        Log.Write("Saving items...", nLog.Type.Info);
        if (Items.Count == 0) return;
        Dictionary<int, List<nItem>> cItems = new Dictionary<int, List<nItem>>(Items);

        List<string> queries = new List<string>();

        foreach (KeyValuePair<int, List<nItem>> kvp in cItems)
        {
            int UUID = kvp.Key;
            string json = JsonConvert.SerializeObject(kvp.Value);
            queries.Add($"UPDATE `inventory` SET items='{json}' WHERE uuid={UUID}");
        }

        await MySQL.QueryAsync(queries);
        Log.Write("Items has been saved to DB.", nLog.Type.Success);
    }
    catch (Exception e)
    {
        Log.Write("EXCEPTION AT \"INVENTORY_SAVEALL\":\n" + e.ToString(), nLog.Type.Error);
    }
}
C#:
public static async Task QueryAsync(List<string> queries)
{
    try
    {
        using (MySqlConnection connection = new MySqlConnection(Connection))
        {
            await connection.OpenAsync();

            using (MySqlCommand cmd = new MySqlCommand())
            {
                foreach (var query in queries)
                {
                    if (Debug) Log.Debug("Query to DB:\n" + query);
      
                    cmd.Connection = connection;
                    cmd.CommandText = query;

                    await cmd.ExecuteNonQueryAsync();
                }
  
            }
        }
    }
    catch (Exception e)
    {
        Log.Write(e.ToString(), nLog.Type.Error);
    }
}

3. Допуск неэкранированных строк к запросу. Последствия очевидны: SQL-инъекции. Я не буду останавливаться на данной проблеме, так как в интернете есть исчерпывающая информация по эксплуатации данной уязвимости и защите от неё.
Проблема: допуск произвольных строк к исполнению при запросе влечёт за собой возможность исполнения произвольного запроса в базу данных.
Решение: экранирование строк.
Пример:

C#:
await MySQL.QueryAsync($"UPDATE accounts SET character{slot}=-1 WHERE login='{Login}'");
C#:
// Я не экранировал slot, так как можно считать, что данная переменная не может иметь
// произвольное значение, взятое извне.
using MySqlCommand cmd = new MySqlCommand("UPDATE accounts SET character{slot}=-1 WHERE login='@login'");
cmd.Parameters.AddWithValue("@login", Login);
await MySQL.QueryAsync(cmd);

В моде есть ещё и другие проблемы с базой данных, но в рамках данного FAQ я не стал рассматривать все мелочи. Данные мною решения позволят оптимизировать самые критичные для производительности задачи, а последнее немного улучшит безопасность. Буду рад видеть ваши комментарии и критику. Если FAQ получит положительный отклик, то я с радостью выпущу ещё одну статью с критикой безопасности данного мода.


Другие статьи:
Как не стоит делать: безопасность ивентов и клиентских скриптов
[JS] Как подключить отладчик (Debugger) в PhpStorm для server-side
 
Последнее редактирование:

dobriy

Специалист
14 Сен 2020
140
14
80
Спасибо! больше бы таких полезных информации.
 
  • Like
Реакции: welaurs

Inoi

/dev/null
VIP
15 Окт 2020
2,352
1,403
208
34
глобально - всё абсолютно верно
вся редага - на синхронных неэкранированных запросах, и это конечно первое, от чего моментально вытекают глаза

единственное что раз уж это гайд, стоило бы ну для совсем маленьких уточнить - что и методы придётся переписывать в асинхронные таски
не все разумеется, асинк в основном нужен на и\о процессах, но тем не менее
местами прописывая эвейт своих асинхронных запросов - а если ломануться переписывать всё в асинк то местами это конечно придётся делать
и делать это нужно прямо по всей цепочке, начиная с запроса и дальше по всей ветке вызовов
попутно спотыкаясь о то, что вещи типа телефона например - не созданы под асинхронные таски вообще, и работать не будут
(привет колбекам от всяких менюшек с вылезанием в бд типа покупки дома - интепритатор сожрёт изврат типа async void, но метод просто крашнет сервак)

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

Код:
public static Task<T> RunReturn<T>(this GTANetworkMethods.Task task, Func<T> func)
        {
            var taskCompletionSource = new TaskCompletionSource<T>();
            task.Run(() =>
            {
                var result = func();
                taskCompletionSource.SetResult(result);
            });
            return taskCompletionSource.Task;
        }

но глобально - совершенно верные размышления, и практически жизненно необходимые действия
сотни синхронных вызовов в бд выглядят как лютый ад и удивительно, что это вообще первый раз когда я вижу подобную тему
хотя откровенно говоря сомневаюсь, что кто-то толпами ломанётся переписывать сейчас свой мод
это довольно объёмная местами задача, а СРОЧНО НУЖЕН ОНЛАЙН 800ККККК НА СТАРТЕ СЕГОДНЯ ДОБАВИЛИ КАЕН проживут и так, не сильно задумываясь об оптимизации кода

помнится мне я видел "ВЫ ЧО ВЫ ВИДЕЛИ СОКА ТАМ ОБРАЩЕНИЙ В БД 100+ ЭТО ЧО ВЕЗДЕ ПЕРЕПИСЫВАТЬ НА ЭКРАНИРОВАНИЕ ДА ЗАЧЕМ РАБОТАЛО ЖЕ НОРМ"
че уж тут говорить о переписывании всего в асинхрон
но подход разумеется правильный
 
Последнее редактирование:

welaurs

Начинающий специалист
Автор темы
30 Ноя 2021
19
46
48
@Serafim благодарю за внесение корректировок. Обязательно дополню шапку твоими уточнениями.
 

Inoi

/dev/null
VIP
15 Окт 2020
2,352
1,403
208
34
бтв посмотрел на код

я заранее прошу прощения так сказать, возможно это очевидно для тебя - но ты привёл конкретный пример
слепо следуя которому, по гайдику, человек который начнёт переписывать свой мод в асинк - выхватит неиллюзорных приколдесов

1639572167573.png



я бы рекомендовал избегать использования конструкции async void вообще, и взять за правило везде херачить таски
async void - это абсолютно странная конструкция, которая может быть допустимой разве что в случаях "запустил и забыл"
это просто тоталли плохая практика
например конкретно в этом случае, когда ты лезешь в бд у оффлайн персонажа - в целом, почему бы и нет, тебе глобально плевать на результат
но зачем про неё вообще вспоминать?
если уж как грица делать заебис - так сразу везде

когда рядовой пользователь станет вызывать асинхронные методы void в чуть более динамическом коде - начнётся веселье

async void - не выдаёт исключения вне метода, например

Код:
public async void AsyncVoidMetod()
{
    throw new Exception("Меня не замечают");
}

public void KakoyToMetod()
{
    try
    {
        AsyncVoidMetod();
    }
    catch(Exception ex)
    {
        // Ты никогда не окажешься здесь
        Debug.WriteLine(ex.Message);
    }
}

в случае с async Task - ты спокойно ловишь такие исключения вне асинхронного метода.

- ты никогда не узнаешь, выполнилась ли задача, которую ты запустил и не можешь её ждать
В случае с Таском - await, Task.WhenAny, Task.WhenAll всё отлично отработает, запустить await KakoitoAsyncVoid() - ты не можешь в принципе, ты не можешь ожидать пустого метода

И это очень бегло. На практике - работать с async void тупа сложно из-за того что даже просто не отловить нормально ошибки
Если, короче, коротко - есть замечательная статья msdn где черным по белому написано
"Старайтесь избегать async void в ваших практиках асинхронного программирования, потому что это очень ситуативная залупа"

Ещё раз сорян еси чо, мб это подразумевалось, просто в единственном примере в твоём коде - выбрана именно эта конструкция, (ну кроме обращения в бд конечно, кекв - иначе бы это был бы возмутительно бесполезный код) и для стороннего наблюдателя, который заинтересуется переходом в асинк методы - это будет абсолютно неочевидным.
А разницу ты собсно получается не поясняешь, приведя пример с конструкцией, которая вызовет невероятное количество проблем, если начать её не сильно вдумываясь херачить во все методы подряд.
Вроде с командой так и можно сделать - а вроде бы и зачем вообще юзать где-либо async void если это плохая практика.
В рамках такой замечательной статьи "Как не стоит делать" - это опасно xD

п.с. здарова, я тожи мамкин графоман
 
Последнее редактирование:
  • Like
Реакции: uvanov, welaurs и dobriy

akudinov28

Гуру
24 Фев 2021
262
119
105
Привет коллегам по цеху от диванного эксперта-лежебоки.
Вопрос к знающим ленивцам: стоит ли таки делать дополнительную валидацию на серверной части, если есть клиентская валидация, которая просто не пускает запрос к серверу, пока все условия не будут выполнены? Я про формы. Понимаю, что есть теоретическая возможность подделки запроса, но насколько реальна вообще эта проблема именно в рейдже?
 

Inoi

/dev/null
VIP
15 Окт 2020
2,352
1,403
208
34
Привет коллегам по цеху от диванного эксперта-лежебоки.
Вопрос к знающим ленивцам: стоит ли таки делать дополнительную валидацию на серверной части, если есть клиентская валидация, которая просто не пускает запрос к серверу, пока все условия не будут выполнены? Я про формы. Понимаю, что есть теоретическая возможность подделки запроса, но насколько реальна вообще эта проблема именно в рейдже?

Если ты говоришь о формах, содержимое которых лезет в скуль - то ты же в любом случае проводишь их через сервак и какую-то свою существующую там MySQL.Query(cmd);
Почему нет?
Это никак тебя не нагружает, и совершенно точно не будет лишним
Экранировать данные во всех запросах - это не то что бы "стоит ли", это необходимое правило хорошего тона
 
Последнее редактирование:
  • Like
Реакции: dobriy и XDeveluxe

akudinov28

Гуру
24 Фев 2021
262
119
105
Если ты говоришь о формах, содержимое которых лезет в скуль - то ты же в любом случае проводишь их через сервак и какую-то свою существующую там MySQL.Query(cmd);
Почему нет?
Это никак тебя не нагружает, и совершенно точно не будет лишним
Экранировать данные во всех запросах - это не то что бы "стоит ли", это необходимое правило хорошего тона
Тут вопрос не об экранировании, ибо я использую ORM, которая сама по себе экранирует переменные в запросах. Тут вопрос именно о валидации (пустое/не пустое, сколько символов, alphanum и т.д.)
 

Inoi

/dev/null
VIP
15 Окт 2020
2,352
1,403
208
34
Тут вопрос не об экранировании, ибо я использую ORM, которая сама по себе экранирует переменные в запросах. Тут вопрос именно о валидации (пустое/не пустое, сколько символов, alphanum и т.д.)

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

welaurs

Начинающий специалист
Автор темы
30 Ноя 2021
19
46
48
Привет коллегам по цеху от диванного эксперта-лежебоки.
Вопрос к знающим ленивцам: стоит ли таки делать дополнительную валидацию на серверной части, если есть клиентская валидация, которая просто не пускает запрос к серверу, пока все условия не будут выполнены? Я про формы. Понимаю, что есть теоретическая возможность подделки запроса, но насколько реальна вообще эта проблема именно в рейдже?
ORM защищает от SQL-инъекций, но не защищает от подмены данных клиентом. Даже при условии использования ORM необходимо валидировать ВСЕ запросы, поступающие от клиента из-за элементарного принципа: доверяй клиенту как можно меньше. Любой реверсер с опытом более месяца хукнёт сначала функцию загрузки клиентских скриптов в v8 (а в случае использования C# на клиенте просто сдампит DLL, после чего откроет её в ILSpy/dnSpy/любом другом софте), посмотрит какие вызовы ты делаешь с клиентской части и вызовет эти методы на сервере.
 
  • Like
Реакции: XDeveluxe и Inoi

welaurs

Начинающий специалист
Автор темы
30 Ноя 2021
19
46
48
И это очень бегло. На практике - работать с async void тупа сложно из-за того что даже просто не отловить нормально ошибки
Если, короче, коротко - есть замечательная статья msdn где черным по белому написано
"Старайтесь избегать async void в ваших практиках асинхронного программирования, потому что это очень ситуативная залупа"

Ещё раз сорян еси чо, мб это подразумевалось, просто в единственном примере в твоём коде - выбрана именно эта конструкция, (ну кроме обращения в бд конечно, кекв - иначе бы это был бы возмутительно бесполезный код) и для стороннего наблюдателя, который заинтересуется переходом в асинк методы - это будет абсолютно неочевидным.
А разницу ты собсно получается не поясняешь, приведя пример с конструкцией, которая вызовет невероятное количество проблем, если начать её не сильно вдумываясь херачить во все методы подряд.
Вроде с командой так и можно сделать - а вроде бы и зачем вообще юзать где-либо async void если это плохая практика.
В рамках такой замечательной статьи "Как не стоит делать" - это опасно xD
В случае использования async void при отсутствии await любая IDE скажет тебе, что ты не перехватываешь исключения. Последствия использования данной конструкции, как мне кажется, очевидны :) Но дополнение имеет место быть, думаю имеет смысл вынести в отдельную статью по асинхронным задачам в RAGE и их особенностям.
 
  • Like
Реакции: Inoi

Inoi

/dev/null
VIP
15 Окт 2020
2,352
1,403
208
34
В случае использования async void при отсутствии await любая IDE скажет тебе, что ты не перехватываешь исключения. Последствия использования данной конструкции, как мне кажется, очевидны :) Но дополнение имеет место быть, думаю имеет смысл вынести в отдельную статью по асинхронным задачам в RAGE и их особенностям.
Ну это тебе очевидны, а тем кто гайд будет читать, или нагуглит его когда-нибудь со своими ошибками или дедлоком - не особо :D
 
  • Like
Реакции: welaurs