Scrivere Web Services in Python: i socket

Andare alle origini della programmazione web è sempre affascinante. Questo articolo aiuta a capire come funziona il web attraverso azioni sui socket.

Questo post è la traduzione pressoché letterale dell’articolo del blog Iximiuz di Ivan Velichko. Sue anche le illustrazioni e gli esempi di codice funzionante. Lo ringrazio tantissimo per la gentilezza e l’entusiasmo che ha dimostrato per il mio interesse.

In coda trovate i suoi riferimenti. Io ho solamente integrato con qualche esempio applicativo e qualche commento qua e la, sempre segnalato.

Cos’è un web server?

Iniziamo rispondendo alla domanda: che cos’è un server web?

Prima di tutto, è un server (nessun gioco di parole). Un server è un processo [sic] che serve i client.

Frequentemente pensiamo al server come ad una macchina, ma in realtà è solo un processo che gira in una certa macchina – assieme a molti altri processi.

Quindi, che ci sorprendiamo o no, un server non ha nulla a che fare con l’hardware. È solo un normale software eseguito da un sistema operativo. Come la maggior parte degli altri programmi in circolazione, un server acquisisce alcuni dati dal proprio input, trasforma i dati in base ad alcune logiche di business e quindi produce alcuni dati di output. Nel caso di un server web, l’input e l’output avvengono sulla rete tramite Hypertext Transfer Protocol (HTTP). Per un server Web, l’input è costituito da richieste HTTP dai suoi client: browser Web, applicazioni mobili, dispositivi IoT o persino altri servizi Web. E l’output è costituito da risposte HTTP, spesso sotto forma di pagine HTML, ma sono supportati anche altri formati.

client-server-opt
client-server-opt

Cos’è questo protocollo di trasferimento ipertestuale (HTTP)? Bene, a questo punto, basterebbe pensarlo come un protocollo di scambio dati basato su testo (cioè leggibile dall’uomo). E la parola protocollo può essere spiegata come una sorta di convenzione tra due o più parti sul formato e sulle regole di trasferimento dei dati. Nessun problema, ci sarà un articolo che coprirà i dettagli di HTTP in seguito, mentre il resto di questo articolo sarà incentrato su come i computer inviano dati arbitrari sulla rete.

Che aspetto ha la programmazione di rete?

Nei sistemi operativi simili a Unix, è abbastanza comune trattare i dispositivi I/O come file. Proprio come avviene per i normali file su disco, i mouse, le stampanti, i modem, ecc. possono essere aperti, letti/scritti e quindi chiusi.

dev-files-opt
dev-files-opt

Per ogni file aperto, il sistema operativo crea un cosiddetto descrittore di file. Semplificando un po’, un descrittore di file è solo un identificatore intero univoco di un file all’interno di un processo. Il sistema operativo fornisce una serie di funzioni chiamate di sistema per manipolare file che accettano un descrittore di file come argomento. Ecco un esempio canonico con le operazioni read() e write():

// C-ish pseudocode

int fd = open("/path/to/my/file", ...);

char buffer[2048];
read(fd, buffer, 2048);
write(fd, "some data", 10);

close(fd);

Poiché anche la comunicazione di rete è una forma di I/O, sarebbe ragionevole aspettarsi anche che si riduca a manipolazione di file. E in effetti, esiste un tipo speciale di file per questo tipo di I/O, chiamato socket.

Un socket è un altro pezzo di astrazione fornito dal sistema operativo. Come spesso accade con le astrazioni del computer, il concetto è stato preso in prestito dal mondo reale, in particolare dalle prese di alimentazione CA, quelle attaccate al muro in cui inseriamo la spina del frigo o della TV. Una coppia di socket consente a due processi di dialogare tra loro. In particolare, in rete. Un socket può essere aperto, i dati possono essere scritti sul socket o letti da esso. E, naturalmente, quando la presa non è più necessaria, ci si dovrebbe scollegare.

ac-power-socket-opt
ac-power-socket-opt

Di socket ce ne sono di parecchi tipi diversi e ci sono molti modi per usarli per la comunicazione tra processi. Ad esempio, i socket di rete possono essere utilizzati quando due processi risiedono su macchine diverse. Per i processi locali, i socket di dominio Unix possono essere una scelta migliore. Ma anche questi due tipi di socket possono essere di tipo diverso: datagram (o UDP), stream (o TCP), raw socket, ecc. Questa varietà può sembrare complicata all’inizio, ma fortunatamente esiste un approccio più o meno generico su come utilizzare socket di qualsiasi tipo nel codice. Imparare a programmare uno di questi tipi di socket ti darà la possibilità di estendere la conoscenza ad altri tipi.

// C-ish pseudocode again

int fd = socket(SOCK_TYPE_TCP);

sockaddr serv_addr = { /* ... */ };
connect(fd, serv_addr);

char buffer[2048];
read(fd, buffer, 2048);
write(fd, "some data", 10);

close(fd);

Se conforntiamo i due pseudocodici ci accorgiamo che facciamo delle operazioni simili: nel primo caso (I/O su disco) apriamo un file, nel secondo (I/O su rete) ci connettiamo ad un socket (“infiliamo una spina”).

Più avanti in questo articolo, ci concentreremo su una forma di comunicazione client-server tramite socket di rete utilizzando lo stack di protocolli TCP/IP. Apparentemente, questa è la forma più utilizzata al giorno d’oggi, in particolare perché i browser la utilizzano per accedere ai siti web.

Come i programmi comunicano in rete

Immagina che ci sia un’applicazione che vuole inviare un pezzo di testo relativamente lungo sulla rete. Supponiamo che il socket sia già stato aperto e che il programma stia per scriverewrite – (o, nel gergo della rete, inviaresend) questi dati al socket. Come verranno trasmessi questi dati?

I computer vivono in un mondo discreto. Le schede di interfaccia di rete (NIC) trasmettono i dati in piccole porzioni, poche centinaia di byte contemporaneamente. Allo stesso tempo, in generale, la dimensione dei dati che un programma può voler inviare non è comunque limitata e può superare le centinaia di gigabyte. Per trasmettere un pezzo di dati arbitrario di grandi dimensioni sulla rete, è necessario che venga suddiviso in blocchi (chunked) e ogni blocco deve essere inviato separatamente. Logicamente, la dimensione massima del blocco non deve superare la limitazione della scheda di rete.

chunking-opt
chunking-opt

Ogni blocco è composto da due parti: le informazioni di controllo e l’informazione vera e propria (payload). Le informazioni di controllo includono gli indirizzi di origine e di destinazione, la dimensione del blocco, un checksum, ecc. Mentre il payload è… be’, i dati effettivi che il programma vuole inviare. È la vera merce che i computer si scambiano, ad esempio il contenuto di un singolo post di Twitter o di una intera pagina web.

Il più delle volte, per indirizzare i computer nella rete, vengono assegnati i cosiddetti indirizzi IP. L’acronimo IP sta per Internet Protocol, un famoso protocollo che ha reso possibile l’interconnessione delle reti (networks) dando vita a Internet. Il protocollo Internet è principalmente responsabile di 3 cose:

  • indirizzare le interfacce degli host (dei computer);
  • incapsulare i dati del payload in pacchetti (cioè il suddetto chunking);
  • instradare i pacchetti da una sorgente a una destinazione attraverso una o più reti IP.

Sul problema del chuncking: la semplificazione che adotto è non ortodossa ma ai fini della comprensione non è fuorviante (disclaimer).

IP è un cosiddetto protocollo Layer 3 della suite di protocolli Internet. I protocolli della suite formano uno stack (una pila) in cui ogni protocollo di livello superiore si basa su quello sottostante. Cioè, nel caso di IP, dovrebbe esserci un protocollo Layer ad un livello inferiore 2, o Link layer o livello collegamento (es. Ethernet o, in parole povere, Wi-Fi). I protocolli di livello collegamento si concentrano sui dettagli di trasmissione dei dati di livello inferiore e il loro ambito è limitato dalla comunicazione della rete locale (LAN) (ovvero nessuna consapevolezza del routing). La verità è che il chunking, (o framing nel gergo di rete), avviene anche a quel livello. Poiché IP è consapevole di tale limitazione, rende i suoi pacchetti abbastanza piccoli da poter essere inseriti nei frame di livello 2 perché, alla fine, l’unità di trasmissione sarà un frame, non tutto il pacchetto IP stesso. Sebbene importanti, questi dettagli sono comunque piuttosto irrilevanti per questo articolo.


