Come padroneggiare le basi della comunicazione P2P con socket in Python

La comunicazione P2P (peer-to-peer) è una tecnologia che consente la comunicazione diretta tra dispositivi senza passare attraverso un server centrale. Impariamo le basi della comunicazione P2P utilizzando socket in Python, comprendendo i concetti fondamentali e esplorando esempi applicativi pratici. In questo articolo, partiamo dai concetti base della comunicazione tramite socket, passando per l’implementazione in Python, considerazioni sulla sicurezza, e infine esempi applicativi concreti.

Indice

Cos’è la comunicazione tramite socket?

La comunicazione tramite socket è un mezzo per inviare e ricevere dati attraverso una rete. I socket funzionano come punti terminali di comunicazione, identificando il destinatario tramite indirizzo IP e numero di porta. Rivestono un ruolo fondamentale sia nello scambio di dati tra client e server, sia nella comunicazione peer-to-peer (P2P). La comunicazione tramite socket consente una comunicazione a basso livello e flessibile, indipendente dai protocolli.

Tipi di socket e i loro usi

Esistono principalmente due tipi di socket: socket TCP e socket UDP.

Socket TCP

I socket TCP (Transmission Control Protocol) offrono un trasferimento di dati affidabile. Garantiscono che i dati arrivino in ordine e senza perdite o duplicazioni, rendendoli ideali per applicazioni in cui l’affidabilità è essenziale, come il trasferimento di file e la navigazione web.

Socket UDP

I socket UDP (User Datagram Protocol) offrono un trasferimento di dati leggero e veloce. Tuttavia, non garantiscono né l’ordine né l’integrità dei dati, risultando più adatti per applicazioni in tempo reale come lo streaming o i giochi online.

È importante scegliere il tipo di socket in base allo scopo specifico dell’applicazione.

Fondamenti della programmazione con socket in Python

La programmazione con socket in Python è facilmente implementabile grazie al modulo standard socket. Vediamo come creare e gestire un socket di base.

Creazione di un socket

Per prima cosa, vediamo come creare un socket. Di seguito è riportato un esempio per la creazione di un socket TCP.

import socket

# Creazione del socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Connessione del socket

Successivamente, colleghiamo il socket a un indirizzo IP e a una porta specifici.

# Indirizzo e porta di destinazione
server_address = ('localhost', 65432)

# Connessione al server
sock.connect(server_address)

Invio e ricezione di dati

Una volta stabilita la connessione, è possibile inviare e ricevere dati.

# Invio dei dati
message = 'Hello, Server!'
sock.sendall(message.encode('utf-8'))

# Ricezione dei dati
data = sock.recv(1024)
print('Received:', data.decode('utf-8'))

Chiusura del socket

Quando la comunicazione è terminata, è importante chiudere il socket.

# Chiusura del socket
sock.close()

Abbiamo ora completato la programmazione di base con socket in Python. Successivamente, esploreremo la struttura di base di server e client.

Struttura di base di server e client

Nella comunicazione tramite socket, server e client svolgono ciascuno ruoli specifici. Qui esaminiamo la struttura di base e i metodi di implementazione di un server e di un client.

Struttura di base del server

Un server attende le richieste di connessione dei client e le gestisce. Di seguito è riportato un esempio di base di un server TCP.

import socket

# Indirizzo e porta del server
server_address = ('localhost', 65432)

# Creazione del socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Binding del socket
server_socket.bind(server_address)

# In attesa di connessioni
server_socket.listen(1)
print('Waiting for a connection...')

# Accettazione della connessione
connection, client_address = server_socket.accept()
try:
    print('Connection from', client_address)

    # Ricezione e invio dei dati
    while True:
        data = connection.recv(1024)
        if data:
            print('Received:', data.decode('utf-8'))
            connection.sendall(data)
        else:
            break
finally:
    # Chiusura della connessione
    connection.close()
    server_socket.close()

Struttura di base del client

Un client si connette al server e scambia dati con esso. Di seguito è riportato un esempio di base di un client TCP.

import socket

# Indirizzo e porta del server
server_address = ('localhost', 65432)

# Creazione del socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connessione al server
client_socket.connect(server_address)

try:
    # Invio dei dati
    message = 'Hello, Server!'
    client_socket.sendall(message.encode('utf-8'))

    #

 Ricezione dei dati
    data = client_socket.recv(1024)
    print('Received:', data.decode('utf-8'))
finally:
    # Chiusura del socket
    client_socket.close()

Il server attende la connessione, e una volta che il client invia una richiesta, la comunicazione ha inizio. Questo consente lo scambio di dati tra server e client. Ora passeremo a un esempio concreto di comunicazione P2P.

Esempio semplice di comunicazione P2P

