У меня никогда не было особых причин беспокоиться о «порядке байтов» моих двоичных данных при работе над проектами 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 г.