Nel suo percorso dall’origine alla destinazione, un pacchetto IP di solito passa una manciata di host intermedi. Questa serie di host costituisce un percorso. Potrebbe esserci (e di solito c’è) più di un percorso per una coppia arbitraria (origine, destinazione). E poiché sono possibili più percorsi contemporaneamente, va benissimo che i pacchetti IP con la stessa coppia (origine, destinazione) prendano percorsi diversi. Tornando al problema dell’invio di un lungo pezzo di testo sulla rete, può succedere che i blocchi, ovvero i pacchetti IP su cui il testo è stato suddiviso, impiegheranno percorsi diversi verso l’host di destinazione. Tuttavia, percorsi diversi possono avere ritardi diversi. Inoltre, c’è sempre una probabilità di perdita di pacchetti perché né gli host intermedi né i collegamenti sono completamente affidabili. Pertanto, i pacchetti IP possono arrivare alla destinazione in un ordine alterato.

reassembly-opt
reassembly-opt

In generale, non tutti i casi d’uso richiedono un ordinamento rigoroso dei pacchetti. Ad esempio, il traffico voce e video è progettato per tollerare una certa quantità di perdita di pacchetti (perché la loro mancanza rappresenta un difetto pressoché inintelligibile dal fruitore: è probabile che non se ne accorga nemmeno o il disturbo sia piccolo e di brevissima durata: un burst o una fugace pixellatura anomala, per esempio) mentre la ritrasmissione dei pacchetti comporterebbe un aumento inaccettabile della latenza (e rappresenterebbe un disturbo ben più importante).

Tuttavia, quando un browser carica una pagina Web utilizzando HTTP (si osservi che parliamo di file di dimensioni molto inferiori rispetto ad uno stream audio/video), ci aspettiamo che le lettere e le parole su di essa vengano ordinate esattamente nello stesso modo in cui sono state pensate dal creatore della pagina. È così che nasce la necessità di un meccanismo di consegna dei pacchetti affidabile, ordinato e controllato dagli errori.

Come probabilmente avrai già notato, i problemi nel dominio di rete tendono a essere risolti introducendo sempre più protocolli. E in effetti, esiste un altro famoso protocollo Internet chiamato Transmission Control Protocol o semplicemente TCP.

TCP si basa sul suo protocollo sottostante, IP. L’obiettivo principale di TCP è fornire la consegna affidabile e ordinata di un flusso di byte tra le applicazioni. Pertanto, se inviamo il nostro testo (codificato) a un socket TCP su una macchina, può essere letto inalterato dal socket sulla macchina di destinazione. Per non preoccuparsi dei problemi di consegna dei pacchetti, HTTP si basa sulle capacità di TCP.

ip-tcp-payload-opt
ip-tcp-payload-opt

Per ottenere una consegna in ordine e affidabile, TCP aumenta le informazioni di controllo di ogni blocco con il numero di sequenza di incremento automatico e il checksum. Dal lato ricevente, il riassemblaggio dei dati avviene in base non all’ordine di arrivo dei pacchetti, ma al numero di sequenza TCP. Inoltre, il checksum viene utilizzato per convalidare il contenuto dei blocchi in arrivo. I blocchi non corretti vengono semplicemente rifiutati e non riconosciuti. Il lato mittente dovrebbe ritrasmettere i blocchi che non sono stati riconosciuti. Ovviamente, per implementarlo è necessaria una sorta di buffering da entrambe le parti.

Su una singola macchina alla volta possono esserci molti processi che comunicano tramite socket TCP. Pertanto, dovrebbero esserci tanti numeri di sequenza e buffer indipendenti quante sono le sessioni di comunicazione. Per risolvere questo problema, TCP introduce il concetto di connessione. Semplificando un po’, una connessione TCP è una sorta di accordo tra la parte trasmittente e ricevente sui numeri di sequenza iniziali e lo stato corrente della trasmissione. È necessario stabilire una connessione (scambiando alcuni pacchetti di controllo all’inizio, il cosiddetto handshake), mantenerla viva (alcuni pacchetti devono essere inviati in entrambe le direzioni, altrimenti la connessione potrebbe scadere, il cosiddetto keepalive) e quando la connessione non serve più, va chiusa (scambiando qualche altro pacchetto di controllo).

Ultimo ma non meno importante… Un indirizzo IP definisce un host di rete nel suo insieme. Tuttavia, tra due host qualsiasi, potrebbero esserci molte connessioni TCP simultanee. Se le uniche informazioni di indirizzamento nei nostri blocchi fossero gli indirizzi IP, sarebbe praticamente impossibile determinare l’affiliazione dei blocchi con le connessioni. Pertanto, sono necessarie alcune informazioni di indirizzamento aggiuntive. Per questo, TCP introduce il concetto di porte. Ogni connessione ottiene una coppia di numeri di porta (uno per il mittente, uno per il destinatario) che identifica in modo univoco la connessione TCP tra coppie di IP. Quindi, qualsiasi connessione TCP può essere completamente identificata dalla seguente tupla: (IP di origine, porta di origine, IP di destinazione, porta di destinazione).

Implementazione di un semplice server TCP

È ora di esercitarsi! Proviamo a creare il nostro piccolo server TCP in Python. Per questo, avremo bisogno del modulo socket dalla libreria standard.

Per un novizio, la principale complicazione con i socket è l’esistenza di un rituale apparentemente magico di preparare i socket per funzionare. Tuttavia, combinare il background teorico dall’inizio di questo articolo con la parte pratica di questa sezione dovrebbe trasformare la magia in una sequenza di azioni significative.

Nel caso di TCP, i flussi di lavoro del socket lato server e lato client sono diversi. Un server attende passivamente la connessione dei client. A priori, l’indirizzo IP e la porta TCP del server sono noti a tutti i suoi potenziali client. Al contrario, il server non conosce gli indirizzi dei suoi client fino al momento in cui questi si connettono. Vale a dire, i client svolgono il ruolo di iniziatori della comunicazione connettendosi attivamente ai server.

Tuttavia, c’è di più oltre a questo. Sul lato server, ci sono in realtà due tipi di socket coinvolti: il suddetto socket del server in attesa di connessioni e, sorpresa, sorpresa – i socket client! Per ogni connessione stabilita, c’è un altro socket creato sul lato server, simmetrico alla sua controparte lato client. Pertanto, per N client connessi, ci saranno sempre N+1 socket sul lato server.

Creare i socket TCP del server

Quindi, creiamo un socket del server:

# python3

import socket

serv_sock = socket.socket(
    socket.AF_INET,      # set protocol family to 'Internet' (INET)
    socket.SOCK_STREAM,  # set socket type to 'stream' (i.e. TCP)
    proto=0              # set the default protocol (for TCP it's IP)
)

print(type(serv_sock))   # <class 'socket.socket'>

“Aspetta un attimo, ma… dov’è l’ int fd = open("/path/to/my/socket") che mi avevi promesso?”

La verità è che la chiamata di sistema open() è troppo limitata per il caso d’uso del socket perché non consente di passare tutti i parametri necessari, come famiglia di protocollo, tipo di socket, ecc. Pertanto, per i socket, è stato introdotta una chiamata di sistema dedicata socket(). Analogamente a open(), dopo aver creato un terminale per la comunicazione, socket() restituisce un descrittore di file che fa riferimento a tale endpoint. Per quanto riguarda parte mancante fd = ..., Python è un linguaggio orientato agli oggetti. Invece di funzioni, tende a utilizzare classi e metodi. Il modulo socket della libreria standard di Python (che abbiamo usato nel primo esempio sopra) è in realtà un OO-wrapper leggero attorno all’insieme di chiamate relative ai socket. Semplificando drasticamente, può essere pensata come qualcosa del genere:

