
Apparaten met elkaar verbinden om informatie uit te wisselen: daar draait netwerken om. Sockets zijn een essentieel onderdeel van effectieve netwerkcommunicatie. Ze vormen het onderliggende concept om berichten te versturen tussen apparaten op lokale of wereldwijde netwerken en tussen processen op dezelfde machine. Ze bieden een low-level interface die je fijne controle geeft over het verkeer dat je wilt verzenden of ontvangen.
Die low-level aard maakt het mogelijk om zeer performante communicatiekanalen (of eigen protocollen) te bouwen voor specifieke use-cases met weinig overhead, in tegenstelling tot traditionele protocollen die boven op socketcommunicatie zijn gebouwd.
Dat maakt sockets bijzonder nuttig in real-time client-serversystemen die afhankelijk zijn van directe berichtuitwisseling of die met enorme hoeveelheden data werken.
In dit artikel behandelen we de basis van socketprogrammering en geven we een stapsgewijze gids voor het maken van client- en serverapplicaties op basis van sockets met Python.
Basisprincipes van netwerken
Netwerken maakt communicatie en het delen van informatie van allerlei aard mogelijk. Een verzameling van zulke onderling verbonden apparaten noemen we een netwerk.
We zien in de fysieke wereld veel netwerken: luchtvaart- of elektriciteitsnetwerken of steden die via snelwegen met elkaar verbonden zijn, zijn goede voorbeelden.
Op vergelijkbare wijze zijn er talloze netwerken in de informatietechnologie; het bekendste voorbeeld is het internet, het wereldwijde netwerk van netwerken dat ontelbare apparaten verbindt — waarschijnlijk ook het netwerk dat je nu gebruikt om dit artikel te lezen.
Soorten netwerken
Het internet omvat talloze andere netwerken die onderling verschillen in schaal of eigenschappen: bijvoorbeeld local area networks (LAN's), die meestal computers met elkaar verbinden die zich dicht bij elkaar bevinden. Machines binnen bedrijven of andere instellingen (banken, universiteiten, enz.) of je thuisapparaten die op een router zijn aangesloten vormen zo'n netwerk.
Er zijn ook grotere en kleinere typen netwerken, zoals PAN's (personal area network) — dat kan simpelweg je smartphone zijn die via Bluetooth met een laptop is verbonden — MAN's (metropolitan area network), die apparaten in een hele stad verbinden, en WAN's (wide area network), die hele landen of de wereld kunnen omvatten. En ja, het grootste WAN is het internet zelf.
Het spreekt voor zich dat computernetwerken erg complex kunnen zijn en uit veel elementen bestaan. Een van de meest basale en cruciale bouwstenen is een communicatieprotocol.
Soorten netwerkcommunicatieprotocollen
Communicatieprotocollen leggen de regels vast voor hoe en in welk formaat informatie wordt verzonden en ontvangen. Deze protocollen zijn in een hiërarchie opgebouwd om de verschillende taken van netwerkcommunicatie te beheren.
Enkele veelgebruikte en bekende netwerkcommunicatieprotocollen zijn:
Wi-Fi
Een voorbeeld van een link-layerprotocol, wat betekent dat het dicht bij de hardware zit en verantwoordelijk is voor het fysiek versturen van data van het ene apparaat naar het andere in een draadloze omgeving.
IP (Internet Protocol)
IP is een netwerklaagprotocol dat vooral verantwoordelijk is voor routering van pakketten en IP-adressering.
TCP (Transmission Control Protocol)
Een betrouwbaar, verbinding-georiënteerd protocol dat full-duplexcommunicatie biedt en dataintegriteit en levering garandeert. Dit is een transportlaagprotocol dat verbindingen beheert, fouten detecteert en informatiestromen regelt.
UDP (User Datagram Protocol)
Een protocol uit dezelfde suite als TCP. Het belangrijkste verschil is dat UDP eenvoudiger en sneller is, maar onbetrouwbaar en zonder verbinding werkt: het voert geen leveringscontroles uit en volgt het "fire-and-forget"-principe. Net als TCP bevindt UDP zich op de transportlaag.
HTTP (Hypertext Transfer Protocol)
Een applicatielaagprotocol en het meest gebruikte protocol voor communicatie tussen browser en server op het web, met name voor het aanbieden van websites. Het spreekt voor zich dat ook dit artikel via HTTP is aangeboden. Het HTTP-protocol bouwt boven op TCP en beheert en verstuurt informatie die relevant is voor webapplicaties, zoals headers voor metadata en cookies, verschillende HTTP-methoden (GET, POST, DELETE, UPDATE) enz.
MQTT (Message Queue Telemetry Transport)
Nog een voorbeeld van een applicatielaagprotocol, gebruikt voor apparaten met beperkte rekenkracht en batterij, die werken in onbetrouwbare netwerkomstandigheden (bijv. gassensoren op een mijnlocatie of een slimme lamp in huis). MQTT is een standaard berichtprotocol voor IoT (Internet of Things). Het is lichtgewicht en eenvoudig te gebruiken, ontworpen met ingebouwde hertransmissiemechanismen voor meer betrouwbaarheid. Als je dit protocol met Python wilt gebruiken, lees dan deze gids Python MQTT, met een diepgaande uitleg van de Paho MQTT-client.
Een belangrijke observatie is dat al deze protocollen onder de motorkap sockets gebruiken, maar daarbovenop hun eigen logica en dataverwerking toevoegen. Dat komt doordat sockets een low-level interface vormen voor alle netwerkcommunicatie in moderne apparaten, zoals we in de volgende sectie bespreken.
Belangrijke concepten en termen
Er zijn natuurlijk nog veel andere belangrijke concepten en termen in de context van netwerken. Hier is een beknopt overzicht van enkele prominente begrippen die in de rest van de tutorial kunnen terugkomen:
- Packet: een standaard eenheid voor datatransmissie in een computernetwerk (in spreektaal vergelijkbaar met "bericht").
- Endpoint: een bestemming waar pakketten aankomen.
- IP-adres: een numerieke identifier die een apparaat op het netwerk uniek identificeert. Een voorbeeld: 192.168.0.0
- Poorten: een numerieke identifier die een proces op een apparaat uniek identificeert dat specifieke netwerkcommunicatie afhandelt, bijvoorbeeld het aanbieden van je website via HTTP. Waar een IP-adres het apparaat identificeert, identificeert een poort de applicatie (elke applicatie is een proces of bestaat uit processen). Enkele bekende voorbeelden: poort 80, conventioneel gebruikt door servers voor HTTP-verkeer, en poort 443 voor HTTPS (beveiligde HTTP).
- Gateway: een speciaal soort netwerknode (apparaat) die fungeert als toegangspunt van het ene netwerk naar het andere. Deze netwerken kunnen zelfs verschillende protocollen gebruiken, dus een gateway kan protocolvertaling moeten uitvoeren. Een voorbeeld is een router die een thuisnetwerk verbindt met het internet.
Sockets begrijpen
Wat is een socket?
Een socket is een interface (poort) voor communicatie tussen verschillende processen op dezelfde of op verschillende machines. In het laatste geval spreken we over netwerk-sockets.
Netwerk-sockets abstraheren het beheer van verbindingen. In Unix-systemen zijn sockets eenvoudigweg bestanden die dezelfde schrijf-/leesbewerkingen ondersteunen, maar alle data via het netwerk versturen.
Wanneer een socket luistert of aan het verbinden is, is hij altijd gebonden aan een combinatie van een IP-adres plus een poortnummer, die de host (machine/apparaat) en het proces identificeren.
Hoe socketverbindingen werken
Sockets kunnen luisteren naar binnenkomende verbindingen of zelf uitgaande verbindingen opzetten. Wanneer een verbinding tot stand komt, wordt de luisterende socket (server-socket) bovendien gebonden aan het IP en de poort van de verbindende kant.
Of er wordt een nieuwe socket aangemaakt die nu is gebonden aan twee paren van IP-adressen en poortnummers van een luisteraar en een aanvrager. Zo kunnen twee verbonden sockets op verschillende machines elkaar identificeren en een enkele verbinding delen voor datatransmissie, zonder de luisterende socket te blokkeren die ondertussen blijft luisteren naar andere verbindingen.
In het geval van de verbindende socket (client-socket) wordt deze bij het initiëren van de verbinding impliciet gebonden aan het IP-adres van het apparaat en een willekeurig vrij poortnummer. Vervolgens, zodra de verbinding is opgezet, vindt de binding met het IP en de poort van de andere kant op dezelfde manier plaats als bij een luisterende socket, maar dan zonder een nieuwe socket aan te maken.
Socketprogrammering
Bij socketprogrammering maken we in onze code socketobjecten aan en voeren we er bewerkingen op uit (luisteren, verbinden, ontvangen, verzenden enz.). In deze context zijn sockets gewoon speciale objecten die we in ons programma maken en die speciale methoden hebben om met netwerkverbindingen en -verkeer te werken.
Onder de motorkap roepen die methoden je besturingssysteemkernel aan, of specifieker: de netwerkstack, een speciaal deel van de kernel dat verantwoordelijk is voor het beheren van netwerkoperaties.
Sockets en client-servercommunicatie
Sockets hebben betrekking op verbindingen; het zijn verbindinghandlers. Op het web start je, wanneer je data wilt verzenden of ontvangen, een verbinding (die wordt geïnitieerd via de interface die sockets heet).
Jij of de partij waarmee je verbinding maakt, fungeert als server en de andere partij als client. Een server levert data aan clients; clients leggen proactief verbinding en vragen data op bij een server. Een server luistert via een luister-socket naar nieuwe verbindingen, legt die vast, ontvangt de verzoeken van de client en stuurt de gevraagde data terug in zijn response.
Een client maakt op zijn beurt een socket aan met het IP-adres en de poort van de server waarmee hij wil verbinden, start de verbinding, verstuurt zijn verzoek naar de server en ontvangt data als antwoord. Deze naadloze uitwisseling tussen client- en server-sockets vormt de ruggengraat van uiteenlopende netwerkapplicaties.
Sockets als basis voor netwerkprotocollen
Dat sockets de ruggengraat vormen, betekent ook dat er diverse protocollen bovenop worden gebouwd en gebruikt. Veelvoorkomende zijn UDP en TCP, waar we al kort op ingingen. Sockets die een van deze transportprotocollen gebruiken, heten UDP- of TCP-sockets.
IPC-sockets
Naast netwerk-sockets zijn er ook andere typen. Bijvoorbeeld IPC-sockets (Inter Process Communication). IPC-sockets zijn bedoeld om data te verzenden tussen processen op dezelfde machine, terwijl netwerk-sockets hetzelfde over het netwerk kunnen doen.
Het voordeel van IPC-sockets is dat ze veel overhead vermijden van het opbouwen van pakketten en het bepalen van routes om de data te versturen. Omdat zender en ontvanger lokaal zijn in de IPC-context, heeft communicatie via IPC-sockets doorgaans lagere latency.
Unix-sockets
Een goed voorbeeld van IPC-sockets zijn Unix-sockets, die, zoals alles in Unix, gewoon bestanden op het bestandssysteem zijn. Ze worden niet geïdentificeerd door IP-adres en poort, maar door het bestandspad op het bestandssysteem.
Netwerk-sockets als IPC-sockets
Let op: je kunt ook netwerk-sockets gebruiken voor inter-processcommunicatie als zowel server als ontvanger op localhost draaien (d.w.z. IP-adres 127.0.0.1).
Enerzijds voegt dit extra latency toe vanwege de overhead van verwerking door de netwerkstack, maar anderzijds hoef je je minder zorgen te maken over het onderliggende besturingssysteem, omdat netwerk-sockets op alle systemen aanwezig zijn en werken, in tegenstelling tot IPC-sockets die specifiek zijn voor een bepaald OS of OS-familie.
Python Socket-bibliotheek
Voor socketprogrammering in Python gebruiken we de officiële ingebouwde socketbibliotheek van Python, bestaande uit functies, constanten en klassen om sockets te maken, te beheren en ermee te werken. Enkele veelgebruikte functies zijn:
- socket(): maakt een nieuwe socket aan.
- bind(): koppelt de socket aan een specifiek adres en poort.
- listen(): begint te luisteren naar binnenkomende verbindingen op de socket.
- accept(): accepteert een verbinding van een client en geeft een nieuwe socket terug voor communicatie.
- connect(): maakt een verbinding met een externe server.
- send(): verstuurt data via de socket.
- recv(): ontvangt data van de socket.
- close(): sluit de socketverbinding.
Voorbeeld van sockets in Python
Laten we socketprogrammering bekijken met een praktisch voorbeeld in Python. Het doel is twee applicaties met elkaar te verbinden en te laten communiceren. We gebruiken de Python-socketbibliotheek om een serverapplicatie te maken die via het netwerk met een client communiceert en informatie uitwisselt.
Aandachtspunten en beperkingen
Let op: voor educatieve doeleinden is ons voorbeeld vereenvoudigd en draaien de applicaties lokaal en niet over het daadwerkelijke netwerk — we gebruiken een loopback localhost-adres om de client met de server te verbinden.
Dit betekent dat zowel client als server op dezelfde machine draaien en dat de client een verbinding initieert met dezelfde machine waarop hij draait, zij het met een ander proces dat de server vertegenwoordigt.
Draaien op verschillende machines
Je kunt de applicaties ook op twee verschillende apparaten draaien die beide met dezelfde wifi-router zijn verbonden, wat een lokaal netwerk vormt. Dan kan de client op het ene apparaat verbinden met de server op een ander apparaat.
In dat geval moet je wel de IP-adressen kennen die je router aan je apparaten heeft uitgedeeld en die gebruiken in plaats van het localhost (127.0.0.1) loopbackadres (om IP-adressen te zien, gebruik je de terminalopdracht ifconfig op Unix-achtige systemen of ipconfig op Windows). Na het achterhalen van de IP-adressen van je applicaties kun je die in de code aanpassen en werkt het voorbeeld nog steeds.
Een socketserver maken in Python
We beginnen met het maken van een socketserver (specifiek een Python TCP-server, aangezien we met TCP-sockets werken, zoals we zo zullen zien) die berichten uitwisselt met clients.
Volg hiervoor de onderstaande stappen:
Een Python-bestand met wat boilerplate maken
-
Maak een bestand met de naam
server.py -
Importeer de module
socketin je Python-script.
import socket
- Voeg een functie toe met de naam
run_server. We plaatsen daar het grootste deel van onze code. Vergeet niet correct in te springen wanneer je code aan de functie toevoegt:
def run_server():
# your code will go here
Socketobject instantieren
Maak in run_server vervolgens een socketobject aan met de functie socket.socket().
Het eerste argument (socket.AF_INET) specificeert de IP-adresfamilie voor IPv4 (andere opties: AF_INET6 voor IPv6 en AF_UNIX voor Unix-sockets)
Het tweede argument (socket.SOCK_STREAM) geeft aan dat we een TCP-socket gebruiken.
Bij TCP maakt het besturingssysteem een betrouwbare verbinding met levering in volgorde, foutdetectie en hertransmissie, en flow control. Je hoeft al die details niet zelf te implementeren.
Er is ook een optie voor een UDP-socket: socket.SOCK_DGRAM. Dit creëert een socket die alle kenmerken van UDP onder de motorkap implementeert.
Wil je nog laagdrempeliger werken en je eigen transportlaagprotocol bouwen boven op het TCP/IP-netwerklaagprotocol dat door sockets wordt gebruikt, dan kun je socket.SOCK_RAW gebruiken als tweede argument. In dat geval behandelt het besturingssysteem geen hogere protocolfuncties voor je en moet je zelf alle headers, bevestiging van verbindingen en hertransmissiefuncties implementeren als je die nodig hebt. Er zijn ook andere waarden; lees erover in de documentatie.
# create a socket object
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Serversocket binden aan IP-adres en poort
Definieer de hostnaam of het server-IP en de poort om aan te geven op welk adres de server bereikbaar is en waar hij naar binnenkomende verbindingen luistert. In dit voorbeeld luistert de server op de lokale machine — dat wordt bepaald door de variabele server_ip met waarde 127.0.0.1 (ook wel localhost).
De variabele port is ingesteld op 8000, het poortnummer waarmee de serverapplicatie door het besturingssysteem wordt geïdentificeerd (het is aan te raden waarden boven 1023 te gebruiken om botsingen met poorten van systeemtaken te vermijden).
server_ip = "127.0.0.1"
port = 8000
Maak de socket klaar om verbindingen te ontvangen door hem te binden aan het IP-adres en de poort die we hierboven hebben gedefinieerd.
# bind the socket to a specific address and port
server.bind((server_ip, port))
Luisteren naar binnenkomende verbindingen
Zet de serversocket in luistermodus met de functie listen. Het argument backlog bepaalt hoeveel binnenkomende verbindingen in de wachtrij kunnen staan voordat ze geaccepteerd worden.
In ons voorbeeld geven we 0 door, wat op de meeste systemen de server effectief beperkt tot één client tegelijk — extra verbindingspogingen worden geweigerd zolang hij bezig is. Een hogere waarde (bijvoorbeeld 1 of meer) laat zoveel clients in de wachtrij wachten; zodra accept wordt aangeroepen voor een client, verlaat die de wachtrij en komt er een plek vrij.
Laat je het argument weg, dan valt het terug op een systeembrede standaard (op Unix te zien via /proc/sys/net/core/somaxconn).
# listen for incoming connections
server.listen(0)
print(f"Listening on {server_ip}:{port}")
Binnenkomende verbindingen accepteren
Wacht vervolgens op en accepteer binnenkomende clientverbindingen. De methode accept blokkeert de uitvoering totdat een client verbinding maakt. Daarna retourneert hij een tuple (conn, address), waarbij address een tuple is met het IP-adres en de poort van de client, en conn een nieuw socketobject is dat de verbinding met de client deelt en gebruikt kan worden om ermee te communiceren.
accept maakt een nieuwe socket om met de client te communiceren in plaats van de luister-socket (in ons voorbeeld server) te binden aan het clientadres en die voor communicatie te gebruiken, omdat de luister-socket moet blijven luisteren naar andere clients; anders zou hij geblokkeerd raken. In ons geval behandelen we weliswaar maar één client en weigeren we de rest tijdens die periode, maar dit wordt relevanter in het multithreaded servervoorbeeld verderop.
# accept incoming connections
client_socket, client_address = server.accept()
print(f"Accepted connection from {client_address[0]}:{client_address[1]}")
Communicatielus maken
Zodra de verbinding met de client is opgezet (na het aanroepen van accept), starten we een oneindige lus voor de communicatie. In deze lus roepen we de methode recv van het object client_socket aan. Deze methode ontvangt het opgegeven aantal bytes van de client — in ons geval 1024.
1024 bytes is een gangbare conventie voor de grootte van de payload, omdat het een macht van twee is en potentieel beter te optimaliseren dan een willekeurige waarde. Je kunt deze waarde naar wens aanpassen.
Omdat de door de client ontvangen data in de variabele request in ruwe binaire vorm is, zetten we die om van een bytevolgorde naar een string met de functie decode.
Daarna hebben we een if-statement dat de communicatielus verlaat als we een ”close”-bericht ontvangen. Dat betekent dat zodra onze server de string ”close” ontvangt, hij een bevestiging terugstuurt naar de client en de verbinding beëindigt. Anders printen we het ontvangen bericht naar de console. De bevestiging is in ons geval simpelweg het sturen van de string ”closed” naar de client.
Merk op dat de methode lower die we op de request-string gebruiken, deze eenvoudig naar kleine letters omzet. Zo maakt het niet uit of close oorspronkelijk in hoofd- of kleine letters stond.
# receive data from the client
while True:
request = client_socket.recv(1024)
request = request.decode("utf-8") # convert bytes to string
# if we receive "close" from the client, then we break
# out of the loop and close the connection
if request.lower() == "close":
# send response to the client which acknowledges that the
# connection should be closed and break out of the loop
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
Antwoord naar de client sturen
Nu moeten we de normale reactie van de server naar de client afhandelen (dus wanneer de client de verbinding niet wil sluiten). Voeg in de while-lus, direct na print(f"Received: {request}"), de volgende regels toe. Die zetten een antwoordstring (”accepted” in ons geval) om naar bytes en sturen die naar de client. Zo zal de server, wanneer hij een bericht ontvangt dat niet ”close” is, de string ”accepted” als antwoord terugsturen:
response = "accepted".encode("utf-8") # convert string to bytes
# convert and send accept response to the client
client_socket.send(response)
Resources vrijgeven
Zodra we uit de oneindige while-lus breken, is de communicatie met de client voltooid. We sluiten dan de client-socket met de methode close om systeemresources vrij te geven. We sluiten ook de server-socket met dezelfde methode, wat de server effectief uitschakelt. In een echte situatie wil je natuurlijk meestal dat de server blijft luisteren naar andere clients en niet stopt na communicatie met slechts één, maar maak je geen zorgen: verderop volgt nog een voorbeeld.
Voeg voor nu de volgende regels toe na de oneindige while-lus:
# close connection socket with the client
client_socket.close()
print("Connection to client closed")
# close server socket
server.close()
Let op: vergeet niet de functie run_server aan het einde van je bestand server.py aan te roepen. Gebruik simpelweg de volgende regel code:
run_server()
Complete voorbeeldcode van de server-socket
Hier is de volledige broncode van server.py:
import socket
def run_server():
# create a socket object
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_ip = "127.0.0.1"
port = 8000
# bind the socket to a specific address and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen(0)
print(f"Listening on {server_ip}:{port}")
# accept incoming connections
client_socket, client_address = server.accept()
print(f"Accepted connection from {client_address[0]}:{client_address[1]}")
# receive data from the client
while True:
request = client_socket.recv(1024)
request = request.decode("utf-8") # convert bytes to string
# if we receive "close" from the client, then we break
# out of the loop and close the conneciton
if request.lower() == "close":
# send response to the client which acknowledges that the
# connection should be closed and break out of the loop
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
response = "accepted".encode("utf-8") # convert string to bytes
# convert and send accept response to the client
client_socket.send(response)
# close connection socket with the client
client_socket.close()
print("Connection to client closed")
# close server socket
server.close()
run_server()
Merk op dat we om dit basale voorbeeld niet te compliceren de foutafhandeling hebben weggelaten. Je wilt uiteraard try-except-blokken toevoegen en ervoor zorgen dat je de sockets altijd sluit in de finally-clausule. Lees verder voor een geavanceerder voorbeeld.
Een client-socket maken in Python
Na het opzetten van je server is de volgende stap een client opzetten die verbinding maakt en verzoeken naar je server stuurt. Laten we beginnen met de onderstaande stappen:
Een Python-bestand met wat boilerplate maken
- Maak een nieuw bestand met de naam
client.py - Importeer de socketbibliotheek:
import socket
- Definieer de functie
run_clientwaarin we al onze code plaatsen:
def run_client():
# your code will go here
Socketobject instantieren
Gebruik vervolgens de functie socket.socket() om een TCP-socketobject te maken dat als aanspreekpunt van de client bij de server dient.
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Verbinden met de serversocket
Specificeer het IP-adres en de poort van de server om ermee te verbinden. Deze moeten overeenkomen met het IP-adres en de poort die je eerder in server.py instelde.
server_ip = "127.0.0.1" # replace with the server's IP address
server_port = 8000 # replace with the server's port number
Maak een verbinding met de server met de methode connect op het client-socketobject. Merk op dat we de client-socket niet aan een IP-adres of poort hebben gebonden. Dat is normaal voor de client, omdat connect automatisch een vrije poort kiest en een IP-adres selecteert dat de beste route naar de server biedt vanaf de netwerkinterfaces van het systeem (127.0.0.1 in ons geval) en de client-socket daaraan bindt.
# establish connection with server
client.connect((server_ip, server_port))
Communicatielus maken
Na het tot stand brengen van de verbinding beginnen we een oneindige communicatielus om meerdere berichten naar de server te sturen. We vragen invoer van de gebruiker met Pythons ingebouwde functie input, coderen die vervolgens naar bytes en snoeien af tot maximaal 1024 bytes. Daarna sturen we het bericht naar de server met client.send.
while True:
# input message and send it to the server
msg = input("Enter message: ")
client.send(msg.encode("utf-8")[:1024])
Reactie van de server afhandelen
Zodra de server een bericht van de client ontvangt, reageert hij daarop. In onze clientcode willen we nu de reactie van de server ontvangen. Daarvoor gebruiken we in de communicatielus de methode recv om maximaal 1024 bytes te lezen. Vervolgens zetten we de reactie met decode om van bytes naar een string en controleren dan of die gelijk is aan ”closed”. Als dat zo is, breken we uit de lus, wat zoals we later zien de verbinding van de client beëindigt. Anders printen we de reactie van de server naar de console.
# receive message from the server
response = client.recv(1024)
response = response.decode("utf-8")
# if server sent us "closed" in the payload, we break out of the loop and close our socket
if response.lower() == "closed":
break
print(f"Received: {response}")
Resources vrijgeven
Sluit tenslotte na de while-lus de client-socket met de methode close. Zo worden resources netjes vrijgegeven en wordt de verbinding beëindigd (d.w.z. wanneer we het bericht “closed” ontvangen en uit de while-lus breken).
# close client socket (connection to the server)
client.close()
print("Connection to server closed")
Let op: Vergeet opnieuw niet de functie run_client, die we hierboven hebben geïmplementeerd, aan het einde van het bestand aan te roepen:
run_client()
Complete voorbeeldcode van de client-socket
Hier is de volledige code van client.py:
import socket
def run_client():
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_ip = "127.0.0.1" # replace with the server's IP address
server_port = 8000 # replace with the server's port number
# establish connection with server
client.connect((server_ip, server_port))
while True:
# input message and send it to the server
msg = input("Enter message: ")
client.send(msg.encode("utf-8")[:1024])
# receive message from the server
response = client.recv(1024)
response = response.decode("utf-8")
# if server sent us "closed" in the payload, we break out of the loop and close our socket
if response.lower() == "closed":
break
print(f"Received: {response}")
# close client socket (connection to the server)
client.close()
print("Connection to server closed")
run_client()
Test je client en server
Voer het volgende uit om de server- en clientimplementatie van hierboven te testen:
- Open twee terminalvensters tegelijk.
- Ga in één terminalvenster naar de map waar het bestand
server.pystaat en voer het volgende commando uit om de server te starten:
python server.py
Hiermee bindt de server-socket aan het localhost-adres (127.0.0.1) op poort 8000 en begint hij te luisteren naar binnenkomende verbindingen.
- Ga in het andere terminalvenster naar de map waar het bestand
client.pystaat en voer het volgende commando uit om de client te starten:
python client.py
Je wordt dan om invoer gevraagd. Typ je bericht en druk op Enter. Dit verstuurt je invoer naar de server en toont die in het terminalvenster van de server. De server stuurt zijn reactie naar de client en die zal je opnieuw om invoer vragen. Dit gaat door totdat je de string ”close” naar de server stuurt.
Werken met meerdere clients – multithreading
We hebben gezien hoe een server op verzoeken van één client reageert in het vorige voorbeeld, maar in veel praktijksituaties moeten meerdere clients tegelijk met één server kunnen verbinden. Dit is waar multithreading om de hoek komt kijken. Multithreading wordt gebruikt wanneer je meerdere taken (bijv. verschillende functies) tegelijk moet afhandelen.
Het idee is een thread te starten: een onafhankelijke instructieset die door de processor kan worden uitgevoerd. Threads zijn veel lichter dan processen, omdat ze binnen een proces zelf leven en je er niet veel resources voor hoeft te alloceren.
Beperkingen van multithreading in Python
In de standaard CPython-build draait er maar één thread tegelijk Python-bytecode, dankzij de Global Interpreter Lock (GIL). Voor socketprogrammering is dat prima — de GIL wordt vrijgegeven tijdens netwerk-I/O, dus threads kunnen echt parallel op verbindingen wachten. Sinds Python 3.13 (oktober 2024) biedt CPython ook een free-threaded build (PEP 703) die de GIL verwijdert, en asyncio is de gangbare moderne keuze voor socketcode met hoge concurrency.
Voorbeeld van een multithreaded server
Kijk naar het onderstaande voorbeeld om te zien hoe je multithreading aan je server toevoegt om een groot aantal clients te verwerken. Let op dat we ditmaal ook basisfoutafhandeling toevoegen met try-except-finally-blokken. Volg om te beginnen de onderstaande stappen:
Een serverfunctie maken die threads start
Importeer in je Python-bestand de modules socket en threading om met zowel sockets als threads te kunnen werken:
import socket
import threading
Definieer de functie run_server die, net als in het bovenstaande voorbeeld, een server-socket maakt, die bindt en naar binnenkomende verbindingen luistert. Roep vervolgens in een oneindige while-lus accept aan. Dit zorgt ervoor dat er altijd naar nieuwe verbindingen wordt geluisterd. Nadat accept een binnenkomende verbinding heeft ontvangen en is teruggekeerd, maak je een thread met de constructor threading.Thread. Deze thread voert de functie handle_client uit, die we zo definiëren, en je geeft client_socket en addr als argumenten mee (addr is een tuple met IP-adres en poort van de verbonden client). Nadat de thread is gemaakt, roepen we start aan om de uitvoering te beginnen.
Onthoud dat de aanroep accept blokkeert, dus in de eerste iteratie van de while-lus, wanneer we de regel met accept bereiken, pauzeren we en wachten we op een clientverbinding zonder iets anders uit te voeren. Zodra de client verbinding maakt, retourneert de methode accept en gaan we verder: we starten een thread die die client afhandelt en gaan door naar de volgende iteratie, waar we opnieuw pauzeren bij de aanroep van accept en wachten op de volgende client.
Aan het einde van de functie hebben we foutafhandeling die ervoor zorgt dat de server-socket altijd wordt gesloten als er iets onverwachts gebeurt.
def run_server():
server_ip = "127.0.0.1" # server hostname or IP address
port = 8000 # server port number
# create a socket object
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to the host and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen()
print(f"Listening on {server_ip}:{port}")
while True:
# accept a client connection
client_socket, addr = server.accept()
print(f"Accepted connection from {addr[0]}:{addr[1]}")
# start a new thread to handle the client
thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
thread.start()
except Exception as e:
print(f"Error: {e}")
finally:
server.close()
Let op: de server in ons voorbeeld wordt alleen gestopt als er een onverwachte fout optreedt. Anders luistert hij onbeperkt naar clients en moet je de terminal beëindigen om hem te stoppen.
Een clientafhandelingsfunctie maken voor een aparte thread
Definieer nu, boven de functie run_server, een andere functie genaamd handle_client. Deze functie draait in een aparte thread voor elke clientverbinding. Hij ontvangt het socketobject van de client en de tuple addr als argumenten.
Binnen deze functie doen we hetzelfde als in het single-threaded voorbeeld plus wat foutafhandeling: we starten een lus om berichten van de client te ontvangen met recv.
Daarna controleren we of we een close-bericht hebben gekregen. Zo ja, dan reageren we met de string ”closed” en sluiten we de verbinding door uit de lus te breken. Anders printen we de request-string van de client naar de console en gaan we door naar de volgende iteratie om het volgende bericht te ontvangen.
Aan het einde van deze functie hebben we foutafhandeling voor onverwachte gevallen (except-clausule) en ook een finally-clausule waarin we client_socket vrijgeven met close. Deze finally-clausule wordt altijd uitgevoerd, wat garandeert dat de client-socket netjes wordt vrijgegeven.
def handle_client(client_socket, addr):
try:
while True:
# receive and print client messages
request = client_socket.recv(1024).decode("utf-8")
if request.lower() == "close":
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
# convert and send accept response to the client
response = "accepted"
client_socket.send(response.encode("utf-8"))
except Exception as e:
print(f"Error when handling client: {e}")
finally:
client_socket.close()
print(f"Connection to client ({addr[0]}:{addr[1]}) closed")
Wanneer handle_client terugkeert, wordt de thread die hem uitvoerde automatisch vrijgegeven.
Let op: Vergeet niet de functie run_server aan het einde van je bestand aan te roepen.
Complete voorbeeldcode van de multithreaded server
Laten we nu de volledige multithreading-servercode samenvoegen:
import socket
import threading
def handle_client(client_socket, addr):
try:
while True:
# receive and print client messages
request = client_socket.recv(1024).decode("utf-8")
if request.lower() == "close":
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
# convert and send accept response to the client
response = "accepted"
client_socket.send(response.encode("utf-8"))
except Exception as e:
print(f"Error when hanlding client: {e}")
finally:
client_socket.close()
print(f"Connection to client ({addr[0]}:{addr[1]}) closed")
def run_server():
server_ip = "127.0.0.1" # server hostname or IP address
port = 8000 # server port number
# create a socket object
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to the host and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen()
print(f"Listening on {server_ip}:{port}")
while True:
# accept a client connection
client_socket, addr = server.accept()
print(f"Accepted connection from {addr[0]}:{addr[1]}")
# start a new thread to handle the client
thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
thread.start()
except Exception as e:
print(f"Error: {e}")
finally:
server.close()
run_server()
Let op: In echte code is het, om mogelijke problemen zoals racecondities of datainconsistenties bij multithreaded servers te voorkomen, cruciaal om rekening te houden met threadveiligheid en synchronisatietechnieken. In ons eenvoudige voorbeeld speelt dat echter geen rol.
Clientvoorbeeld met basale foutafhandeling
Nu we een serverimplementatie hebben die meerdere clients gelijktijdig kan afhandelen, kunnen we dezelfde client gebruiken als in de eerste basale voorbeelden of hem licht aanpassen en foutafhandeling toevoegen. Hieronder vind je de code, identiek aan het vorige clientvoorbeeld, met toevoeging van try-except-blokken:
import socket
def run_client():
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_ip = "127.0.0.1" # replace with the server's IP address
server_port = 8000 # replace with the server's port number
# establish connection with server
client.connect((server_ip, server_port))
try:
while True:
# get input message from user and send it to the server
msg = input("Enter message: ")
client.send(msg.encode("utf-8")[:1024])
# receive message from the server
response = client.recv(1024)
response = response.decode("utf-8")
# if server sent us "closed" in the payload, we break out of
# the loop and close our socket
if response.lower() == "closed":
break
print(f"Received: {response}")
except Exception as e:
print(f"Error: {e}")
finally:
# close client socket (connection to the server)
client.close()
print("Connection to server closed")
run_client()
Het multithreadingvoorbeeld testen
Als je de multi-clientimplementatie wilt testen, open dan meerdere terminalvensters voor clients en één voor de server. Start eerst de server met python server.py. Start daarna een paar clients met python client.py. In het serverterminalvenster zie je hoe nieuwe clients verbinding maken met de server. Je kunt nu berichten versturen vanaf verschillende clients door in de respectieve terminals tekst in te voeren; ze worden allemaal afgehandeld en op de console aan de serverkant geprint.
Toepassingen van socketprogrammering in data science
Hoewel elke netwerkapplicatie onder de motorkap door het OS aangemaakte sockets gebruikt, zijn er talloze systemen die specifiek zwaar op socketprogrammering steunen, hetzij voor bepaalde speciale use-cases, hetzij om de prestaties te verbeteren. Maar hoe is socketprogrammering precies nuttig in de context van data science? Het speelt zeker een betekenisvolle rol wanneer er snel enorme hoeveelheden data moeten worden ontvangen of verzonden. Daarom wordt socketprogrammering vooral gebruikt voor dataverzameling en real-time verwerking, gedistribueerd rekenen en inter-processcommunicatie. Laten we enkele concrete toepassingen binnen data science nader bekijken.
Real-time dataverzameling
Sockets worden veel gebruikt om real-time data te verzamelen uit verschillende bronnen voor verdere verwerking, doorsturen naar een database of een analysetraject enz. Zo kan een socket direct data ontvangen van een financieel systeem of socialmedia-API voor latere verwerking door data scientists.
Gedistribueerd rekenen
Data scientists kunnen socketconnectiviteit gebruiken om de verwerking en berekening van enorme datasets over meerdere machines te verdelen. Socketprogrammering wordt veel gebruikt in Apache Spark en andere frameworks voor gedistribueerd rekenen voor de communicatie tussen nodes.
Modeldeployment
Socketprogrammering kan worden ingezet bij het aanbieden van machinelearningmodellen aan gebruikers, waardoor voorspellingen en suggesties onmiddellijk geleverd kunnen worden. Om real-time besluitvorming te faciliteren, kunnen data scientists performante, socketgebaseerde serverapplicaties gebruiken die grote hoeveelheden data innemen, verwerken met getrainde modellen en de resultaten snel teruggeven aan de client.
Inter-Process Communication (IPC)
Sockets kunnen worden gebruikt voor IPC, waarmee verschillende processen op dezelfde machine met elkaar kunnen communiceren en data kunnen uitwisselen. Dit is nuttig in data science om complexe en resource-intensieve berekeningen over meerdere processen te verdelen. De subprocessing-bibliotheek van Python wordt hiervoor vaak gebruikt: die start meerdere processen om meerdere processorcores te benutten en de performance te verhogen bij zware berekeningen. Communicatie tussen zulke processen kan via IPC-sockets worden geïmplementeerd.
Samenwerking en communicatie
Socketprogrammering maakt real-time communicatie en samenwerking tussen data scientists mogelijk. Voor effectieve samenwerking en kennisdeling worden socketgebaseerde chatapps of collaboratieve data-analyseplatforms gebruikt.
Het is het vermelden waard dat data scientists in veel van bovenstaande toepassingen niet direct met sockets werken. Ze gebruiken meestal bibliotheken, frameworks en systemen die alle low-level details van socketprogrammering abstraheren. Onder de motorkap zijn al deze oplossingen echter gebaseerd op socketcommunicatie en maken ze gebruik van socketprogrammering.
Uitdagingen en best practices bij socketprogrammering
Omdat sockets een low-level concept voor verbindingsbeheer zijn, moeten ontwikkelaars die ermee werken alle vereiste infrastructuur eromheen implementeren om robuuste en betrouwbare applicaties te bouwen. Dat brengt natuurlijk de nodige uitdagingen met zich mee. Er zijn echter best practices en algemene richtlijnen om deze problemen te tackelen. Hieronder staan enkele van de meest voorkomende problemen bij socketprogrammering, met algemene tips:
Verbindingsbeheer
Werken met veel gelijktijdige verbindingen; meerdere clients beheren en zorgen voor efficiënte afhandeling van gelijktijdige verzoeken kan uitdagend en niet triviaal zijn. Het vereist zorgvuldig resourcemanagement en coördinatie om bottlenecks te vermijden.
Best practices
- Houd actieve verbindingen bij met datastructuren zoals lijsten of dictionaries. Of gebruik geavanceerde technieken zoals connection pooling, die ook helpen met schaalbaarheid.
- Gebruik threading of asynchrone programmeertechnieken om meerdere clientverbindingen tegelijk af te handelen.
- Sluit verbindingen netjes om resources vrij te geven en geheugenlekken te voorkomen.
Foutafhandeling
Omgaan met fouten, zoals verbindingsproblemen, time-outs en transmissieproblemen, is cruciaal. Het afhandelen van deze fouten en het geven van passende feedback aan clients kan uitdagend zijn, zeker bij low-level socketprogrammering.
Best practices
- Gebruik try-except-finally-blokken om specifieke fouttypes op te vangen en af te handelen.
- Voorzie informatieve foutmeldingen en overweeg logging in te zetten om problemen te onderzoeken.
Schaalbaarheid en performance
Zorgen voor optimale prestaties en het minimaliseren van latency zijn belangrijke aandachtspunten bij grote datastromen of real-time applicaties.
Best practices
- Optimaliseer je code voor performance door onnodige dataverwerking en netwerkoverhead te minimaliseren.
- Implementeer bufferingstechnieken om grote datatransfers efficiënt te verwerken.
- Overweeg loadbalancing om clientverzoeken over meerdere serverinstances te verdelen.
Beveiliging en authenticatie
Het beveiligen van socketgebaseerde communicatie en het implementeren van goede authenticatiemechanismen kan lastig zijn. Zorgen voor dataprivacy, het voorkomen van ongeautoriseerde toegang en bescherming tegen kwaadwillende activiteiten vereisen zorgvuldige afweging en implementatie van veilige protocollen.
Best practices
- Gebruik SSL/TLS om dataoverdracht te beveiligen door informatie te versleutelen.
- Zorg voor clientidentiteit met veilige authenticatiemethoden zoals token-based authenticatie, public-keycryptografie of gebruikersnaam/wachtwoord.
- Zorg dat gevoelige data, zoals wachtwoorden of API-sleutels, worden beschermd en versleuteld of idealiter niet opgeslagen (alleen hun hashes indien nodig).
Netwerkbetrouwbaarheid en veerkracht
Omgaan met netwerkonderbrekingen, fluctuerende bandbreedte en onbetrouwbare verbindingen kan uitdagingen opleveren. Een stabiele verbinding behouden, ontkoppelingen gracieus afhandelen en mechanismen voor opnieuw verbinden implementeren zijn essentieel voor robuuste netwerkapplicaties.
Best practices
- Gebruik keep-alive-berichten om inactieve of verbroken verbindingen te detecteren.
- Implementeer time-outs om eindeloos blokkeren te voorkomen en tijdige afhandeling te garanderen.
- Implementeer exponentiële backoff bij het opnieuw verbinden om de verbinding te herstellen als die is weggevallen.
Onderhoudbaarheid van code
Last but not least: onderhoudbaarheid. Door het low-level karakter van socketprogrammering schrijven ontwikkelaars vaak meer code. Dat kan snel onoverzichtelijke spaghetti worden, dus het is essentieel om de code vroeg te organiseren en structureren en extra aandacht te besteden aan je architectuur.
Best practices
- Verdeel je code in klassen of functies die idealiter niet te lang zijn.
- Schrijf vroeg unit tests door je client- en serverimplementaties te mocken.
- Overweeg hoger-niveau bibliotheken te gebruiken voor verbindingen, tenzij je per se socketprogrammering moet gebruiken.
Afronding: socketprogrammering in Python
Sockets zijn een integraal onderdeel van alle netwerkapplicaties. In dit artikel hebben we socketprogrammering in Python bekeken. Dit zijn de belangrijkste punten om te onthouden:
- Sockets zijn interfaces die het beheer van verbindingen abstraheren.
- Sockets maken communicatie mogelijk tussen verschillende processen (meestal een client en een server), lokaal of via een netwerk.
- In Python werk je met sockets via de bibliotheek
socket, die onder meer een socketobject biedt met verschillende methoden zoalsrecv,send,listen,close. - Socketprogrammering kent diverse toepassingen die nuttig zijn in data science, waaronder dataverzameling, inter-processcommunicatie en gedistribueerd rekenen.
- Uitdagingen bij socketprogrammering zijn onder meer verbindingsbeheer, dataintegriteit, schaalbaarheid, foutafhandeling, beveiliging en onderhoudbaarheid van code.
Met vaardigheden in socketprogrammering kunnen ontwikkelaars efficiënte, real-time netwerkapplicaties bouwen. Door de concepten en best practices te beheersen, kun je het volledige potentieel van socketprogrammering benutten voor betrouwbare en schaalbare oplossingen.
Socketprogrammering is echter een zeer low-level techniek, die lastig is omdat applicatie-engineers met elk detail van de applicatiecommunicatie rekening moeten houden.
Tegenwoordig hoeven we vaak niet direct met sockets te werken, omdat hogere-niveaulibraries en -frameworks dit doorgaans afhandelen, tenzij er echt behoefte is om de laatste prestaties uit de applicatie te persen of op te schalen.
Toch leidt begrip van sockets en inzicht in hoe dingen onder de motorkap werken tot een beter totaaloverzicht als ontwikkelaar of data scientist, en is het altijd een goed idee.
Wil je meer leren over de rol van Python in netwerkanalyse, bekijk dan onze cursus Intermediate Network Analysis in Python. Je kunt ook onze Python Programming skill track volgen om je Pythonvaardigheden te verbeteren.

Serhii is een software engineer met 3 jaar ervaring, vooral in back-endtechnologieën zoals DBMS, databasespecifieke talen, Python, Node.js enzovoort. Hij heeft ook ervaring met front-endframeworks zoals Vue.js en React.
FAQs
Waarvoor wordt socketprogrammering in Python gebruikt?
Socketprogrammering in Python laat applicaties data uitwisselen over een netwerk of tussen lokale processen. Veelgebruikte toepassingen zijn chatapps, multiplayergames, IoT-communicatie, gedistribueerd rekenen en het aanbieden van machinelearningmodellen. De ingebouwde bibliotheek socket ondersteunt zowel client- als servercode.
Wat is het verschil tussen TCP- en UDP-sockets in Python?
TCP-sockets (socket.SOCK_STREAM) bieden betrouwbare, geordende levering met foutcontrole — ideaal voor chatapps, bestandsoverdracht en webservers. UDP-sockets (socket.SOCK_DGRAM) zijn sneller maar onbetrouwbaar "fire and forget" en daardoor geschikt voor videostreaming, gaming en IoT-sensoren.
Moet ik Python-sockets of asyncio gebruiken voor netwerkprogrammering?
Gebruik raw sockets als je fijngranulaire controle nodig hebt of wilt leren hoe netwerken werken. Gebruik asyncio voor de meeste productiesituaties — het verwerkt duizenden gelijktijdige verbindingen efficiënter dan threading. FastAPI en aiohttp zijn beide gebouwd op asyncio.
Hoe behandel ik meerdere clients met Python-sockets?
De eenvoudigste aanpak is multithreading — start een nieuwe thread per verbinding met threading.Thread. Voor hogere concurrency gebruik je asyncio om veel clients in één thread af te handelen. Voor productie biedt de standaardbibliotheekmodule socketserver kant-en-klare klassen zoals ThreadingTCPServer en ForkingTCPServer.
Zijn Python-socketverbindingen veilig?
Raw Python-sockets verzenden standaard onversleutelde data — iedereen die het verkeer onderschept, kan het lezen. Voor veilige communicatie wikkel je je socket in TLS met de ingebouwde module ssl (ssl.SSLContext.wrap_socket()). Gebruik in productie altijd TLS voor gevoelige data.
