У меня никогда не было особых причин беспокоиться о «порядке байтов» моих двоичных данных при работе над проектами Elixir. По большей части все в приложении будет внутренне непротиворечивым, и все, полученное из внешних источников, будет преобразовано в собственный порядок машины на несколько уровней абстракции ниже того, где я обычно работаю.
Это блаженное невежество подошло к концу, когда я обнаружил, что использую Elixir для создания пакетов, соответствующих протоколу одноранговой сети Биткойн.
Биткойн-протокол
Биткойн-протокол — это протокол на основе TCP, используемый биткойн-узлами для связи по одноранговой специальной сети.
Реальные спецификации протокола определены как все, что делает эталонный клиент, но это может быть трудно выделить из кода. К счастью, биткойн-вики поддерживает фантастическое техническое описание протокола.
Структуры, используемые в протоколе, представляют собой мешанину порядка следования байтов. Как объясняет вики, «почти все целые числа закодированы с прямым порядком байтов», но ожидается, что многие другие поля, такие как контрольные суммы, строки, сетевые адреса и порты, будут иметь обратный порядок байтов.
Структура net_addr
— отличный пример путаницы с порядком байтов. Ожидается, что поля time
и services
будут закодированы с прямым порядком байтов, но поля IPv6/4
и port
должны быть закодированы с обратным порядком байтов.
Как мы построим это с помощью Эликсира?
Первая попытка
Моей первой попыткой построить эту двоичную структуру net_addr
было создание функции net_addr
, которая принимает аргументы time
, services
, ip
и port
и возвращает двоичный файл окончательной структуры в правильном порядке смешанного порядка следования байтов.
def net_addr(time, services, ip, port) do
end
При ручном построении двоичных файлов Эликсир по умолчанию использует порядок байтов с обратным порядком байтов. Это означает, что мне нужно преобразовать time
и services
в порядок байтов с прямым порядком байтов, прежде чем добавлять их в окончательный двоичный файл.
Моей первой попыткой преобразования байтов было создание вспомогательной функции reverse/1
, которая брала бы двоичный файл, преобразовывала его в список байтов, используя :binary.bin_to_list
, переворачивала этот список байтов, преобразовывала его обратно в двоичный файл, используя :binary.list_to_bin
, и возвращала результат. :
def reverse(binary) do
binary
|> :binary.bin_to_list
|> Enum.reverse
|> :binary.list_to_bin
end
Прежде чем я смог передать time
и services
в reverse/1
, мне нужно было сначала преобразовать их в двоичные файлы. К счастью, это легко сделать с помощью бинарной специальной формы Эликсира.
Например, мы можем преобразовать time
в четырехбайтный (32
бит) двоичный код с прямым порядком байтов, а затем обратить его, чтобы создать соответствующее представление с прямым порядком байтов:
reverse(<<time::32>>)
Используя наш помощник, мы можем создать окончательный двоичный файл net_addr
:
<<
<<time::32>> |> reverse::binary,
<<services::64>> |> reverse::binary,
:binary.decode_unsigned(ip)::128,
port::16
>>
Это работает, но есть возможности для улучшения.
Более быстрая вторая попытка
Проведя некоторое исследование, я обнаружил этот набор тестов для нескольких различных методов реверсирования двоичного файла в Эликсире (спасибо Evadne Wu!).
Я понял, что могу значительно повысить производительность процесса построения пакета, заменив медленное решение на основе списка решением, использующим необязательный аргумент Endianness
для :binary.decode_unsigned/2
и :binary.encode_unsigned/2
:
def reverse(binary) do
binary
|> :binary.decode_unsigned(:little)
|> :binary.encode_unsigned(:big)
end
Хотя это было улучшение, я все еще не был доволен своим решением. Использование моей функции reverse/1
означало, что я должен был преобразовать свои числа в двоичный код, прежде чем инвертировать их и, в конечном итоге, объединить их в окончательный двоичный файл. Эта вложенная двоичная структура была неудобной и запутанной.
После запроса руководства в Твиттере аккаунт ElixirLang обратился с мудрым советом:
Использование больших и малых модификаторов
Модификаторы big
и little
— это бинарные модификаторы специальной формы, очень похожие на типы bitstring
и binary
. Их можно использовать для указания конечного порядка байтов при преобразовании значений integer
, float
, utf16
или utf32
в двоичный файл.
Например, мы можем заменить наши вызовы, меняющие местами двоичные файлы time
и services
в нашей окончательной двоичной конкатенации, просто добавив big
к конечному размеру каждого:
<<
time::32-little,
services::64-little,
:binary.decode_unsigned(ip)::128,
port::16
>>
Потрясающий! Это гораздо легче понять.
Хотя Elixir по умолчанию использует формат с прямым порядком байтов для созданных вручную двоичных файлов, это не помешает быть явным. Мы знаем, что наши ip
и port
должны быть закодированы с обратным порядком байтов, поэтому давайте пометим их так:
<<
time::32-little,
services::64-little,
:binary.decode_unsigned(ip)::128-big,
port::16-big
>>
Красивый.
Последние мысли
Я постоянно поражаюсь количеству, разнообразию и качеству инструментов, которые поставляются из коробки с Эликсиром и Эликсиром. Даже когда речь идет о такой нише, как низкоуровневая манипуляция с бинарными файлами, инструменты Elixir первоклассны.
Если вы хотите увидеть полные примеры кода преобразования порядка байтов, показанные в этой статье, ознакомьтесь с модулем BitcoinNetwork.Protocol.NetAddr
в моем новом проекте bitcoin_network
на Github.
Первоначально опубликовано на www.petecorey.com 19 марта 2018 г.