class socket:  # Yep, the name of the class starts from a lowercase letter...
               # NDR: implementazione-fantoccio della libreria di sistsma.
    def __init__(self, sock_family, sock_type, proto):
        self._fd = system_socket(sock_family, sock_type, proto)

    def write(self, data):
        system_write(self._fd, data)

    def fileno(self):
        return self._fd

Cioè, se qualcuno ne avesse davvero bisogno, può ottenere il descrittore del file (un numero intero) come segue:

print(serv_sock.fileno())  # 3 or some other small integer

Associare il socket del server all’interfaccia di rete

Poiché, in generale, una singola macchina server può avere più di una scheda di rete, dovrebbe esserci un modo per associare il socket del server a una particolare interfaccia assegnando un indirizzo locale di questa interfaccia al socket:

# Use '127.0.0.1' to bind to localhost
# Use '0.0.0.0' or '' to bind to ALL network interfaces simultaneously
# Use an actual IP of an interface to bind to a specific address.

serv_sock.bind(('127.0.0.1', 6543))

Inoltre, bind() richiede che venga specificata una porta. Il server attenderà o, nel gergo della rete, ascolterà le connessioni client su quella porta.

Dopo questa chiamata, il sistema operativo rende il socket del server pronto per accettare le connessioni in entrata. Tuttavia, il nostro codice non è ancora pronto per questo. Ma prima, tocchiamo brevemente la parte del backlog.

Quando più client si connettono al server, il server conserva le richieste in arrivo in una coda, una struttura dati di tipo FIFO. In parole semplici, il parametro backlog specifica il numero di connessioni in sospeso che la coda di connessioni conterrà.

Come già sappiamo, la comunicazione di rete avviene tramite l’invio di pacchetti. Allo stesso tempo, TCP chiede di stabilire delle connessioni. Quindi, per stabilire una connessione TCP, un client e un server devono scambiare alcuni pacchetti di controllo (cioè senza dati effettivi) (il cosiddetto handshake). E a causa dei ritardi della rete, non è una procedura istantanea.

Di solito, TCP è implementato a livello di sistema operativo e nei nostri programmi non ci occupiamo dei dettagli di livello inferiore, come l’handshake. I parametri di backlog definiscono la dimensione della coda delle connessioni stabilite. Fino a quando il numero di client connessi non eccederà la dimensione del backlog, il sistema operativo stabilirà nuove connessioni e le metterà in coda. Tuttavia, quando il numero della connessione stabilita raggiunge la dimensione del backlog, qualsiasi nuova connessione verrà esplicitamente rifiutata o implicitamente ignorata (dipende dalle configurazioni del sistema operativo). Un sito sotto attacco dDOS di solito è congegnato per portare la coda di connessioni a questo stato.

L’accettazione di connessioni client

Per prelevare una connessione dalla coda del backlog, dobbiamo fare quanto segue:

client_sock, client_addr = serv_sock.accept()

Tuttavia, la coda delle connessioni stabilite potrebbe essere vuota. In tal caso, la chiamata accept() bloccherà l’esecuzione del programma fino a quando il client successivo non si connette (o il programma viene interrotto da un segnale, ma è fuori tema per questo articolo).

Dopo aver accettato la prima connessione client, ci saranno due socket lato server: il già familiare serv_sock nello stato LISTEN e il nuovo client_sock nello stato ESTABLISHED. È interessante notare che il client_sock sul lato server e il socket corrispondente sul lato client sono i cosiddetti endpoint peer (terminali accoppiati). Cioè sono dello stesso tipo, i dati possono essere scritti o letti da ognuno di essi ed entrambi possono essere chiusi usando la chiamata close() che termina in modo efficiente la connessione. Nessuna di queste azioni influenzerà comunque il serv_socket in ascolto.

Ottenere l’indirizzo IP e la porta del socket client

Diamo un’occhiata agli indirizzi degli endpoint peer del server e del client. Ogni socket TCP può essere identificato da due coppie di numeri: (IP locale, porta locale) e (IP remoto, porta remota).

Per conoscere l’IP remoto e la porta del client appena connesso, il server può ispezionare la variabile client_addr restituita dalla chiamata (qualora sia effettuata con successo) accept():

print(client_addr)  # E.g. ('127.0.0.1', 54614)

In alternativa, il metodo socket.getpeername() dell’endpoint lato server client_sock può essere utilizzato per apprendere l’indirizzo remoto del client connesso. E per conoscere l’indirizzo locale assegnato dal sistema operativo del server per l’endpoint peer lato server, è possibile utilizzare il metodo socket.getsockname().

Nel caso del nostro server potrebbe assomigliare a questo:

serv_sock:
  laddr (ip=<server_ip>, port=6543)
  raddr (ip=0.0.0.0, port=*)

client_sock:  # peer
  laddr (ip=<client_ip>, port=51573)  # 51573 is a random port assigned by the OS
  raddr (ip=<server_ip>, port=6543)   # it's a server's listening port

Inviare e ricevere dati tramite socket

Ecco un semplice esempio di ricezione di alcuni dati dal client e poi di invio (il cosiddetto echo-server):

# echo-server

data = client_sock.recv(2048)
client_sock.send(data)

Bene, dove sono le chiamate read() e write() promesse? Sebbene sia possibile utilizzare queste due chiamate con descrittori di file socket, come con la chiamata di sistema socket(), esse non consentono di specificare tutte le potenziali opzioni necessarie. Pertanto, per i socket, sono state introdotte le chiamate di sistema send() e recv(). Nel manale Unix man 2 send si legge:

The only difference between send() and write() is the presence of flags. With a zero flags argument, send() is equivalent to write().

e su man 2 recv si legge

The only difference between recv() and read() is the presence of flags. With a zero flags argument, recv() is generally equivalent to read().

Dietro l’apparente semplicità del frammento di cui sopra c’è un problema serio. Entrambe le chiamate recv() e send() funzionano effettivamente attraverso i cosiddetti buffer di rete. La chiamata a recv() ritorna non appena alcuni dati appaiono nel buffer sul lato ricevente. E, naturalmente, alcuni raramente significa tutti. Pertanto, se il client desidera trasmettere, diciamo 1800 byte di dati, recv() può restituire non appena vengono ricevuti i primi 1500 byte (i numeri sono arbitrari in questo esempio) perché la trasmissione è stata suddivisa in due parti.

Lo stesso vale per il metodo send(). Restituisce il numero effettivo di byte che sono stati scritti nel buffer. Tuttavia, se il buffer ha meno spazio disponibile rispetto al dato tentato, ne verrà scritta solo una parte. Quindi, spetta al mittente assicurarsi che il resto dei dati venga eventualmente trasmesso. Fortunatamente, Python fornisce un pratico helper socket.sendall() che fa il ciclo di invio per te sotto il cofano.

Questo in realtà porta a considerazioni interessanti quando si tratta di progettare lo scambio di dati su TCP:

i messaggi devono essere di lunghezza fissa (yuck) o essere delimitati (shrug) o indicare quanto sono lunghi (molto meglio) o terminare interrompendo la connessione.

Rilevare quando il client ha terminato di inviare (shutdown)

Si noti che le prime tre opzioni possono comunque portare a una situazione in cui il socket sul lato server attenderà che la chiamata a recv() restituisca il suo valore per un tempo indefinito. Può succedere se il server desidera ricevere K messaggi dal client mentre il client desidera inviare solo M messaggi, dove M < K. Pertanto, spetta ai progettisti di protocollo di livello superiore decidere le regole di comunicazione.

Tuttavia, esiste un modo semplice per indicare che il client ha terminato l’invio. Il socket client può eseguire uno shutdown(how) della connessione specificando per il parametro how il valore SHUT_WR. Ciò porterà a una chiamata recv() sul lato server che restituisce 0 byte. Pertanto, possiamo riscrivere il codice ricevente come segue:

