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 
iie: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:alub2:ab. - Listy, znajdują się między znakami 
liei mają postaćlCIĄG_DANYCHe. Przy czymCIĄG_DANYCHmoż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ę zabci liczby3. - Słowniki, znajdują się miedzy znakami 
diei mają postaćdSŁOWNIKe, gdzieSŁOWNIKto zbiór klucz i zaraz po niej występująca wartość przy czym klucze są posortowane leksykograficznie i też są kodowane. Czyli paraklucz1i wartośc5mają postać6:klucz1i5e. Jakbyśmy mieli do tego dodaćklucz2i 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.