Nella comunicazione P2P, ogni peer svolge il ruolo sia di client che di server, scambiando dati direttamente con altri peer. Qui forniamo un esempio di base di comunicazione P2P utilizzando Python.

Struttura di base di un nodo P2P

Un nodo P2P possiede sia funzionalità server che client per comunicare con altri peer. Di seguito è riportata la struttura di base.

Ruolo come server

La parte server che accetta connessioni da altri peer.

import socket
import threading

def handle_client(client_socket):
    while True:
        data = client_socket.recv(1024)
        if not data:
            break
        print('Received:', data.decode('utf-8'))
        client_socket.sendall(data)
    client_socket.close()

def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('localhost', 65432))
    server_socket.listen(5)
    print('Server listening on port 65432')

    while True:
        client_socket, addr = server_socket.accept()
        print('Accepted connection from', addr)
        client_handler = threading.Thread(target=handle_client, args=(client_socket,))
        client_handler.start()

server_thread = threading.Thread(target=start_server)
server_thread.start()

Ruolo come client

La parte client che si connette ad altri peer e invia dati.

import socket

def start_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('localhost', 65432))

    try:
        message = 'Hello, P2P Peer!'
        client_socket.sendall(message.encode('utf-8'))

        response = client_socket.recv(1024)
        print('Received:', response.decode('utf-8'))
    finally:
        client_socket.close()

client_thread = threading.Thread(target=start_client)
client_thread.start()

Esecuzione del nodo P2P

Eseguendo la parte server e la parte client in thread separati, è possibile simulare un nodo P2P. In una rete P2P reale, più nodi sono connessi tra loro e scambiano dati.

Questo esempio di base di comunicazione P2P può essere espanso per sviluppare applicazioni più complesse. Ora discuteremo le considerazioni sulla sicurezza nella comunicazione P2P.

Sicurezza nella comunicazione P2P

La comunicazione P2P è una tecnologia potente e conveniente, ma presenta anche problematiche di sicurezza. Qui esamineremo le principali considerazioni di sicurezza nella comunicazione P2P.

Autenticazione e autorizzazione

Per garantire che i nodi partecipanti alla rete P2P siano affidabili, è necessario implementare meccanismi di autenticazione e autorizzazione. Questo aiuta a prevenire l’accesso di nodi non autorizzati e a proteggere i dati da modifiche o intercettazioni.

Esempio di implementazione dell’autenticazione

Un semplice esempio di autenticazione utilizzando la crittografia a chiave pubblica.

import hashlib
import os

def generate_hash(data):
    return hashlib.sha256(data.encode()).hexdigest()

def authenticate_peer(peer_data, expected_hash):
    return generate_hash(peer_data) == expected_hash

# Dati del peer e loro hash
peer_data = "PeerData123"
expected_hash = generate_hash(peer_data)

# Verifica dell'autenticazione
if authenticate_peer(peer_data, expected_hash):
    print("Peer authenticated")
else:
    print("Peer authentication failed")

Crittografia

Per proteggere i dati nella comunicazione P2P, è possibile crittografare il traffico. Questo impedisce che i dati vengano intercettati da terze parti.

Esempio di implementazione della crittografia

Un esempio di crittografia SSL/TLS utilizzando il modulo ssl di Python.

import ssl
import socket

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 65432))
server_socket.listen(5)

with context.wrap_socket(server_socket, server_side=True) as ssock:
    client_socket, addr = ssock.accept()
    print('Accepted connection from', addr)
    data = client_socket.recv(1024)
    print('Received encrypted:', data)

Anonimato della rete

Per preservare l’anonimato nella rete P2P, si possono utilizzare tecnologie di anonimizzazione come Tor, che nascondono la posizione e il contenuto delle comunicazioni dei nodi.

Integrità dei dati

Per assicurarsi che i dati trasmessi non vengano alterati, si possono utilizzare firme digitali e funzioni hash per verificarne l’integrità.

La sicurezza nella comunicazione P2P richiede un approccio multilivello. Implementare autenticazione, crittografia, anonimato e integrità dei dati è fondamentale per garantire una comunicazione sicura. Passiamo ora a un esempio applicativo concreto di costruzione di un’app per la condivisione di file.

Esempio Applicativo: Costruzione di un’App di Condivisione File

Costruendo un’app di condivisione file utilizzando comunicazioni P2P, puoi acquisire competenze pratiche. Di seguito, presentiamo i passaggi per creare una semplice app di condivisione file.

Panoramica dell’App

In questa app di condivisione file, l’utente può inviare i file specificati ad altri nodi sulla rete P2P o ricevere file da altri nodi.

Condivisione dei File come Server

Prima di tutto, creiamo la parte server che fornirà i file.

import socket
import threading