chunks = []
while True:
    data = client_sock.recv(2048)
    if not data:
        break
    chunks.append(data)

Chiudere i socket

Quando si è finito con un socket, bisogna chiuderlo:

socket.close()

La chiusura esplicita di un socket comporterà lo svuotamento dei suoi buffer e la chiusura regolare della connessione.

Semplice esempio di server TCP

Infine, ecco il codice completo dell’echo-server TCP:

# python3

import socket

# Create server socket.
serv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto=0)

# Bind server socket to loopback network interface.
serv_sock.bind(('127.0.0.1', 6543))

# Turn server socket into listening mode.
serv_sock.listen(10)

while True:
    # Accept new connections in an infinite loop.
    client_sock, client_addr = serv_sock.accept()
    print('New connection from ', client_addr)

    chunks = []
    while True:
        # Keep reading while the client is writing.
        data = client_sock.recv(2048)
        if not data:
            # Client is done with sending.
            break
        chunks.append(data)
    print('Received data: ', chunks)
    client_sock.sendall(b''.join(chunks))
    client_sock.close()

Salvatelo su server.py ed eseguitelo tramite python3 server.py

Nota di traduzione

Ho leggermente modificato i sorgenti per rendere più interattiva la comunicazione client-server.

Semplice implementazione del client TCP.

Le cose sono molto più semplici dal lato client. Non esiste una cosa come un socket di ascolto sul lato client. Dobbiamo solo creare un singolo endpoint socket e collegarlo connect() al server prima di inviare alcuni dati:

# python3

import socket

# Create client socket.
client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect to server (replace 127.0.0.1 with the real server IP).
client_sock.connect(('127.0.0.1', 6543))

# Send some data to server.
str = input('Please, send some message to your mate: ')
client_sock.sendall(str.encode('ascii'))
client_sock.shutdown(socket.SHUT_WR)

# Receive some data back.
chunks = []
while True:
    data = client_sock.recv(2048)
    if not data:
        break
    chunks.append(data)
print('Received back ', repr(b''.join(chunks)))

# Disconnect from server.
client_sock.close()

Salvatelo su client.py ed eseguitelo tramite python3 client.py.

Test (aggiunta mia alla traduzione)

Mettiamo il server in ascolto:

$ python3 server.py
<in attesa>

Apriamo una seconda console (Ctrl+T su Ubuntu) e digitiamo

$ python3 client.py 
Please, send some message to your mate: Ciao Ivan
Received back  b'Ciao Ivan'

Torniamo nella console del server e vedremo:

New connection from ('127.0.0.1', 55484)
[b'Ciao Ivan']

Nota che se spediamo un secondo messaggio con il client, ancorché la porta del client è sempre 6543, la porta del server sarà diversa:

Client

$ python3 client.py 
Please, send some message to your mate: Ciao Marco              
Received back  b'Ciao Marco'

Server:

New connection from ('127.0.0.1', 55488)
[b'Ciao Marco']

Per curiosità mi faccio stampare tutta la struttura dati del socket, ed è molto istruttivo:

$ python3 server.py 
New connection from  ('127.0.0.1', 55492)
Socket  <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6543), raddr=('127.0.0.1', 55492)>
Received data:  [b'ciao Mamma']

Come si vede, il numero di porta è stato incrementato di 4 unità, en passant il file descriptor è 4 e il left address è il client (‘127.0.0.1’, 6543) ed il right address è il server (‘127.0.0.1’, 55492).

È istruttivo anche vedere con nestat -at che c’è un server TCP in ascolto sulla porta 6543:

$ netstat -at
Connessioni Internet attive (server e stabiliti)
Proto CodaRic CodaInv Indirizzo locale        Indirizzo remoto       Stato     
.........
tcp        0      0 192.168.122.1:domain    0.0.0.0:*               LISTEN     
tcp        0      0 localhost:6543          0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:11211           0.0.0.0:*               LISTEN  
.........

Socket server vs HTTP server

Il server che abbiamo implementato sopra è chiaramente un semplice server TCP. Tuttavia, non è (ancora) un server web. Mentre (quasi?) ogni server web è un server TCP, ovviamente non tutti i server TCP sono un server web. Per trasformare questo server in un server web, dovremmo insegnargli come gestire HTTP. Cioè. i dati trasmessi tramite socket dovrebbero essere formattati secondo l’insieme di regole definite dall’Hypertext Transfer Protocol e il nostro codice dovrebbe saperli analizzare.

In conclusione

Memorizzare le cose senza capirle è una strategia scadente per uno sviluppatore (per ogni professione, aggiungerei, NdR). La programmazione di socket è un esempio perfetto di quanto leggere il codice senza il background teorico possa essere veramente frustrante. Tuttavia, una volta acquisita la comprensione delle parti mobili e dei vincoli, tutte queste manipolazioni magiche con l’API del socket si trasformano in un insieme di azioni che hanno un senso. E tu, non aver paura di dedicare tempo alle basi. La programmazione di rete è una conoscenza fondamentale che è vitale per lo sviluppo e la risoluzione dei problemi di successo di servizi Web avanzati.

Approfondimenti

Bibliografia

  • Articolo originale in inglese scritto da Ivan Velichko
  • Il profilo Twitter di Ivan: @iximiuz

Il carattere “a capo” (line-feed)

line-feed-ascii-code-10
From https://theasciicode.com.ar/
line-feed-ascii-code-10

Tra il mondo Unix e il mondo Windows esistono molte barriere che complicano l’interscambio di dati: una di questa barriere è la codifica del carattere di “a capo”.

Nell’epoca del Cloud, della Blockchain e dell’Intelligenza Artificiale sembra assurdo doversi confrontare con problemi come questo, ma succede.

Il carattere di “a capo”, come quello che fa suonare un campanello o il carattere di “spazio”, sono semplicemente caratteri come tutti gli altri, ognuno con la propria codifica. Quando pensiamo ad un file di testo – come ad un video o ad un audio – dovremmo sempre pensarlo come uno stream ininterrotto di bit tutti in fila. In binario non esistono “a capo” ma è solo una nostra rappresentazione perché siamo abitutati a fogli non infiniti. In realtà sarebbe meglio pensare ad un nastro infinito come quello della macchina di Turing.

Nella codifica ASCII (American Standard Code for Information Interchange), che è una delle codifiche più vecchie da cui si sono evolute quelle moderne fino all’onnicomprensivo UTF-16, il carattere di “a capo” o “nuova linea” è codificato con il numero decimale 10, in esadecimale A, in ottale 0x12, in binario a 8 bit 00001100. Il suo simbolo è LF (Line-Feed) e spesso viene visualizzato negli editor di testo con ^M e viene codificato con la sequenza \n nell’ambito delle espressioni regolari.

Per indicare il carattere corrispondente ad un codice x useremo questa notazione: CHR(x)

Sennonché Microsoft ha codificato in modo custom il carattere di a capo come l’unione di due caratteri: nei file prodotti con Windows troveremo che il carattere di a capo è CR + LF (Carriage-Return + Line-Feed), quindi CHR(10) + CHR(12) . Per Linux/Unix e MacOSX (che è un fork di Unix) è semplicemente LF.

Carriage Return sarebbe il movimento di ritorno all’inizio della riga, ma (soprattutto per chi ricorda le macchine per scrivere come la Olivetti Lettera 22) c’è anche l’avanzamento di una riga del foglio dovuto idealmente alla rotazione del rullo che trasporta la carta, che è proprio il Line Feed. Tutte cose che ormai si sono perse nella nebbia.

Il carattere di CR nella tabella ASCII è identificato con il codice 14 = Hex D = 0x14 = 00001110.

Negli editor vediamo sempre prima il carattere ^M e poi la linea va a capo, quindi la sequenza in cui si succedono i caratteri è CR + LF.

Quando importiamo in Linux un file di testo puro proveniente da Windows (a proposito: non sto parlando di cose antiquate e sorpassate: nella scrittura di software utilizzare file di testo puro è la regola perché i sorgenti sono e rimangono scritti così), vedremo alla fine di ogni riga il carattere ^M e poi il resto a capo.

