• Highscore-server i Erlang

    Sa nu ar det antligen dags att borja knacka lite kod.

    I detta exempel kommer vi ignorera OTP helt och hallet och bara skriva ren Erlang, for enkelhetens skull. Att anvanda OTP for sin server kommer i senare artikel.

    Servern

    Forst skriver vi en liten server i Erlang. Nedanfor beskriver jag logiken som servern kor:
    1. Server skapar 10 stycket processer som alla lystnar pa port 2000 samtidigt (Ja detta gar alldeles utmarkt i Erlang, nar en klient ansluter far en godtycklig process anslutningen).
    2. Nar en klient kopplar upp sig mot denna port sa skapas en ny port dar kommunikationen fortsatter (detta port nummer valjs automatiskt av operativsystemen). En del nyborjare inom natverksprogrammering tror att all trafik gar genom port 2000 bara for att server lyssnar pa den porten initialt. Detta ar bara sant for UDP, men inte for TCP som skapar en ny port for varje klient.
    3. Nar en klient har kopplat upp sig sa skapar den process som tar emot klienten en ny lyssnar-process (detta ar for att det alltid skall finnas ett minst 10 processer for att hantera nya klienter.
    4. Nu borjar processen ta emot data fran klienten tills porten stangs.


    Notera att lyssnar-processen borjar som en server-process som lyssnar pa nya klienten, men sedan "muterar" till att ta hand om endast en klient, detta ar lite annorlunda fran vad man ar van vid men garanterar att ingen serverprocess kors hela tiden utan nya skapas varje gang en klient kopplar upp sig. Detta ar bra da Erlang-system mar bast av att ha sa kortlivade processer som mojligt. Processer som kor lange tenderar till att ha mer fel i sig.

    Har ar hela koden for Erlang-servern. Kopera eller skriv av (jag rekommendar det senare) in i en fil som heter highscore_srv.erl. Heter inte filen exakt detta kommer koden inte fungera.

    Vi kommer efter detta ga igenom stycke for stycke och forklara syntax samt vad som hander.

    Kod:
    -module(highscore_srv).
    
    -define(NR_LISTENERS, 10).
    -define(PORTNR, 2000).
    -define(TCP_OPTS, [binary, {packet, raw}, {active, false},
        {reuseaddr, true}]).
    
    % External exports
    -export([
        start/0
        ]).
    
    % Internal exports
    -export([
        listener_loop/1,
        handle_client_loop/1
        ]).
    
    start() ->
        {ok, LSocket} = gen_tcp:listen(?PORTNR, ?TCP_OPTS),
        spawn_listeners(LSocket, ?NR_LISTENERS).
    
    spawn_listeners(_LSocket, 0) ->
        done;
    
    spawn_listeners(LSocket, Nr) ->
        spawn_listener(LSocket),
        spawn_listeners(LSocket, Nr - 1). 
    
    spawn_listener(LSocket) ->
        spawn(?MODULE, listener_loop, [LSocket]).
    
    listener_loop(LSocket) ->
        case gen_tcp:accept(LSocket) of
            {ok, ClientSocket} ->
                spawn_listener(LSocket),
                ?MODULE:handle_client_loop(ClientSocket);
            {error, Reason} ->
                error_logger:error_report({gen_tcp, accept_error, Reason}),
                spawn_listener(LSocket)
        end.
    
    handle_client_loop(Socket) ->
        {ok, HeaderSize} = wait_for_header(Socket),
        io:format("got_header: ~p.~n", [HeaderSize]),
        {ok, Data} = wait_for_data(Socket, HeaderSize),
        io:format("got_data: ~p.~n", [Data]),
        ?MODULE:handle_client_loop(Socket).
    
    wait_for_header(Socket) ->
        case gen_tcp:recv(Socket, 2) of
            {ok, <<HeaderSize:16/integer-unsigned>>} ->
                {ok, HeaderSize};
            {error, closed} ->
                handle_client_socket_closed()
        end.
    
    wait_for_data(Socket, HeaderSize) ->
        case gen_tcp:recv(Socket, HeaderSize) of
            {ok, <<Data/binary>>} ->
                {ok, Data};
            {error, closed} ->
                handle_client_socket_closed()
        end.
    
    handle_client_socket_closed() ->
        io:format("Client disconnected.~n", []),
        exit(normal).

    Kod:
    -module(highscore_srv).
    All kod i Erlang ar strukturerat i moduler, i regel maste filen heta samma namn som modulen (det finns specialfall da detta kan kringgas, men det ar fulkod och sadant sysslar inte vi med).
    I borjar av filen maste man specifiera modulens namn, detta gor man specifierar man genom att skriva -module(<namn>).
    Tips: Du kan faktiskt specifiera vad som helst, exempelvis -anything(what_ever). Detta gor det latt att hitta pa egna specifikationer. Nagra av dessa anvander Erlang eller doxygen, exempelvis kan du ange forfattaren av koden via -author('christian@example.com').
    Kod:
    -define(NR_LISTENERS, 10).
    -define(PORTNR, 2000).
    -define(TCP_OPTS, [binary, {packet, raw}, {active, false}, 
        {reuseaddr, true}]).
    Defines ar macron och fungerar som i andra sprak. Ett macro anropar du genom fragetecknet, exempelvis ?NR_LISTENERS. Da ersatts ?NR_LISTENERS med 10.

    Kod:
    % External exports
    -export([
        start/0
        ]).
    Om du vill anropa en funktion i en modul utifran, maste du exportera den. Detta gor du genom att specifiera en lista pa detta satt:
    -export[funktion1/argument, funktion2/argument]).

    Vart att notera ar att du anger funktionen samt det antal argument som funktionen tar. Detta maste du gora for att en funktion med samma namn, men som har olika antal argument behandlas som tva helt separate funktioner i Erlang.
    Du kan ha alla funktioner i en enda stor lista, eller ha flera -export specifikationer.
    Tips: Antal argument namns med det fina ordet "arity" i Erlang, sa nar Erang-kodare namner ordet "arity" vet du att dom referar till antal argument en funktion tar.
    Kod:
    % Internal exports
    -export([
        listener_loop/1,
        handle_client_loop/1
        ]).
    Sa detta ar lite underligt, har exporterar vi funktioner som egentligen ar interna. Nar vi skapar en ny process i Erlang, maste funktionen som processen skall kora vara exporterad. Aven for att kunna upgradera kod under korning, maste funktion som processen kor vara exporterad. Detta ar anledningar varfor vi ibland vill exportera aven interna funktioner.

    Kod:
    start() ->
        {ok, LSocket} = gen_tcp:listen(?PORTNR, ?TCP_OPTS),
        spawn_listeners(LSocket, ?NR_LISTENERS).
    Denna funktion startar sjalva servern.
    Vad som hander ar att den skapar en ny tcp port pa nummer 2000 och sedan skapar 10 st processer som lyssnar pa denna port.

    Notera att Erlang syntax ar lite annorlunda vad man ar van vid i C, C++, Java, Python etc.
    En funktion skrivs helt enkelt bara ratt i modulen:

    funktions_namn(Argument1, Argument2) ->

    Funktioner och matching inom satser, som if och case paborjas med minus och hogerpil ->
    En variabel i Erlang maste borja med stor bokstav.
    Varje sats/funktion slutar med punkt .
    Flera satser och funktioner kan delas upp med semikolon ;

    Skall man ha flera rader kod efter varandra maste men avgransa dessa med komma ,

    Hello = 3,
    Foobar = 5,

    Notera aven att den sista raden i en funktin ar det varde som returneras, det finns igen "return" nyckelord utan du ar begransad nar du kan returnera.
    Exempelvis denna funktion returnerar alltid atomen foobar (ifall inte funktionerna som kallas hinner krasha processen innan):
    Kod:
    my_function() ->
         do_something(),
         do_something_else(),
         foobar.
    Har man en case-sats i en funktion man kan returna olika varden
    Kod:
    check_value(Value) ->
             case SomeInteger of
                     1 -> 
                             {ok, value_is_one};
                     2 -> 
                             {ok, value_is_two};
                     TooHigh ->
                             {error, value_to_high}
         end.
    Ifall argumentet till funktion ar 1, kommer den forsta matching i case-satsen matcha och funktionen returernar {ok, value_is_one}, om man skickar in 10 i funktionen kommer den returna {error, value_to_high}.

    Notera att dom tva forsta matcherna i case-satsen begransas med semikolon, medans den sista matchningen inte avslutas med nagonting, den sista raden som avslutar case-satsen avslutas med punk. Detta ar lite markligt, men man vanjer sig

    Tips: gen_tcp ar en inbyggd modul som foljer med Erlang, vi behover inte importera moduler utan kan anvanda dom direkt genom att skriva modulnamn:funktion(Argument, ...)
    Man kan dock importera moduler med det rekommendar jag inte da det blir svart att veta vart funktionerna som man kallar pa finns.
    Exempelvis kunde man skriva:
    -import(gen_tcp).
    For att sedan bara anropa:
    listen(?PORTNR, ?TCP_OPTS),
    Kod:
    spawn_listeners(_LSocket, 0) ->
        done;
    
    spawn_listeners(LSocket, Nr) ->
        spawn_listener(LSocket),
        spawn_listeners(LSocket, Nr - 1).
    Detta funktion anvands for att skapa en X antal lyssnar-processer, eftersom for-loopar inte finns, skapar man en funktion med tva olika matchningar.
    Den forsta matchar nar andra argumentet ar 0 och returerar done.
    Den andra matchar nar andra argumentat inte ar noll, en process skapas sedan kallar den pa sig sjalv med Nr minus 1.

    Detta betyder att funktionern kallar pa sig sjalv anda tills numret blir 0.

    Tips: Om man skickar in ett negativt tal kommer denna process skapa nya processer for evigt tills minet tar slut. Detta kan man fixa genom att lagga till en till matching for funktionen med en "guard":
    Kod:
    spawn_listeners(_LSocket, Nr) when Nr < 0 -> 
        {error, number_must_be_positive};
    Underscore kallas aven "I don't care variable". Om man har en variabel som inte anvands kommer Erlang varna for oanvand variabel, detta kan man tysta genom att lagga till underscore i variabelnamnet.
    Kod:
    spawn_listener(LSocket) ->
        spawn(?MODULE, listener_loop, [LSocket]).
    Denna funtion skapar en ny Erlang process, detta gors genom att kalla pa den inbyggda funktionen spawn.
    Funktionen spawn tar 3 argument, modul, funktion samt en lista med argument.
    ?MODULE ar ett inbyggt makro och ar alltid samma atom som den nuvarande modulen, dvs i varat fall highscore_srv.

    Kod:
    listener_loop(LSocket) ->
        case gen_tcp:accept(LSocket) of
            {ok, ClientSocket} ->
                spawn_listener(LSocket),
                ?MODULE:handle_client_loop(ClientSocket);
            {error, Reason} ->
                error_logger:error_report({gen_tcp, accept_error, Reason}),
                spawn_listener(LSocket)
        end.
    Denna funktion kommer dom 10 lyssnar-processerna som skapas koras.
    Vi kallar pa gen_tcp:accept som blockerar anda tills en ny klient ansluter.
    Nar detta hander far vi en antigen en ny socket {ok, Socket} eller nagot fel {error, Reason}.
    Om allting gar bra, skapar vi en ny lyssnar processes som borjar lyssna pa den socket som ar bunden till port 2000. Samt att vi gar in i den loopande funktionen handle_client_loop.
    Om nagot gar fel sa skapar vi en ny lyssnar funktion, eftersom vi inte gor nagot mer har kommer vi na end-blocket i var case-sats och processen kommer avslutas normalt. Vi vill inte fortsatta kora denna process eftersom nagonting gick fel, men vi loggar felet till Erlangs inbyggda logsystem.

    Kod:
    handle_client_loop(Socket) ->
        {ok, HeaderSize} = wait_for_header(Socket),
        io:format("got_header: ~p.~n", [HeaderSize]),
        {ok, Data} = wait_for_data(Socket, HeaderSize),
        io:format("got_data: ~p.~n", [Data]),
        ?MODULE:handle_client_loop(Socket).
    Denna funktion borjar processen kora nar en klient har anslutit.
    Funktionen vantar pa en header pa 2 byte och skriver ut storleken pa denna till skarmen.
    Nar man fatt headern sa hamtar man lika mycket data som headern angett och skriver ut denna pa skarmen.
    Sedan kallar den pa sig sjalv for att ta emot nasta omgang med data fran klienten i all evighet tills dess att klienten stanger ned.
    Tips: Notera att vi kallar pa oss sjavla genom att ange module:funktion(Argument) istallet for bara funktion(Argument). Nar aven anger modulen, sa kommer Erlang kolla om den ny version av koden finns, varje gang funtionen kors.
    Detta innebar att om vi har en eller flera anslutingar igang, kan vi uppgradera koden som hanterar anslutningarna utan att behova avbryta for klienterna. Detta kallas for "hot code upgrade" i Erlang.
    Kod:
    wait_for_header(Socket) ->
        case gen_tcp:recv(Socket, 2) of
            {ok, <<HeaderSize:16/integer-unsigned>>} ->
                {ok, HeaderSize};
            {error, closed} ->
                handle_client_socket_closed()
        end.
    Denna funktion vantar pa att tva 2 bytes skall skickas, dessa tva byte sparas i variabeln HeaderSize.
    Vi andvander Erlangs bit-syntax for att hamta ut denna data, vi anger att vi forvantar oss 16 bits i form av en unsigned integer. Unsigned betyder att det ar ett positivt heltal.
    Ifall klienten stangar ned i detta steg sa anropas funktionen handle_client_socket_closed.

    Tips: Notera att vi inte bryr oss om andra fel an just nedstangning fran klient-sidan. Ifall det skulle ske nagon annat fel, exemplevis nagot med natverket och gen_tcp_recv skulle returna nagot som inte matcher, exemplevis {error, unknown_error}, da kommer processen krasha. Erlang loggar dock automatiskt krasher i log-systemet och man kan ga in och undersoka stacktrace dar.
    Kod:
    wait_for_data(Socket, HeaderSize) ->
        case gen_tcp:recv(Socket, HeaderSize) of
            {ok, <<Data/binary>>} ->
                {ok, Data};
            {error, closed} ->
                handle_client_socket_closed()
        end.
    Denna funktion tar emot ett X antal byte (lika manga bytes som anges av argumentet HeaderSize) och returnerar denna data som en binar.
    Ifall klienten stangar ned i detta steg sa anropas funktionen handle_client_socket_closed.

    Tips: Notera att vi inte ar speciellt sakra i denna kod. I detta fall skulle det vara enkelt att overbelasta severn med processer som gar fatt en header pa exempelvis 10 bytes, men klienten skickar mindre.
    Vad man brukar gora at ett lagga en timeout pa gen_tcp:recv-funktionen
    Exempelvis:
    case gen_tcp:recv(Socket, HeaderSize, 2000)
    Da kommer funktionen returna {error, timeout} om inte tillrackligt manga bytes togs emot inom 2 sekunder.
    Kod:
    handle_client_socket_closed() ->
        io:format("Client disconnected.~n", []),
        exit(normal).
    Detta ar en liten hjalpfunktion som skriver ut pa skarmen nar en klient stanger ned sin sida av uppkopplingen.
    Eftersom dessa nedstangingar inte ar egentliga fel sa avslutar vi processen med exit och anger atomen normal som anledning. Detta gor man for att Erlang skall uppfatta att processen avslutades korrekt.
    Tips: I OTP-biblioteken sa finns det supervisors som automatiskt startar om processern om dessa inte avslutats med atomen normal som anledning. Nar man kor ren Erlang-kod sa spellar det mindre roll vad man anger som argument till exit-funktionen.
    Klienten

    Har ar python klienten, eftersom detta ar en Erlang/server tutorial sa kommer jag inte forklara steg for steg denna kod. Har ni fragor sa kan ni lamna en kommentar.
    Men kort och gott sa valjer klienten en godtyckling text-strang, skickar ivag en 2 byte header med storleken pa texten samt skickar ivag sjalva texten for att sedan avsluta.

    Kod:
    #!/usr/bin/python
    import random
    import socket
    import struct
    
    
    class HighscoreClient:
    
        PORT = 2000
        HOST = "localhost"
        HEADER_SIZE = 2
    
        def __init__(self):
            self.connect()
    
        def connect(self):
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.connect((self.HOST, self.PORT))
    
        def sendRandomString(self):
            random_str = random.choice(["foo", "bar", "hello" "world"])
            print("Sending data %s" % random_str)
            self.sendHeader(len(random_str))
            self.sendData(random_str)
    
        def sendHeader(self, size):
            data = struct.pack('H', socket.htons(size))
            self.socket.send(data)
    
        def sendData(self, random_str):
            self.socket.send(random_str)
    
    
    if __name__ == '__main__':
        highscoreClient = HighscoreClient()
        highscoreClient.sendRandomString()
    Korning

    Om du inte installerat Erlang annu, sa ga till www.erlang.org och gor detta.
    Om du kors Windows sa sok efter erl.exe och lagg till mappen detta program finns i in din path.

    Via en terminal (dos-promt eller powershell pa windows) sa ga till samma katalog dar du sparade filen och kor:
    erl

    Da bor du se nagot i stll med:
    Erlang R15B (erts-5.9) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]
    Eshell V5.9 (abort with ^G)
    1>

    Du skriver du bara detta for att kompilera Erlang-koden:
    c(highscore_srv).

    Tips: c star for compile.
    Du bor se nagot i stil med:
    2> c(highscore_srv).
    {ok,highscore_srv}

    Nu kan du starta servern genom att kalla pa start-funktionen i var highscore_srv modul:
    2> highscore_srv:start().
    done

    Testa nu att kora python-koden i ett nytt fonster for att ansluta:
    python client.py
    Sending data helloworld

    Om du kollar i fonstret dar Erlang kors sa borde du se nagot i stil med:
    got_header: 10.
    got_data: <<"helloworld">>.
    Client disconnected.
    Om detta inte fungerar, eller om du hittar nagot fel, lamna garna en kommentar.
    Nasta artikel
    Nu har vi fatt grundlaggade kommunikation igang mellan servern och klienten, i nasta artikel ar det tags att borja spara lite highscore.