9.13. TCP-socketit

TCP-socketeja on kahta muotoa, jotka näyttävät erilaisilta mutta jakavat saman taustalla olevan tyypin: asiakas-socketit, jotka käyttävät connect() -metodia muodostaakseen yhteyden etäpalvelimeen, ja palvelin-socketit, jotka käyttävät bind()-, listen()- ja accept() -metodeja saapuville yhteyksille. Molemmat roolit käyttävät samaa socket -luokkaa, joka esiteltiin sivulla Socket-objektit; vain niillä kutsuttavat metodit eroavat.

9.13.1. TCP-asiakas

Yksinkertaisin asiakas avaa yhteyden, lähettää pyynnön, lukee vastauksen ja sulkee yhteyden:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))

s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)

s.close()

connect() suorittaa kolmivaiheisen kättelyn, joka käsiteltiin sivulla TCP – luotettava tavuvirta, ja palaa, kun yhteys on auki. send() kirjoittaa tavuja yhteyteen; recv() lukee yhteydestä enintään annetun määrän tavuja. Kun sovellus on valmis, close() sulkee yhteyden.

Sama skripti with -lauseen idiomilla käärittynä sivulta Socket-objektit, jolloin socket suljetaan, vaikka jokin nostaisi poikkeuksen:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("192.168.1.20", 9000))
    s.send(b"hello\n")
    print(s.recv(1024))

9.13.1.1. Lukeminen loppuun asti

Yksittäinen recv() palauttaa enintään pyydetyn määrän tavuja – se voi palauttaa vähemmän, koska TCP on virta eikä viestien jono. Sovelluksen on jatkettava lukemista, kunnes se on saanut koko vastauksen:

chunks = []
while True:
    chunk = s.recv(1024)
    if not chunk:                  # empty bytes -> other side closed
        break
    chunks.append(chunk)
reply = b"".join(chunks)

Silmukka päättyy, kun recv() palauttaa tyhjän bytes -arvon. Näin käy, kun toinen osapuoli on sulkenut siististi oman puolensa yhteydestä; tämäntyyppisessä protokollassa sovellus tulkitsee ”virran lopun” samaksi kuin ”viestin loppu”.

9.13.1.2. Lähettäminen loppuun asti

Päinvastainen varaus koskee send() -metodia: se saattaa lähettää vähemmän tavuja kuin pyydettiin ja palauttaa todella kirjoitettujen tavujen määrän. Suurten hyötykuormien kohdalla lähetä uudelleen lähettämättä jäänyt loppuosa:

payload = some_big_bytes
while payload:
    n = s.send(payload)
    payload = payload[n:]

sendall() tekee silmukan sisäisesti, joten useimmat koodit voivat vain kutsua sitä ja välttää manuaalisen uudelleenyrityksen:

s.sendall(some_big_bytes)

9.13.2. TCP-palvelin

Palvelinpuoli on neljä vaihetta: varaa portti, vaihda socket kuuntelutilaan, hyväksy yhteydet yksi kerrallaan ja keskustele kullakin hyväksytyllä socketilla. Minimaalinen echo-palvelin:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")

while True:
    conn, addr = server.accept()
    print("connection from", addr)

    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.send(data)            # echo back

    conn.close()

Vaihe vaiheelta:

  • bind() varaa kameralta isännän ja portin. "0.0.0.0" hyväksyy yhteyksiä millä tahansa rajapinnalla; sen korvaaminen tietyllä IP-osoitteella rajoittaa kuuntelijan kyseiseen rajapintaan.

  • listen() vaihtaa socketin tavallisesta socketista kuuntelevaksi socketiksi. Argumentti on backlog eli kuinka monta odottavaa yhteyttä MicroPython jonottaa sovelluksen ollessa varattuna. Valitse pieni luku; 1 riittää useimmissa tapauksissa.

  • accept() estyy, kunnes asiakas muodostaa yhteyden, ja palauttaa sitten (conn, addr): uuden socketin, joka edustaa tätä yhtä yhteyttä, sekä asiakkaan osoitteen. Kuunteleva socket itse pysyy auki hyväksymään lisää yhteyksiä.

  • Kaikki keskustelun tavut kulkevat conn-socketin, eli uuden socketin, kautta. Lukeminen ja kirjoittaminen käyttävät samoja recv() / send() -kutsuja kuin asiakaspuolella.

  • Kun asiakas sulkee yhteyden, recv() palauttaa b""; sisempi silmukka päättyy ja palvelin sulkee oman päänsä close() -metodilla.

Ulompi while True hyppää takaisin accept() -metodiin odottamaan seuraavaa asiakasta. Palvelin käsittelee tässä muodossa yhtä asiakasta kerrallaan; useamman asiakkaan ajaminen rinnakkain vaatii joko säikeitä tai asyncio -moduulia. Jälkimmäinen on seuraavan sivun aihe.

9.13.3. Yleisiä sudenkuoppia

  • recv():n kohteleminen viestimuotoisena. Se ei ole sitä. Kaksi send(b"hi") -kutsua saattavat saapua yhtenä recv(4)-kutsuna, joka tuottaa b"hihi", tai kahtena recv(2)-kutsuna. Sovelluksen on lisättävä kehystys, jos viestien rajat ovat tärkeitä – rivinvaihto, pituusetuliite, mikä tahansa.

  • Lyhyiden lähetysten uudelleenyrityksen unohtaminen. Käytä sendall() -metodia kaikkeen, joka ylittää muutaman sadan tavun.

  • Hyväksytyn socketin sulkemisen unohtaminen. Jokainen conn on erillinen socket; kuuntelevan socketin sulkeminen ei sulje hyväksyttyjä socketteja. with -lohkot molemmilla tekevät tämän vaikeaksi tehdä väärin:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Uudelleensitominen porttiin, joka on yhä TIME_WAIT-tilassa. Kun palvelin käynnistyy uudelleen muutaman sekunnin sisällä sulkemisesta, bind() saattaa epäonnistua virheellä ”address in use”, koska MicroPython pitää porttia yhä varattuna edelliselle yhteydelle. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) ennen bind() -kutsua poistaa tämän.

9.13.4. Mitä seuraavaksi

Estyminen accept() -metodissa tarkoittaa, että palvelin voi palvella vain yhtä asiakasta kerrallaan. Estyminen recv() -metodissa tarkoittaa, että yksi hidas asiakas jumittaa koko silmukan. Vakiovastaus kameralla on asyncio – aja jokainen yhteys omana tehtävänään ja anna tapahtumasilmukan jakaa suoritusvuoroa niiden välillä. Seuraava sivu käsittelee tällä sivulla esitellyn kaiken asyncio-versiot.