Di solito gli editor possono anche avere l’opzione di ignorare e non mostrare i caratteri di CR (^M) per cui aprendo il file non vediamo niente di strano.

Ma è possibile visualizzarli con questi comandi per esempio:

$ cat -v testo.txt
ciao,^M
come va?

Per rimuovere i caratteri di CR ci sono molti modi, ad esempio utilizzando sed (Stream EDitor, potentissimo) oppure più semplicemente questo:

$ dos2unix testo.txt
$ cat -v testo.txt
ciao,
come va?

Poi c’è anche da dire che utility come FTP operano automaticamente la conversione quando si trasferiscono file tra sistemi eterogenei per cui di solito non ci preoccupiamo di questo aspetto. Di solito.

Risorse web su line-feed

Quali DNS sto usando?

dns
dns

Con Linux (Ubuntu) ci sono diversi modi di sincerarsi di quali server dns si stanno usando, ma quello migliore mi sembra questo:

$ nmcli dev show | grep DNS
IP4.DNS[1]:                             10.1.23.101
IP4.DNS[2]:                             10.1.23.102

Anche questo non è male:

$ systemd-resolve --status
Global
       Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: foreign
      DNS Domain: -- home myCustomer.it

Link 2 (enp1s0)
    Current Scopes: DNS
         Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 10.1.23.101
       DNS Servers: 10.1.23.101 10.1.23.102
        DNS Domain: myCustomer.it

Link 3 (wlp2s0)
Current Scopes: none
     Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Link 4 (virbr0)
Current Scopes: none
     Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Paginazione risultati con Laravel

Installare innanzitutto la paginazione con Laravel (non sarebbe nescessario, ma se si vuole personalizzare occorre farlo)

$ php artisan vendor:publish --tag=laravel-pagination

Vengono installate delle nuove view sotto resources/view/vendor/pagination

Delle viste che vengono installate, ho trovato che il tradeoff più conveniente sia questa: bootstrap-4.

Modificare così a view (es. resources/views/companies/index.blade.php):

            @foreach($companies as $v)
            ...
            @endforeach
            <tr>
                <td colspan="9">
                    {{ $companies->links('vendor.pagination.bootstrap-4') }}
                </td>
            </tr>

Il controller deve richiamare la stessa vista; l’ho un attimo modificato per aggiungere anche un filtro di ricerca:

    public function index(Request $request)
    {
        if (($request->has('name')) and ($request->input('name') != '')) {
            $name = $request->input('name');
            $param = "%$name%";
            $companies = Company::where('name', 'like', $param)->orderBy('name', 'asc')->paginate(config('app.results_per_page'));
        } else {
            $companies = Company::select('*')->orderBy('name', 'asc')->paginate(config('app.results_per_page'));
        }

        return view('companies.index', compact('companies'));
    }

È conveniente estrapolare la costante “numero di risultati per pagina” nel file delle constanti applicative (config/app.php):

    /*
    |--------------------------------------------------------------------------
    | Local constants
    |--------------------------------------------------------------------------

    */
    'results_per_page' => 5,
];

Il risultato è il seguente

Laravel pagination
Paginazione risultati con Laravel

Risorse web

Pillole Java: Gestione della memoria

Architettura Hotspot JVM

La Java Virtual Machine è un software che fornisce un ambiente operativo virtuale per applicazioni Java.

L’architettua Java Hotspot è composta di tre elementi

  • il motore di esecuzione:
    • è composto di un compilatore a tempo immediato (just in time o JIT compiler) che traduce il codice in chiaro in bytecode (compilazione) al momento in cui deve essere utilizzato
    • e di un garbage collector (che è l’aspetto su cui ci focalizziamo in questo articolo), un collettore di spazzatura che fa la raccolta differenziata degli oggetti usati e li rimuove con una specifica politica
  • l’area dati di esecuzione (runtime data area) che è una memoria organizzata in cui vengono caricati i metodi, i threads, lo heap (che è la zona che ci interessa) e altre zone. In particolare lo heap (mucchio) è la zona riservata ai dati transienti trattati dall’applicazione Java
  • il class loader che è un regista che istanza in memoria RAM gli oggetti e i metodi quando richiesti
Gestione della memoria in Java: Architettura HotSpot JVM
Gestione della memoria in Java: Architettura HotSpot JVM

Lo heap

Lo heap è una zona di memoria RAM nella quale Hotspot gestisce i dati dell’applicazione. In questa zona viene implementata una vera e propria raccolta differenziata degli oggetti in base all’età di occupazione. L’età di occupazione è calcolata con un contatore per ogni oggetto: ogni volta che viene istanziato, incremento il suo contatore. Ogni volta che lo distruggo (imposto la variabile) a null, lo decremento di 1. Quando il contatore raggiunge 0 è tempo di buttare via l’oggetto. Quindi programmaticamente è buona norma impostare a null una variabile se non la si vuole più usare, perché così riesco a raggiungere la condizione che scatena l’evento minor garbage collection che consiste nello spostamento o l’eliminazione fisica dell’area di memoria che contiene la variabile.

I dati vengono diversificati in tre zone “temporali”:

  • Young Generation
  • Old Generation
  • Permanent Generation
Struttura dello Java Heap (Wikipedia)
Struttura dello Java Heap (Wikipedia)

La zona Young Generation è quella in cui vengono allocati tutti gli oggetti istanziati da poco tempo; è a sua volta suddivisa in Eden (oggetti appena creati) e Survivor. Il raccoglitore differenziato (minor garbage collector) sposta gli oggetti dall’Eden alla zone di Sopravvivenza e da qui alla Old Generation. Uno spostamento da Young a Old è considerato un evento di minor garbage collcection.

Gli oggetti un po’ più datati vivono nella Old generation; vengono spostati da qui dal racccoglitore differenziato maggiore ed eliminati, Questo evento è condierato una major garbage collection.

Nella zona Permament Generation vengono collezionati metodi e classi, che sono considerati metadati. Anche l’eliminazione da parte del GC degli oggetti di questa zona è considerata major.

Quindi gli oggetti vengono spostati gradualmente da una zona all’altra fino alla loro eliminazione completa.

Come si produce garbage?

Consideriamo questo programma

public void factory (int num) {
  Die d1 = new Die();
  Die d2 = new Die(num);
  int numfaces = d1.getFaces();
  Die d3 = d2;
} 

Questo metodo istanzia due oggetti d1 e d2, poi un terzo d3; e una variabile locale numfaces.

Ci sono due tipi di variabili: le variabili di istanza o campi e le variabili locali o parametri.

  • Le variabili di istanza sono le aree di variabili dove memorizziamo le proprietà della classe
  • Le variabili locali sono variabili temporanee definite all’interno del metodo. I parametri sono semplicemente variabili locali di Java con la differenza che esse non vengono inizializzate dal metodo ma dal programma chiamante.

Il tempo di vita delle variabili locali dal tempo di esecuzione del metodo che le contiene, da quando viene chiamato a quando ritorna al programma chiamante.

A questo proposito dobbiamo tenere a mente che le variabili locali o i parametri di tipo primitivo vengono memorizzati in un’area di memoria detto stack (pila); ogni metodo ha il suo stack. Mentre le variabili locali e i parametri di tipo oggetto vengono invece memorizzati nello heap. Tuttavia nello stack vengono mantenuti i riferimenti (i puntatori) a questi oggetti dello heap, come illustrato nella figura seguente

Stack e heap per il metodo Java factory
Stack e heap per il metodo Java factory

Il tempo di vita dell’oggetto è in sostanza il tempo di esecuzione del metodo. Ora d1, d2 e d3 sono variabili locali del metodo sopra.

Se aggiungiamo una ulteriore riga alla fine

   d1 = d3

cosa succede?

Ciò che succede è che ogni riferimento all’oggetto Die 1 viene perso e l’oggetto può essere rimosso dalla RAM dal garbage collector.

