GenServer (سرور عمومی) یک رفتار است که تعاملات رایج کلاینت-سرور میان فرایندهای Elixir را انتزاع میکند.
حلقهی دریافت را از زمانی که دربارهی فرایندها یاد گرفتیم به خاطر دارید؟ رفتار GenServer انتزاعهایی برای پیادهسازی چنین حلقههایی و برای تبادل پیام با فرایندی که چنین حلقهای را اجرا میکند فراهم میکند. این کار نگهداشتن وضعیت و اجرای کد ناهمگام را آسانتر میکند.
بدانید که اسم GenServer بار معنایی زیادی دارد. از آن برای توصیف ماژولی هم استفاده میشود که از رفتار GenServer استفاده میکند، و همچنین برای فرایندی که از ماژولی که از رفتار GenServer استفاده میکند راهاندازی شده است.
رفتار GenServer یک کالبک الزامی، init/1، و چند کالبک اختیاری جالب تعریف میکند: handle_call/3، handle_cast/2 و handle_info/3. قرار نیست _کلاینتها_یی که از GenServer استفاده میکنند این کالبکها را مستقیم فراخوانی کنند. در عوض، ماژول GenServer توابعی فراهم میکند که کلاینتها میتوانند با آنها با یک فرایند GenServer ارتباط برقرار کنند.
غالباً یک ماژول واحد هم API کلاینت را تعریف میکند، یعنی مجموعهای از توابع که سایر بخشهای برنامهی Elixir شما میتوانند برای ارتباط با این فرایند GenServer فراخوانی کنند، و هم پیادهسازیهای کالبک سرور را، که منطق این GenServer را در خود دارند.
بیایید اول نگاهی به یک مثال ساده از GenServer بیندازیم و بعد ببینیم هر کالبک چه معنایی دارد.
این یک سرور نمونه است که میتواند به پرسشهای تکراری مسافران مزاحم در طول یک سفر جادهای طولانی پاسخ دهد، دقیقتر بگوییم به این پرسش: «هنوز نرسیدیم؟». تعداد دفعاتی که این پرسش پرسیده شده را میشمارد و پاسخهایی با دلخوری روزافزون برمیگرداند.
defmodule AnnoyingPassengerAutoresponder do
use GenServer
# Client API
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg)
end
def are_we_there_yet?(pid) do
GenServer.call(pid, :are_we_there_yet?)
end
# Server callbacks
@impl GenServer
def init(_init_arg) do
# the initial count of questions asked is always 0
state = 0
{:ok, state}
end
@impl GenServer
def handle_call(:are_we_there_yet?, _from, state) do
reply =
cond do
state <= 3 -> "No."
state <= 10 -> "I told you #{state} times already. No."
true -> "..."
end
# increase the count of questions asked
new_state = state + 1
# reply to the caller
{:reply, reply, new_state}
end
end
init/1یک سرور را میتوان با فراخوانی GenServer.start/3 یا GenServer.start_link/3 راهاندازی کرد. تفاوت این توابع را در مفهوم پیوندها یاد گرفتیم.
این دو تابع:
GenServer را پیادهسازی میکند به عنوان آرگومان اول میپذیرند.init_arg میپذیرند. همانطور که از اسمش برمیآید، این آرگومان به کالبک init/1 پاس داده میشود.راهاندازی یک سرور با فراخوانی GenServer.start/3 یا GenServer.start_link/3 کالبک init/1 را بهصورت مسدودکننده فراخوانی میکند. مقدار بازگشتی init/1 تعیین میکند که آیا سرور میتواند با موفقیت راهاندازی شود یا نه.
کالبک init/1 معمولاً یکی از این مقادیر را برمیگرداند:
{:ok, state}. سرور حلقهی دریافت خود را با state به عنوان وضعیت اولیهاش آغاز میکند. state میتواند از هر نوعی باشد.{:stop, reason}. reason میتواند از هر نوعی باشد. سرور حلقهی دریافت خود را آغاز نمیکند. فرایند با دلیل دادهشده خارج میشود.امکانات پیشرفتهتری هم وجود دارد که الان به آنها نمیپردازیم.
اگر حلقهی دریافت سرور آغاز شود، توابع GenServer.start/3 و GenServer.start_link/3 یک تاپل {:ok, pid} برمیگردانند. در غیر این صورت {:error, reason} را برمیگردانند
handle_call/3پیامی که نیاز به پاسخ دارد را میتوان با GenServer.call/2 به یک فرایند سرور فرستاد. این تابع به عنوان آرگومان اول، pid یک فرایند سرور در حال اجرا را انتظار دارد، و به عنوان آرگومان دوم، پیام را. پیام میتواند از هر نوعی باشد.
کالبک handle_call/3 مسئول رسیدگی و پاسخ دادن به پیامهای همگام است. سه آرگومان دریافت میکند:
۱. message: مقداری که به عنوان آرگومان دوم به GenServer.call/2 پاس داده میشود.
۲. from: pid فرایندی که GenServer.call/2 را فراخوانی میکند. بیشتر اوقات میتوان این آرگومان را نادیده گرفت.
۳. state: وضعیت فعلی سرور. به یاد داشته باشید که مقدار اولیهاش در کالبک init/1 تعیین شده بود.
کالبک handle_call/3 معمولاً یک تاپل ۳تایی به شکل {:reply, reply, state} برمیگرداند. یعنی عنصر دوم تاپل، reply که میتواند از هر نوعی باشد، به فراخواننده فرستاده میشود. عنصر سوم تاپل، state، وضعیت جدید سرور پس از رسیدگی به این پیام است.
امکانات پیشرفتهتری هم وجود دارد که الان به آنها نمیپردازیم.
برای اینکه از روی اسم این کالبک یادتان بماند چه کاری میکند، به این فکر کنید که دارید کسی را با تلفن «call» میکنید.
اگر آن شخص در دسترس باشد، بلافاصله (بهصورت همگام) پاسخ میگیرید.
handle_cast/2پیامی که نیازی به پاسخ ندارد را میتوان با GenServer.cast/2 به یک فرایند سرور فرستاد. آرگومانهایش همانند آرگومانهای GenServer.call/2 هستند.
کالبک handle_cast/2 مسئول رسیدگی به این پیامها است. دو آرگومان دریافت میکند، message و state، که همان آرگومانهای کالبک handle_call/3 هستند (بهجز from).
کالبک handle_cast/2 معمولاً یک تاپل ۲تایی به شکل {:noreply, state} برمیگرداند.
امکانات پیشرفتهتری هم وجود دارد که الان به آنها نمیپردازیم.
برای اینکه از روی اسم این کالبک یادتان بماند چه کاری میکند، به یاد داشته باشید که «cast» معنای «پرتاب کردن» را هم میدهد.
اگر پیامی را در بطری به دریا بیندازید، انتظار ندارید بلافاصله پاسخ بگیرید، یا شاید هرگز.
call استفاده کنم یا از cast؟تقریباً همیشه از call استفاده کنید، حتی اگر کد کلاینت شما به پاسخ سرور نیازی نداشته باشد.
استفاده از call منتظر پاسخ میماند، که به عنوان مکانیزمی برای اعمال فشار معکوس عمل میکند (تا مانع ارسال پیامهای بیشازحد کلاینتها در یک لحظه شود). دریافت پاسخ از سرور همچنین تنها راه مطمئن شدن از این است که سرور پیام کلاینت را دریافت و رسیدگی کرده است.
handle_info/2پیامها میتوانند به روشهایی غیر از فراخوانی GenServer.call/2 یا GenServer.cast/2 هم به صندوق ورودی سرور برسند، مثلاً با فراخوانی تابع سادهی send/2.
برای رسیدگی به چنین پیامهایی، از کالبک handle_info/2 استفاده کنید. این کالبک دقیقاً به همان روش handle_cast/2 کار میکند.
رفتار GenServer یک پیادهسازی همهگیر از handle_info/2 فراهم میکند که خطاهای مربوط به پیامهای غیرمنتظره را ثبت میکند. اگر آن پیادهسازی پیشفرض را بازنویسی کنید، مطمئن شوید که همیشه پیادهسازی همهگیر خودتان را هم بگنجانید. اگر فراموش کنید، سرور در صورت دریافت پیامی غیرمنتظره از کار میافتد.
مقدار بازگشتی هر یک از چهار کالبک توضیحدادهشده در بالا را میتوان با یک عنصر دیگر تاپل گسترش داد: یک مهلت زمانی. مثلاً به جای برگرداندن {:ok, state} از init/1، {:ok, state, timeout} را برگردانید.
از مهلت زمانی میتوان برای تشخیص نبود پیام در صندوق پیام طی یک بازهی زمانی مشخص استفاده کرد. اگر سرور از یکی از کالبکهایش یک مهلت زمانی برگرداند و تعداد میلیثانیهی مشخصشده بگذرد بدون آنکه پیامی برسد، کالبک handle_info/2 با :timeout به عنوان آرگومان اول فراخوانی میشود.