Каркас протокола: кадрирование, буфер и рукопожатие
Это первый настоящий кусок сервера. Пока он ничего не умеет, кроме того что умеет правильно: принимать байты от чужого человека и не дать ему ничего сделать с нами. Формат кадра — четыре байта магии 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 и без сети, как и всё остальное в проекте.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# Builds rsx-protocol into dist\.
|
||||
#
|
||||
# Only a JDK is required, same as the loader: javac with --release 17, no Gradle
|
||||
# and no network. The protocol is deliberately dependency free so both ends can
|
||||
# carry it and so its tests run without a game, a server or a socket library.
|
||||
#
|
||||
# NOTE: ASCII on purpose. Windows PowerShell 5.1 reads a .ps1 with no byte order
|
||||
# mark using the system ANSI code page, which mangles UTF-8 and breaks the
|
||||
# parser. Keep script messages English.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Repo = Split-Path -Parent $PSScriptRoot
|
||||
if (-not $Repo) { $Repo = $PSCommandPath | Split-Path -Parent | Split-Path -Parent }
|
||||
$Dist = Join-Path $Repo 'dist'
|
||||
$Out = Join-Path $Repo 'build'
|
||||
|
||||
function Say($text) { Write-Host " $text" -ForegroundColor Cyan }
|
||||
|
||||
if (-not (Get-Command javac -ErrorAction SilentlyContinue)) {
|
||||
Write-Host ' A JDK (javac) is required.' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Dist, $Out | Out-Null
|
||||
|
||||
$mainOut = Join-Path $Out 'classes'
|
||||
Remove-Item $mainOut -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $mainOut | Out-Null
|
||||
|
||||
$mainFiles = @(Get-ChildItem (Join-Path $Repo 'src\main\java') -Recurse -Filter '*.java' |
|
||||
ForEach-Object { $_.FullName })
|
||||
if ($mainFiles.Count -eq 0) {
|
||||
Write-Host ' No sources found.' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -d $mainOut $mainFiles
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ' Protocol failed to compile.' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Say "protocol compiled ($($mainFiles.Count) files)"
|
||||
|
||||
$manifest = Join-Path $Out 'manifest.txt'
|
||||
@"
|
||||
Manifest-Version: 1.0
|
||||
Implementation-Title: RustNotMC protocol
|
||||
Implementation-Version: 0.1.0
|
||||
"@ | Set-Content -LiteralPath $manifest -Encoding ASCII
|
||||
|
||||
$jar = Join-Path $Dist 'rsx-protocol.jar'
|
||||
if (Test-Path -LiteralPath $jar) { Remove-Item -LiteralPath $jar -Force }
|
||||
& jar cfm $jar $manifest -C $mainOut .
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ' Could not package the jar.' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Say "rsx-protocol.jar ($((Get-Item $jar).Length) bytes)"
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' Done:' -ForegroundColor Green
|
||||
Write-Host " $jar"
|
||||
@@ -0,0 +1,42 @@
|
||||
# Compiles rsx-protocol plus its tests and runs them.
|
||||
#
|
||||
# ASCII only, for the same reason as build.ps1.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Repo = Split-Path -Parent $PSScriptRoot
|
||||
if (-not $Repo) { $Repo = $PSCommandPath | Split-Path -Parent | Split-Path -Parent }
|
||||
$Out = Join-Path $Repo 'build'
|
||||
|
||||
function Say($text) { Write-Host " $text" -ForegroundColor Cyan }
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Out | Out-Null
|
||||
|
||||
$mainOut = Join-Path $Out 'classes'
|
||||
Remove-Item $mainOut -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $mainOut | Out-Null
|
||||
|
||||
$mainFiles = @(Get-ChildItem (Join-Path $Repo 'src\main\java') -Recurse -Filter '*.java' |
|
||||
ForEach-Object { $_.FullName })
|
||||
& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -d $mainOut $mainFiles
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host ' Main failed.' -ForegroundColor Red; exit 1 }
|
||||
|
||||
$testOut = Join-Path $Out 'test-classes'
|
||||
Remove-Item $testOut -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $testOut | Out-Null
|
||||
|
||||
$testFiles = @(Get-ChildItem (Join-Path $Repo 'src\test\java') -Recurse -Filter '*.java' |
|
||||
ForEach-Object { $_.FullName })
|
||||
& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -cp $mainOut -d $testOut $testFiles
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host ' Test failed to compile.' -ForegroundColor Red; exit 1 }
|
||||
Say "compiled $($mainFiles.Count) main + $($testFiles.Count) test files"
|
||||
|
||||
& java -cp "$mainOut;$testOut" net.rustnotmc.protocol.ProtocolTest
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ''
|
||||
Write-Host ' PROTOCOL TESTS FAILED' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' Protocol tests passed.' -ForegroundColor Green
|
||||
Reference in New Issue
Block a user