Az ERLANG programozási nyelv

Hálózati alkalmazások

Hálózati alkalmazásokat Erlangban socketek segítségével lehet készíteni. A nyelv elrejti a nyers hálózati csomagokat a felhasználó elől, és egy felhasználóbarát API-t kínál a UDP és TCP protokollokhoz. Ennek használatát mutatják be a következő fejezetek.

User Datagram Protocol (UDP)

A UDP egy kapcsolat nélküli protokoll. A kommunikáció UDP csomagokkal (packet) történik, és a csomagok megérkezése nem garantált (elveszhetnek útközben), továbbá, mivel az egyes csomagok akár más-más útvonalon is eljuthatnak a célhoz, a csomagok megérkezésének sorrendje is eltérhet a küldési sorrendtől.

UDP-t alkalmaznak például olyan esetekben, ahol egy csomagot érdemes inkább eldobni, mint várni annak újraküldésére. Ilyen lehet például: video-streaming, voice-over-IP alkalmazások (pl. internetes telefon), illetve bizonyos online játékok.

Az Erlangban a gen_udp modul tartalmazza a UDP protokollal kapcsolatos függvényeket.

Egy egyszerű példa UDP socketek használatára:

  1. Nyissunk meg egy-egy UDP socketet egy-egy szabad porton mindkét hálózati végponton.
  2. Az egyik végpontról küldjük el a <<"Hello World">> üzenetet a másik végpont megfelelő portjára.
  3. Ezután küldjük el a "Hello World" üzenetet.
  4. A másik végpontra megérkezett üzeneteket az Erlang shellben a flush(). paranccsal nézhetjük meg.
  5. Ezután zárjuk le a socketeket mindkét végponton.

A kliensen a következőképpen nézhet ez ki:

