Server web dan framework Python bukan satu program yang sama. Server menerima koneksi dari internet, sementara framework berisi kode aplikasi kita. Agar keduanya bisa saling bertukar, komunitas Python mendefinisikan dua standar antarmuka: WSGI untuk dunia sinkron dan ASGI untuk dunia asinkron. Memahami keduanya menjelaskan mengapa aplikasi Flask bisa dijalankan gunicorn, dan mengapa FastAPI membutuhkan uvicorn.
WSGI: Kontrak untuk Aplikasi Sinkron #
WSGI (Web Server Gateway Interface) didefinisikan dalam PEP 3333 dengan kontrak yang mengejutkan sederhananya: aplikasi hanyalah sebuah callable yang menerima dua argument — environ (dictionary berisi data request) dan start_response (fungsi untuk mengirim status dan header) — lalu mengembalikan iterable berisi body response. Kode berikut adalah aplikasi WSGI lengkap, dijalankan dengan server wsgiref bawaan standard library, lalu diminta data oleh client sungguhan:
import threading
import urllib.request
from wsgiref.simple_server import WSGIRequestHandler, make_server
class HandlerSenyap(WSGIRequestHandler):
def log_message(self, format, *args):
pass # matikan log akses agar contoh tetap deterministik
def application(environ, start_response):
status = "200 OK"
headers = [("Content-Type", "text/plain; charset=utf-8")]
start_response(status, headers)
isi = (
f"metode={environ['REQUEST_METHOD']} "
f"path={environ['PATH_INFO']} "
f"query={environ['QUERY_STRING']}"
).encode("utf-8")
return [isi]
server = make_server("127.0.0.1", 8001, application, handler_class=HandlerSenyap)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
response = urllib.request.urlopen("http://127.0.0.1:8001/produk?urutan=nama")
print("Status:", response.status)
print("Isi:", response.read().decode("utf-8"))
server.shutdown()Output yang Dihasilkan:
Status: 200
Isi: metode=GET path=/produk query=urutan=namaMari kita telaah bagian intinya:
- Baris 11 (
def application(environ, start_response)): seluruh kontrak WSGI ada di signature ini — server memanggil fungsiapplicationdengan dua argument tersebut. - Baris 14 (
start_response(status, headers)): aplikasi memberi tahu server bahwa response berstatus200 OKdengan headerContent-Typekhusus. - Baris 15-19 (
isi = ...): data request dibaca dari dictionaryenviron— metode, path, dan query string sudah dipisahkan server, tinggal dipakai. - Baris 20 (
return [isi]): aplikasi mengembalikan list berisibytes, bukan string.
Dictionary environ adalah jendela aplikasi ke seluruh request. Pada eksekusi nyata di atas, nilainya antara lain:
Kunci environ | Nilai | Arti |
|---|---|---|
REQUEST_METHOD | 'GET' | metode HTTP yang diminta |
PATH_INFO | '/produk' | path setelah domain |
QUERY_STRING | 'urutan=nama' | bagian setelah tanda ? |
HTTP_USER_AGENT | 'Python-urllib/3.12' | header User-Agent |
wsgiref hanya untuk belajar dan pengembangan. Di produksi, aplikasi WSGI dijalankan oleh server seperti gunicorn atau uWSGI dengan perintah semacam gunicorn app:application.
ASGI: Kontrak untuk Dunia Asinkron #
WSGI lahir tahun 2003 dan hanya mengenal model satu request satu thread. Padahal kita sudah mempelajari di Section Asynchronous Programming bahwa async/await memungkinkan satu event loop menangani ribuan koneksi. ASGI (Asynchronous Server Gateway Interface) memperluas WSGI dengan tiga perubahan utama: aplikasi menjadi async def, argument environ diganti dictionary scope, dan pengiriman response dilakukan lewat await send(...) bertahap. Berikut aplikasi ASGI minimal yang dijalankan server uvicorn sungguhan:
import threading
import time
import httpx
import uvicorn
async def application(scope, receive, send):
if scope["type"] != "http":
return
await receive() # tunggu event body request (kosong untuk GET)
isi = f"metode={scope['method']} path={scope['path']}".encode("utf-8")
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/plain; charset=utf-8")],
}
)
await send({"type": "http.response.body", "body": isi})
config = uvicorn.Config(application, host="127.0.0.1", port=8002, log_level="warning")
server = uvicorn.Server(config)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
while not server.started:
time.sleep(0.01)
response = httpx.get("http://127.0.0.1:8002/produk")
print("Status:", response.status_code)
print("Isi:", response.text)
server.should_exit = True
thread.join()
print("Server uvicorn berhenti.")Output yang Dihasilkan:
Status: 200
Isi: metode=GET path=/produk
Server uvicorn berhenti.Perhatikan pola pengiriman dua tahap: http.response.start mengirim status dan header, lalu http.response.body mengirim isinya. Bentuk bertahap inilah yang membuat ASGI mampu menangani WebSocket dan long-polling — protokol yang mengirim banyak pesan pada satu koneksi yang sama, sesuatu yang mustahil bagi WSGI.
Sinkron vs Asinkron #
| Aspek | WSGI | ASGI |
|---|---|---|
| Model eksekusi | sinkron, satu request sekaligus | sinkron dan asinkron |
| Bentuk aplikasi | def app(environ, start_response) | async def app(scope, receive, send) |
| Server populer | gunicorn, uWSGI | uvicorn, hypercorn |
| Framework pengguna | Flask | FastAPI (Django mendukung keduanya) |
| Kebutuhan khusus | halaman web klasik | WebSocket, realtime, trafik padat |
Hubungannya dengan Framework #
Flask dan FastAPI pada dasarnya adalah pembungkus yang rapi di atas kedua kontrak ini. Flask menyembunyikan environ menjadi object request, sedangkan FastAPI menyembunyikan scope menjadi parameter bertipe. Bukti paling langsung: object aplikasi dari keduanya adalah callable, persis seperti kontraknya:
from fastapi import FastAPI
from flask import Flask
flask_app = Flask("cek-wsgi")
print("Flask app callable:", callable(flask_app))
fastapi_app = FastAPI()
print("FastAPI app callable:", callable(fastapi_app))Output yang Dihasilkan:
Flask app callable: True
FastAPI app callable: TrueKetika gunicorn memanggil flask_app(...), gunicorn sedang menjalankan kontrak WSGI. Ketika uvicorn memanggil fastapi_app(scope, receive, send), uvicorn sedang menjalankan kontrak ASGI. Framework hanyalah lapisan kenyamanan — kontrak tetap dipegang server.