شب ۶: درخواست و پاسخ HTTP و نوشتن کلاینت پایتون
فهرست نوشته
برای اینکه برنامههای دیگر بتوانند از فهرست سرورها استفاده کنند، به یک قرارداد شبکهای نیاز داریم. در این شب ساختار درخواست و پاسخ HTTP را با یک سرور محلی میبینیم و سپس پایتون را در نقش کلاینت قرار میدهیم. پیشنیاز، JSON و محیط مجازی است. در پایان میتوانید پاسخ سالم، مسیر ناموجود و شکست اتصال را از هم تشخیص دهید.
قرارداد میان کلاینت و سرور#
HTTP پروتکل تبادل درخواست و پاسخ در وب است. کلاینت درخواست را آغاز میکند و سرور پاسخ میدهد. یک API، یعنی رابط برنامهنویسی، مسیرهایی با ورودی و خروجی تعریفشده در اختیار برنامههای دیگر قرار میدهد. در نمونهٔ ما، مسیر /health وضعیت برنامه را به شکل JSON برمیگرداند. دریافت این پاسخ فقط سلامت همین نقطه را نشان میدهد؛ دربارهٔ همهٔ وابستگیهای سرویس نتیجهگیری نمیکنیم.
درخواست یک روش مانند GET، یک مسیر، هدرها و گاهی بدنه دارد. هدر اطلاعاتی دربارهٔ پیام حمل میکند؛ مثلاً Content-Type قالب بدنه را اعلام میکند. پاسخ هم کد وضعیت، هدرها و در صورت نیاز بدنه دارد. کل پاسخ با بدنه یکی نیست؛ JSON فقط محتوای بدنهٔ مثال ماست. این تفکیک هنگام کار با کتابخانهٔ کلاینت اهمیت پیدا میکند.
| روش | کاربرد معمول |
|---|---|
GET | خواندن یک منبع |
POST | ایجاد یا انجام عملیات روی منبع |
PUT | جایگزینی نمایش منبع |
PATCH | تغییر بخشی از منبع |
DELETE | حذف منبع |
در این دوره از GET برای خواندن و POST برای ثبت استفاده میکنیم. پاسخ 200 موفقیت، 201 ایجاد، 404 نبود مسیر یا منبع و 500 شکست غیرمنتظرهٔ سرور را نشان میدهد. این کدها به کلاینت اجازه میدهند بدون حدسزدن معنای یک پیام متنی تصمیم بگیرد. یک پاسخ دارای JSON لزوماً موفق نیست؛ کد وضعیت هم باید بررسی شود.
سرور کوچک برای دیدن اجزای پیام#
پوشهٔ ~/python-backend/night-06 را بسازید و کد زیر را در health_server.py بگذارید. BaseHTTPRequestHandler کلاس آمادهٔ کتابخانهٔ استاندارد برای رسیدگی به درخواست است؛ با تعریف do_GET() رفتار روش GET را تعیین میکنیم. فعلاً هدف فهم HTTP است و این سرور آموزشی را بهعنوان سرور عمومی یا عملیاتی استفاده نمیکنیم.
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class HealthHandler(BaseHTTPRequestHandler):
def send_json(self, status_code: int, data: dict) -> None:
body = json.dumps(data).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
if self.path == "/health":
self.send_json(200, {"status": "ok", "service": "inventory-api"})
else:
self.send_json(404, {"error": "endpoint not found"})
server = HTTPServer(("127.0.0.1", 8000), HealthHandler)
print("Listening on http://127.0.0.1:8000")
server.serve_forever()
json.dumps() متن میسازد و encode() آن را به بایت تبدیل میکند، چون روی اتصال شبکه بایت میفرستیم. طول بدنه را پس از تبدیل حساب میکنیم تا Content-Length تعداد بایت را اعلام کند. اتصال به 127.0.0.1 یعنی فقط از همین میزبان در دسترس است. اگر درگاه اشغال باشد، برنامه شروع نمیشود؛ سرویس تمرین دیگری را متوقف کنید یا درگاه همهٔ فرمانهای مرتبط را هماهنگ تغییر دهید.
python3 health_server.py
در ترمینال دوم، فرمانهای زیر را اجرا کنید. گزینهٔ -i هدرها را همراه بدنه نشان میدهد. سرور استاندارد این مثال معمولاً خط وضعیت HTTP/1.0 تولید میکند؛ این ویژگی پیادهسازی نمونه است و قرار نیست همهٔ APIها همین نسخه را نمایش دهند. پاسخ موفق باید JSON با status و service و پاسخ ناموجود باید کد 404 داشته باشد.
curl -i http://127.0.0.1:8000/health
curl -i http://127.0.0.1:8000/missing
پایتون در نقش درخواستکننده#
در ترمینال دوم محیط مجازی همین پوشه را بسازید و Requests را نصب کنید. این بسته بخشی از کتابخانهٔ استاندارد نیست و نصب آن به دسترسی به مخزن بسته یا نسخهٔ از پیش دریافتشده نیاز دارد. سرور ترمینال اول با کتابخانهٔ استاندارد کار میکند و لازم نیست در محیط مجازی دوباره اجرا شود.
cd ~/python-backend/night-06
python3 -m venv .venv
source .venv/bin/activate
python -m pip install requests
فایل health_client.py را بسازید. ترتیب سه مرحله مهم است: درخواست، بررسی وضعیت و خواندن JSON. اگر فقط .json() را صدا بزنید، ممکن است بدنهٔ یک پاسخ خطا را بهعنوان دادهٔ سالم تفسیر کنید. در مقابل، موفقیت وضعیت هم تضمین نمیکند بدنه JSON معتبر یا دارای کلیدهای قراردادی باشد.
import requests
url = "http://127.0.0.1:8000/health"
try:
response = requests.get(url, timeout=3)
response.raise_for_status()
data = response.json()
except requests.exceptions.RequestException as error:
print(f"Health check failed: {error}")
else:
print(f"HTTP status: {response.status_code}")
print(f"Content type: {response.headers.get('Content-Type')}")
print(f"Service: {data['service']}")
print(f"Health: {data['status']}")
python health_client.py
کد 200، نوع محتوای JSON، نام inventory-api و وضعیت ok انتظار میرود. raise_for_status() برای پاسخهای خطای 4xx و 5xx استثنا ایجاد میکند. JSON ممکن است شیء، آرایه یا حتی مقدار ساده باشد؛ نتیجهٔ .json() همیشه دیکشنری نیست. این مثال به قرارداد سرور خودمان تکیه دارد. کلید گمشده هنوز KeyError ایجاد میکند و قرار نیست بهاشتباه خطای اتصال نام بگیرد.
مقدار timeout=3 محدودیت انتظار اتصال و دریافت داده را تنظیم میکند؛ سقف دقیق سهثانیهای برای کل عمر درخواست نیست. اگر سرور داده را کمکم بفرستد یا چند تلاش اتصال انجام شود، زمان کل میتواند بیشتر شود. بدون timeout، برنامه ممکن است برای مدت نامعلومی منتظر بماند. این ظرافت در راهنمای رسمی Requests
توضیح داده شده است.
سرور را با Ctrl+C متوقف کنید و کلاینت را دوباره اجرا کنید؛ باید پیام کنترلشدهٔ شکست اتصال ببینید. سپس سرور را برگردانید و نشانی کلاینت را موقتاً به /missing تغییر دهید؛ این بار اتصال برقرار میشود ولی raise_for_status() خطای HTTP میسازد. تفاوت این دو حالت را یادداشت کنید، چون در عیبیابی یکی به دسترسی شبکه و دیگری به پاسخ برنامه مربوط است.
تمرین فهرست از راه شبکه#
سرور را گسترش دهید تا روی /servers دو رکورد pve01 با وضعیت روشن و nexus01 با وضعیت خاموش برگرداند. بدنه باید دیکشنری دارای کلید servers باشد و مقدار آن فهرست رکوردها. پاسخ /health و خطای سایر مسیرها را حفظ کنید. در این نسخه همهٔ مسیرها روی درگاه 8000 هستند تا برخلاف جابهجایی درگاه در تمرین منبع، قرارداد آزمایشگاه یکدست بماند.
در inventory_client.py درخواست سهثانیهای بفرستید، وضعیت را بررسی و بدنه را بخوانید. با حلقه، نام هر میزبان و UP یا DOWN را چاپ کنید و تعداد کل و روشنها را بشمارید. با دو رکورد بالا، کل دو و روشن یک انتظار میرود. حالت سرور متوقف و مسیر اشتباه را هم امتحان کنید؛ هر دو باید پیام خطا بدهند و گزارش موفقیت چاپ نشود.
برای خودآزمایی سه جزء پاسخ را نام ببرید، توضیح دهید چرا JSON بودن پاسخ موفقیت را ثابت نمیکند و بگویید چرا timeout را با مهلت کل درخواست یکی نمیگیریم. اگر برای افزودن هر مسیر هنوز مجبورید هدر و بایت را دستی بسازید، همین تجربه دلیل استفاده از چارچوبی مانند FastAPI در شب بعد را روشن میکند.
هنگام بررسی نتیجه، پیامهای ثبتشده در ترمینال سرور را با خروجی کلاینت اشتباه نگیرید. سرور ممکن است دریافت درخواست را در خروجی خطای استاندارد گزارش کند، در حالی که بدنهٔ پاسخ از اتصال شبکه به curl یا برنامهٔ کلاینت میرسد. این دو مسیر جدا هستند. اگر در کلاینت بدنهٔ مورد انتظار را نمیبینید، ابتدا کد وضعیت و نوع محتوا را بررسی کنید و سپس داده را بخوانید؛ صرف دیدن یک خط درخواست در ترمینال سرور، پاسخ صحیح را ثابت نمیکند.