[Блог] Пытаюсь писать свой сервер

Сеть в Unity3D

[Блог] Пытаюсь писать свой сервер

Сообщение [COAD] H00JAMBRA 10 янв 2018, 14:34

Всем добрый день! ;)
Я "программист" - самоучка, с нашей любимой [unity 3D] уже около 2х лет.
С недавних пор задался целью написать сервер на сокетах, чтобы моя затея не осталась лишь в голове я решил публиковать сюда прогресс и рассказывать о разработке. Времени на это дело сейчас не очень много, но я постараюсь как можно детальней рассказывать обо всем. Возможно кому то будет интересно и он научится чему то у меня либо поможет и научит меня :)

Итак поехали, что должен уметь наш сервер:
1. Регистрация и авторизация пользователей / восстановление аккаунта
2. Чат (обмен сообщениями пользователей: общий чат и приватный)
3. Непосредственно сам сервер для взаимодействия игроков.
Последний раз редактировалось [COAD] H00JAMBRA 11 янв 2018, 03:10, всего редактировалось 1 раз.
Аватара пользователя
[COAD] H00JAMBRA
UNец
 
Сообщения: 46
Зарегистрирован: 09 июл 2017, 13:51

Re: [Блог] Пытаюсь писать свой сервер

Сообщение IDoNotExist 10 янв 2018, 14:52

[COAD] H00JAMBRA писал(а):1. Регистрация и авторизация пользователей / восстановление аккаунта
2. Чат (обмен сообщениями пользователей: общий чат и приватный)
3. Непосредственно сам сервер для взаимодействия игроков, планирую для начала один игровой мир до 5 000 онлайна (думаю это реально сделать т.к. к примеру даже паленные серваки для Linage2 способны такое выдержать).

Ты забыл пару "незначительных" пунктов:
-1. Разруливание соединений, распределение их по потокам, предотвращение ДДОС атак.
0. Сериализация/десериализация сообщений.
Аватара пользователя
IDoNotExist
Адепт
 
Сообщения: 1432
Зарегистрирован: 23 мар 2011, 09:18
Skype: iamnoexist

Re: [Блог] Пытаюсь писать свой сервер

Сообщение [COAD] H00JAMBRA 10 янв 2018, 16:23

Буду разбираться, опыта не имею в этих делах, думал treadpool сам потоки распределяет)) Только что расписывал подключение к БД в итоге ноут перезагружаться надумал, все в топку x_x Так что буду выкладывать код с небольшими комментариями просто..

Создал новый проект в нем класс для работы с БД, подключил библиотеки
MySQL.Data
System.Data

Вот сам класс Database.cs
Синтаксис:
Используется csharp
using System;
using MySql.Data.MySqlClient;

namespace MMO_SERVER
{
        public class Database
        {
                string _dbHostName = "sql2.freemysqlhosting.net";
                string _dbName = "sql22****";
                string _dbUserName = "sql22****";
                string _dbPassword = "qwerty123";
                string _dbTableName = "players";

                MySqlConnection _dbConnection;
               
