Mi sono imbattuto in un comportamento piuttosto curioso utilizzando DBeaver con un server MySQL locale. All’avvio di DBeaver, il tentativo di connessione falliva con il messaggio: La cosa sorprendente era che bastava aprire un terminale …
Per anni avevo sentito ripetere che Docker è una virtualizzazione leggera. È una frase comoda per iniziare, ma in realtà è fuorviante. L’ho capito nel modo migliore: cercando di capire perché MySQL si comportasse in …
Aprendo Thunderbird mi è comparso un avviso che invitava a intervenire entro il 21 luglio 2026 per continuare a ricevere gli aggiornamenti ESR (Extended Support Release). Partiamo con le istruzioni operative per chi sa già …
Negli ultimi tempi termini come LLM, embedding e database vettoriali sono usciti rapidamente dall’ambito della ricerca e sono diventati argomenti quasi quotidiani. Sono mattoncini coi quali possiamo costruire sistemi capaci di interrogare grandi collezioni di …
Ho condotto un esperimento di scrittura di software per analizzare i risultati di una esportazione delle mie conversazioni con ChatGPT. Ma ho deciso di aiutarmi nell’analisi con un Large Language Model che sicuramente sa fare molto bene questo lavoro e in più con un LLM in locale, per vedere come funziona.
Dovevo cercare una determinata stringa in una colonna TESTO di una tabella Oracle definita come LONG (contiene informazioni di testo, XML nel mio caso). Un’operazione semplice, pensavo.
Ho utilizzato l’operatore LIKE:
SELECT ID, TESTO
FROM TEMPLATE
WHERE UPPER(TESTO) LIKE '%LOREM%'
ma con scarso successo, infatti l’errore che si è verificato è il seguente:
ORA-00932: tipi di dati incoerenti: previsto CHAR, ottenuto LONG
00932. 00000 - "inconsistent datatypes: expected %s got %s"
*Cause:
*Action:
Errore alla riga: 2, colonna: 13
Infatti non posso utilizzare l’operatore LIKE su campi LONG (in genere lo uso con i campi VARCHAR2).
Da [1] apprendo che addirittura i campi LONG non possono nemmeno comparire nelle clausole di filtro (WHERE) delle query.
Non solo.
Le colonne di tipo LONG non possono comparire in vincoli di integrità (fatta eccezione per NULL/NOT NULL).
Le colonne di tipo LONG non possono comparire in una espressione regolare.
Ogni tabella può avere al più una colonna di tipo LONG.
Eccetera.
Insomma una serie di condizioni molto vincolanti. Ma soprattutto non posso cercare il testo che voglio.
Il tipo LONG è tuttavia un tipo di dato deprecato in favore del tipo CLOB, atto a contenere testo che eccede i 4000 byte
Sempre in [1] trovo la soluzione: occorre trasformare il dato in un Large Object (LOB). Creo quindi una nuova tabella così:
CREATE TABLE ZZZ_TEMPLATE AS
SELECT ID, TO_LOB(TESTO) AS TESTO
FROM TEMPLATE;
-- Creato table ZZZ_TEMPLATE.
Dunque posso eseguire la query:
SELECT * FROM zzz_template
WHERE UPPER(TESTO) LIKE '%LOREM%';
--
137 "<?xml version="1.0" encoding="ISO-8859-1"?>..."
312 "<?xml version="1.0" encoding="ISO-8859-1"?>..."
311 "<?xml version="1.0" encoding="ISO-8859-1"?>..."
310 "<?xml version="1.0" encoding="ISO-8859-1"?>..."
Questo post verte soltanto su un aspetto se volete marginale – ma molto utile – della IDE che uso per progetti PHP / Python / Groovy: IntelliJ IDEA di JetBrains: come la IDE ci aiuta a non commettere errori del tipo “leggere valori di variabili private”.
Il codice è suo e qui lo analizzo soltanto (faccio solo piccole, innocue personalizzazioni). Precisamente: descrivo un po’ più in dettaglio un comportamento della IDE che mi aiuta a scrivere meglio il software.
Sto scrivendo (più precisamente: analizzando il funzionamento di) una classe di test con Spock che riguarda una classe Groovy Polygon (posto gli screenshot perché risulta evidente il comportamento della IDE di cui voglio parlare): la classe ha una sola proprietà che si chiama numberOfSides, il numero di lati del poligono, che è un attributo privato:
Spock for Groovy – private variable
Ora scriviamo un test (nel gergo di Spock i test si chiamano Specifications) che controlla se l’attribuzione del numero di lati del poligono funziona come ci aspettiamo; utilizziamo il costrutto given – when – then (che è l’oggetto di questa parte del tutorial di Trisha):
Spock: scrivere un test di tipo given-when-then
Notiamo che la proprietà numberOfSides dell’oggetto è evidenziata; passando con il mouse sopra la parte messa in risalto, la IDE ci informa sul perché di questa evidenziazione (Access to ‘numberOfSides’ exceedes its access rights):
Spock: tentativo di assegnare direttamente il valore di una variabile privata
Stiamo infatti tentando di accedere ad un valore di una variabile privata da un’istanza della classe e non dall’interno della classe stessa.
Potremmo correggere questa cosa in due modi: o attribuendo il qualificatore public alla variabile oppure definendo un getter. Percorro questa seconda via; aggiungo quindi il metodo getNumberOfSides() alla classe – è una costruzione standard: get + nome della variabile (con la regola CamelCase):
int getNumberOfSides() {
return numberOfSides
}
Ecco che la IDE non mette più in evidenza la proprietà numberOfSides:
Spock: l’uso della variabile privata non è più segnalato come scorretto.
Si noti che la IDE riconosce la presenza di un getter secondo la convenzione espressa sopra – se non seguo la convenzione la IDE tornerà a segnalare il problema. Nota che non ho cambiato la scrittura (non ho utilizzato esplicitamente il getter nell’assegnazione, ma l’ho lasciata così com’è) ma Groovy lo farà durante il build, sostituendo all’assegnazione diretta l’invocazione del getter come risulta dal tooltip che viene visualizzato quando passiamo con il mouse sopra l’istruzione:
Spock: assegnare di una variabile privata con getter
Non ci resta che lanciare il test cliccando sulla freccetta verde accanto alla definizione del metodo (notare come sia possibile utilizzare stringhe custom per definire il nome dei metodi, una caratteristica molto utile per documentare ciò che il test vuole fare):
Come partire da una situazione incasinata con molte versioni di Java e diversi JDK e fare ordine con SDKMAN.
Prendo spunto dall’articolo di Gunter Rosaert di DZone.
Situazione di partenza
Ecco quello che ho a bordo della mia macchina Linux:
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 21.10
Release: 21.10
Codename: impish
$ update-alternatives --list java
/usr/lib/jvm/java-11-openjd-amd64/bin/java
/usr/lib/jvm/java-8-openjd-amd64/jre/bin/java
/usr/lib/jvm/jd1.8.0_202/bin/java
/usr/local/java/jre1.8.0_73/bin/java
Premetto che mi servono tutte queste versioni per fare girare diversi client di epoca diversa come:
Mirth Connect, versioni 2.8, 3.2, 3.5
Oracle SqlDeveloper
Poi ho la IDE, Intellij IDEA, che si porta appresso un’altra versione
$ ls .jdks/
openjdk-17
SDK
Ho già una versione di sdk a bordo ma deve essere aggiornata:
$ sdk version
==== BROADCAST =================================================================
* 2022-04-12: quarkus 2.8.0.Final available on SDKMAN! https://github.com/quarkusio/quarkus/releases/tag/2.8.0.Final
* 2022-04-11: neo4jmigrations 1.5.4 available on SDKMAN! https://github.com/michael-simons/neo4j-migrations/releases/tag/1.5.4
* 2022-04-10: jreleaser 1.0.0 available on SDKMAN! https://github.com/jreleaser/jreleaser/releases/tag/v1.0.0
================================================================================
SDKMAN 5.14.3
ATTENTION: A new version of SDKMAN is available...
The current version is 5.15.0, but you have 5.14.3.
Would you like to upgrade now? (Y/n):
Scelgo di fare l’update, che risulta velocissimo:
Updating SDKMAN...
######################################################################## 100,0%
Install scripts...
sdkman_auto_answer=false
......
Successfully upgraded SDKMAN!
Open a new terminal to start using SDKMAN 5.15.0.
To join our BETA channel, simply follow the instructions on:
http://sdkman.io/install
Enjoy!!!
Lista dei JDK
Una lista completa di tutte versioni di Java esistenti si ottiene con il comando sdk list. In particolare si possono vedere tutti i tool installabili, da Ant a Tomcat a Grails e a Groovy; l’output è rediretto a less così da poter navigare e accedere anche al menu (stampato in alto):
$ sdk list
================================================================================
Available Candidates
================================================================================
q-quit /-search down
j-down ?-search up
k-up h-help
--------------------------------------------------------------------------------
Apache ActiveMQ (Classic) (5.16.2) https://activemq.apache.org/
Apache ActiveMQ® is a popular open source, multi-protocol, Java-based message
.....
$ sdk install activemq
--------------------------------------------------------------------------------
Ant (1.10.12) https://ant.apache.org/
Apache Ant is a Java library and command-line tool whose mission is to drive
...
$ sdk install ant
--------------------------------------------------------------------------------
eccetera. Vengono listati tutti i tool con descrizione e relativo comando di installazione. Aggiungendo il parametro java invece vengono listate tutte le versioni di Java installabili (da Java.net, a Oracle, a Amazon, a versioni Unclassified) e vengono rilevate quelle installate nel sistema:
================================================================================
Available Java Versions for Linux 64bit
================================================================================
Vendor | Use | Version | Dist | Status | Identifier
--------------------------------------------------------------------------------
Corretto | | 18 | amzn | | 18-amzn
| | 17.0.2.8.1 | amzn | | 17.0.2.8.1-amzn
................................................................................
Temurin | | 18 | tem | | 18-tem
| | 17.0.2 | tem | | 17.0.2-tem
| >>> | 11.0.14 | tem | installed | 11.0.14-tem
Unclassified | | openjdk11 | none | local only | openjdk11
| | jdk1.8.0_202 | none | local only | jdk1.8.0_202
================================================================================
Ho limitato la lunga lista alle prime righe e alle versioni che ho effettivamente installato.
Se si vuole installare una versione si digita sdk install java [TAB] e compare la lista
Ora si potrebbe fare pulizia delle versioni non in uso. Però, attenzione, ho altro software da far funzionare.
Ad esempio mi collego ad un server Mirth di un cliente con il client MirthConnect 3.2.0 che usa Java WebStart 11.202.2, il quale insiste su JRE 1.8.0_73-b02 Java HotSpot(TM) 64-Bit Server VM, che è l’ultima indicata nelle alternatives.
Anche il client MirthConnect Administrator 3.5.2 utilizza lo stesso JRE.
Configurare il JDK da usare per il build in Intellij
Ora il passaggio fondamentale è questo: configurare il kit da usare con l’IDE (per me Intellij IDEA). Può essere fatto a livello globale (per tutti i progetti) o per progetto (si può derogare dallo standard e prenderne una specifica per un progetto). Il build non presenta particolari problemi. Il problema bloccante si manifesta invece all’avvio dell’applicazione (run app).
Ottengo infatti questo un errore bloccante:
/home/marcob/.jdks/openjdk-17/bin/java
...
Failed to compile intellij-command-proxy.groovy: BUG! exception in phase 'semantic analysis' in source unit 'intellij-command-proxy.groovy' Unsupported class file major version 61 (Use --stacktrace to see the full trace)
Ma come si vede in realtà il kit utilizzato per esegue l’applicazione non è quello che abbiamo installato.
Si può definire un kit di default per tutti i progetti Java creati cona IDE raggiungendo questa voce dal menu: File > Project Structure e selezionare la voce SDKs sotto “Platform Settings” nel menu di sinistra:
Selezionare il SDK di default per i nuovi progetti Grails
Quindi devo recarmi nelle impostazioni del progetto IntelliJ e compiere queste due oprazioni: File > Project Structure e selezionare la voce Project sotto “Project Settings” nel menu di sinistra:
SDK di progetto
Nella selezione SDK seleziono temurin-11.
La SDK di solito va impostata quando si crea il progetto:
Impostazione generale della SDK per l’intera IDE
Se si vuole usare la Temurin per tutti i progetti, bisogna impostare il SDK Globale con la voce Platform Settings > SDKs nel menu di sinistra (nota: se vedete differenze in queste schermate rispetto alla vostra IDE, la mia versione è IntelliJ IDEA 2021.3.3 (Ultimate Edition)):
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 blogIximiuz 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
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
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 funzionichiamate di sistema per manipolare file che accettano un descrittore di file come argomento. Ecco un esempio canonico con le operazioni read() e write():
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
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.
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 scrivere – write – (o, nel gergo della rete, inviare – send) 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
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
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
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.
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.
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.
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
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
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)
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
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).
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 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
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
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
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 raster
Gestire set di dati di immagini contigue molto grandi e raster a mosaico.
Set di dati a mosaico
Un 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.
Cataloghi raster
Gestire un layer raster affiancato, in cui ogni riquadro è un raster separato.
Gestire qualsiasi serie di raster in un DBMS
Gestire una serie temporale raster.
Colonne di attributi raster nelle tabelle
Archivia immagini o documenti scansionati come attributi nelle tabelle.
Utilizziamo tecnologie come i cookie per memorizzare e/o accedere alle informazioni del dispositivo. Lo facciamo per migliorare l'esperienza di navigazione e per mostrare annunci personalizzati. Il consenso a queste tecnologie ci consentirà di elaborare dati quali il comportamento di navigazione o gli ID univoci su questo sito. Il mancato consenso o la revoca del consenso possono influire negativamente su alcune caratteristiche e funzioni.
Funzionale
Sempre attivo
L'archiviazione tecnica o l'accesso sono strettamente necessari al fine legittimo di consentire l'uso di un servizio specifico esplicitamente richiesto dall'abbonato o dall'utente, o al solo scopo di effettuare la trasmissione di una comunicazione su una rete di comunicazione elettronica.
Preferenze
L'archiviazione tecnica o l'accesso sono necessari per lo scopo legittimo di memorizzare le preferenze che non sono richieste dall'abbonato o dall'utente.
Statistiche
L'archiviazione tecnica o l'accesso che viene utilizzato esclusivamente per scopi statistici.L'archiviazione tecnica o l'accesso che viene utilizzato esclusivamente per scopi statistici anonimi. Senza un mandato di comparizione, una conformità volontaria da parte del vostro Fornitore di Servizi Internet, o ulteriori registrazioni da parte di terzi, le informazioni memorizzate o recuperate per questo scopo da sole non possono di solito essere utilizzate per l'identificazione.
Marketing
L'archiviazione tecnica o l'accesso sono necessari per creare profili di utenti per inviare pubblicità, o per tracciare l'utente su un sito web o su diversi siti web per scopi di marketing simili.
Commenti recenti