8-bo‘lim
TCP va uning ishonchliligi
Uch qadamli qo'l berish, bayt oqimi tushunchasi, tartib raqamlari va ACK, qayta yuborish hamda xabar chegarasini o'zingiz qurish.
Ushbu bo‘lim mundarijasi
UDP hech narsani kafolatlamaydi. TCP esa aksincha - u to'rt narsani va'da qiladi:
| Va'da | Qanday bajariladi |
|---|---|
| Yetib boradi | Tasdiq va qayta yuborish |
| Tartibda keladi | Har baytning raqami bor |
| Takrorlanmaydi | Raqam bo'yicha tekshiriladi |
| Buzilmaydi | Nazorat yig'indisi |
Buning evaziga u murakkabroq va sekinroq.
Uch qadamli qo'l berish #
TCP da ma'lumot yuborishdan oldin ulanish o'rnatiladi. Bu uch xabar almashish orqali bo'ladi.
class Tomon:
def __init__(self, nom, boshlangich_seq):
self.nom = nom
self.holat = "YOPIQ"
self.seq = boshlangich_seq
self.kutilayotgan = None
def holat_qoy(self, yangi):
print(f" {self.nom:<7} {self.holat} -> {yangi}")
self.holat = yangi
mijoz = Tomon("mijoz", 1000)
server = Tomon("server", 5000)
print("1-qadam: mijoz SYN yuboradi")
mijoz.holat_qoy("SYN-YUBORILDI")
server.holat_qoy("TINGLAMOQDA")
print(f" SYN seq={mijoz.seq}")
server.kutilayotgan = mijoz.seq + 1
print("2-qadam: server SYN-ACK bilan javob beradi")
server.holat_qoy("SYN-QABUL")
print(f" SYN-ACK seq={server.seq} ack={server.kutilayotgan}")
mijoz.kutilayotgan = server.seq + 1
print("3-qadam: mijoz ACK yuboradi")
mijoz.seq += 1
mijoz.holat_qoy("ULANGAN")
print(f" ACK seq={mijoz.seq} ack={mijoz.kutilayotgan}")
server.seq += 1
server.holat_qoy("ULANGAN")
print()
print("ikkala tomon ham ULANGAN holatida:",
mijoz.holat == server.holat == "ULANGAN")
print("mijoz keyingi bayt raqami :", mijoz.seq)
print("server keyingi bayt raqami:", server.seq)
1-qadam: mijoz SYN yuboradi
mijoz YOPIQ -> SYN-YUBORILDI
server YOPIQ -> TINGLAMOQDA
SYN seq=1000
2-qadam: server SYN-ACK bilan javob beradi
server TINGLAMOQDA -> SYN-QABUL
SYN-ACK seq=5000 ack=1001
3-qadam: mijoz ACK yuboradi
mijoz SYN-YUBORILDI -> ULANGAN
ACK seq=1001 ack=5001
server SYN-QABUL -> ULANGAN
ikkala tomon ham ULANGAN holatida: True
mijoz keyingi bayt raqami : 1001
server keyingi bayt raqami: 5001
Misolda 1000 va 5000 deb qo'ydik, lekin haqiqiy TCP da boshlang'ich tartib raqami tasodifiy tanlanadi.
Sabab xavfsizlik: raqam oldindan taxmin qilinsa, tashqaridagi hujumchi ulanishga soxta paket qo'shib qo'yishi mumkin.
1990-yillarda ba'zi tizimlar raqamni oddiy sanoqchi bilan berardi va bu jiddiy zaiflikka olib kelgan edi.
Qo'l berishning ikkinchi qadamidan keyin server holatni eslab qoladi va uchinchi xabarni kutadi.
Hujumchi minglab SYN yuborib, ACK ni hech qachon yubormasligi mumkin. Server esa har biri uchun joy ajratib, kutib qoladi.
Himoya - SYN cookie: server holatni umuman saqlamaydi, uning o'rniga kerakli ma'lumotni tartib raqamining o'ziga "yashirib" yuboradi. Uchinchi xabar kelganda holatni tiklab oladi.
Bu Linux da standart yoqilgan.
TCP - bu bayt oqimi #
UDP da nima yuborsangiz, o'sha butunligicha kelardi. TCP da bunday emas. TCP uchun ma'lumot - uzluksiz bayt oqimi.
import socket
PORT = 9400
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", PORT))
server.listen(1)
mijoz = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mijoz.connect(("127.0.0.1", PORT))
ulanish, _ = server.accept()
# Uchta alohida sendall - UDP da bu uchta datagramma bo'lardi
mijoz.sendall(b"birinchi")
mijoz.sendall(b"ikkinchi")
mijoz.sendall(b"uchinchi")
mijoz.shutdown(socket.SHUT_WR)
# Endi to'rt baytlik bo'laklarda o'qiymiz
print("to'rt baytlab o'qiymiz:")
bolaklar = []
while True:
bolak = ulanish.recv(4)
if not bolak:
break
bolaklar.append(bolak)
print(" ", bolak)
print()
print("o'qishlar soni:", len(bolaklar))
print("birlashtirilgani:", b"".join(bolaklar).decode())
print("xabar chegarasi saqlandimi:", b"birinchi" in bolaklar)
mijoz.close()
ulanish.close()
server.close()
to'rt baytlab o'qiymiz:
b'biri'
b'nchi'
b'ikki'
b'nchi'
b'uchi'
b'nchi'
o'qishlar soni: 6
birlashtirilgani: birinchiikkinchiuchinchi
xabar chegarasi saqlandimi: False
birinchi so'zi biri va nchi ga bo'linib ketdi. TCP
uchun bu shunchaki 24 bayt - qayerda bir xabar tugab,
ikkinchisi boshlanishi uni qiziqtirmaydi.
Yangi boshlovchilar recv bir marta chaqirilsa butun xabar
keladi deb o'ylaydi.
Sinovda bu ishlaydi ham: kichik xabar, tez tarmoq, hammasi bir bo'lakda keladi.
Keyin ishlab chiqarishda, katta yuk ostida, xabar ikkiga bo'linadi va dastur tushunarsiz tarzda buziladi.
Qoida: recv so'ralgan hajmdan kam qaytarishi mumkin va bu
normal. Kod har doim shunga tayyor bo'lishi kerak.
Xabar chegarasini o'zingiz quring #
Yechim sodda: har xabar oldiga uzunligini yozish.
import socket
import struct
PORT = 9401
def xabar_yubor(s, matn):
"""Oldiga 4 baytlik uzunlik qo'yib yuboradi."""
yuk = matn.encode()
s.sendall(struct.pack("!I", len(yuk)) + yuk)
def aniq_oqi(s, nechta):
"""Kerakli baytlar to'liq kelguncha o'qiydi."""
bufer = b""
while len(bufer) < nechta:
bolak = s.recv(nechta - len(bufer))
if not bolak:
raise ConnectionError("ulanish uzildi")
bufer += bolak
return bufer
def xabar_oqi(s):
uzunlik = struct.unpack("!I", aniq_oqi(s, 4))[0]
return aniq_oqi(s, uzunlik).decode()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", PORT))
server.listen(1)
mijoz = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mijoz.connect(("127.0.0.1", PORT))
ulanish, _ = server.accept()
for matn in ["birinchi", "ikkinchi", "uchinchi xabar uzunroq"]:
xabar_yubor(mijoz, matn)
for i in range(3):
print(f"{i + 1}-xabar: {xabar_oqi(ulanish)}")
mijoz.close()
ulanish.close()
server.close()
1-xabar: birinchi
2-xabar: ikkinchi
3-xabar: uchinchi xabar uzunroq
Endi xabarlar butun holda keladi. E'tibor bering: butun sir
aniq_oqi funksiyasida - u kerakli baytlar to'liq kelguncha
o'qiydi.
Bu naqshni yodlab qo'ying. TCP ustida ishlaydigan har bir protokol shu muammoni bir yo'l bilan hal qiladi:
| Protokol | Chegarani qanday belgilaydi |
|---|---|
| HTTP | Content-Length sarlavhasi |
| Redis | Uzunlik prefiksi |
| SMTP | Alohida qatorda nuqta |
| WebSocket | Kadr sarlavhasidagi uzunlik |
Yo'qolgan paket qanday tiklanadi? #
Har baytning raqami bor. Qabul qiluvchi har safar keyingi kutilayotgan bayt raqamini qaytaradi.
XABAR = "Salom Husanboy"
BOLAK = 4
# Jo'natuvchi har baytga raqam beradi
paketlar = []
raqam = 1
for i in range(0, len(XABAR), BOLAK):
bolak = XABAR[i:i + BOLAK]
paketlar.append({"seq": raqam, "matn": bolak})
raqam += len(bolak)
print("jo'natiladigan paketlar:")
for p in paketlar:
print(f" seq={p['seq']:<3} [{p['matn']}]")
# Ikkinchi paket yo'lda yo'qoladi
YOQOLGAN = 2
print()
print(f"--- {YOQOLGAN}-paket yo'lda yo'qoldi")
qabul = {}
kutilayotgan = 1
for n, p in enumerate(paketlar, 1):
if n == YOQOLGAN:
continue
qabul[p["seq"]] = p["matn"]
# Qabul qiluvchi HAR DOIM "keyingi kutilayotgan bayt" ni aytadi
while kutilayotgan in qabul:
kutilayotgan += len(qabul[kutilayotgan])
print(f" seq={p['seq']:<3} keldi -> ACK {kutilayotgan}")
print()
print("takroriy ACK soni jo'natuvchiga yo'qolishni bildiradi")
print("jo'natuvchi seq =", kutilayotgan, "dan qayta yuboradi")
# Qayta yuborish
p = paketlar[YOQOLGAN - 1]
qabul[p["seq"]] = p["matn"]
while kutilayotgan in qabul:
kutilayotgan += len(qabul[kutilayotgan])
print(f" seq={p['seq']:<3} qayta keldi -> ACK {kutilayotgan}")
tiklangan = "".join(qabul[k] for k in sorted(qabul))
print()
print("tiklangan xabar:", tiklangan)
print("asl bilan bir xilmi:", tiklangan == XABAR)
jo'natiladigan paketlar:
seq=1 [Salo]
seq=5 [m Hu]
seq=9 [sanb]
seq=13 [oy]
--- 2-paket yo'lda yo'qoldi
seq=1 keldi -> ACK 5
seq=9 keldi -> ACK 5
seq=13 keldi -> ACK 5
takroriy ACK soni jo'natuvchiga yo'qolishni bildiradi
jo'natuvchi seq = 5 dan qayta yuboradi
seq=5 qayta keldi -> ACK 15
tiklangan xabar: Salom Husanboy
asl bilan bir xilmi: True
Diqqat: seq=9 va seq=13 yetib keldi, lekin ACK baribir
5 bo'lib qoldi. Chunki 5-baytdan boshlangan bo'lak hali yo'q.
Uchta bir xil ACK ketma-ket kelishi jo'natuvchi uchun aniq signal: o'sha paket yo'qolgan. U kutish vaqti tugashini kutmasdan darhol qayta yuboradi. Bunga tez qayta yuborish (fast retransmit) deyiladi.
Bu kichik, lekin nozik tanlov.
"Oxirgi kelgan" desangiz, jo'natuvchi qaysi baytdan davom etishni o'zi hisoblashi kerak bo'lardi.
"Keyingi kutilayotgan" esa to'g'ridan-to'g'ri javob beradi: shu raqamdan yuboravering.
Bundan tashqari bu to'plamli tasdiq imkonini beradi: bitta ACK bir necha paketni birdan tasdiqlaydi.
Ulanishni yopish #
Ochish uch qadam bo'lsa, yopish to'rt qadam:
| Qadam | Kim | Ma'nosi |
|---|---|---|
| 1 | A | FIN - "menda yuboradigan narsa qolmadi" |
| 2 | B | ACK - "eshitdim" |
| 3 | B | FIN - "menda ham qolmadi" |
| 4 | A | ACK - "eshitdim" |
Sabab shundaki, TCP ikki yo'nalishli. A gapirishni tugatganida B hali gapirayotgan bo'lishi mumkin. Shuning uchun har yo'nalish alohida yopiladi.
Yuqoridagi misolda mijoz.shutdown(socket.SHUT_WR) aynan shuni
qildi: "men yozishni tugatdim, lekin o'qishda davom etaman".
TIME_WAIT nima uchun kerak?Yopilgan ulanish darhol yo'qolmaydi - u bir muddat (odatda
1-2 daqiqa) TIME_WAIT holatida turadi.
Sabab: kechikib qolgan paketlar hali yo'lda bo'lishi mumkin. Agar o'sha port darhol qayta ishlatilsa, eski paket yangi ulanishga tushib qolishi mumkin edi.
Serverni qayta ishga tushirganda "Address already in use"
xatosi aynan shundan chiqadi. Yechim - SO_REUSEADDR, biz
misollarda aynan shuni ishlatdik.
- Qo'l berish misolini ishga tushiring.
- Unga to'rt qadamli yopilishni ham qo'shing.
- Bayt oqimi misolini ishga tushiring va
recv(4)nirecv(100)ga o'zgartiring. - Nima uchun natija o'zgarganini tushuntiring.
- Uzunlik prefiksli misolni ishga tushiring.
aniq_oqidagiwhileni olib tashlang va nima buzilishini yozing.- To'rt bayt o'rniga ikki baytlik uzunlik ishlating - cheklov nima bo'ldi?
- Yo'qolgan paket misolida
YOQOLGANni 3 ga o'zgartiring. - Ikkita paketni yo'qotib ko'ring va ACK larni kuzating.
- UDP va TCP ni jadval qilib solishtiring.
Xulosa #
- TCP to'rt narsani kafolatlaydi: yetkazish, tartib, takrorlanmaslik, butunlik.
- Ulanish uch qadamli qo'l berish bilan ochiladi: SYN, SYN-ACK, ACK.
- Boshlang'ich tartib raqami xavfsizlik uchun tasodifiy tanlanadi.
- TCP - bayt oqimi: xabar chegarasi saqlanmaydi.
recvso'ralgan hajmdan kam qaytarishi mumkin - kod shunga tayyor bo'lsin.- Chegarani protokol o'zi qo'yadi: uzunlik prefiksi yoki ajratuvchi bilan.
- Har baytning tartib raqami bor; ACK "keyingi kutilayotgan bayt" ni bildiradi.
- Takroriy ACK yo'qolish signali - tez qayta yuborish ishga tushadi.
- Yopilish to'rt qadam, chunki har yo'nalish alohida yopiladi.
TIME_WAITeski paketlar aralashib ketmasligi uchun kerak;SO_REUSEADDRyordam beradi.
Keyingi bo'limda portlar va soket API si bilan yaqindan tanishamiz.
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.