Kontynuujemy przygodę z ElTorrento – klientem BitTorrent napisanym w elixir. Dziś skoncentruje się na implementacji Bencode – kodowanie użyte w BitTorrent do przechowywania i przesyłania struktur danych. Nie jest to skomplikowane, ale jest podstawą jeżeli chcemy napisać klienta. Chociażby po to by przeczytać plik torrent. A więc nie ważne jakie kroki dalej będziemy mieć i co będziemy pisać, Bencode będzie nam potrzebny by ruszyć z tematem.
Moja implementacja Bencode nie będzie jedyną w elixir, ale stwierdziłem, że akurat tę część będę chciał napisać zamiast wykorzystać już istniejącą implementację. Jeżeli jednak absolutnie nie interesuję Cię ten fragment :) to dostępne są następujące implementacje:
Dużo jest tego, ale to dobrze, będzie łatwiej znaleźć rozwiązanie na problem jeżeli na ów natrafimy pisząc kod! :)
Co to jest Bencode?
Bencode to nic innego jak kodowanie znaków. Można by powiedzieć, że kodowanie w user readble format, który wpiera jedynie 4 typy danych: liczby, stringy, listy i mapy. To co jest wspólne pomiędzy typami kodowania to, to, że każde kodowanie zaczyna się od określonej litery i kończy na określonej literze, zaś wszystko co pomiędzy jest daną albo już z deserializowaną, albo daną zawierającą dane, które trzeba z deserializować.
Wyrózniamy następujące typy (wszystkie dane są ograniczone przez ASCII):
- Liczby, liczba znajduje się pomiędzy
i
ie
:iNUMe
. Liczby ujemne zawierają minus, 0 lub brak liczby to liczba 0. Dla przykładui-10e
,i10e
. - Ciągi, mają postać
LICZBA_ZNAKÓW:ciąg
– na przykład0:
lub1:a
lub2:ab
. - Listy, znajdują się między znakami
l
ie
i mają postaćlCIĄG_DANYCHe
. Przy czymCIĄG_DANYCH
może zawierać kolejne dane serializowane takie jak liczba czy nawet ciąg znaków. Problem polega na tym że nie ma tutaj żadnego rozdzielenie wartości, dla przykładul3:abci3ee
, to lista składająca się zabc
i liczby3
. - Słowniki, znajdują się miedzy znakami
d
ie
i mają postaćdSŁOWNIKe
, gdzieSŁOWNIK
to zbiór klucz i zaraz po niej występująca wartość przy czym klucze są posortowane leksykograficznie i też są kodowane. Czyli paraklucz1
i wartośc5
mają postać6:klucz1i5e
. Jakbyśmy mieli do tego dodaćklucz2
i zamknąć to w słowniku to:d6:klucz1i5e6:klucz24:teste
Ogólnie nie jest to ciężkie kodowanie i elixir się idealnie nadaje do implementacji dzięki pattern maching.
Implementacja
Można było do tematu podejść na dwa sposoby. Mógłbym stworzyć protokół i poszczególne implementacje. Mógłbym też to zamknąć w jednym module. Długo się zastanawiałem jaką opcję wybrać i w końcu wybrałem opcję drugą – jeden moduł. Protokół traktuję jako coś co można rozszerzać o nowe typy. Specyfikacja jednak nie zmieniła się od wielu lat więc raczej nie liczę na to, że to się zmieni w przyszłości.
Oczywiście protokół dałby czystszą implementację – w sensie, mały moduł robiący tylko jedną rzecz ale to by była sztuka dla sztuki i nic by pozytywnego nie wnosiła. Może też w unit testach było by przejrzyściej ale… jakoś naprawdę mi ten protokół tutaj nie pasował.
Zaczynamy wszystko o stworzenia pod-projektu w naszym projekcie głównym:
el_torrento/apps$ mix new bencode
Następnie w pliku el_torrento/apps/bencode/lib/bencode.ex
dodajemy dwie metody które będą nam potrzebne:
defmodule Bencode do @moduledoc """ Bencode (pronounced like B encode) is the encoding used by the peer-to-peer file sharing system BitTorrent for storing and transmitting loosely structured data. It supports four different types of values: * `byte strings`, * `integers`, * `lists`, and * `dictionnaries` (associative arrays) """ @doc """ Decode bencoded into Elixir data structures ## Examples iex> Bencode.decode("i1e") 1 iex> Bencode.decode("4:test") "test"" iex> Bencode.decode("li1e4:teste") [1, "test"] iex> Bencode.decode("d4:testi1ee") %{"test" => 1} """ def decode (input) do {:error, "not implemented"} end @doc """ Encode Elixir data structure into bencode ## Examples iex> Bencode.encode(1) "i1e" iex> Bencode.encode("test"") "4:test" iex> Bencode.encode([1, "test"]) "li1e4:teste" iex> Bencode.encode(%{"test" => 1}) "d4:testi1ee" """ def encode (input) do {:error, "not implemented"} end end
Na razie nic skomplikowanego, dwie metody, dwie zwracające not implemented. Dwie zawierające dziwne @doc
. Wytłumaczenie @doc
pozostawię sobie na kiedy indziej (pewnie przyszły tydzień lub dopiero za dwa tygodnie). To co możemy zrobić i to co jest proste i szybkie do zrealizowanie to encode
. Spróbujmy więc to zaimplementować.
def encode (input) do do_encode(input) end defp do_encode(input) when is_integer(input), do: "i#{input}e" defp do_encode(input) when is_binary(input), do: "#{byte_size(input)}:#{input}" defp do_encode([]]), do: "le" defp do_encode(input) when is_list(input) do input |> Enum.reduce("", fn(x, acc) -> acc <> encode(x) end) |> (&("l#{&1}e")).() end defp do_encode(input) when is_map(input) and map_size(input) == 0, do: "de" defp do_encode(input) when is_map(input) do input |> Map.keys |> Enum.reduce("", fn(x, acc) -> acc <> encode(x) <> encode(Map.get(input, x)) end) |> (&("d#{&1}e")).() end # do nothing if we can't handle it defp do_encode(_input) do end
Jak widać nie jest to rocket sience, trzeba jednak znać API. Moim zdaniem i tak tego optymalnie nie zrobiłem. Na pewno coś da się tutaj poprawić, ktoś coś? :)
Podsumowanie
Cały kod oczywiście znajduje się na github. W przyszłym tygodniu zajmiemy się decode jak i testowaniem tego kodu gdyż warto zapoznać i zaprzyjaźnić się ze wsparciem do testów jednostkowych w elixir. Cały kolejny kod będzie już zawierał testy normalnie, więc też by się przydało byśmy potrafili to czytać ze zrozumieniem :) Choć jak tak teraz myślę to może się okazać, że będzie zbyt dużo kodu i z testami się za tydzień nie wyrobię. Zobaczymy. Niezależnie od tego wciąż obowiązuje u mnie zasada – 1h tygodniowo. Więc i tak super, że tyle się udało co dzisiaj :)
To czego nie poruszyłem w ogóle to komponenty BitTorrent i tym pewnie zajmę się też jakoś osobnym postem. Komponenty – czyli wszystko to co, co powinno zostać zaimplementowane :)
Co jeżeli ciąg znaków zawiera literę “i”, która może być też informacją o rozpoczęciu liczby?
Czytamy aż do końca i szukamy e, ale zastanawiają mnie takie łączenia jak ie de le. Dlatego też czasowo nie wyrobiłem sie z napisaniem decode :) na szczęście są oficjalne implementacje i edytory to będę weryfikował
Mamy info jak długi jest ciąg znaków, więc nie powinno to być problemem :)
ooo nowy punkt widzenia :) dzięki!
A dlaczego wrapujesz funkcję do_encode funkcją encode?
Mógłbym to zrobić publiczne z guards, ale wolałem to ukryć. Łatwiej się zarządza czymś co ma jedną metodę publiczną niż wiele. Jakbym później miał to zmieniać to wolałbym nie męczyć się z 8 metodami.
Zresztą teraz na przykład nawet myślę, że powinienem zwracać
{:ok, encoded}
zamiast po prostuencoded
. A więc będę miał też mniej roboty :)Dodatkowy powód był taki, że przy wstępnej implementacji myślałem o rekurencji a się okazało, że da się bez.
Elixir #28 – Bencode – Jakub Gutkowski
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl
[…] Elixir #28 – Bencode […]
Comments are closed.