วันนี้จะลองเขียน Server Process ดูบ้าง
เริ่มต้นง่ายๆที่ Echo Server
เริ่มต้นด้วยการประกาศ module
-module(echo).
-export([start/0,loop/1]).
function start จะทำหน้าที่ spawn process ขึ้นมาใหม่
แล้วทำการ register process เข้ากับชื่อ "echo"
(การ register จะช่วยให้ client สามารถส่ง message มาให้ server ได้ง่ายขึ้น
แทนที่จะต้องรู้ว่า server มี process id เป็นอะไร
ก็ให้ระบุชื่อ server แทน)
start() ->
register(echo,
spawn(echo, loop, [nil])).
ในส่วนของ function loop ก็คือ loop ที่เราจะรอรับ message จาก client
ให้สังเกตว่ากรณี message ประเภท
echo
พอตอบ message กลับไปแล้ว
จะมีการ recursive เรียกใช้ loop อีกครั้ง
ส่วนกรณี
stop
จะไม่มีการเรียก loop ซ้ำ ซึ่งก็หมายความ process จะ terminate
loop(State) ->
receive
{From, {echo, Msg}} ->
From ! {ok, Msg},
loop(State);
{From, {stop}} ->
true
end.
ใน echo server เราสนใจ message อยู่ 2 แบบคือ
{From, {echo, Msg}}
From ก็คือ process id ของ client
จะได้ส่งข้อมูลกลับไปให้ client ได้
echo ขึ้นต้นด้วยตัวเล็ก แสดงว่ามันเป็น atom หรือ Constants แบบหนึ่ง
Msg คือ ข้อความที่ user ส่งมา
- {From, {stop}}
เวลาใช้งาน ก็จะทำดังนี้
Erlang (BEAM) emulator version 5.4.13 [source] [hipe]
Eshell V5.4.13 (abort with ^G)
1> c("/Users/pphetra/projects/erlang/echo2/src/echo", [{outdir, "/Users/pphetra/projects/erlang/echo2/src/"}]).
/Users/pphetra/projects/erlang/echo2/src/echo.erl:13: Warning: variable 'From' is unused
{ok,echo}
2> echo:start().
true
3> echo ! {self(), {echo, "helloworld"}}.
{<0.30.0>,{echo,"helloworld"}}
4> echo ! {self(), {stop}}.
{<0.30.0>,{stop}}
5>
<0.30.0>
คือเลข pid ของ echo server
- self() คือ function ที่ return pid ของ process ที่เรียกคำสั่งนี้
ใน erlang มี concept ที่เรียกว่า Behavior
ซึ่งก็คือการดึงเอา pattern ที่ซ้ำๆกันออกมาเป็น code ต่างหาก
แล้วเปิดให้เราเขียน code ที่ implement behavior เหล่านี้ได้
erlang เตรียม behavior ของ server ไว้แล้ว
โดยมีชื่อว่า gen_server
ลองดูว่า echo server implement behavior นี้แล้ว code จะเปลี่ยนโฉมไปยังไงบ้าง
เริ่มต้นด้วย การประกาศ
behavior(gen_server)
-module(echo).
-behavior(gen_server).
-export([handle_call/3,start/0,init/1,terminate/2]).
gen_server บังคับให้เรา implement callback function ที่ชื่อ init
ซึ่งกรณีของเรา ไม่ได้มีการ allocate resources อะไร
ก็เลยแค่ return ok พร้อม state [] ว่างๆกลับไป
start() ->
gen_server:start_link({local, echo}, echo, [], []).
init(_Arg) ->
{ok, []}.
ตัว handle_call คือ callback ที่ใช้รองรับ syncronous call (พวกต้องการรับค่ากลับ)
จะเห็นว่าเรา return กลับเป็น
{reply, Msg}
ซึ่ง gen_server จะส่ง message กลับไปให้เอง
ส่วน function
terminate
ก็เป็น callback ที่ gen_server call
เมื่อพบ message
{stop, normal, ..}
ซึ่งในการทำงานจริง ตรงนี้จะเป็นการคืนค่าทรัพยากรที่จองไว้ กลับคืนสู่ระบบ
handle_call(Msg, _From, State) ->
case Msg of
{echo, Text} ->
{reply, {ok, Text}, State};
{stop} ->
{stop, normal, State}
end.
terminate(normal, _) ->
ok.
ดูแล้ว มันเทอะทะขึ้นอีกนิด แต่ว่าผลประโยชน์ที่ได้จากการ implement gen_server behavior ก็คือ
เราสามารถใช้ pattern supervisor มาช่วย monitor server เรา
ถ้ามีการตาย หรือ terminate แบบผิดธรรมชาติ เช่นมี bug หรือ IO มีปัญหา
supervisor สามารถ restart server ให้เราอัติโนมัติได้
ซึ่งช่วยเพิ่ม reliability ให้กับระบบเรา