L’algoritmo di gestione dello hep consente, soprattutto per programmi di grandi dimensioni, di rendere efficiente la raccolta differenziata.

Un algoritmo più ingenuo potrebbe passare in rassegna tutti gli oggetti istanziati e controllare se il programma in qualche punto li chiama (cioè: se esiste un riferimento). Gli oggetti lasciati fuori da questa scansione possono essere buttati via. Però questo algoritmo diventa rapidamente inefficiente all’aumentare del numero di oggetti istanziati; ovvero, in ultima analisi, dalla complessità del programma.

L’analisi che ha portato a questo tipo di gestione, si rifaceva allo studio della quantità di memoria allocata per oggetti che venivano eliminati quasi subito: ad esempio iterators che duravano per un solo ciclo. Questa statistica ha messo in luce la preponderanza di molti oggetti che durano poco, una specie di mortalità infantile, per cui ci si è concentrati su algoritmi che liberavano prima questa parte di memoria.

Tipi di GC

I garbage collectors di Hotspot sono 4:

  • serial
  • parallel
  • CMS
  • G1

Questi si comportano sostanzialmente in modo diverso, ma dobbiamo pensare che ci sono processi per così dire “attivi” che portano avanti l’esecuzione del programma principale e processi per così dire “passivi” che invece si occupano di fare pulizia. A volte i secondi devono interrompere i primi e questa modalità è detta “Stop-the-world mode GC”.

Serial è un unico thread che si occupa di eliminare molti garbage uno alla volta.

Parallel si occupa di eventi minori (come lo spostamento da un contentore all’altro) e ogni garbage è spostato da un thread separato, quindi si gestiscono più segmenti alla volta.

CMS (Concurrent Mark-sweep) vengono marcati e cancellati più segmenti tramite thread paralleli per minimizzare l’arresto del programma principale (per minimizzare gli Stop-the-world).

G1 (Garbage First) è un’alternativa concorrente di CMS.

Compattazione

Il modo di gestire la cancellazione degli oggetti non più usati è allo stesso modo critica: si possono adottare diverse tecniche, la più ingenua è cancellare loggetto sic et simpliciter (tecnica Mark-Sweep), ma questo reimpirebbe la memoria di “buchi” e sarebbe diffficile riutilizzae lo spazio reso disponibile.

Mark-sweep

Quindi ci sono dei metodi più evoluti, come il Copy o il Mark-Compact che riallocano i dati da cancellare in modo da renderli contigui e creare delle zone libere più grandi possibili e continue (senza buchi).

Mark-Compact

Risorse nel blog

Fondamentali di JVM: la differenza tra JDK e JRE

Bibliografia

Fondamenti sui formati raster

Sto lavorando ad una applicazione GIS, per cui mi documento sugli aspetti teorici di base dell’applicazione.

Queste informazioni si trovano sul sito di ArcGIS anche se in questo lavoro sto utilizzando principalmente QGIS.

Faccio una breve premessa.

Raster è sinonimo di reticolo: le informazioni di un file raster sono l’unione di una informazione geometrica (coordinate piane bidimensionali) e del valore scalare di una o più grandezze associate al punto geometrico:

(x, y, g_1, g_2, \ldots)

Più specificatamente, nell’ambito GIS, l’informazione geometrica è in realtà un’informazione geografica, una copia di coordinate piane che in genere sono associate alla rappresentazione di una porzione del geoide secondo un determinato sistema di riferimento locale.

C’è infatti il problema matematico di rappresentare localmente le coordinate curvilinee di una sfera su un piano attraverso una mappatura univoca e che deformi il meno possibile. Ci sono molti modi di mappare una superficie sferica su una piana e questi modi prendono il nome di proiezioni. Queste nozioni fanno parte della disciplina della Matematica che sia chiama Geodesia.

Lo standard di riferimento per i file raster utilizzati in cartografia è il GeoTIFF. Nei riferimenti web c’è un link alle specifiche.

I set di dati raster rappresentano le caratteristiche geografiche ottenute dividendo il terreno in celle discrete quadrate o rettangolari disposte su una griglia. Ogni cella ha un valore che viene utilizzato per rappresentare una grandezza fisica di quella posizione, come la temperatura, l’elevazione o un valore spettrale.

raster dataset diagram
raster 1

I set di dati raster sono comunemente usati per rappresentare e gestire immagini, modelli digitali di elevazione e numerosi altri fenomeni. Spesso i raster vengono utilizzati per rappresentare feature di punti, linee e poligoni. Nell’esempio seguente, puoi vedere come una serie di linee e poligoni (set di dati vettoriali) viene rappresentata come un set di dati raster.

Un diagramma vettoriale rappresentato come un raster
Un diagramma vettoriale rappresentato come un raster

I raster possono essere utilizzati per rappresentare tutte le informazioni geografiche (caratteristiche, immagini e superfici) e dispongono di un ricco set di operatori di geoprocessing analitico. Oltre ad essere un tipo di dati universale per contenere le immagini in GIS, i raster sono anche ampiamente utilizzati per rappresentare le caratteristiche, consentendo a tutti gli oggetti geografici di essere utilizzati nella modellazione e nell’analisi basate su raster.

Raster nel geodatabase

Un raster è un insieme di celle disposte in righe e colonne — una matrice bitmap — ed è un set di dati comunemente usato in GIS. Gli utenti in genere utilizzano molti file raster, tuttavia molti utenti vedono una crescente necessità di gestire i dati raster, insieme alle altre informazioni geografiche, in un DBMS. Il geodatabase fornisce un mezzo molto efficace per la gestione dei dati raster.

Strategie di gestione raster

Sono importanti due strategie di gestione dei dati per i raster:

  • Approvvigionamento di raster: mettere rapidamente “in gioco” i set di dati raster nel tuo GIS significa che molto probabilmente li utilizzerai così come sono, in genere come una serie di file raster. Può trattarsi di una serie di file indipendenti oppure è possibile utilizzare una tecnologia come l’estensione Image per ArcGIS for Server per gestire e servire questi set di dati esistenti come una raccolta.
  • Raster nel geodatabase: questa strategia è utile quando si desidera gestire i raster, aggiungere comportamenti e controllare lo schema; gestire un insieme ben definito di set di dati raster come parte del tuo DBMS; necessità di ottenere prestazioni elevate senza perdita di contenuti e informazioni (nessuna compressione); e un’unica architettura dati per la gestione di tutti i tuoi contenuti.

Proprietà geografiche dei dati raster

In genere vengono registrate quattro proprietà geografiche per tutti i set di dati raster. Questi diventano utili per la georeferenziazione e aiutano a spiegare come sono strutturati i file di dati raster. Questo concetto è importante da capire: aiuta a spiegare come vengono archiviati e gestiti i raster nel geodatabase.

I set di dati raster hanno un modo speciale per definire la posizione geografica. Una volta che le celle o i pixel possono essere georeferenziati con precisione, è facile avere un elenco ordinato di tutti i valori delle celle in un raster. Ciò significa che ogni set di dati raster ha in genere

  • un record di intestazione che ne contiene le proprietà geografiche e
  • il corpo del contenuto è semplicemente un elenco ordinato di valori di cella.

Le proprietà geografiche per un raster in genere includono

  • il suo sistema di coordinate
  • Una coordinata di riferimento o una posizione x,y (in genere l’angolo superiore sinistro o inferiore sinistro del raster)
  • le dimensioni della cella
  • Il conteggio di righe e colonne

Queste informazioni possono essere utilizzate per trovare la posizione di una cella specifica. Avendo queste informazioni disponibili, la struttura dei dati raster elenca tutti i valori delle celle in ordine dalla cella in alto a sinistra lungo ogni riga alla cella in basso a destra, come illustrato di seguito.

Diagramma dei valori delle celle
Diagramma dei valori delle celle

La tabella dei blocchi raster nel geodatabase

I dati raster sono in genere di dimensioni molto maggiori rispetto alle funzionalità e richiedono una tabella laterale per l’archiviazione. Ad esempio, una tipica ortoimmagine può avere fino a 6.700 righe per 7.600 colonne (più di 50 milioni di valori di cella).

