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.

🕑 9 daqiqa o‘qish 📄 858 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Uch qadamli qo'l berish
  2. TCP - bu bayt oqimi
  3. Xabar chegarasini o'zingiz quring
  4. Yo'qolgan paket qanday tiklanadi?
  5. Ulanishni yopish
  6. Xulosa

UDP hech narsani kafolatlamaydi. TCP esa aksincha - u to'rt narsani va'da qiladi:

Va'daQanday bajariladi
Yetib boradiTasdiq va qayta yuborish
Tartibda keladiHar baytning raqami bor
TakrorlanmaydiRaqam bo'yicha tekshiriladi
BuzilmaydiNazorat 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.

Uch qadamli qo'l berish Mijoz Server SYN seq=1000 "gaplashamizmi? mening raqamim 1000" SYN-ACK seq=5000 ack=1001 "ha. meniki 5000. sendan 1001 ni kutaman" ACK seq=1001 ack=5001 "qabul qildim. sendan 5001 ni kutaman" Nima uchun aynan uchta? Har tomon o'z boshlang'ich raqamini aytishi VA uning qabul qilinganini bilishi kerak
Ikki xabar yetmaydi: ikkinchi tomon ham tasdiq olishi shart
Python
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)
Natija
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
Boshlang'ich raqam nima uchun tasodifiy?

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.

SYN toshqini

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.

Python
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()
Natija
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.

Bu eng ko'p uchraydigan tarmoq xatosi

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.

Python
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()
Natija
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:

ProtokolChegarani qanday belgilaydi
HTTPContent-Length sarlavhasi
RedisUzunlik prefiksi
SMTPAlohida qatorda nuqta
WebSocketKadr sarlavhasidagi uzunlik

Yo'qolgan paket qanday tiklanadi? #

Har baytning raqami bor. Qabul qiluvchi har safar keyingi kutilayotgan bayt raqamini qaytaradi.

Python
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)
Natija
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.

Nima uchun "keyingi kutilayotgan", "oxirgi kelgan" emas?

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:

QadamKimMa'nosi
1AFIN - "menda yuboradigan narsa qolmadi"
2BACK - "eshitdim"
3BFIN - "menda ham qolmadi"
4AACK - "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.

Amaliy topshiriq
  1. Qo'l berish misolini ishga tushiring.
  2. Unga to'rt qadamli yopilishni ham qo'shing.
  3. Bayt oqimi misolini ishga tushiring va recv(4) ni recv(100) ga o'zgartiring.
  4. Nima uchun natija o'zgarganini tushuntiring.
  5. Uzunlik prefiksli misolni ishga tushiring.
  6. aniq_oqi dagi while ni olib tashlang va nima buzilishini yozing.
  7. To'rt bayt o'rniga ikki baytlik uzunlik ishlating - cheklov nima bo'ldi?
  8. Yo'qolgan paket misolida YOQOLGAN ni 3 ga o'zgartiring.
  9. Ikkita paketni yo'qotib ko'ring va ACK larni kuzating.
  10. 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.
  • recv so'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_WAIT eski paketlar aralashib ketmasligi uchun kerak; SO_REUSEADDR yordam beradi.

Keyingi bo'limda portlar va soket API si bilan yaqindan tanishamiz.

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.