7fbb26092f
Это первый настоящий кусок сервера. Пока он ничего не умеет, кроме того что умеет правильно: принимать байты от чужого человека и не дать ему ничего сделать с нами. Формат кадра — четыре байта магии RSX1, версия, id пакета, длина тела и само тело. Магия стоит в начале, чтобы порт мог сразу ответить, а не ждать объяснений. Главное здесь — границы. Сервер живёт тем, что вынимает числа из сокетов чужих людей, и разница между дешёвой проверкой и аллокацией — это разница между «противник стоил нам пары тактов» и «противник стоил нам кучи». - Длина тела проверяется до того, как под неё что-то зарезервировано. Единственное, на что полезна вредная длина, — заставить сервер выделить память. - Varint ограничен пятью байтами для int и десятью для long. Без этого десять байт 0xFF превращают пятибайтовое целое в бесконечный цикл. - Длина строки ограничена до того, как под неё читают байты. - Декодер держит не больше одного максимального кадра плюс неполный заголовок. Неполные данные и испорченные — это разные вещи, и раньше это не было разделено: конверт, разрезанный сетью посреди varint, выглядел как испорченный и рвал соединение. IncompleteException означает «остальное уже в пути», ProtocolException — «потока больше не будет». TCP режет кадры почти всегда, так что первое поведение было бы отказом от большинства соединений. Позиция упакована одним varint, но доли у осей не равные: три поля по 26 бит это 78 бит, они не влезают в 64. X и Z получают по 26 бит с шагом 1/32 блока, Y — оставшиеся 12 бит целыми блоками. Со смещением на 2048 не вышло бы: для диапазона 0..4095 нужен тринадцатый бит, и это ровно тот бит, по которому значение читается обратно отрицательным. Первый прогон тестов нашёл все три ошибки сразу, две из них — в упаковке позиции. Проверено 17 сценариев, включая «по одному байту», пятьдесят кадров за одно чтение, чужую магию и объявленное тело в два гигабайта. Собирается и тестируется одним JDK, без Gradle и без сети, как и всё остальное в проекте.