Per ottenere prestazioni elevate con questi set di dati raster più grandi, un geodatabase raster viene suddiviso in riquadri più piccoli (denominati blocchi) con una dimensione tipica di circa 128 righe per 128 colonne o 256 x 256. Questi blocchi più piccoli vengono quindi tenuti in un lato tabella per ogni raster. Ogni riquadro separato è contenuto in una riga separata in una tabella a blocchi come mostrato di seguito.

Diagramma di visualizzazione della tabella a blocchi
Diagramma di visualizzazione della tabella a blocchi

Questa semplice struttura significa che solo i blocchi per un’estensione devono essere recuperati quando sono necessari, invece dell’intera immagine. Inoltre, i blocchi ricampionati utilizzati per costruire le piramidi raster possono essere archiviati e gestiti nella stessa tabella di blocchi come righe aggiuntive.

Ciò consente di gestire raster di enormi dimensioni in un DBMS; produrre prestazioni molto elevate; e fornire un accesso multiutente e sicuro.

Estensione dei raster

I raster sono molto e sempre più utilizzati nelle applicazioni GIS. Il geodatabase può gestire i raster per molti scopi: come singoli insiemi di dati, raccolte logiche di insiemi di dati e attributi di immagini nelle tabelle.

Una serie di funzionalità del geodatabase consente agli utenti di estendere il modo in cui gestiscono le proprie informazioni raster come segue:

Usa…… se devi…
Set di dati rasterGestire set di dati di immagini contigue molto grandi e raster a mosaico.
diagramma di raster
Set di dati a mosaicoUn mosaico di dati è un modello di dati che è un ibrido di un catalogo raster e un set di dati raster, che rappresenta una vista al volo di un catalogo raster. Consentono di archiviare, gestire, visualizzare ed eseguire query su raccolte di dati di immagini raster.
Set di dati a mosaico
Cataloghi rasterGestire un layer raster affiancato, in cui ogni riquadro è un raster separato.
Schema piastrelle

Gestire qualsiasi serie di raster in un DBMS
Serie di diagrammi raster

Gestire una serie temporale raster.
Diagramma della serie temporale
Colonne di attributi raster nelle tabelleArchivia immagini o documenti scansionati come attributi nelle tabelle.

Raster come diagramma degli attributi

Riferimenti web

Applicazione AJAX con Laravel

ajax logo
ajax logo

Premetto che, a quanto ho visto, non c’è un vero e proprio standard in Laravel per gestire chiamate asincrone (AJAX). Quindi il tutto rimane sempre un po’ sporco perché si è costretti a mescolare il codice generando, ad esempio, codice HTML al volo con Javascript. Non ho trovato un modo diverso.

Laravel ci viene incontro con le rotte, i controller, ma il momento topico di AJAX, cioè il momento in cui un singolo brano della pagina web viene aggiornato asincroncamente, è del tutto dirty.

Ma andiamo con ordine. Il mio esempio è una semplice form di ricerca in un tabella che visualizza i risultati mano a mano che si digita nella casella di testo.

Step 1: rotta con la quale invocare il server

La chiamata server to server è definita nel file routes/web.php oppure in route/api.php ed è la seguente

Route::post('companies/search', 'App\Http\Controllers\CompanyController@search')->name('companies.search');

Definisco cioè una nuova rotta che viene invocata con il metodo POST con la quale utilizzerò un nuovo metodo del controller.

Nota: il metodo POST implica che il payload (la stringa di ricerca) sia trasmesso al server nel corpo del messaggio, non nell’URL. Quindi non si deve aggiungere la stringa di ricerca nell’url:

Route::post('companies/search/{nome}', 'App\Http\Controllers\CompanyController@search')->name('companies.search');

Step 2: il controller

Il controller implementa un nuovo metodo, search(), che accetta un parametro di ingresso che è la Request che contiene la stringa che scriveremo nella casella di testo, esegue una query e ritorna il recordset con encoding JSON:

    public function search(Request $request)
    {
        $name = $request->all()['name'];
        $companies = Company::where('name', 'LIKE', '%'.$name.'%')->get();
        return json_encode($companies);
    }

So far so good.

Step 3: il controller per il Frontoffice

Ho creato un secondo controller che ha il ruolo di disegnatore della form del frontoffice: non è strettamente necessario ma ho in mente una serie di funzinalità che non sono soltanto inerenti al modello Company per cui astraggo da quest’ultimo.

<?php

namespace App\Http\Controllers;

use App\Models\Company;
use Illuminate\Http\Request;

class FrontofficeController extends Controller
{
    function index()
    {
        return view('frontoffice.index');
    }

}

Step 4: la view

@extends('layouts.app')

@section('content')
<div class="container">
    <form action="" method="post">
        <div class="form-group">
            <label for="name">Company</label>
            <input type="text" name="name" id="name" class="form-control" placeholder="Nome" autocomplete="off">
        </div>
        <div class="form-group">
            <button class="btn btn-success btn-submit">Invia</button>
        </div>
    </form>

    <div class="table">
        <table class="table" id="companies">
            <thead>
            <tr>
                <th scope="col">id</th>
                <th scope="col">name</th>
                <th scope="col">website</th>
                <th scope="col">email</th>
            </tr>
            </thead>
            <tbody>
            </tbody>
        </table>
    </div>
</div>
@endsection

Nota 1: La casella di testo che servirà da trigger per la chiamata AJAX si chiama #name.

Nota 2: c’è un blocco <tbody/> che andrà popolato asincronicamente

Step 5: jQuery does the magic!

Questo metodo, anche se lo trovate in 10000 forum, ha una sua complessità che spiego un po’ alla volta.

L’ho implementato dentro al file <APP>/resources/layouts/app.blade.php, anche se potrei isolarlo in un file javascript a parte e includerlo nel blocco HTML di head, tra i tag di <script>:

       $(document).ready(function() {
            $.ajaxSetup({
                headers: {
                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                }
            });

            $("#name").keyup(function (e) {
                e.preventDefault();

                let name = $("input[name=name]").val();
                let CSRF_TOKEN = '{{ csrf_token() }}';

                if (name != '') {
                    $.ajax({
                        type: 'POST',
                        url: "{{ route('companies.search') }}",
                        data: {name: name, _token: CSRF_TOKEN},
                        success: function (data) {
                            let result_tag = "";
                            $.each(JSON.parse(data), function (i, item) {
                                result_tag += "<tr>" +
                                    "<td>" + item.id + "</td>" +
                                    "<td>" + item.name + "</td>" +
                                    "<td>" + item.website + "</td>" +
                                    "<td>" + item.email + "</td>" +
                                    "</tr>";
                            });
                            $('#companies tbody').html(result_tag);
                        }
                    });

                } else {
                    $('#companies tbody').html('');
                }
            });
        });

L’evento che scatena il Javascript è keyup, quando rilascio il tasto parte la chiamata asincrona:

Leggo il valore della casella di testo (linea 11); se nella variabile name c’è qualcosa, preparo la ricerca AJAX (linea 15), predisponendo il metodo POST (linea 16), l’url che ho definito nella rotta (linea 17) ed il passaggio dati nel corpo del messaggio HTTP (linea 18).

Se il servizio web risponde con un 200 (linea 19) creo i tag HTML di riga contenenti i risultati provenienti in JSON dal servizio web (il controller infatti li codifica in JSON) . Terminato il ciclo metto tutto il contenuto all’interno dei tag <tbody> (linea 29).

Nota che il tbody viene riscritto ad ogni pressione del tasto sulla tastiera.

Quest’ultimo Step è un po’ messy, si mescolano in un solo file HTML, Blade (e fin qui OK) ma anche Javascript. È una soluzione sporca, che non mi piace. Il problema è che qui affido la generazione dell’HTML ad un programma Javascript anziché a Blade. In sostanza però la logica di generazione del’HTML è in un file blade, quindi a rigore non starei derogando del tutto dal paradigma MVC.