               // создаю подключение
                public Database ()
                {
                        _dbConnection = new MySqlConnection(
                               
                                "server="   + _dbHostName + ";" +
                                "user="     + _dbUserName + ";" +
                                "database=" + _dbName     + ";" +
                                "password=" +_dbPassword  + ";"
                        );

                        _dbConnection.Open ();
                        Console.WriteLine ("Connected to Database: " + _dbName);

                        //RegisterNewPlayer ("Karloss", "qwerty", "karloss@gmail.com"); // пример запроса на регистрацию
                        //LoginUser("Bob", "0000"); // пример запроса на авторизацию

                        Console.ReadKey ();
                        _dbConnection.Close ();
                        Console.WriteLine ("Database connection closed!");

                }
                // проверяем имя на совпадение
                private bool CheckUserName(string _userName)
                {
                        string checkSQL = "SELECT `name` FROM `"+ _dbTableName +"`" +
                                "WHERE `name` = '" + _userName + "'";

                        MySqlCommand checkCommand = new MySqlCommand (checkSQL, _dbConnection);

                        string result = (String)checkCommand.ExecuteScalar ();
                        Console.WriteLine (result);
                        if (result == _userName)
                                return true;
                        else
                                return false;
                }
                // проверяем не зарегистрирован ли уже емейл
                private bool CheckUserEmail(string _userEmail)
                {
                        string checkSQL = "SELECT `email` FROM `"+ _dbTableName +"`" +
                                "WHERE `email` = '" + _userEmail + "'";

                        MySqlCommand checkCommand = new MySqlCommand (checkSQL, _dbConnection);

                        string result = (String)checkCommand.ExecuteScalar ();

                        if (result == _userEmail)
                                return true;
                        else
                                return false;
                }
                // проверка для авторизации зарегистрирован ли игрок
                private bool CheckUser(string _userName, string _userPassword)
                {
                        string checkSQL = "SELECT * FROM `"+ _dbTableName +"`" +
                                "WHERE `password` = '" + _userPassword + "' AND `name` = '" + _userName + "'";

                        MySqlCommand checkCommand = new MySqlCommand (checkSQL, _dbConnection);

                        MySqlDataReader MyDataReader = checkCommand.ExecuteReader();
                        string _readName = null;
                        string _readPass = null;
                        while (MyDataReader.Read())
                        {
                                _readName = MyDataReader.GetString(1);
                                _readPass = MyDataReader.GetString(2);
                        }
                        MyDataReader.Close();

                        if (_readName == _userName && _readPass == _userPassword)
                                return true;
                        else
                                return false;
                }

                // сама регистрация, сначала  проверяем нету ли в базе логина и мейла, потом проверяем пароль и если ок - регистрируем
                public void RegisterNewPlayer(string _name, string _password, string _email)
                {
                        if (CheckUserName (_name))
                                Console.WriteLine ("User name already registered!");
                        else if (CheckUserEmail (_email))
                                Console.WriteLine ("User email already registered!");
                        else if (_password.Length < 6)
                                Console.WriteLine ("Password must have 6 or more chars!");
                        else
                        {
                                string registerSQL = "INSERT INTO `"+ _dbTableName +"`(`id`, `name`, `password`, `email`) " +
                                        "VALUES ('','"+ _name +"','"+ _password +"','"+ _email +"')";

                                MySqlCommand regCommand = new MySqlCommand(registerSQL, _dbConnection);
                                regCommand.ExecuteNonQuery ();

                                Console.WriteLine (string.Format("Registered new player: nickname = {0}, email = {1}", _name, _email));
                        }
                }

                // авторизация
                public void LoginUser(string _name, string _password)
                {
                        if (CheckUser (_name, _password))
                        {
                                Console.WriteLine ("User: [" + _name + "] join to server!");   
                        }
                        else
                                Console.WriteLine ("Incorrect name or password!");
                }                      
        }
}


 



Теперь нужно подумать об защите от SQL инъекций и шифровании пароля.

Вопрос к разбирающимся в этих делах:
-защитит ли SqlCommand.Parameters от SQL inject?
-когда нужно открывать/закрывать подключение к БД? На старте сервера и при завершении работы или я не прав?
-существует ли метод отвечающий за завершение работы консольного приложения, что то вроде OnApplicationQuit() как в юньке, или же нужно создать ивент?

UPD. Еще вопрос подъехал: как считаете стоит ли использовать SSL/TLS протокол для защиты передачи данных клиент-сервер или достаточно будет их шифрования простыми способами?
Последний раз редактировалось [COAD] H00JAMBRA 11 янв 2018, 01:32, всего редактировалось 1 раз.
Аватара пользователя
[COAD] H00JAMBRA
UNец
 
Сообщения: 46
Зарегистрирован: 09 июл 2017, 13:51

Re: [Блог] Пытаюсь писать свой сервер

Сообщение [COAD] H00JAMBRA 11 янв 2018, 00:35

