در ساخت ایجنتهای مدرن، ارتباط پایدار میان مدل زبانی و ابزارهای محلی نقشی حیاتی دارد. استاندارد JSON-RPC 2.0 سالهاست که زبان مشترک اجرای رویههای راه دور و بسترهای انتقال محلی محسوب میشود. زمانی که ابزارها را در قالب فرایندهای مجزا اجرا میکنید، استفاده از ورودی و خروجی استاندارد (stdio) سادهترین و در عین حال تمیزترین راهکار برای ایجاد این پل ارتباطی است.
اگر لایهی انتقال و فریمبندی پیامها بهدرستی پیادهسازی نشود، بروز یک خطای ساده در دادهی JSON ورودی میتواند کل ارتباط را قطع کند یا به بنبستهای اجرایی منجر شود. در این درس یاد میگیرید که چگونه یک بستر انتقال مبتنی بر خط جدید (newline-delimited) بسازید که انواع پیامها، اعلانها و خطاهای استاندارد را بدون آلوده کردن ادامهی جریان داده مدیریت کند.
بستر انتقال میان کلاینتِ مدل و سرور ابزار، پروتکل JSON-RPC روی ورودی و خروجی استاندارد (stdio) است. اگر یک بار خودتان آن را پیادهسازی کنید، متوجه میشوید لایههای فریمبندی داده دقیقاً چه هزینهای به همراه دارند.
اهداف یادگیری
- کار با پروتکل JSON-RPC 2.0 در قالب JSON خطبهخط (newline-delimited) روی ورودی استاندارد (stdin) و خروجی استاندارد (stdout).
- نگاشت پنج کد خطای استاندارد (32700-، 32600-، 32601-، 32602- و 32603-) و بازگرداندن آنها با بار معنایی درست.
- تفکیک درخواستها، پاسخها، اعلانها (notifications) و دستهها (batches) بدون افزودن کلیدهای جدید به ساختار پیام.
- مدیریت یک خطای تجزیه در هر خط بدون آلوده کردن ادامهی جریان داده (stream).
- ساخت یک دموی خودپایان با استفاده از
io.BytesIOبرای اجرای درس بدون نیاز به ایجاد فرایند فرزند.
چرا JSON-RPC همچنان زبان مشترک سیستمهاست؟
یک ایجنت برنامهنویس در سال 2026 شاید در طول یک نشست کاری با دوازده سرور ابزار مختلف ارتباط برقرار کند. هر سرور یا یک فرایند مجزاست یا یک اندپوینت راهدور. قالب تبادل داده روی خط ارتباطی از سال 2013 دستنخورده باقی مانده است. مستندات JSON-RPC 2.0 فقط دو صفحه است؛ اما با این حال دوام آورده، چون گزینههای جایگزین (مثل gRPC، درخواستهای HTTP مجزا برای هر فراخوانی، یا قالبهای باینری سفارشی) همگی نوعی مصالحه را تحمیل میکنند که JSON-RPC از آن آزاد است: آنها مجبورتان میکنند بین استریمینگ، ارسال دستهای، یا وابستگی شدید به بستر انتقال یکی را انتخاب کنید. در مقابل، JSON-RPC روی stdio، سوکتها، وبسوکتها و HTTP کاملاً متقارن عمل میکند؛ بهطوریکه اگر هر دو طرف به مشخصات پایبند باشند، کلاینت میتواند بهراحتی سروری را که تا به حال ندیده هدایت کند.
در این درس نسخهی مبتنی بر stdio را میسازیم؛ یعنی JSON تفکیکشده با خط جدید. هر درخواست یک خط است، هر پاسخ یک خط، و مرز بستر انتقال فقط نویسهی \n خواهد بود.
ساختار داده روی خط ارتباطی
چهار ساختار پیام وجود دارد: دو ساختار را کلاینت ارسال میکند و دو ساختار را سرور.
اعلان فاقد شناسه (id) است؛ بنابراین سرور نباید به آن پاسخ دهد. اگر سروری به یک اعلان پاسخ بفرستد، کلاینت هیچ راهی برای انتساب آن به محل فراخوانی نخواهد داشت. همین یک قانون ساده، محاسبات فریمبندی پیام را بسیار سرراست نگه میدارد.
درخواست دستهای در واقع یک آرایهی JSON از درخواستها یا اعلانهاست. سرور نیز در پاسخ، آرایهای از پاسخها را با هر ترتیبی، دقیقاً به ازای هر ورودیِ غیرِاعلان برمیگرداند. اگر تمام ورودیهای یک دسته از نوع اعلان باشند، سرور در پاسخ هیچ چیزی ارسال نمیکند.
پنج کد خطای استاندارد
-32700 Parse error JSON could not be parsed
-32600 Invalid Request Envelope shape is wrong
-32601 Method not found
-32602 Invalid params
-32603 Internal error
کدهای بین 32000- تا 32099- برای خطاهای تعریفشده توسط سرور رزرو شدهاند. بقیهی کدها را برنامه تعریف میکند. در این درس به همین پنج کد اصلی پایبند میمانیم. اگر تابع رسیدگیکنندهی شما استثنایی بالا بیاورد، بستر انتقال آن را با کد 32603- و همراه با نام کلاس استثنا در data.exception بستهبندی میکند.
خطای تجزیه یک قانون ویژه دارد: شناسهی id در پاسخ برابر با null خواهد بود، چرا که درخواست اصلاً بهاندازهای تجزیه نشده که بتوان شناسهای از درون آن بیرون کشید.
فریمبندی با خط جدید و دموی BytesIO
بستر انتقال دادهها را خطبهخط میخواند؛ یعنی بایتها را تا نویسهی \n (شامل خود آن) دریافت میکند. اگر خطی تجزیه نشود، بستر انتقال یک پاسخ 32700- با id: null مینویسد و به کارش ادامه میدهد. بهاینترتیب، جریان داده آلوده نمیشود و خط بعدی بهطور مستقل و از نو تجزیه خواهد شد.
برای این درس، یک جفت شیء io.BytesIO را در قالب stdin و stdout میپیچیم. سرور درخواستها را تا رسیدن به پایان فایل (EOF) میخواند، برای هر کدام پاسخی مینویسد و کارش را تمام میکند. کلاینت هم در ادامه پاسخها را بازخوانی میکند. نه فرایندی ایجاد میشود و نه نیازی به تایماوت هست؛ رفتار بستر انتقال دقیقاً شبیه به یک پایپ واقعی در زیرفرایندها خواهد بود، چون واسط io در پایتون قرارداد متدهای .readline() و .write() را دقیقاً به همان صورت پیاده کرده است.
هدایت متدها (Method Dispatch)
بستر انتقال از وجود متدها بیخبر است؛ همهچیز را به یک شیء قابلفراخوانی به نام handler(method, params) میسپارد که بستر آزمایشی در اختیارش گذاشته است. این رسیدگیکننده یا نتیجه را برمیگرداند یا استثنا ایجاد میکند. کدهای خطا از دل سه کلاس استثنای مشخص بیرون میآیند.
MethodNotFound -> -32601
InvalidParams -> -32602
Anything else -> -32603 with exception name in data
بستر انتقال هرگز سر و کاری با رجیستری ابزار ندارد؛ رجیستری پشت رسیدگیکننده قرار میگیرد. این همان لایهبندی مدنظر ماست: بستر انتقال به زبان JSON-RPC صحبت میکند، رجیستری ساختار ابزارها را میشناسد، و توزیعکننده (درس بیستوسوم) این دو بخش را به هم پیوند میزند.
رفتار جریان داده هنگام مواجهه با خطا
client writes server reads server writes
--------------- ----------- -------------
{...valid request...} parses ok {...response, id matches...}
{...broken json... parse fails {id:null, error: -32700}
{...valid request...} parses ok {...response, id matches...}
{...missing method...} invalid envelope {id:X, error: -32600}
یک خط JSON خراب حلقهی اجرا را متوقف نمیکند. نبودِ فیلد method حلقه را از کار نمیاندازد. وقوع استثنا در رسیدگیکننده نیز باعث توقف حلقه نمیشود. بستر انتقال به خواندن دادهها تا رسیدن به EOF ادامه میدهد.
اعلانها و جریانهای کاری نامتقارن
اعلانها از نوع «بفرست و فراموش کن» (fire-and-forget) هستند. بستر آزمایشی از اعلانها برای ارسال وقایع مربوط به پیشرفت، سیگنالهای لغو و ثبت لاگها استفاده میکند. با همین اعلانهاست که یک ابزار زمانبر میتواند وضعیت خود را بهشکل استریم گزارش دهد، بدون اینکه لازم باشد برای هر کدام منتظر یک رفتوبرگشت کامل بماند.
در این درس یک تابع کمکی برای ارسال اعلان به بیرون پیاده میکنیم: write_notification. سرور از این تابع برای اعلام وضعیت پیشرفت، درست حین اجرای درخواست استفاده میکند. دمو این الگو را به نمایش میگذارد: درخواستی دریافت میشود، رسیدگیکننده دو اعلانِ پیشرفت میفرستد و سپس پاسخ نهایی را مینویسد.
راهنمای مطالعهی کد
در فایل code/main.py کلاس StdioTransport، تابع کمکی تجزیه (parse_request)، سه تابع کمکی نوشتن داده (write_response، write_error و write_notification) و حلقهی توزیع متد (serve) تعریف شدهاند. ثابتهای مربوط به کدهای خطا در دامنهی ماژول قرار دارند.
فایل code/tests/test_transport.py بخشهای مختلف را بررسی میکند: پنج کد خطا، اعلانها (بدون نوشتن پاسخ)، دستهها (ورودی آرایه، خروجی آرایه، نادیدهگرفتن اعلانها)، خطوط JSON خراب (خطای تجزیه و سپس ادامهی مسیر) و جریان کاری نامتقارنی که در آن رسیدگیکننده حین فراخوانی یک اعلان میفرستد.
برای مطالعهی بیشتر
این بستر انتقال نیاز درسهای بعدی را برطرف میکند. بسترهای انتقالی که در محیط عملیاتی (پروداکشن) استفاده میشوند، سه قابلیت دیگر نیز به همراه دارند: فیلدی برای شناسهی تطبیق که هنگام هدایت پیام به گرههای دیگر از بین نرود (شناسهی id همین کار را میکند، ولی در یک شبکهی مش به یک شناسهی ردگیری بیرونی هم نیاز دارید)، کانالی برای لغو عملیات (اعلانی مثل $/cancelRequest همراه با شناسهی فراخوانیِ در حال اجرا)، و سازوکار دستدادن برای توافق روی نوع محتوا (content-type negotiation) تا همان سوکت بتواند همزمان از JSON-RPC و پروتکل HTTP جریانی پشتیبانی کند. هیچکدام از این موارد ساختار پایهای داده روی خط را تغییر نمیدهند؛ فقط فراداده به آن اضافه میکنند.