Ultimo step: il csfr_token

Eh, se avete letto con attenzione fin qui, vi sarete chiesti come mai sorvolavo sempre su questo aspetto. Era importante dedicargli una sezione a parte.

Come detto in quest’altro articolo, è importante verificare che l’interrogzione del webservice search sia fatta dal sito stesso e non da qualcun altro, per evitare i problemi anche gravi conseguenti alla non adozione di contromisure.

La tecnica è quella di aggiungere al form html il tag @csfr che fa inviare al server una stringa complessa che poi deve venire controllata lato server. Siccome solo il server conosce questa stringa ci siamo protetti dagli abusi.

Il tag @csfr si può anche definire in un tag meta dellHTML e leggerlo poi in sede di servizo web confrontandolo con quello che ci arriva.

In realtà dobbiamo solo preoccuparci della trasmissione del token in quanto già Laravel controlla per noi la congurenza: se non arriva il token arriva con un valore sbagiato il server Apache emetterà un errore 419 e non funzionerà i servizio.

Il token può essere generato con la direttva @csfr scritta nel Form html , oppure con la funzione di blade {{csrf_token()}}, oppure inserito in un tag meta dell’intestazione html:

<!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

per poi essere letta dal modulo javascript:

$('meta[name="csrf-token"]').attr('content')

Visto che è la stessa pagina posso anche calcolarlo direttamente (come ho fatto in linea 12) per poi essere spedito in più modi al server; o nel payload della richesta (linea 18) oppure nell’intestazione HTTP (linee 2-6). O in entrambe.

L’omissione di questo passaggio provoca un errore 419 (unknown status).

È tutto.

Riferimenti in rete

Laravel: i18n di una applicazione

i18n localizzazione di un'applicazione
i18n localizzazione di un’applicazione

La localizzazione (o internationalization o i18n) di Laravel è già gestita da un apposito middleware. Seguiamo questi pochi passi necessari ad atttivare la funzionalità multilingua per un’applicazione Laravel.

Attivazione e configurazione del middleware i18n

$ php artisan make:middleware Localization

Questo comando crea un nuovo file in app/Http/Middleware/Localization.php che va personalizzato se vogliamo gestire la lingua con le variabili di sessione. L’alternativa sarebbe specificare sempre in ogni rotta la lingua che si vuole usare. Un incubo!

Modifichiamo così il file:

    public function handle(Request $request, Closure $next)
    {
        if (Session::has('locale')) {
            App::setLocale(Session::get('locale'));
        }

        return $next($request);
    }

Con il comando setLocale si valorizza la lingua usata in quel momento dall’applicazione con il valore contenuto dalla variabile di sessione.

Configurazione della rotta per lo switch della lingua

Ora occorre un metodo per fare lo switch e lo facciamo in una rotta che scriviamo in routes/web.php:

Route::get('language/{locale}', function ($locale) {
    app()->setLocale($locale);
    session()->put('locale', $locale);
    return redirect()->back();
});

Possiamo anche scegliere di portare il codice che si trova nella funzione anonima dentro al controller HomeController.

Creazione dei file di localizzazione

Poi creiamo due file di lingua dentro a resources/lang/ e li chiamiamo en.json e it.json. Possiamo fare quanti file vogliamo, attenzione che ad ogni rotta deve corrispondere un file!

Per esempio un estratto del file italiano può essere il seguente:

{
    "Dashboard": "Cruscotto",
    "logout": "Esci",
    "Companies": "Aziende",
    "Users": "Utenti",
    "Name": "Ragione Sociale",
    "Website": "Sito web",
    "Email":  "Email",
    "Main Phone #": "Telefono principale",
    "latitude":  "Latitudine",
    "longitude": "Longitudine",
    "company_type":  "Tipo Azienda",
    "Commands": "Comandi",
    "Create new": "Nuovo record"
}

Comandi HTML per lo switch

Ora inseriamo i comandi per cambiare lingua nell’header dell’applicazione: lo facciamo in un modo grezzo che può essere migliorato, ma è questione di Blade e Vue:

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav me-auto">
                        <li class="nav-item">
                            <a class="nav-link" href="dashboard/">{{ __('Dashboard') }}</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ route('companies.index') }}">{{ __('Companies') }}</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ route('users.index') }}">{{ __('Users') }}</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/language/it">it</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/language/en">en</a>
                        </li>
                    </ul>

Sostituire tutte le stringhe cablate con i segnaposto

Qui si può vedere anche come vengono utilizzati i segnaposto definiti in resources/lang: occorre sostituire ad esempio la stringa “Companies” cablata nell’HTML/Blade con il segnaposto {{__('Companies')}}.

Risorse web

Laravel primi passi: generare model, migrazione e seeding in un solo comando

Laravel logo
Laravel logo

La creazione di un model in Laravel può generare contestualmente anche altre classi, come le migrazioni per la tabella associata, il controller, le rotte resource, fino addirittura alle classi Faker/Seeder per riempire di dati di test la tabella.

$ php artisan make:model Role -mcrf

Questo comando oltre a creare il model creerà anche la migrazione (m), il controller (c), le resources (r) o azioni CRUD del controller. Inoltre creerà le classi Faker e Seeder (f) che consentono di specificare il tipo di dati di test con cui riempire le tabelle (Faker) e istruire il Seeder con il numero di record da creare.

Tutto in un solo comando!

Poi però occorre personalizzare ogni classe creata in base a cosa vogliamo fare.

Ho già creato il model, voglio creare le resources

Si possono dare i comandi anche in modo asincrono. In questo esempio:

$ php artisan make:controller UserController --resources

Sul package Faker

François Zaninotto è l’autore di questo package veramente formidabile. Egli ha deciso di abbandonare il progetto poiché “has a design problem”. Un difetto di progettazione iniziale ha fatto sì che le varie localizzazioni non potessero sessere scaricate separatamente dal pacchetto ma si dovesse scaricar tutto insieme.

Nel tempo le dimensioni del package con tutte le localizzazioni ha raggiunto dimensioni ragguardevoli e il numero di download è aumentato esponenzialmente (a fine 2020 era a 125 M download) e Zaninotto si è posto il problema dell’impatto ecologico in termini carbon footprint.

Be’ il suo calcolo lo ha impressionato, ha preferito chiudere e abbandonare il progetto, che ora è solo readonly.

Tutta la storia completa la potete leggere qui.

Altre risorse Laravel

Laravel: protezione con login Auth/Vue.js

Vue.js
Vue.js

Aggiungiamo Auth + Vue.js, il modo più diretto di aggiungere al progetto Laravel una protezione con password. Il plugin Auth è uno scaffold di autenticazione che utilizza Vue.js.

Installazione di Vue/Auth

composer require laravel/ui:^2.4
php artisan ui vue --auth

npm install
npm run dev
php artisan migrate

Il comando ui installa le componenti Vue.js e lo scaffolding necessario per le maschere di autenticazione, recupero password, eccetera e le funzionalità javascript delle interfacce.

npm

Il Node.js Packet Manager va invocato perché il prodotto Auth ha parecchie component javascript da installare.

Infine, il comando di migrazione serve per le tabelle dei recuperi password e dei token (“Ricordami questo accesso”).

Al termine dell’installazione saranno già state impostate tutte le rotte per Auth:

laravel route list con Auth
laravel route list con Auth

Vengono già predisposti i comandi di registrazione, recuper passord, login, logout. Però i conenuti rimangono ad accesso libero quidni va gestito cosa vogliamo proteggere con la login e cosa rimane publico.

Per fare questo occorre agire sul controller.

Per prtoteggere l’intera applicazione occorre fare in modo che ogni volta che si invoca la classe controller del modello che si vuole proteggere: avvenga l’interposizone del mìddleware Auth. Per esempio si può deìcidere tabella per tabella se si vuole la protezione oppure no:

class CompanyController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

in questo caso il CRUD della tabella Company sarà intercettato dal middleware Auth e se si tenta di accedere ad una delle rotte, si presenterà la maschera di login:

Login
Login

Risorse