Решил для безопасности пароля не изобретать велосипед и воспользоваться стандартным уже проверенным решением - Rfc2898DeriveBytes. Вкратце расскажу что к чему: при регистрации нового игрока генерируется рандомное значение "соль" (своеобразный мусор предназначенный для усложнения вычислений при взломе пароля) и подсчитывается хэш от "наш пароль + соль", во время проверки пароля осуществляется тоже самое и сравниваются размеры. Если кому интересно детальней можно почитать тут

Изменения в Database.cs
Синтаксис:
Используется csharp
private bool CheckUser(string _userName, string _userPassword)
{

        string checkSQL = "SELECT `name`, `password` FROM `"+ _dbTableName +"`" +
                "WHERE `name` = '" + _userName + "'";

        MySqlCommand checkCommand = new MySqlCommand (checkSQL, _dbConnection);

        string _readName = null;
        string _readPass = null;

        MySqlDataReader MyDataReader = checkCommand.ExecuteReader();

        while (MyDataReader.Read())
        {
                _readName = MyDataReader.GetString(0);
                _readPass = MyDataReader.GetString(1);
        }


        MyDataReader.Close();

        if (_readName == _userName && SecurityManager.VerifyHashedPassword(_readPass, _userPassword)) // сравниваем хэши
                return true;
        else
                return false;
}

public void RegisterNewPlayer(string _name, string _password, string _email)
{
        if (CheckUserName (_name))
                Console.WriteLine ("User name already registered!");
        else if (CheckUserEmail (_email))
                Console.WriteLine ("User email already registered!");
        else if (_password.Length < 6)
                Console.WriteLine ("Password must have 6 or more chars!");
        else
        {
                _password = SecurityManager.HashPassword (_password); // хэшируем пароль

                string registerSQL = "INSERT INTO `"+ _dbTableName +"`(`id`, `name`, `password`, `email`) " +
                        "VALUES ('','"+ _name +"','"+ _password +"','"+ _email +"')";

                MySqlCommand regCommand = new MySqlCommand(registerSQL, _dbConnection);
                regCommand.ExecuteNonQuery ();

                Console.WriteLine (string.Format("Registered new player: nickname = {0}, email = {1}", _name, _email));
        }
}
 


И добавился новый класс SecurityManager.cs
Синтаксис:
Используется csharp
using System;
using System.Security.Cryptography;

namespace MMO_SERVER
{
        static class SecurityManager
        {
                private const int IterationCount = 1000;  // к-во итераций, чем больше тем надежней
                private const int hashKeySize = 256/8;          // 256 bits
                private const int saltSize = 128/8;             // 128 bits

                public static string HashPassword(string password)
                {
                        if (password == null)
                        {
                                throw new ArgumentNullException("password");
                        }

                        byte[] salt;
                        byte[] subkey;
                        using (var deriveBytes = new Rfc2898DeriveBytes(password, saltSize, IterationCount))
                        {
                                salt = deriveBytes.Salt;
                                subkey = deriveBytes.GetBytes(hashKeySize);
                        }

                        var outputBytes = new byte[1 + hashKeySize + saltSize];
                        Buffer.BlockCopy(salt, 0, outputBytes, 1, saltSize);
                        Buffer.BlockCopy(subkey, 0, outputBytes, 1 + saltSize, hashKeySize);
                        return Convert.ToBase64String(outputBytes);
                }

                public static bool VerifyHashedPassword(string hashedPassword, string password)
                {
                        if (hashedPassword == null)
                        {
                                return false;
                        }
                        if (password == null)
                        {
                                throw new ArgumentNullException("password");
                        }

                        var hashedPasswordBytes = Convert.FromBase64String(hashedPassword);

                        if (hashedPasswordBytes.Length != (1 + hashKeySize + saltSize) || hashedPasswordBytes[0] != 0x00)
                        {
                                return false;
                        }

                        var salt = new byte[saltSize];
                        Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, saltSize);
                        var storedSubkey = new byte[hashKeySize];
                        Buffer.BlockCopy(hashedPasswordBytes, 1 + saltSize, storedSubkey, 0, hashKeySize);

