что значит custom в расте
Rust в деталях: пишем масштабируемый чат с нуля, часть 1
Часть 1: Реализуем WebSocket. Введение.
В этом цикле статей мы рассмотрим процесс создания масштабируемого чата, который будет работать в реальном времени.
Целью данного обзора является пошаговое изучение основ быстро набирающего популярность языка программирования Rust на практике, с попутным охватом системных интерфейсов.
В первой части мы рассмотрим начальную настройку окружения и реализацию простейшего WebSocket-сервера. Чтобы понять технические детали статьи вам не потребуется опыта работы с языком Rust, хотя знание основ системных API (POSIX) и C/C++ лишним не будет. Прежде чем начинать чтение, заготовьте немного времени (и кофе) — статья описывает все максимально подробно и поэтому довольно длинная.
1 Rust — причина выбора
Я заинтересовался языком программирования Rust из-за давнего увлечения системным программированием, которое дело хоть и занимательное, но и весьма сложное — все потому что и новичков, и опытных разработчиков поджидает большое количество совершенно неочевидных моментов и каверзных проблем.
И, пожалуй, наиболее сложной проблемой тут можно назвать безопасную работу с памятью. Именно некорректная работа с памятью является причиной множества багов: переполнения буфера, утечки памяти, двойных освобождений памяти, висячих ссылок, разыменований указателей на уже освобожденную память, и т.п. И подобные ошибки порой влекут за собой серьезные проблемы в безопасности — например, причиной не так давно нашумевшего бага в OpenSSL, Heartbleed, является ни что иное как небрежное обращение с памятью. И это только верхушка айсберга — никому неизвестно, сколько подобных брешей таится в программном обеспечении которым мы пользуемся ежедневно.
То есть, на уровне языка нет обязательного условия применять подобные практики — вместо этого считается, что “хорошие разработчики” их всегда используют сами и никогда не делают ошибок. Однако я же считаю, что наличие подобных критических проблем в коде никак не связано с уровнем разработчиков, потому что люди не могут досконально проверять вручную большие объемы кода — это задача компьютера. В какой-то степени здесь помогают инструменты статического анализа — но, опять же, их используют далеко не все и не всегда.
У языка Rust другой подход к проблеме — можно сказать, золотая середина — автоматическое освобождение памяти и ресурсов без дополнительного потребления памяти или процессорного времени и без необходимости самостоятельного отслеживания каждого шага. Достигается это за счет применения концепций владения и заимствования.
В основе языка лежит утверждение, что у каждого значения может быть исключительно один владелец — то есть, может существовать только одна изменяемая переменная указывающая на определенную область памяти:
У такого подхода есть интересные последствия: поскольку значение связано исключительно с одной переменной, ресурсы связанные с этим значением (память, файловые дескрипторы, сокеты, и т.п.) автоматически освобождаются при выходе переменной из области видимости (которая задается блоками кода внутри фигурных скобок, < и >).
Такие искусственные ограничения могут выглядеть ненужными и излишне переусложненными, но если хорошо подумать, то, по большому счету, это и есть “киллер-фича” Rust, которая появилась исключительно из практических соображений. Именно такой подход позволяет Rust выглядеть языком высокого уровня при сохранении эффективности низкоуровневого кода, написанного на C/C++.
2 Цели
Я предпочитаю изучать новые языки и концепции разрабатывая относительно простые проекты с применением в реальном мире. Таким образом, возможности языка изучаются именно тогда, когда они становятся нужными. В качестве проекта для изучения Rust’а я выбрал сервис анонимных чатов наподобие Chat Roulette и многих других. На мой взгляд, это подходящий выбор по той причине, что чаты, как правило, требовательны к низкому времени отклика от сервера и подразумевают наличие большого количества одновременных подключений. Мы будем рассчитывать на несколько тысяч — так мы сможем посмотреть на потребление памяти и производительность программ написанных на Rust’е в реальном окружении.
Конечным результатом должен стать бинарный файл программы со скриптами для развертывания нашего сервера на различных облачных хостингах.
Но прежде чем мы начнем писать код, нужно сделать небольшое отступление для пояснения некоторых моментов с вводом-выводом, так как правильная работа с ним — ключевой момент при разработке сетевых сервисов.
3 Варианты работы с вводом-выводом
Для выполнения поставленных задач нашему сервису необходимо отправлять и получать данные через сетевые сокеты.
На первый взгляд, задача простая, но на самом деле существует множество возможных способов ее решения различной сложности и различной эффективности. Основное различие между ними кроется в подходе к блокировкам: стандартной практикой тут является прекращение работы процессора во время ожидания поступления новых данных в сокет.
Поэтому смасштабировать многопоточную программу до большого количества подключений может быть довольно сложно, и в нашем случае это вообще вряд ли разумно — ведь мы планируем несколько тысяч одновременно подключенных пользователей. В конце концов, надо быть готовыми к Хабраэффекту!
4 Цикл обработки событий
Оба этих API устроены довольно похожим образом, и общая идея проста: мы, вместо того чтобы ждать, когда в сокеты через сеть поступят новые данные, просим сокеты оповещать нас о пришедших байтах.
Оповещения в форме событий поступают в общий цикл, который в данном случае и выступает блокировщиком. То есть, вместо того, чтобы постоянно проверять тысячи сокетов на наличие в них новых данных, мы просто ждем, когда сокеты сами нам об этом сообщат — и разница довольно существенна, поскольку довольно часто подключенные пользователи находятся в режиме ожидания, ничего не отправляя и не получая. Особенно это характерно для приложений, использующих WebSocket. К тому же, используя асинхронный ввод-вывод мы практически не имеем накладных расходов — все, что требуется хранить в памяти — это файловый дескриптор сокета и состояние клиента (в случае чата это несколько сотен байт на одно подключение).
Любопытной особенностью такого подхода является возможность использовать асинхронный ввод-вывод не только для сетевых подключений, но и, например, для чтения файлов с диска — цикл обработки событий принимает любые типы файловых дескрипторов (а сокеты в мире *NIX именно ими и являются).
5 Начинаем проект
Дальнейший текст подразумевает, что у вас уже установлен Rust. Если еще нет — то следуйте документации на официальном сайте.
Именно это нам и нужно в данный момент, так что давайте попробуем открыть терминал и набрать такую команду:
В результате у нас появятся два файла:
Cargo.toml содержит описание и ссылки на зависимости проекта (схоже с package.json в JavaScript).
src/main.rs — главный исходный файл и точка входа в нашу программу.
6 Обработка событий в Rust
Перейдем от теории к практике. Давайте попробуем запустить простейший цикл событий, который будет ожидать появления новых сообщений. Для этого нам не нужно вручную подключать различные системные API — достаточно воспользоваться уже существующей библиотекой для работы с асинхронным I/O под названием “Metal IO” или mio.
Как вы помните, зависимостями занимается программа Cargo. Она загружает библиотеки из репозитория crates.io, но помимо того позволяет получать их и из Git-репозиториев напрямую — такая возможность бывает полезна в тех случаях, когда нам нужно использовать последнюю версию библиотеки, которая еще не была загружена в репозиторий пакетов.
На момент написания статьи у mio в репозитории доступна только уже устаревшая версия 0.3 — в находящейся в разработке версии 0.4 появилось много полезных изменений, к тому же, несовместимых со старыми версиями. Поэтому подключим ее напрямую через GitHub, добавив такие строки в Cargo.toml :
После того, как мы определили зависимость в описании проекта, добавим импорт в main.rs :
Хотя в языке Rust нет поддержки “традиционного” объектно-ориентированного программирования, структуры во многом аналогичны классам, и они похожим на классический ООП образом могут имплементировать интерфейсы, которые регламентируются в языке через типажи.
Давайте определим новую структуру:
И реализуем типаж Handler для нее:
Теперь запустим цикл событий:
Здесь нам впервые встречается применение заимствований (borrows): обратите внимание на &mut на последней строчке. Это обозначает, что мы временно передаем “право владения” значением, связывая его с другой переменной с возможностью изменения (mutation) данных.
Проще говоря, можно представить себе принцип работы заимствований следующим образом (псевдокод):
Вышеприведенный код эквивалентен этому:
На каждую область видимости у переменной может быть только одно изменяемое заимствование (mutable borrow), и даже владелец значения не может его читать или изменять до тех пор, пока заимствование не выйдет из области видимости.
Ничего страшного, если такое описание показалось вам недостаточно понятным — рано или поздно наступит интуитивное понимание, поскольку заимствования в Rust используются повсеместно, и по ходу чтения статьи вы найдете больше практических примеров.
Теперь давайте вернемся к нашему проекту. Запускайте команду “ cargo run ” и Cargo скачает все необходимые зависимости, скомпилирует программу (с некоторыми предупреждениями, которые мы можем пока проигнорировать), и запустит ее.
В итоге мы увидим окно терминала с мигающим курсором. Не очень интересный результат, но он, по крайней мере, показывает, что программа выполняется корректно — мы успешно запустили цикл событий, хоть он пока ничего полезного и не делает. Давайте это положение дел исправим.
Чтобы прервать выполнение программы, воспользуйтесь комбинацией клавиш Ctrl+C.
7 TCP-сервер
Давайте рассмотрим его построчно.
Распарсим строку «0.0.0.0:10000» в структуру, описывающую адрес и привяжем к этому адресу сокет:
Создаем слушающий сокет и запускаем прослушивание:
Вы также могли заметить, что мы почти везде вызываем unwrap для результата выполнения функции — это паттерн обработки ошибок в Rust, и мы скоро вернемся к этой теме.
Теперь давайте добавим созданный сокет в цикл событий:
Вызов register посложнее — функция принимает следующие аргументы:
Разница между двумя подходами, названия которых позаимствованы из электроники, заключается в моменте, когда сокет оповещает нас о произошедшем событии — например, при событии поступления данных (т.е. если мы подписаны на событие readable() ) в случае срабатывания по уровню мы получаем оповещение, если в буфере сокета есть доступные для чтения данные. В случае же сигнала по фронту оповещение мы получим в тот момент, когда в сокет поступят новые данные — т.е., если при обработке события мы не прочитали все содержимое буфера, то мы не получим новых оповещений до тех пор, пока не поступят новые данные. Более подробное описание (на английском) есть в ответе на Stack Overflow.
8 Принимаем соединения
Все соединения по протоколу WebSocket начинаются с подтверждения установления связи (т.н. handshake) — специальной последовательности запросов и ответов, передаваемых по HTTP. Это означает, что прежде чем приступить к реализации ВебСокета мы должны научить наш сервер общаться по базовому протоколу, HTTP/1.1.
Заголовки запроса на соединение по протоколу WebSocket.
Но прежде чем мы начнем реализовать HTTP, нам необходимо написать код для установления соединений с клиентами и подписки на поступающие от них события.
Рассмотрим базовую реализацию:
Кода получилось много, поэтому давайте рассмотрим его детальнее — шаг за шагом.
В первую очередь нам нужно добавить состояние в серверную структуру WebSocketServer — она будет хранить серверный сокет и сокеты подключенных клиентов.
Для хранения клиентских сокетов используем структуру данных HashMap из стандартной библиотеки коллекций, std::collections — это стандартная реализация для хеш-таблиц (также известных как словари и ассоциативные массивы). В качестве ключа мы будем использовать уже знакомые нам токены, которые должны быть уникальными для каждого подключения.
Далее нам снова пригождается типаж Handler из библиотеки mio :
Слушающий сокет генерирует события типа readable в тот момент, когда новый клиент поступил в очередь ожидающих соединения. Но прежде, чем мы начнем соединяться, нам нужно убедиться в том, что источник события — именно слушающий сокет. Мы легко можем это проверить используя сопоставление с образцом:
Что это значит? Синтаксис match напоминает стандартную конструкцию switch из “традиционных” императивных языков программирования, но дает намного больше возможностей. Например, в Java конструкция switch ограничена определенным набором типов и работает только для чисел, строк, и перечислений enum. В Rust же match позволяет делать сопоставление практически для любого типа, включая множественные значения, структуры, и т.п. Помимо сопоставления match также позволяет захватывать содержимое или части образцов, схожим с регулярными выражениями образом.
В вышеприведенном примере мы сопоставляем токен с образцом Token(0) — как вы помните, он связан со слушающим сокетом. И чтобы наши намерения были более понятными при чтении кода, мы определили этот токен в виде константы SERVER_TOKEN :
Теперь, когда мы уверены, что имеем дело с серверным сокетом, мы можем установить соединение с клиентом:
Так что давайте распакуем возвращенный accept() ‘ом результат:
Продолжаем сопоставлять результаты с образцами:
Полученный сокет мы сохраняем в хеш-таблице, не забывая увеличить счетчик токенов:
Наконец, нам нужно подписаться на события от сокета, с которым мы только что установили соединение — давайте зарегистрируем его в цикле событий. Делается это точно таким же образом, как и с регистрацией серверного сокета, только теперь мы в качестве параметров предоставим другой токен, и, конечно же, другой сокет:
Ну и, наконец, из-за того, что наша структура WebSocketServer усложнилась, нам нужно поменять код регистрации сервера в цикле событий. Изменения достаточно простые и в основном касаются инициализации новой структуры:
9 Парсим HTTP
Теперь, когда мы установили соединение с клиентом, согласно протоколу нам нужно распарсить входящий HTTP-запрос и “переключить” (upgrade) соединение на протокол WebSocket.
Поскольку это довольно скучное занятие, мы не будем все это делать вручную — вместо этого воспользуемся библиотекой http-muncher для парсинга HTTP, добавив ее в список зависимостей. Библиотека адаптирует для Rust парсер HTTP из Node.js (он же по совместительству парсер в nginx), который позволяет обрабатывать запросы в потоковом режиме, что как раз будет очень полезным для TCP-соединений.
Давайте добавим зависимость в Cargo.toml :
Не будем рассматривать API библиотеки в деталях, и сразу перейдем к написанию парсера:
И еще нам нужно внести некоторые изменения в реализацию функции ready в структуре WebSocketServer :
Давайте опять попробуем рассмотреть новый код построчно.
Первым делом мы импортируем библиотеку и добавляем контролирующую структуру для парсера:
Однако, тут есть одна деталь: парсер HTTP имеет свое состояние, а это значит, что нам нужно будет создавать новый экземпляр структуры HttpParser для каждого нового клиента. Учитывая, что каждый клиент будет хранить у себя состояние парсера, давайте создадим новую структуру, описывающую отдельного клиента:
Так как теперь мы можем там же хранить и клиентский сокет, можно заменить определение HashMap на HashMap в структуре сервера.
Этой функции не нужно принимать никаких параметров — у нас уже есть вне необходимое состояние внутри самой структуры.
Теперь мы можем начать читать поступающие от клиента данные:
Вызов try_read может завершиться ошибкой, поэтому мы проводим сопоставление с образцом по типу Result :
Затем мы провереяем, остались ли еще байты для чтения в буфере TCP-сокета:
try_read возвращает результат Ok(None) в том случае, если мы прочитали все доступные данные, поступившие от клиента. Когда это происходит, мы прерываем бесконечный цикл и продолжаем ждать новых событий.
И, наконец, вот обработка случая, когда вызов try_read записал данные в наш буфер:
Здесь мы отправляем полученные данные парсеру и сразу же проверяем имеющиеся HTTP-заголовки на наличие запроса на “переключение” соединения в режим WebSocket (точнее говоря, мы ожидаем заголовок Connection: Upgrade ).
Есть еще пара деталей. Обратите внимание, что мы не используем ключевое слово return в явном виде — Rust позволяет автоматически возвращать последнее выражение функции в качестве ее результата.
И эта строчка требует пояснения:
Теперь, когда мы разобрались с клиентами, мы можем вернуться к серверному коду, в котором в обработчике ready мы вносим такие изменения:
В конце мы должны перерегистрировать клиента в цикле событий (из-за oneshot() ):
Вот и все — теперь мы знаем, когда клиент хочет установить соединение по протоколу WebSocket, и теперь мы можем подумать над тем, как отвечать на подобные запросы.
10 Подтверждение соединения
По сути, мы могли бы отправить в ответ такой простой набор заголовков:
В стандартной библиотеке Rust нет функций для работы с SHA-1 и base64, но все нужные библиотеки есть в репозитории crates.io, так что давайте их добавим в наш Cargo.toml :
Функция, которая генерирует ответный ключ довольно-таки простая:
Сам по себе этот код достаточно простой, но тут мы сталкиваемся с новой важной концепцией — совместным владением.
Правда, тут есть нюанс — значение, которое содержит Rc неизменяемое, и из-за ограничений компилятора мы не можем на него как-то повлиять. По сути, это всего лишь следствие правила Rust насчет изменяемости данных — можно иметь сколько угодно заимствований переменной, но изменять ее можно только если владелец один.
И тут опять наступает противоречие — ведь нам нужно добавлять новые заголовки в список, при том, что мы уверены, что мы изменяем эту переменную только в одном месте, так что формально правил Rust мы не нарушаем. Только компилятор с нами на этот счет не соглаится — при попытке изменить содержимое Rc произойдет ошибка компиляции.
Давайте рассмотрим эти строки из обработчика HttpParser :
Но так как мы не можем просто так отправлять данные в неблокирующие сокеты, нужно для начала попросить цикл событий о том, чтобы он нас оповестил о доступности сокета для записи. Сделать это просто — нужно поменять набор флагов EventSet на EventSet::writable() в момент перерегистрации сокета.
Помните эту строку?
Мы можем хранить набор интересующих нас событий в состоянии клиента — изменим структуру WebSocketClient :
Теперь изменим процедуру перерегистрации соответствующим образом:
Нам осталось только менять значение interest в нужных местах. Чтобы упростить этот процесс, давайте формализуем его с помощью состояний подключения:
Добавим переменную состояния в клиентскую структуру:
И добавим начальные значения новых переменных в конструктор:
Поменяем заглушку в блоке условия is_upgrade() на код смены состояния соединения:
Осталась самая малость — нам нужно собрать по частям и отправить ответную строку:
Давайте попробуем подключиться к нашему серверу. Откройте консоль разработки в вашем любимом браузере (нажав F12, например), и введите следующий код:
Похоже, что все работает — мы соединились с сервером!
Заключение
Наше увлекательное путешествие по возможностям и непривычным концепциям языка Rust подошло к концу, но мы затронули только самое начало — серия статей будет продолжена (разумеется, продолжения будут такими же длинными и скучными! :)). Нам нужно рассмотреть еще много других интересных вопросов: безопасные подключения по TLS, многопоточные циклы событий, нагрузочное тестирование и оптимизация, и, конечно, самое главное — нам еще нужно закончить реализацию протокола WebSocket и написать само приложение чата.
Но перед тем, как мы дойдем до приложения, нужно будет заняться небольшим рефакторингом и отделением библиотечного кода от кода приложения. Скорее всего, мы также рассмотрим публкацию своей библиотеки на crates.io.
Весь текущий код доступен на Гитхабе, вы можете форкнуть репозиторий и попробовать что-нибудь в нем поменять.
Чтобы следить за появлением следующих частей статьи предлагаю подписаться на меня в твиттере.
Заметки
[1] Стоит отметить, что Rust по сути использует умные указатели на уровне языка — идея заимствований во многом схожа с типами unique_ptr и shared_ptr из C++.
[3] Простые алгоритмы сборки мусора применять довольно легко, однако более сложные варианты вроде многопоточной сборки могут потребовать немалых усилий в реализации. Например, в языке Go многопоточная сборка мусора появилась только к версии 1.5, которая вышла почти спустя 3 года после первой.
[4] Вообще говоря многие реализации функций malloc() и free() имеют ту же проблему из-за фрагментации памяти.
[5] “Грейдон Хоар […] начал работу над новым языком программирования под названием Rust в 2006 г.” — InfoQ: “Interview On Rust”
[6] В странице man pthread_create(3) говорится о 2 МБ на 32-битной системе Linux.
[7] Для сравнения epoll с другими системными API рекомендую ознакомиться с публикацией “Comparing and Evaluating epoll, select, and poll Event Mechanisms”, Университет Ватерлоо, 2004 (англ.)
Выражаю признательность за помощь:
podust за иллюстрации и вычитку.
VgaCich за чтение черновиков и корректирование.
Чего не стоит делать в Rust, если начали играть в 2021 году
Rust – это необычный симулятор выживания, который привлек к себе внимание огромное количество геймеров. При этом новички часто думают, что в этом проекте нет ничего сложного, и уже с самого начала делают все то, что и в других играх с элементами выживания.
К сожалению, Rust не отличается особым гостеприимством по отношению к новым игрокам, поэтому стартовать бывает довольно сложно. Перед вами подборка главных ошибок, которые делают новички, решившие поиграть в Rust в 2021 году.
Одному будет тяжело
Rust – далеко не самая лучшая многопользовательская игра для одного человека. Здесь есть несколько этапов развития, и добраться до каждого из них можно только за счет продолжительного гринда. Если играть в команде со своими друзьями, то вы гораздо быстрее достигните цели, чем в одиночку.
Также стоит отметить, что 99% других игроков не дадут вам мирно существовать в виртуальном мире игры. Вам постоянно придется отбиваться от обезумивших «дикарей», которые захотят отобрать ваши вещи и ресурсы. Естественно, ни у одного новичка не получится защитить себя от оравы более опытных игроков, поэтому лучше изначально залетать в Rust хотя бы с парой друзей.
Никому нельзя верить
Этот пункт частично противоречит предыдущему, но при этом он еще более важен. Прежде всего вам стоит забыть о том, что взаимодействие с другими игроками в многопользовательских проектах – это норма. Rust вообще не та игра, где нужно объединяться с незнакомыми людьми, чтобы вместе получить больше лута или ресурсов. Здесь вы можете рассчитывать только на себя, и если начнете доверять первому встречному игроку, то очень скоро поймете, почему этого нельзя делать. Особенно это касается товарищей с хорошей экипировкой, которых вы встретите на своем пути.
Дело в том, что в Rust каждый играет сам за себя, а опытные игроки очень часто обманывают новичков самыми разными способами. Незнакомец, который предложит побегать с ним по виртуальному миру и при этом будет носить броню заметно лучше вашей, скорей всего грифер. Это такой игрок, который при первой же удобной возможности просто вас убьет и заберет все вещи. Так что, начиная играть в Rust, никому не доверяйте!
Курс юного строителя
Если вы вдруг не знали, то в Rust есть строительство, и здесь оно играет довольно важную роль. При этом данная механика имеет ряд особенностей, которые придется изучить в самом начале знакомства с игрой, иначе ваши архитектурные «шедевры» будут попросту разваливаться, а вы впустую потратите ценные ресурсы.
Прежде всего стоит отметить, что у каждого строительного блока есть мягкая и твердая сторона. Во время строительства блок всегда нужно устанавливать таким образом, чтобы твердая сторона находилась снаружи будущего здания. Если не соблюдать это правило и размещать материалы как попало, то вашу постройку сможет развалить первый попавшийся игрок, причем с помощью обычного топора или кирки. Согласитесь, будет не очень приятно наблюдать за тем, как несколько часов ваших трудов кто-то разбирает по кирпичикам за считаные минуты.
Все вещи в одном месте
Огромное количество игроков в Rust вообще не уделяют время крафту. Они считают, что гораздо проще украсть готовые предметы у других пользователей, чем стоять у станка и пытаться что-то сделать. Именно поэтому в этой игре противопоказано хранить все свои вещи в одном месте.
Ни в коем случае не размещайте абсолютно все запасы на единственной базе, да еще и в конкретном помещении. В таком случае после случайного налета кучки любителей халявы вы потеряете абсолютно все. Конечно, вряд ли у новичка хватит ресурсов, чтобы построить себе 4-5 домов и правильно распределить по ним ценные предметы, но хотя бы попробуйте сделать что-то подобное. Неплохим решением будет на территории одной базы построить несколько «нычек» и распределить по ним ресурсы и предметы.
Не забывайте про аптечки
Если вы решите, что аптечки вам не нужны и со своим крутым автоматом вы сможете одолеть кого угодно, то Rust очень быстро вас разочарует. Здесь очень просто погибнуть, и иногда вы даже не будете понимать, почему это вообще произошло. В результате игрок, у которого было полно аптечек, просто завалит вас рандомной палкой и заберет тот самый крутой автомат.
Поставьте себе домофон
Если же вы не можете сделать кодовый замок или уже поставили везде обычные двери, то не делайте ключ. Пускай доступ к зданию будет только у вас. Отсутствие ключа гарантировано защитит ваши владения, даже если вы внезапно погибнете.
Не используйте факел
Дело в том, что свет от факела моментально привлечет к вам внимание других игроков. Часть из них будет гриферами, которые быстро прибегут на ваш «сигнал» и просто убьют. На этом ваш многообещающий забег в Rust просто закончится и придется начинать все сначала. Первое время лучше бегайте без факела и пытайтесь ориентироваться на карте с помощью своего зрения.
Вы всегда в опасности
Многие новички ошибочно думают, что после того, как они построят себе укрытие и обзаведутся хоть какой-то экипировкой, можно просто расслабиться и наслаждаться игровым процессом Rust. Этот проект не об этом, вы всегда будете под прицелом у других игроков! Причем если у вас вдруг все слишком хорошо и на это обратят внимание остальные пользователи игровой сессии, то очень скоро вас ждет набег незваных гостей.
Перестрелка – не самая лучшая идея
Некоторые новички в Rust почему-то считают, что это экшен-шутер, в котором прямо-таки необходимо ввязываться в перестрелки и каждую минуту показывать, кто здесь круче. На самом деле проект про выживание, и я вам гарантирую, что ваша беготня с автоматом закончится очень быстро, если вы вдруг решите, что можете держать всю карту в страхе.
Вот такие советы мы решили дать новичкам, которые только надумали залететь в Rust! Делая все эти вещи, вы гарантировано проживете в виртуальном мире игры чуточку дольше и при этом гораздо лучше узнаете все тонкости проекта. Главное, не забывайте всегда быть начеку, здесь нет зоны комфорта.