def handle_client(client_socket):
    file_name = client_socket.recv(1024).decode('utf-8')
    try:
        with open(file_name, 'rb') as f:
            data = f.read()
            client_socket.sendall(data)
        print(f"Sent file {file_name} to client")
    except FileNotFoundError:
        client_socket.sendall(b"File not found")
    client_socket.close()

def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('localhost', 65432))
    server_socket.listen(5)
    print('Server listening on port 65432')

    while True:
        client_socket, addr = server_socket.accept()
        print('Accepted connection from', addr)
        client_handler = threading.Thread(target=handle_client, args=(client_socket,))
        client_handler.start()

server_thread = threading.Thread(target=start_server)
server_thread.start()

Download dei File come Client

Successivamente, creiamo la parte client che richiede e riceve i file.

import socket

def start_client(file_name):
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('localhost', 65432))

    try:
        client_socket.sendall(file_name.encode('utf-8'))
        data = client_socket.recv(1024)
        if data == b"File not found":
            print("File not found on server")
        else:
            with open(f"downloaded_{file_name}", 'wb') as f:
                f.write(data)
            print(f"Received file {file_name} from server")
    finally:
        client_socket.close()

# Esempio di richiesta per il file "example.txt"
client_thread = threading.Thread(target=start_client, args=("example.txt",))
client_thread.start()

Esecuzione dell’App di Condivisione File

Eseguendo la parte server e la parte client sopra descritte, è possibile avviare l’app di condivisione file. Il server attende le connessioni sulla porta specificata e invia i file quando il client li richiede.

Basandosi su questa semplice app di condivisione file, è possibile aggiungere funzionalità più avanzate per sviluppare un’applicazione P2P pratica. Di seguito, presentiamo alcuni esercizi per approfondire la comprensione.

Esercizi

Comprendere le basi della comunicazione P2P e mettere in pratica queste conoscenze ti aiuterà ad approfondire la comprensione. Prova a risolvere gli esercizi seguenti.

Esercizio 1: Estensione del Trasferimento di File

Estendi l’app di condivisione file per permettere il trasferimento di più file contemporaneamente. Implementa una funzionalità in cui il client invia un elenco di file richiesti e il server risponde inviando i file presenti nella lista.

Esercizio 2: Aggiunta di Sicurezza

Aggiungi la crittografia SSL/TLS all’app di condivisione file per migliorare la sicurezza delle comunicazioni. Utilizza il modulo ssl di Python per crittografare il trasferimento dei dati tra server e client.

Suggerimento

import ssl

# Creazione di un contesto SSL
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

# Creazione del socket SSL lato server
server_socket = context.wrap_socket(server_socket, server_side=True)

# Creazione del socket SSL lato client
client_socket = context.wrap_socket(client_socket, server_side=False)

Esercizio 3: Costruzione di una Rete P2P

Costruisci una rete P2P in cui più nodi si connettono tra loro. Ogni nodo funziona sia come server che come client, permettendo la comunicazione con altri nodi. Implementa una funzione di ricerca dei file nella rete, consentendo di individuare i nodi che possiedono un file specifico.

Suggerimento

  • Assegna un ID univoco a ciascun nodo
  • Trasmetti l’elenco dei file posseduti da ciascun nodo
  • Rispondi alle richieste di file ricevute da altri nodi

Esercizio 4: Aggiunta di una GUI

Aggiungi una semplice GUI all’app di condivisione file per renderla più intuitiva per gli utenti. Utilizza il modulo tkinter di Python per implementare funzionalità come la selezione dei file, l’invio e la visualizzazione dello stato di ricezione.

Suggerimento

import tkinter as tk
from tkinter import filedialog

def select_file():
    file_path = filedialog.askopenfilename()
    print("Selected file:", file_path)

root = tk.Tk()
button = tk.Button(root, text="Select File", command=select_file)
button.pack()
root.mainloop()

Attraverso questi esercizi, migliora ulteriormente le tue competenze pratiche sulla comunicazione P2P. Successivamente, riassumiamo il contenuto trattato finora.

Conclusione

Abbiamo esplorato i fondamenti della programmazione di socket e della comunicazione P2P utilizzando Python. Dall’introduzione ai concetti base della comunicazione via socket, fino alla realizzazione di un semplice esempio di comunicazione P2P, e alla costruzione di un’app di condivisione file come esempio applicativo, con attenzione alla sicurezza e funzionalità avanzate. Infine, abbiamo presentato degli esercizi per rafforzare ulteriormente le tue competenze pratiche.

La comunicazione P2P, permettendo la comunicazione diretta senza passare per un server centrale, offre flessibilità ed efficienza, ma richiede anche una particolare attenzione alla sicurezza e affidabilità. Basandoti su quanto appreso in questo contenuto, esplora le potenzialità della comunicazione P2P sviluppando applicazioni più avanzate.

Indice