                        byte[] generatedSubkey;
                        using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, IterationCount))
                        {
                                generatedSubkey = deriveBytes.GetBytes(hashKeySize);
                        }
                        return AreHashesEqual(storedSubkey, generatedSubkey);
                }
                       
                private static bool AreHashesEqual(byte[] firstHash, byte[] secondHash)
                {
                        int _minHashLength = firstHash.Length <= secondHash.Length ? firstHash.Length : secondHash.Length;
                        var xor = firstHash.Length ^ secondHash.Length;
                        for (int i = 0; i < _minHashLength; i++)
                                xor |= firstHash[i] ^ secondHash[i];
                        return 0 == xor;
                }

        }
}
 
Последний раз редактировалось [COAD] H00JAMBRA 11 янв 2018, 03:00, всего редактировалось 1 раз.
Аватара пользователя
[COAD] H00JAMBRA
UNец
 
Сообщения: 46
Зарегистрирован: 09 июл 2017, 13:51

Re: [Блог] Пытаюсь писать свой сервер

Сообщение [COAD] H00JAMBRA 11 янв 2018, 01:26

Добавил параметризованные запросы к БД для защиты от SQL инъекций.
Расскажу как внедрить на примере чтобы не сбрасывать весь код:
Синтаксис:
Используется csharp
     /*
           Это строка нашего запроса на проверку имени вместо
           переменной записываем параметр @Name без кавычек как ниже
     */

     string checkSQL = "SELECT `name`, `password` FROM `"+ _dbTableName +"`" +
                                 "WHERE `name` = @Name";  

     MySqlCommand checkCommand = new MySqlCommand (checkSQL, _dbConnection);

      // создаем параметр с именем Name, типом  VarChar, и размером 20 символов
     checkCommand.Parameters.Add("@Name", MySqlDbType.VarChar, 20);
     // присваиваем нашему параметру значение
     checkCommand.Parameters["@Name"].Value = _userName;
 


Основываясь на этом примере нужно те же действия проделать в остальных функциях где имеются запросы к БД.

По поводу восстановления аккаунта все просто подключаем библиотеку для работы с почтой и согласно туториалу делаем все. Если у Вас пароль как у меня хэшируется - генерируйте новый и отсылайте пользователю на почту, если без хэша - можно просто взять с БД и отправить старый.
Главное не забыть обновить пароль в БД (если изменили на новый), делается это просто через запрос, например:
Синтаксис:
Используется csharp
string changePasswordSQL = "UPDATE `"+ _dbTableName +"` SET `password` = @Password WHERE `email` = @Email";
 


Ключевое слово UPDATE, и параметры которые передаем - новый пароль + емейл для того чтобы знать где меняем.
Аватара пользователя
[COAD] H00JAMBRA
UNец
 
Сообщения: 46
Зарегистрирован: 09 июл 2017, 13:51

Re: [Блог] Пытаюсь писать свой сервер

Сообщение Woolf 11 янв 2018, 06:43

По моему, вы не с того начали. Если речь о сервере, для начала, наладьте коммуникацию, коннект/дисконнект, прием-передача сообщений, неблокируемые сокеты, потоки обработки сообщений, рассылки. И только потом уже можно прикручивать базу.

ЗЫ да бог с ними, с неблокируюмыми, вы хотя бы с блокируемыми сделайте. Кстати, вот тут один из немногих случаев, когда я порекомендовал бы сначала порисовать схемки.
Далее, посмотрел ваш класс работы с БД. Как вы собираетесь к нему обращаться из других потоков? Где блокировки? Далее, мускуль прекрасно умеет работать параллельно с несколькими коннектам, зачем вы будете заставлять игрока ждать, пока кто-то там отработает с базой? Почему бы вам не сделать пулл коннектов, и по запросу всегда браться свободный коннект, чтобы, когда игроку надо будет что-то записать или считать с базы, он не ждал бы своей очереди, а сразу начинал работу с БД?
Разработчик theFisherOnline - там, где клюёт
Разработчик Atom Fishing II - Первая 3D MMO про рыбалку
Разработчик Atom Fishing - Рыбалка на поплавок, донку, нахлыст, блесну в постъядерный период.
Аватара пользователя
Woolf
Адепт
 
