BemÀstra Pythons asyncio lÄgnivÄ-nÀtverk. Denna djupdykning tÀcker Transporter och Protokoll, med praktiska exempel för att bygga högpresterande, anpassade nÀtverksprogram.
Att avmystifiera Pythons Asyncio Transport: En djupdykning i lÄgnivÄ-nÀtverk
I den moderna Python-vÀrlden har asyncio
blivit hörnstenen i högpresterande nÀtverksprogrammering. Utvecklare börjar ofta med dess vackra högnivÄ-API:er och anvÀnder async
och await
med bibliotek som aiohttp
eller FastAPI
för att bygga responsiva applikationer med anmÀrkningsvÀrt lÀtthet. StreamReader
- och StreamWriter
-objekten, som tillhandahÄlls av funktioner som asyncio.open_connection()
, erbjuder ett underbart enkelt, sekventiellt sĂ€tt att hantera nĂ€tverks I/O. Men vad hĂ€nder nĂ€r abstraktionen inte rĂ€cker till? Vad hĂ€nder om du behöver implementera ett komplext, tillstĂ„ndsbundet eller icke-standardiserat nĂ€tverksprotokoll? Vad hĂ€nder om du behöver pressa ut varje droppe prestanda genom att kontrollera den underliggande anslutningen direkt? Det Ă€r hĂ€r den verkliga grunden för asyncios nĂ€tverksförmĂ„ga ligger: det lĂ„gnivĂ„-Transport- och Protokoll-API:et. Ăven om det kan verka skrĂ€mmande till en början, sĂ„ lĂ„ser förstĂ„elsen av denna kraftfulla duo upp en ny nivĂ„ av kontroll och flexibilitet, vilket gör att du kan bygga praktiskt taget alla nĂ€tverksprogram som du kan tĂ€nka dig. Denna omfattande guide kommer att skala av abstraktionslagren, utforska det symbiotiska förhĂ„llandet mellan Transporter och Protokoll och guida dig genom praktiska exempel för att ge dig möjlighet att bemĂ€stra lĂ„gnivĂ„ asynkron nĂ€tverk i Python.
De tvÄ ansiktena av Asyncio-nÀtverk: HögnivÄ vs. LÄgnivÄ
Innan vi dyker djupt ner i lÄgnivÄ-API:erna Àr det avgörande att förstÄ deras plats inom asyncio-ekosystemet. Asyncio tillhandahÄller intelligent tvÄ distinkta lager för nÀtverkskommunikation, vart och ett skrÀddarsytt för olika anvÀndningsfall.
HögnivÄ-API:et: Strömmar
HögnivÄ-API:et, som ofta kallas "Strömmar", Àr vad de flesta utvecklare stöter pÄ först. NÀr du anvÀnder asyncio.open_connection()
eller asyncio.start_server()
fÄr du StreamReader
- och StreamWriter
-objekt. Detta API Àr utformat för enkelhet och anvÀndarvÀnlighet.
- Imperativ stil: Det lÄter dig skriva kod som ser sekventiell ut. Du
await reader.read(100)
för att fÄ 100 byte, sedanwriter.write(data)
för att skicka ett svar. Dettaasync/await
-mönster Àr intuitivt och lÀtt att resonera kring. - BekvÀma hjÀlpare: Det tillhandahÄller metoder som
readuntil(separator)
ochreadexactly(n)
som hanterar vanliga inramningsuppgifter, vilket sparar dig frÄn att manuellt hantera buffertar. - Idealiska anvÀndningsfall: Perfekt för enkla begÀran-svar-protokoll (som en grundlÀggande HTTP-klient), radbaserade protokoll (som Redis eller SMTP), eller alla situationer dÀr kommunikationen följer ett förutsÀgbart, linjÀrt flöde.
Denna enkelhet kommer dock med en avvÀgning. Det strömbaserade tillvÀgagÄngssÀttet kan vara mindre effektivt för högkonkurrenskraftiga, hÀndelsedrivna protokoll dÀr oönskade meddelanden kan komma nÀr som helst. Den sekventiella await
-modellen kan göra det besvÀrligt att hantera samtidiga lÀsningar och skrivningar eller hantera komplexa anslutningstillstÄnd.
LÄgnivÄ-API:et: Transporter och Protokoll
Detta Àr grundlagret som högnivÄ-API:et Strömmar faktiskt Àr byggt pÄ. LÄgnivÄ-API:et anvÀnder ett designmönster baserat pÄ tvÄ distinkta komponenter: Transporter och Protokoll.
- HÀndelsedriven stil: IstÀllet för att du anropar en funktion för att fÄ data, anropar asyncio metoder pÄ ditt objekt nÀr hÀndelser intrÀffar (t.ex. en anslutning görs, data tas emot). Detta Àr ett callback-baserat tillvÀgagÄngssÀtt.
- Separation av intressen: Det separerar tydligt "vad" frÄn "hur". Protokollet definierar vad man ska göra med data (din applikationslogik), medan Transporten hanterar hur data skickas och tas emot över nÀtverket (I/O-mekanismen).
- Maximal kontroll: Detta API ger dig finkornig kontroll över buffring, flödeskontroll (mottryck) och anslutningens livscykel.
- Idealiska anvÀndningsfall: Viktigt för att implementera anpassade binÀra eller textprotokoll, bygga högpresterande servrar som hanterar tusentals bestÀndiga anslutningar, eller utveckla nÀtverksramverk och bibliotek.
TÀnk pÄ det sÄ hÀr: Strömmar-API:et Àr som att bestÀlla en mÄltidskit-tjÀnst. Du fÄr fÀrdigportionerade ingredienser och ett enkelt recept att följa. Transport- och Protokoll-API:et Àr som att vara en kock i ett professionellt kök med rÄvaror och full kontroll över varje steg i processen. BÄda kan producera en fantastisk mÄltid, men den senare erbjuder grÀnslös kreativitet och kontroll.
KÀrnkomponenterna: En nÀrmare titt pÄ Transporter och Protokoll
Kraften i lÄgnivÄ-API:et kommer frÄn den eleganta interaktionen mellan Protokollet och Transporten. De Àr distinkta men oskiljaktiga partners i alla lÄgnivÄ asyncio-nÀtverksprogram.
Protokollet: Din applikations hjÀrna
Protokollet Àr en klass som du skriver. Den Àrver frÄn asyncio.Protocol
(eller en av dess varianter) och innehÄller tillstÄndet och logiken för att hantera en enskild nÀtverksanslutning. Du instansierar inte den hÀr klassen sjÀlv; du tillhandahÄller den till asyncio (t.ex. till loop.create_server
), och asyncio skapar en ny instans av ditt protokoll för varje ny klientanslutning.
Din protokollklass definieras av en uppsÀttning hÀndelsehanteringsmetoder som hÀndelseloopen anropar vid olika tidpunkter i anslutningens livscykel. De viktigaste Àr:
connection_made(self, transport)
Anropas exakt en gÄng nÀr en ny anslutning har upprÀttats. Det hÀr Àr din ingÄngspunkt. Det Àr dÀr du fÄr transport
-objektet, som representerar anslutningen. Du bör alltid spara en referens till det, vanligtvis som self.transport
. Det Àr det perfekta stÀllet att utföra alla per-anslutningsinitialiseringar, som att stÀlla in buffertar eller logga peer-adressen.
data_received(self, data)
HjÀrtat i ditt protokoll. Denna metod anropas nÀr nya data tas emot frÄn den andra Ànden av anslutningen. data
-argumentet Àr ett bytes
-objekt. Det Àr avgörande att komma ihÄg att TCP Àr ett strömprotokoll, inte ett meddelandeprotokoll. Ett enda logiskt meddelande frÄn din applikation kan delas upp över flera data_received
-anrop, eller flera smÄ meddelanden kan paketeras i ett enda anrop. Din kod mÄste hantera denna buffring och parsning.
connection_lost(self, exc)
Anropas nÀr anslutningen stÀngs. Detta kan hÀnda av flera anledningar. Om anslutningen stÀngs rent (t.ex. den andra sidan stÀnger den, eller du anropar transport.close()
) kommer exc
att vara None
. Om anslutningen stÀngs pÄ grund av ett fel (t.ex. nÀtverksfel, ÄterstÀllning) kommer exc
att vara ett undantagsobjekt som beskriver felet. Det hÀr Àr din chans att utföra rensning, logga frÄnkopplingen eller försöka Äteransluta om du bygger en klient.
eof_received(self)
Detta Àr en mer subtil callback. Den anropas nÀr den andra Ànden signalerar att den inte kommer att skicka mer data (t.ex. genom att anropa shutdown(SHUT_WR)
pÄ ett POSIX-system), men anslutningen kan fortfarande vara öppen för dig att skicka data. Om du returnerar True
frÄn den hÀr metoden kommer transporten att stÀngas. Om du returnerar False
(standard) Àr du ansvarig för att stÀnga transporten sjÀlv senare.
Transporten: Kommunikationskanalen
Transporten Àr ett objekt som tillhandahÄlls av asyncio. Du skapar det inte; du fÄr det i ditt protokolls connection_made
-metod. Den fungerar som en högnivÄabstraktion över det underliggande nÀtverkssockeln och hÀndelseloopens I/O-schemalÀggning. Dess primÀra uppgift Àr att hantera sÀndning av data och kontroll av anslutningen.
Du interagerar med transporten genom dess metoder:
transport.write(data)
Huvudmetoden för att skicka data. data
mÄste vara ett bytes
-objekt. Denna metod Àr icke-blockerande. Den skickar inte data omedelbart. IstÀllet placerar den data i en intern skrivbuffert, och hÀndelseloopen skickar den över nÀtverket sÄ effektivt som möjligt i bakgrunden.
transport.writelines(list_of_data)
Ett effektivare sÀtt att skriva en sekvens av bytes
-objekt till bufferten pÄ en gÄng, vilket potentiellt minskar antalet systemanrop.
transport.close()
Detta initierar en graciös avstÀngning. Transporten kommer först att tömma eventuella data som finns kvar i sin skrivbuffert och sedan stÀnga anslutningen. Inga fler data kan skrivas efter att close()
anropats.
transport.abort()
Detta utför en hÄrd avstÀngning. Anslutningen stÀngs omedelbart och alla data som vÀntar i skrivbufferten kasseras. Detta bör anvÀndas under exceptionella omstÀndigheter.
transport.get_extra_info(name, default=None)
En mycket anvÀndbar metod för introspektion. Du kan fÄ information om anslutningen, till exempel peer-adressen ('peername'
), det underliggande socket-objektet ('socket'
) eller SSL/TLS-certifikatinformationen ('ssl_object'
).
Det symbiotiska förhÄllandet
Skönheten med denna design Àr det tydliga, cykliska informationsflödet:
- Installera: HĂ€ndelseloopen accepterar en ny anslutning.
- Instansiering: Loopen skapar en instans av din
Protocol
-klass och ettTransport
-objekt som representerar anslutningen. - LĂ€nkning: Loopen anropar
your_protocol.connection_made(transport)
och lÀnkar samman de tvÄ objekten. Ditt protokoll har nu ett sÀtt att skicka data. - Ta emot data: NÀr data anlÀnder pÄ nÀtverkssockeln vaknar hÀndelseloopen, lÀser data och anropar
your_protocol.data_received(data)
. - Bearbetning: Ditt protokolls logik bearbetar mottagna data.
- Skicka data: Baserat pÄ sin logik anropar ditt protokoll
self.transport.write(response_data)
för att skicka ett svar. Data buffras. - Bakgrunds-I/O: HÀndelseloopen hanterar den icke-blockerande sÀndningen av de buffrade data över transporten.
- Nedmontering: NÀr anslutningen avslutas anropar hÀndelseloopen
your_protocol.connection_lost(exc)
för slutstÀdning.
Bygga ett praktiskt exempel: En ekosserver och -klient
Teori Àr bra, men det bÀsta sÀttet att förstÄ Transporter och Protokoll Àr att bygga nÄgot. LÄt oss skapa en klassisk ekosserver och en motsvarande klient. Servern kommer att acceptera anslutningar och helt enkelt skicka tillbaka alla data den fÄr.
Implementeringen av ekoservern
Först kommer vi att definiera vÄrt serversidiga protokoll. Det Àr anmÀrkningsvÀrt enkelt och visar de grundlÀggande hÀndelsehanterarna.
import asyncio
class EchoServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
# En ny anslutning har upprÀttats.
# HÀmta fjÀrradressen för loggning.
peername = transport.get_extra_info('peername')
print(f"Anslutning frÄn: {peername}")
# Lagra transporten för senare anvÀndning.
self.transport = transport
def data_received(self, data):
# Data tas emot frÄn klienten.
message = data.decode()
print(f"Data mottagen: {message.strip()}")
# Eka tillbaka data till klienten.
print(f"Ekar tillbaka: {message.strip()}")
self.transport.write(data)
def connection_lost(self, exc):
# Anslutningen har stÀngts.
print("Anslutning stÀngd.")
# Transporten stÀngs automatiskt, ingen anledning att kalla self.transport.close() hÀr.
async def main_server():
# FÄ en referens till hÀndelseloopen eftersom vi planerar att köra servern pÄ obestÀmd tid.
loop = asyncio.get_running_loop()
host = '127.0.0.1'
port = 8888
# Korutinen `create_server` skapar och startar servern.
# Det första argumentet Àr protocol_factory, ett anropbart objekt som returnerar en ny protokollinstans.
# I vÄrt fall fungerar det helt enkelt att skicka klassen `EchoServerProtocol`.
server = await loop.create_server(
lambda: EchoServerProtocol(),
host,
port)
addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets)
print(f'Servar pÄ {addrs}')
# Servern körs i bakgrunden. För att hÄlla huvudkorutinen vid liv,
# kan vi vÀnta pÄ nÄgot som aldrig slutförs, som en ny framtid.
# För detta exempel kommer vi bara att köra det "för alltid".
async with server:
await server.serve_forever()
if __name__ == "__main__":
try:
# För att köra servern:
asyncio.run(main_server())
except KeyboardInterrupt:
print("Servern stÀngdes.")
I den hÀr serverkoden Àr loop.create_server()
nyckeln. Den binder till den angivna vÀrden och porten och berÀttar för hÀndelseloopen att börja lyssna efter nya anslutningar. För varje inkommande anslutning anropar den vÄr protocol_factory
(funktionen lambda: EchoServerProtocol()
) för att skapa en ny protokollinstans dedikerad till den specifika klienten.
Implementeringen av ekoklienten
Klientprotokollet Àr nÄgot mer involverat eftersom det behöver hantera sitt eget tillstÄnd: vilket meddelande som ska skickas och nÀr det anser att dess jobb Àr "klart". Ett vanligt mönster Àr att anvÀnda en asyncio.Future
eller asyncio.Event
för att signalera slutförandet tillbaka till huvudkorutinen som startade klienten.
import asyncio
class EchoClientProtocol(asyncio.Protocol):
def __init__(self, message, on_con_lost):
self.message = message
self.on_con_lost = on_con_lost
self.transport = None
def connection_made(self, transport):
self.transport = transport
print(f"Skickar: {self.message}")
self.transport.write(self.message.encode())
def data_received(self, data):
print(f"Mottagen eko: {data.decode().strip()}")
def connection_lost(self, exc):
print("Servern stÀngde anslutningen")
# Signalera att anslutningen Àr förlorad och att uppgiften Àr slutförd.
self.on_con_lost.set_result(True)
def eof_received(self):
# Detta kan anropas om servern skickar en EOF innan den stÀngs.
print("Mottog EOF frÄn servern.")
async def main_client():
loop = asyncio.get_running_loop()
# on_con_lost framtiden anvÀnds för att signalera slutförandet av klientens arbete.
on_con_lost = loop.create_future()
message = "Hello World!"
host = '127.0.0.1'
port = 8888
# `create_connection` upprÀttar anslutningen och lÀnkar protokollet.
try:
transport, protocol = await loop.create_connection(
lambda: EchoClientProtocol(message, on_con_lost),
host,
port)
except ConnectionRefusedError:
print("Anslutningen nekades. Körs servern?")
return
# VÀnta tills protokollet signalerar att anslutningen Àr förlorad.
try:
await on_con_lost
finally:
# StÀng transporten graciöst.
transport.close()
if __name__ == "__main__":
# För att köra klienten:
# Starta först servern i en terminal.
# Kör sedan det hÀr skriptet i en annan terminal.
asyncio.run(main_client())
HÀr Àr loop.create_connection()
klientens motsvarighet till create_server
. Den försöker ansluta till den givna adressen. Om det lyckas instansierar den vÄr EchoClientProtocol
och anropar dess connection_made
-metod. AnvÀndningen av on_con_lost
Future Àr ett kritiskt mönster. Korutinen main_client
await
s denna framtid, vilket effektivt pausar sin egen exekvering tills protokollet signalerar att dess arbete Àr klart genom att anropa on_con_lost.set_result(True)
inifrÄn connection_lost
.
Avancerade koncept och scenarier i den verkliga vÀrlden
Eko-exemplet tÀcker grunderna, men verkliga protokoll Àr sÀllan sÄ enkla. LÄt oss utforska nÄgra mer avancerade Àmnen som du oundvikligen kommer att stöta pÄ.
Hantering av meddelandeinramning och buffring
Det enskilt viktigaste konceptet att förstÄ efter grunderna Àr att TCP Àr en ström av byte. Det finns inga inneboende "meddelande"-grÀnser. Om en klient skickar "Hello" och sedan "World", kan din servers data_received
anropas en gÄng med b'HelloWorld'
, tvÄ gÄnger med b'Hello'
och b'World'
, eller till och med flera gÄnger med partiella data.
Ditt protokoll ansvarar för "inramning" â att Ă„termontera dessa byte-strömmar till meningsfulla meddelanden. En vanlig strategi Ă€r att anvĂ€nda en avgrĂ€nsare, till exempel ett nyradstecken (
).
HÀr Àr ett modifierat protokoll som buffrar data tills det hittar en ny rad och bearbetar en rad i taget.
class LineBasedProtocol(asyncio.Protocol):
def __init__(self):
self._buffer = b''
self.transport = None
def connection_made(self, transport):
self.transport = transport
print("Anslutning upprÀttad.")
def data_received(self, data):
# LĂ€gg till nya data i den interna bufferten
self._buffer += data
# Bearbeta sÄ mÄnga kompletta rader som vi har i bufferten
while b'\n' in self._buffer:
line, self._buffer = self._buffer.split(b'\n', 1)
self.process_line(line.decode().strip())
def process_line(self, line):
# Det hÀr Àr dÀr din applikationslogik för ett enstaka meddelande gÄr
print(f"Bearbetar komplett meddelande: {line}")
response = f"Bearbetat: {line}\n"
self.transport.write(response.encode())
def connection_lost(self, exc):
print("Anslutning förlorad.")
Hantering av flödeskontroll (mottryck)
Vad hÀnder om din applikation skriver data till transporten snabbare Àn nÀtverket eller fjÀrrpeeren kan hantera det? Data samlas i transportens interna buffert. Om detta fortsÀtter okontrollerat kan bufferten vÀxa pÄ obestÀmd tid och förbruka allt tillgÀngligt minne. Detta problem kallas brist pÄ "mottryck".
Asyncio tillhandahÄller en mekanism för att hantera detta. Transporten övervakar sin egen buffertstorlek. NÀr bufferten vÀxer förbi ett visst vattenmÀrke för hög nivÄ anropar hÀndelseloopen ditt protokolls pause_writing()
-metod. Detta Àr en signal till din applikation att sluta skicka data. NÀr bufferten har tömts under ett vattenmÀrke för lÄg nivÄ anropar loopen resume_writing()
och signalerar att det Àr sÀkert att skicka data igen.
class FlowControlledProtocol(asyncio.Protocol):
def __init__(self):
self._paused = False
self._data_source = some_data_generator() # FörestÀll dig en datakÀlla
self.transport = None
def connection_made(self, transport):
self.transport = transport
self.resume_writing() # Starta skrivprocessen
def pause_writing(self):
# Transportbufferten Àr full.
print("Pausar skrivning.")
self._paused = True
def resume_writing(self):
# Transportbufferten har tömts.
print("Ă
terupptar skrivning.")
self._paused = False
self._write_more_data()
def _write_more_data(self):
# Det hÀr Àr vÄr applikations skrivslinga.
while not self._paused:
try:
data = next(self._data_source)
self.transport.write(data)
except StopIteration:
self.transport.close()
break # Inga fler data att skicka
# Kontrollera buffertstorleken för att se om vi ska pausa omedelbart
if self.transport.get_write_buffer_size() > 0:
self.pause_writing()
Utöver TCP: Andra transporter
Ăven om TCP Ă€r det vanligaste anvĂ€ndningsfallet Ă€r Transport/Protokoll-mönstret inte begrĂ€nsat till det. Asyncio tillhandahĂ„ller abstraktioner för andra kommunikationstyper:
- UDP: För anslutningslös kommunikation anvÀnder du
loop.create_datagram_endpoint()
. Detta ger dig enDatagramTransport
och du kommer att implementera enasyncio.DatagramProtocol
med metoder somdatagram_received(data, addr)
ocherror_received(exc)
. - SSL/TLS: Att lÀgga till kryptering Àr otroligt enkelt. Du skickar ett
ssl.SSLContext
-objekt tillloop.create_server()
ellerloop.create_connection()
. Asyncio hanterar TLS-handskakningen automatiskt, och du fÄr en sÀker transport. Din protokollkod behöver inte Àndras alls. - Underprocesser: För kommunikation med barnprocesser via deras standard-I/O-rör kan
loop.subprocess_exec()
ochloop.subprocess_shell()
anvÀndas med enasyncio.SubprocessProtocol
. Detta gör att du kan hantera barnprocesser pÄ ett helt asynkront, icke-blockerande sÀtt.
Strategiskt beslut: NÀr du ska anvÀnda Transporter vs. Strömmar
Med tvÄ kraftfulla API:er till ditt förfogande Àr ett viktigt arkitektoniskt beslut att vÀlja rÀtt för jobbet. HÀr Àr en guide som hjÀlper dig att bestÀmma dig.
VÀlj Strömmar (StreamReader
/StreamWriter
) NĂ€r...
- Ditt protokoll Àr enkelt och begÀran-svar-baserat. Om logiken Àr "lÀs en begÀran, bearbeta den, skriv ett svar", Àr strömmar perfekta.
- Du bygger en klient för ett vÀlkÀnt, radbaserat eller fast lÀngd-meddelandeprotokoll. Till exempel att interagera med en Redis-server eller en enkel FTP-server.
- Du prioriterar kodlÀsbarhet och en linjÀr, imperativ stil. Syntaxen
async/await
med strömmar Àr ofta lÀttare för utvecklare som Àr nya inom asynkron programmering att förstÄ. - Snabb prototyputveckling Àr nyckeln. Du kan fÄ en enkel klient eller server uppe och igÄng med strömmar pÄ bara nÄgra rader kod.
VĂ€lj Transporter och Protokoll NĂ€r...
- Du implementerar ett komplext eller anpassat nÀtverksprotokoll frÄn grunden. Detta Àr det primÀra anvÀndningsfallet. TÀnk pÄ protokoll för spel, finansiella dataflöden, IoT-enheter eller peer-to-peer-applikationer.
- Ditt protokoll Àr starkt hÀndelsestyrt och inte rent begÀran-svar. Om servern kan skicka oönskade meddelanden till klienten nÀr som helst, Àr den callback-baserade karaktÀren av protokoll en mer naturlig passform.
- Du behöver maximal prestanda och minimal overhead. Protokoll ger dig en mer direkt vÀg till hÀndelseloopen, förbi en del av den overhead som Àr associerad med Strömmar-API:et.
- Du krÀver finkornig kontroll över anslutningen. Detta inkluderar manuell buffertshantering, explicit flödeskontroll (
pause/resume_writing
) och detaljerad hantering av anslutningslivscykeln. - Du bygger ett nÀtverksramverk eller bibliotek. Om du tillhandahÄller ett verktyg för andra utvecklare Àr den robusta och flexibla karaktÀren hos API:et Protokoll/Transport ofta rÀtt grund.
Slutsats: Omfamna grunden för Asyncio
Pythons asyncio
-bibliotek Àr ett mÀsterverk av flerskiktsdesign. Medan högnivÄ-API:et Strömmar tillhandahÄller en tillgÀnglig och produktiv utgÄngspunkt, Àr det lÄgnivÄ-API:et Transport och Protokoll som representerar den sanna, kraftfulla grunden för asyncios nÀtverksförmÄga. Genom att separera I/O-mekanismen (Transporten) frÄn applikationslogiken (Protokollet) ger det en robust, skalbar och otroligt flexibel modell för att bygga sofistikerade nÀtverksprogram.
Att förstÄ denna lÄgnivÄabstraktion Àr inte bara en akademisk övning; det Àr en praktisk fÀrdighet som ger dig möjlighet att gÄ utöver enkla klienter och servrar. Det ger dig förtroendet att ta itu med alla nÀtverksprotokoll, kontrollen för att optimera för prestanda under tryck och möjligheten att bygga nÀsta generations högpresterande, asynkrona tjÀnster i Python. NÀsta gÄng du stÄr inför ett utmanande nÀtverksproblem, kom ihÄg kraften som ligger precis under ytan, och tveka inte att nÄ ut efter den eleganta duon av Transporter och Protokoll.