20-bo‘lim
Amaliy loyiha - o'z HTTP serveringiz
Xom soketlar ustida ishlaydigan HTTP/1.1 serveri: so'rovni ajratish, marshrutlash, statik fayllar, xatolar va doimiy ulanish.
Ushbu bo‘lim mundarijasi
Endi hamma o'rganganimizni birlashtiramiz. Hech qanday
freymvork ishlatmaymiz - faqat socket.
Yakunda serveringiz quyidagilarni bajaradi:
| Imkoniyat | Qaysi bo'limdan |
|---|---|
| Bayt oqimidan so'rovni ajratish | 8 - TCP |
Content-Length bilan tanani o'qish | 12 - HTTP |
| Marshrutlash va holat kodlari | 12 - HTTP |
| Statik fayllar va yo'l xavfsizligi | 17 - tashxis |
| Doimiy ulanish | 13 - HTTP versiyalari |
| Xatolarni ushlash | 8 - ishonchlilik |
1-qism: so'rovni o'qish #
Eng muhim qism shu. 8-bo'limda ko'rgandik: TCP - bayt oqimi,
recv bir marta chaqirilsa butun so'rov kelishiga ishonib
bo'lmaydi.
import os
import socket
import threading
class Sorov:
def __init__(self, usul, yol, versiya, sarlavhalar, tana):
self.usul = usul
self.yol = yol
self.versiya = versiya
self.sarlavhalar = sarlavhalar
self.tana = tana
def sarlavha(self, nom, standart=None):
return self.sarlavhalar.get(nom.lower(), standart)
def tirik_qolsinmi(self):
ulanish = self.sarlavha("connection", "").lower()
if self.versiya == "HTTP/1.1":
return ulanish != "close"
return ulanish == "keep-alive"
class SorovOqiuvchi:
"""Bayt oqimidan to'liq so'rovni ajratib oladi (8-bo'lim)."""
CHEGARA = 64 * 1024
def __init__(self, ulanish):
self.ulanish = ulanish
self.bufer = b""
def _yana_oqi(self):
bolak = self.ulanish.recv(4096)
if not bolak:
return False
self.bufer += bolak
if len(self.bufer) > self.CHEGARA:
raise ValueError("so'rov juda katta")
return True
def oqi(self):
while b"\r\n\r\n" not in self.bufer:
if not self._yana_oqi():
return None
bosh, qolgan = self.bufer.split(b"\r\n\r\n", 1)
qatorlar = bosh.decode("latin-1").split("\r\n")
qismlar = qatorlar[0].split(" ")
if len(qismlar) != 3:
raise ValueError("noto'g'ri so'rov qatori")
usul, yol, versiya = qismlar
sarlavhalar = {}
for q in qatorlar[1:]:
if ":" not in q:
raise ValueError("noto'g'ri sarlavha")
nom, qiymat = q.split(":", 1)
sarlavhalar[nom.strip().lower()] = qiymat.strip()
uzunlik = int(sarlavhalar.get("content-length", 0))
while len(qolgan) < uzunlik:
self.bufer = qolgan
if not self._yana_oqi():
return None
qolgan = self.bufer
tana, self.bufer = qolgan[:uzunlik], qolgan[uzunlik:]
return Sorov(usul, yol, versiya, sarlavhalar, tana)
CHEGARA nima uchun kerak?oqi() ichidagi halqa \r\n\r\n kelguncha o'qiydi. Agar
mijoz uni hech qachon yubormasa, halqa cheksiz davom
etadi va bufer xotirani to'ldiradi.
Bu haqiqiy hujum turi - sekin so'rov yuborib, serverning resurslarini band qilish.
Shuning uchun ikki chegara majburiy:
| Chegara | Qanday qo'yiladi |
|---|---|
| Hajm | CHEGARA - buferdagi baytlar soni |
| Vaqt | settimeout - soket darajasida |
Ikkinchisini serverni qurgan joyda qo'yamiz.
Diqqat qiling: self.bufer so'rovdan keyin qolgan
baytlarni saqlaydi. Bu doimiy ulanish uchun zarur: bitta
recv da ikkita so'rov kelgan bo'lishi mumkin.
2-qism: javob #
IZOHLAR = {
200: "OK", 204: "No Content", 400: "Bad Request",
403: "Forbidden", 404: "Not Found",
405: "Method Not Allowed", 500: "Internal Server Error",
}
class Javob:
def __init__(self, kod=200, tana=b"", tur="text/plain; charset=utf-8"):
if isinstance(tana, str):
tana = tana.encode()
self.kod = kod
self.tana = tana
self.sarlavhalar = {"Content-Type": tur}
def baytlar(self, tirik):
izoh = IZOHLAR.get(self.kod, "Unknown")
self.sarlavhalar["Content-Length"] = str(len(self.tana))
self.sarlavhalar["Connection"] = "keep-alive" if tirik else "close"
qatorlar = [f"HTTP/1.1 {self.kod} {izoh}"]
for nom, qiymat in self.sarlavhalar.items():
qatorlar.append(f"{nom}: {qiymat}")
bosh = "\r\n".join(qatorlar) + "\r\n\r\n"
return bosh.encode() + self.tana
Content-Length har doim len(self.tana) dan olinadi -
qo'lda yozilmaydi. 12-bo'limdagi ogohlantirishni eslang:
noto'g'ri uzunlik so'rovni ajratish zaifligiga olib keladi.
3-qism: marshrutlash va statik fayllar #
class Marshrutlovchi:
def __init__(self):
self.qoidalar = {}
def qosh(self, usul, yol, ishlovchi):
self.qoidalar[(usul, yol)] = ishlovchi
def top(self, sorov):
ishlovchi = self.qoidalar.get((sorov.usul, sorov.yol))
if ishlovchi:
return ishlovchi
# Yo'l bor, lekin usul boshqa bo'lsa - 405
for usul, yol in self.qoidalar:
if yol == sorov.yol:
return lambda s: Javob(405, "Usul qo'llab-quvvatlanmaydi")
return None
def statik_ishlovchi(ildiz):
def ishlovchi(sorov):
nisbiy = sorov.yol[len("/statik/"):]
# XAVFSIZLIK: yo'lni normallashtirib, ildizdan chiqmasligini
# tekshiramiz - aks holda ../../etc/passwd o'qib ketiladi
toliq = os.path.realpath(os.path.join(ildiz, nisbiy))
if not toliq.startswith(os.path.realpath(ildiz) + os.sep):
return Javob(403, "Ruxsat yo'q")
if not os.path.isfile(toliq):
return Javob(404, "Fayl topilmadi")
with open(toliq, "rb") as f:
return Javob(200, f.read(), "text/plain; charset=utf-8")
return ishlovchi
Sodda yechim shunday ko'rinadi:
toliq = ildiz + sorov.yol[len("/statik/"):]
open(toliq, "rb")
Bu jiddiy zaiflik. /statik/../../etc/passwd so'ralsa,
server tizim fayllarini o'qib berib yuboradi.
To'g'ri himoya ikki qadamdan iborat:
| Qadam | Nima qiladi |
|---|---|
os.path.realpath | .. va ramziy havolalarni yechadi |
startswith(ildiz + os.sep) | Natija ildiz ichida qolganini tekshiradi |
os.sep ni qo'shish ham muhim: usiz /var/www-maxfiy yo'li
/var/www tekshiruvidan o'tib ketadi.
Faqat satrdan .. ni olib tashlash yetarli emas -
kodlangan shakllari (%2e%2e) va ramziy havolalar qoladi.
4-qism: server #
class Server:
def __init__(self, port, marshrutlovchi, statik=None):
self.marshrutlovchi = marshrutlovchi
self.statik = statik
self.soket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.soket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.soket.bind(("127.0.0.1", port))
self.soket.listen(8)
self.jurnal = []
def ulanishni_ishla(self, ulanish):
oquvchi = SorovOqiuvchi(ulanish)
try:
while True:
try:
sorov = oquvchi.oqi()
except ValueError:
ulanish.sendall(Javob(400, "Buzuq so'rov").baytlar(False))
return
if sorov is None:
return
if self.statik and sorov.yol.startswith("/statik/"):
ishlovchi = self.statik
else:
ishlovchi = self.marshrutlovchi.top(sorov)
if ishlovchi is None:
javob = Javob(404, "Sahifa topilmadi")
else:
try:
javob = ishlovchi(sorov)
except Exception:
javob = Javob(500, "Ichki xato")
tirik = sorov.tirik_qolsinmi()
self.jurnal.append(f"{sorov.usul} {sorov.yol} -> {javob.kod}")
ulanish.sendall(javob.baytlar(tirik))
if not tirik:
return
finally:
ulanish.close()
def nechta_ishla(self, nechta):
for _ in range(nechta):
ulanish, _ = self.soket.accept()
self.ulanishni_ishla(ulanish)
def yop(self):
self.soket.close()
ishlovchi(sorov) chaqiruvi try ichida turibdi. Sabab
oddiy: sizning marshrut kodingizda xato bo'lishi mumkin.
Usiz bitta ZeroDivisionError butun serverni to'xtatardi -
va qolgan barcha foydalanuvchilar xizmatsiz qolardi.
Bu yerda except Exception o'rinli: biz aniq bir xatoni
emas, "ishlovchidagi har qanday nosozlikni" ushlayapmiz va
uni 500 ga aylantiryapmiz.
Ishlab chiqarishda bu yerda xatoni jurnalga yozish ham shart - aks holda nosozlik jimgina yo'qoladi.
Serverni ishga tushiramiz #
import tempfile
PORT = 9980
ildiz = tempfile.mkdtemp()
with open(os.path.join(ildiz, "salom.txt"), "w") as f:
f.write("statik fayl mazmuni")
m = Marshrutlovchi()
m.qosh("GET", "/", lambda s: Javob(200, "Bosh sahifa"))
m.qosh("GET", "/salom", lambda s: Javob(200, "Salom, tarmoq!"))
m.qosh("POST", "/echo", lambda s: Javob(200, s.tana))
m.qosh("GET", "/xato", lambda s: 1 / 0)
server = Server(PORT, m, statik_ishlovchi(ildiz))
oqim = threading.Thread(target=server.nechta_ishla, args=(7,), daemon=True)
oqim.start()
def sorov_yubor(xom):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect(("127.0.0.1", PORT))
s.sendall(xom)
javob = b""
while True:
b = s.recv(4096)
if not b:
break
javob += b
s.close()
bosh, tana = javob.split(b"\r\n\r\n", 1)
return bosh.decode().split("\r\n")[0], tana.decode()
SINOVLAR = [
("bosh sahifa", b"GET / HTTP/1.1\r\nHost: x\r\n"
b"Connection: close\r\n\r\n"),
("mavjud yo'l", b"GET /salom HTTP/1.1\r\nHost: x\r\n"
b"Connection: close\r\n\r\n"),
("yo'q sahifa", b"GET /yoq HTTP/1.1\r\nHost: x\r\n"
b"Connection: close\r\n\r\n"),
("noto'g'ri usul", b"DELETE /salom HTTP/1.1\r\nHost: x\r\n"
b"Connection: close\r\n\r\n"),
("POST tana bilan", b"POST /echo HTTP/1.1\r\nHost: x\r\n"
b"Content-Length: 11\r\nConnection: close\r\n\r\n"
b"salom tana!"),
("statik fayl", b"GET /statik/salom.txt HTTP/1.1\r\nHost: x\r\n"
b"Connection: close\r\n\r\n"),
("yo'ldan chiqish", b"GET /statik/../../etc/passwd HTTP/1.1\r\n"
b"Host: x\r\nConnection: close\r\n\r\n"),
]
for nom, xom in SINOVLAR:
holat, tana = sorov_yubor(xom)
print(f"{nom:<18} {holat:<26} {tana[:28]}")
oqim.join()
server.yop()
print()
print("server jurnali:")
for q in server.jurnal:
print(" ", q)
bosh sahifa HTTP/1.1 200 OK Bosh sahifa
mavjud yo'l HTTP/1.1 200 OK Salom, tarmoq!
yo'q sahifa HTTP/1.1 404 Not Found Sahifa topilmadi
noto'g'ri usul HTTP/1.1 405 Method Not Allowed Usul qo'llab-quvvatlanmaydi
POST tana bilan HTTP/1.1 200 OK salom tana!
statik fayl HTTP/1.1 200 OK statik fayl mazmuni
yo'ldan chiqish HTTP/1.1 403 Forbidden Ruxsat yo'q
server jurnali:
GET / -> 200
GET /salom -> 200
GET /yoq -> 404
DELETE /salom -> 405
POST /echo -> 200
GET /statik/salom.txt -> 200
GET /statik/../../etc/passwd -> 403
Yettita sinov, yettita to'g'ri javob. Eng muhimi - oxirgisi: yo'ldan chiqishga urinish 403 bilan to'xtatildi.
Doimiy ulanish ishlayaptimi? #
13-bo'limda keep-alive ning foydasini ko'rgandik. Serverimiz uni qo'llab-quvvatlaydimi?
PORT2 = 9981
m2 = Marshrutlovchi()
m2.qosh("GET", "/", lambda s: Javob(200, "javob"))
server2 = Server(PORT2, m2)
oqim2 = threading.Thread(target=server2.nechta_ishla, args=(1,), daemon=True)
oqim2.start()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect(("127.0.0.1", PORT2))
# Bitta ulanishda uchta so'rov
for i in range(1, 4):
oxirgi = (i == 3)
ulanish = "close" if oxirgi else "keep-alive"
s.sendall(f"GET / HTTP/1.1\r\nHost: x\r\n"
f"Connection: {ulanish}\r\n\r\n".encode())
javob = s.recv(4096).decode()
birinchi = javob.split("\r\n")[0]
ulanish_sarlavhasi = [q for q in javob.split("\r\n")
if q.startswith("Connection")][0]
print(f" {i}-so'rov: {birinchi} | {ulanish_sarlavhasi}")
s.close()
oqim2.join()
server2.yop()
print()
print("qabul qilingan ulanishlar soni: 1")
print("bajarilgan so'rovlar soni :", len(server2.jurnal))
1-so'rov: HTTP/1.1 200 OK | Connection: keep-alive
2-so'rov: HTTP/1.1 200 OK | Connection: keep-alive
3-so'rov: HTTP/1.1 200 OK | Connection: close
qabul qilingan ulanishlar soni: 1
bajarilgan so'rovlar soni : 3
Bitta accept, uchta so'rov. Ikkita qo'l berish tejaldi.
Tana bo'laklab kelsa-chi? #
Yuqoridagi sinovda POST tanasi kichik edi va sarlavhalar
bilan birga bitta recv da keldi. Bu eng xavfli holat:
kod ishlayotgandek ko'rinadi, lekin haqiqiy sinovdan
o'tmagan.
Tanani ataylab bo'laklab yuboramiz:
PORT3 = 9982
m3 = Marshrutlovchi()
m3.qosh("POST", "/echo", lambda s: Javob(200, f"{len(s.tana)} bayt keldi"))
server3 = Server(PORT3, m3)
oqim3 = threading.Thread(target=server3.nechta_ishla, args=(1,), daemon=True)
oqim3.start()
TANA = b"x" * 5000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect(("127.0.0.1", PORT3))
# Avval faqat sarlavhalarni yuboramiz - tana hali yo'q
s.sendall(f"POST /echo HTTP/1.1\r\nHost: x\r\n"
f"Content-Length: {len(TANA)}\r\n"
f"Connection: close\r\n\r\n".encode())
# Tanani KICHIK bo'laklarda, alohida yuboramiz
for i in range(0, len(TANA), 500):
s.sendall(TANA[i:i + 500])
javob = b""
while True:
b = s.recv(4096)
if not b:
break
javob += b
s.close()
oqim3.join()
server3.yop()
bosh, tana = javob.split(b"\r\n\r\n", 1)
print("yuborilgan tana hajmi:", len(TANA))
print("javob :", tana.decode())
print("to'liq o'qildimi :", tana.decode().startswith(str(len(TANA))))
yuborilgan tana hajmi: 5000
javob : 5000 bayt keldi
to'liq o'qildimi : True
Ochig'ini aytamiz: bu darslikni tayyorlash paytida
oqi() metodidagi halqada bitta qator tushib qolgan edi -
qolgan = self.bufer.
Usiz halqa yangi baytlarni ko'rmaydi va cheksiz aylanadi.
Eng yomoni shundaki, yuqoridagi yettita sinovning hammasi
o'tib ketgan: POST tanasi 11 bayt edi va sarlavhalar
bilan birga bitta recv ga sig'ib ketgan. Xato faqat tana
alohida kelganda ochiladi.
Aynan shu 8-bo'limdagi ogohlantirishning amaldagi ko'rinishi:
| Qachon | Nima ko'rinadi |
|---|---|
| Kichik so'rov, tez tarmoq | Hammasi ishlaydi |
| Katta tana yoki sekin tarmoq | Server qotib qoladi |
Xulosa: bo'laklab kelish holatini ataylab sinash kerak. Uni tasodifga qoldirsangiz, xato ishlab chiqarishda, eng noqulay paytda chiqadi.
Kod o'rganish uchun yozildi va unda ishlab chiqarish uchun zarur ko'p narsa yo'q:
| Yetishmayapti | Nima uchun kerak |
|---|---|
| Bir vaqtda ko'p mijoz | Hozir ketma-ket ishlaydi (18-bo'lim) |
| Soket kutish vaqti | Sekin mijoz serverni ushlab turadi |
| TLS | Hamma narsa ochiq ketadi (15-bo'lim) |
| So'rov tezligini cheklash | Oddiy toshqin serverni yiqitadi |
Transfer-Encoding: chunked | Standart talab qiladi |
| To'liq jurnal va monitoring | Nosozlikni topib bo'lmaydi |
| MIME turlarini aniqlash | Hamma fayl text/plain bo'lib ketyapti |
To'g'ri yondashuv - ilovani sinalgan server ortiga qo'yish (19-bo'limdagi teskari proksi) va TLS ni o'shanga topshirish.
Bu kodning qiymati boshqa narsada: endi o'sha "sinalgan serverlar" ichida nima borligini bilasiz.
- Serverni ishga tushiring va barcha sinovlarni bajaring.
/vaqtmarshrutini qo'shing va joriy vaqtni qaytaring./xatomarshrutiga so'rov yuboring - qanday kod qaytdi?- Fayl kengaytmasiga qarab
Content-Typeni aniqlaydigan funksiya yozing. SorovOqiuvchiga so'rov qatori uzunligi chegarasini qo'shing.- Ulanishga
settimeoutqo'shing va sekin mijozni sinang. - Har ulanishni alohida oqimda ishlaydigan qiling.
- 18-bo'limdagi
selectorsyondashuviga o'tkazing. - So'rovlar sonini sanaydigan
/statistikamarshrutini qo'shing. - Yo'ldan chiqishga uchta turli urinish yozing va hammasi to'xtatilishini tekshiring.
Xulosa #
- HTTP serveri - bu
socketustidagi matn tahlilchisi, boshqa hech narsa emas. - Eng muhim qism - bayt oqimidan to'liq so'rovni ajratib olish.
- Buferda so'rovdan keyin qolgan baytlar saqlanadi - doimiy ulanish uchun zarur.
- Cheksiz o'qishning oldini olish uchun hajm va vaqt chegarasi majburiy.
Content-Lengthhar doimlen(tana)dan hisoblanadi, qo'lda yozilmaydi.- Statik fayllarda yo'ldan chiqish eng ko'p uchraydigan zaiflik.
- Himoya:
realpathbilan normallashtirish va ildiz ichida qolganini tekshirish. - Ishlovchidagi xato
trybilan ushlanadi va 500 ga aylantiriladi. - Doimiy ulanish
Connectionsarlavhasi va HTTP versiyasiga qarab hal qilinadi. - O'quv serverini internetga chiqarmang - uni teskari proksi ortiga qo'ying.
Darslik yakunida #
Yigirma bo'lim oldin brauzerga manzil yozgandan sahifa ochilgunicha nima sodir bo'lishini bilmas edingiz. Endi butun yo'lni ko'rasiz:
| Qadam | Bo'lim |
|---|---|
| Nom manzilga aylanadi | 11 - DNS |
| Manzil qaysi tarmoqda ekani aniqlanadi | 4 - IP |
| Qo'shni bo'lmasa, marshrutizatorga beriladi | 5 - marshrutlash |
| Kadr yig'iladi va yuboriladi | 3 - Ethernet |
| NAT tashqi manzilga o'giradi | 10 - NAT |
| TCP ulanish o'rnatiladi | 8 - TCP |
| TLS qo'l berish bo'ladi | 15 - TLS |
| HTTP so'rovi yuboriladi | 12 - HTTP |
| Javob keladi va sahifa chiziladi | 13 - versiyalar |
Bundan keyin qayerga borish mumkin:
- "Linux server administratsiyasi" - shu bilimlarni haqiqiy serverda qo'llash;
- "Kiberxavfsizlik asoslari" - tarmoq hujumlari va himoya;
- "Amaliy kriptografiya" - TLS ostidagi matematika;
- "Node.js" yoki "PHP" - shu protokollar ustida ilova qurish.
Tarmoq - bu sehr emas. Bu kelishuvlar, baytlar va bir necha oddiy g'oya. Endi ularni bilasiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.