Сообщения: 7179
Зарегистрирован: 02 мар 2009, 16:59

Re: [Блог] Пытаюсь писать свой сервер

Сообщение [COAD] H00JAMBRA 11 янв 2018, 13:11

Честно сказать о блокируемых/не блокируемых даже не слышал сейчас буду гуглить :D
По поводу БД я Вас правильно понял: создаю лист mysqlConnects при коннекте заношу туда игрока при дисконнекте удаляю?
И еще может посоветуете стоит ли для игры использовать SSL протокол? или же обычный TCP будет шустрее?
Аватара пользователя
[COAD] H00JAMBRA
UNец
 
Сообщения: 46
Зарегистрирован: 09 июл 2017, 13:51

Re: [Блог] Пытаюсь писать свой сервер

Сообщение Woolf 11 янв 2018, 16:34

Синтаксис:
Используется csharp
По поводу БД я Вас правильно понял: создаю лист mysqlConnects при коннекте заношу туда игрока при дисконнекте удаляю?

Не так. Создаёте на старте, например, 20 коннектов. Заносите их в очередь. Когда игроку надо обратиться к базе, он берет свободный коннект из пула, работает с базой, возвращает коннект обратно. Таким образом получится, что даже если у вас 20 игрокам одномоментно понадобиться коннект к базе, они все его сразу получат. Мне, например, на 1000 онлайна хватало 32 коннекта.
Разработчик theFisherOnline - там, где клюёт
Разработчик Atom Fishing II - Первая 3D MMO про рыбалку
Разработчик Atom Fishing - Рыбалка на поплавок, донку, нахлыст, блесну в постъядерный период.
Аватара пользователя
Woolf
Адепт
 
Сообщения: 7179
Зарегистрирован: 02 мар 2009, 16:59

Re: [Блог] Пытаюсь писать свой сервер

Сообщение [COAD] H00JAMBRA 11 янв 2018, 17:54

Спасибо большое за такие подробности!

Скажите а разве MySqlConnection не содержит в себе "авто-пулл"? Если добавив параметры (как ниже) не будет тоже что вы предлагаете в плане работы?
Синтаксис:
Используется csharp
_dbConnection = new MySqlConnection(

        "server="   + _dbHostName + ";" +
        "user="     + _dbUserName + ";" +
        "database=" + _dbName     + ";" +
        "password=" +_dbPassword  + "; " +
        "Pooling = true; " +
        "Min Pool Size = 10; " +
        "Max Pool Size = 100; " +
        "Connection Lifetime = 0"
);
 
Аватара пользователя
[COAD] H00JAMBRA
UNец
 
Сообщения: 46
Зарегистрирован: 09 июл 2017, 13:51

Re: [Блог] Пытаюсь писать свой сервер

Сообщение Woolf 11 янв 2018, 18:30

Это несколько не то, это для ускорения создания самого коннекта, актуально для сайтов и скриптов, когда создается сессия коннекта. У вас же коннект поддерживается постоянно, посему этот пул не работает. Вам лучше наоборот этот самый пул отключить, чтобы не тратить зря ресурсы. Кстати, спасибо, что обратили внимание, у меня на серваке этот параметр в дефолте, надо снизить, зачем зря ресурсы жрать.
Разработчик theFisherOnline - там, где клюёт
Разработчик Atom Fishing II - Первая 3D MMO про рыбалку
Разработчик Atom Fishing - Рыбалка на поплавок, донку, нахлыст, блесну в постъядерный период.
Аватара пользователя
Woolf
Адепт
 
Сообщения: 7179
Зарегистрирован: 02 мар 2009, 16:59


Вернуться в Сеть

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

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