1> {ok, Socket} = gen_udp:open(1235). {ok,#Port<0.203>} 2> gen_udp:send(Socket, {127,0,0,1}, 1234, <<"Hello World">>). ok 3> gen_udp:send(Socket, {127,0,0,1}, 1234, "Hello World"). ok 4> gen_udp:close(Socket). ok

Míg a szerveren:

1> {ok, Socket} = gen_udp:open(1234). {ok,#Port<0.576>} 2> flush(). Shell got {udp,#Port<0.576>,{127,0,0,1},1235,"Hello World"} Shell got {udp,#Port<0.576>,{127,0,0,1},1235,"Hello World"} ok 3> gen_udp:close(Socket). ok

Figyeljük meg, hogy mindkét üzenet listaként érkezett a szerverhez, pedig az egyiket binary-ként küldtük. Ez a viselkedés befolyásolható, ennek részletei később következnek.

Socketek létrehozása

UDP socketek létrehozásához a következő függvényhívások használhatók:

gen_udp:open(Port) gen_udp:open(Port, OptionList)

A Port paraméter határozza meg azt a portot, ami a sockethez lesz kötve. Az OptionList segítségével adhatunk meg opciókat, amelyekkel felülbírálhatjuk az alapértelmezett értékeket. A fontosabb opciók a következők:

Aktív és passzív mód
Aktív mód

Az {active, true} opcióval adható meg. Egy socket alapértelmezett esetben aktív módban jön létre. Minden beérkező üzenet továbbítódik a socketet birtokló processz részére Erlang üzenetként, a következő formában:

{udp, Socket, IP, PortNo, Packet}

A Socket a fogadó socket, az IP és a PortNo a küldő IP címe és portszáma, a Packet pedig magát az üzenetet tartalmazza.

Passzív mód

Az {active, false} opcióval adható meg. Passzív módban az üzeneteket a gen_udp:recv/2 és gen_udp:recv/3 függvények segítségével kell lekérdezni.

Hibrid mód

Az {active, once} opcióval adható meg. Ebben a módban az első üzenet automatikusan továbbítódik, az ezután következő üzeneteket viszont a recv függvénnyel kell lekérni.

Visszatérési érték

Socketek lezárása

gen_udp:close(Socket)

A visszatérési érték minden esetben egy ok atom.

Üzenetek küldése

gen_udp:send(Socket, Address, Port, Packet)

Üzenetek fogadása

Passzív mód esetén explicit módon le kell kérni a csomagot a sockettől.

Ez a következő függvényhívások segítségével tehető meg:

gen_udp:recv(Socket, Length) gen_udp:recv(Socket, Length, Timeout)
Visszatérési érték

Ha az időkorláton belül érkezett csomag:

{ok, {Ip, PortNo, Packet}}

Ellenkező esetben:

{error, timeout}

Ha a gen_udp:recv függvényt nem passzív módban hívja meg egy process, akkor {error, einval} hibakódra számíthat, ami az "invalid argument" megfelelője.

Egy egyszerű UDP szerver a következőképpen nézhet ki Erlangban:

server(Port) -> {ok, Socket} = gen_udp:open(Port, [binary]), loop(Socket). loop(Socket) -> receive {udp, Socket, Host, Port, Bin} -> BinReply = ... , gen_udp:send(Socket, Host, Port, BinReply), loop(Socket) end.

Egy egyszerű UDP kliens pedig a következőképpen:

client(Request) -> {ok, Socket} = gen_udp:open(0, [binary]), ok = gen_udp:send(Socket, "localhost" , 4000, Request), Value = receive {udp, Socket, _, _, Bin} -> {ok, Bin} after 2000 -> error end, gen_udp:close(Socket), Value.

A timeoutra azért van szükség, mert a UDP nem megbízható protokoll, és a csomag elveszhet.

A UDP leggyakoribb alkalmazása az SNMP protokoll, ami a Simple Network Management Protocol rövidítése. Az SNMP-t IP-alapú hálózatokban használják a különböző eszközök és rendszerek monitorozására.

Transmission Control Protocol (TCP)

A TCP egy megbízható, kapcsolat-orientált protokoll, amely adatfolyamokkal dolgozik. A csomagok megérkezése garantált, a megérkezés sorrendje pedig a küldés sorrendjével megegyezik. Egy TCP kapcsolat a felépülés után mindaddig megmarad, amíg valamelyik fél le nem zárja azt, vagy valamilyen hiba miatt meg nem szakad.

TCP-t alkalmaznak például a HTTP kérésekben, peer-to-peer alkalmazásokban, azonnali üzenetküldő szolgáltatásokban.

Egy kapcsolat kialakításakor gyakran minden beérkező kérésnek egy-egy új folyamatot indítanak, amelyek a kapcsolatokat mindaddig nyitva tartják, amíg az adott kérés kiszolgálása folyamatban van.

Egy beérkező kérés esetén kétféle megoldást szoktak használni:

Az Erlangban a gen_tcp modul tartalmazza a TCP protokollal kapcsolatos függvényeket.

Üzenetek fogadása

Aktív módban a folyamat {tcp, Socket, Packet} formájú üzeneteket kap, ahol:

Passzív mód esetén a UDP-hez hasonlóan:

gen_tcp:recv(Socket, Length) gen_tcp:recv(Socket, Length, Timeout)

Visszatérési értéke: {ok, Packet}

A Length paraméter csak "raw" módban releváns. Ez a paraméter mondja meg, hogy hány bájt adatot várjon, mielőtt visszatér az eredménnyel. Ha a paraméter értéke 0, akkor minden elérhető adatot megkapunk. Ha a küldő lezárja a kapcsolatot, mielőtt a megfelelő számú bájt elküldésre került volna, ezek az adatok eldobásra kerülnek.

A passzív mód haszna

A passzív módú átvitel egy jó módszer arra, hogy elkerüljük a túl sok kérés beérkezéséből adódó problémákat. Gyakori tervezési minta, hogy a beérkező kérések kezelésére egy-egy új folyamatot indítanak. Ez azonban extrém esetekben, nagy terhelés esetén azzal járhat, hogy a virtuális gép memóriája elfogy, ahogy szaporodnak a kérések, és ezáltal a folyamatok. Passzív módban a TCP puffer használható ennek megelőzésére: ha túl sok kérés jön be, és azok még nem kerültek lekérésre, a puffer megtelik, és a kliens kérése visszautasításra kerül. Az, hogy ilyen védelmi mechanizmusra szükség van-e hirtelen megugró hálózati forgalom esetén, például egy terhelésteszt segítségével dönthető el.

Egy TCP példa

Kliensprogram

A kliens egy hostnevet és egy binary-t vár paraméterül. Létrehoz egy socketet az 1234-es porton, és a binary-t 100 bájtos részekre darabolva elküldi a célgépre.

client(Host, Data) -> {ok, Socket} = gen_tcp:connect(Host, 1234, [binary, {packet, 0}]), send(Socket, Data), ok = gen_tcp:close(Socket).

Az üzenet darabokra bontása a <<Chunk:100/binary, Rest/binary>> kifejezéssel lehetséges. Az első 100 bájt a Chunk-nak, a maradék rész a Rest-nek fog megfelelni. Ha az üzenet kevesebb, mint 100 bájtot tartalmaz, az első klózra történő mintaillesztés sikertelen lesz. A második klózra való mintaillesztés viszont sikeres lesz, és a maradék üzenet elküldésre kerül:

send(Socket, <<Chunk:100/binary, Rest/binary>>) -> gen_tcp:send(Socket, Chunk), send(Socket, Rest); send(Socket, Rest) -> gen_tcp:send(Socket, Rest).

Szerveroldal

A szerveroldalon egy listener folyamat várja a kliensektől a kapcsolatkérelmeket. Amikor a kérés megérkezik, a listener folyamat veszi át a kérés kezelését, és passzív módban fogad binary-ket. Az ezután következő kapcsolatkérelmek figyelésére létrehoz egy új folyamatot, ez lesz az új listener folyamat. Az eredeti listener most a létrejött kapcsolat kezelésével foglalkozik, a kapott adatokat egy listához fűzi, és amikor a socket lezárásra kerül, kiírja az adatokat egy fájlba.

server() -> {ok, ListenSocket} = gen_tcp:listen(1234, [binary, {active, false}]), wait_connect(ListenSocket,0). wait_connect(ListenSocket, Count) -> {ok, Socket} = gen_tcp:accept(ListenSocket), spawn(?MODULE, wait_connect, [ListenSocket, Count+1]), get_request(Socket, [], Count). get_request(Socket, BinaryList, Count) -> case gen_tcp:recv(Socket, 0, 5000) of {ok, Binary} -> get_request(Socket, [Binary|BinaryList], Count); {error, closed} -> handle(lists:reverse(BinaryList), Count) end. handle(Binary, Count) -> {ok, Fd} = file:open("log_file_"++integer_to_list(Count), write), file:write(Fd, Binary), file:close(Fd).

A wait_connect függvény végzi a beérkező kapcsolatok kezelését, és gondoskodik az új folyamatok indításáról is (spawn/3).

A get_request függvény érdekessége, hogy (hatékonysági okokból) a lista elejére teszi az újonnan kapott adatokat, így a socket lezárásakor a listát meg kell fordítani. Ezt a lists:reverse/1 segítségével teszi.

A szervert a tcp:server() paranccsal indíthatjuk el, a klienst pedig a következőképpen:

tcp:client({127,0,0,1}, <<"Kiscica">>).

Kapcsolatok fogadása

Látható, hogy a parancsok nagy része hasonló a UDP példában látottakhoz. A fő különbség ez a hívás:

gen_tcp:listen(PortNumber, Options)

Ez létrehoz egy listener socketet, amely vár egy bejövő kapcsolatra. A paraméterezése nagyon hasonló a gen_udp:open/2-éhez, néhány TCP-specifikus opciót kivéve. Ezek az alábbiak:

A többi flag leírása az inet és gen_tcp modulok dokumentációjában található.

A gen_tcp:listen/2 hívás azonnal visszatér egy socket azonosítóval, amit aztán a következő függvényeknek lehet továbbadni:

gen_tcp:accept(Socket) gen_tcp:accept(Socket, TimeOut)

Az első hívás blokkolja a program futását mindaddig, amíg be nem érkezik egy kapcsolódási kérelem. A második esetben a TimeOut paraméter az időlimit ezredmásodpercben megadott értéke. Amennyiben ez alatt az idő alatt nem érkezik kapcsolódási kérelem, az accept {error, timeout} értékkel tér vissza.

Kapcsolat létrehozása

Kapcsolatot a következő hívással lehet kezdeményezni:

gen_tcp:connect(Address, Port, OptionList)

Üzenetek fogadása

Mivel a példánkban passzív módú socketeket használunk, a gen_tcp:recv/2 és gen_tcp:recv/3 hívásokkal kell lekérni a bejövő üzeneteket. Ha aktív módban futna, a folyamat Erlang üzeneteket kapna, melyek lehetséges tartalma a következő:

Socket lezárása

A gen_tcp:close(Socket) hívással tehető meg, akár a kliens-, akár a szerveroldalon. Mindkét esetben egy {tcp_closed, Socket} üzenet kerül küldésre a túloldal felé, ezzel lezárva a másik oldalon is a socketet.

A vezérlés átadása

A vezérlőfolyamat általában az a folyamat, amely a kapcsolatot egy gen_tcp:accept hívással elfogadta vagy egy gen_tcp:connect hívással kezdeményezte. Az üzeneteket átírányítani, és ezzel a vezérlést egy másik folyamatnak átadni a

gen_tcp:controlling_process(Socket, Pid)

függvény meghívásával lehetséges.

Az előző példánkban a gen_tcp:accept-t hívó processz lett a vezérlőfolyamat, és létrehoztunk egy új listener folyamatot. Ha azt szeretnénk, hogy a listener folyamat megmaradjon, és a beérkezett kapcsolat kezelésére szeretnénk egy másik folyamatot léthrehozni, akkor a kód így nézne ki:

server() -> {ok, ListenSocket} = gen_tcp:listen(1234, [binary, {active, false}]), wait_connect(ListenSocket,0). wait_connect(ListenSocket, Count) -> {ok, Socket} = gen_tcp:accept(ListenSocket), Pid = spawn(?MODULE, get_request, [Socket, [], Count]), gen_tcp:controlling_process(Socket, Pid), wait_connect(ListenSocket, Count+1).

Azaz egy gen_tcp:controlling_process(Socket, Pid) hívással átadtuk az új kapcsolathoz tartozó socket vezérlését az újonnan létrehozott folyamatnak.