Introduzione.

Sto realizzando, per motivi di studio, un negozio elettronico accessibile tramite il WEB realizzato con il linguaggio Java.

Il problema fondamentale che mi sono trovato a dover risolvere è la connessione dinamica tra pagine WEB e base di dati: devo presentare prodotti le cui caratteristiche sono interamente contenute nel DB e dare la possibilità all'utente di consultare cataloghi e, una volta deciso cosa acquistare e comunicati tutti i dati necessari, aggiornare le disponibilità dei vari prodotti sottraendo le quantità acquistate.

In questo modo una potenziale ditta che vuole proporre i suoi prodotti sul WEB non ha nessun onere se non quello di mettere a disposizione del gestore del negozio periodicamente le tabelle estratte dal proprio sistema informativo contenenti i dati sui prodotti da proporre.

La cosa poteva naturalmente essere realizzata anche con strumenti diversi da Java, ma non con analoghe caratteristiche di interattività, di interfaccia, di compatibilità ed indipendenza dalla piattaforma; inoltre il punto focale del progetto sta nell'introduzione del concetto di DISPONIBILITA': sapere in tempo reale la quantità effettivamente disponibile di un dato prodotto, in funzione di ciò che di volta in volta viene acquistato.

Il primo passo di questo progetto è stato quello di creare un applet che possa essere scaricato da un client in una pagina WEB e che consenta di interrogare una base dati remota, inviando, ad esempio una istruzione SQL e leggendo i risultati.

Obiettivo del mio lavoro è anche quello di utilizzare, se possibile, strumenti completamente freeware e scaricabili senza problemi in rete.

Lo strumento utilizzato per realizzare il progetto sono state ovviamente le JDBC.

In particolare ho scelto di utilizzare un ponte JDBC-ODBC in modo da garantire l'accesso a qualunque base dati con un driver ODBC disponibile. Io ho scelto MS Access per mia comodità di progetto.

Lo scenario è molto schematicamente questo:

LATO SERVER

  • un database server (ad es. MS Access)
  • un driver ODBC
  • una fonte dati ODBC che punta ad una base dati presente sulla macchina (ad esempio al classico Northwind.mdb)
  • un http server

LATO CLIENT

  • un browser con macchina virtuale Java

Primo problema: è possibile creare una pagina HTML contenente un applet che tramite JDBC ed il ponte JDBC-ODBC acceda a dati contenuti in Northwind.mdb?

Con uno scenario come quello schematicamente indicato sopra non è possibile utilizzare le JDBC direttamente a lato client per il semplice motivo che non esiste NESSUNO a lato server che resta in attesa delle chiamate ad ODBC.

L'http server da solo riceve solamente chiamate che richiedono la trasmissione di pagine web con tutto il loro contenuto.

Dal punto di vista teorico, occorrerebbe aggiungere alle componenti già indicate QUALCUNO che resti in attesa delle chiamate provenienti dal client, le interpreti interagendo con la base dati tramite ODBC, e spedisca i risultati al client. Parlando in termini UNIX, occorre un demone che resti in attesa, su una certa porta, delle comunicazioni che provengono dall'applet scaricato dal client ed esegua le operazioni sulla base dati, spedendo eventualmente poi i risultati .

Naturalmente la comunicazione client - server può avvenire direttamente con lo scambio delle istruzioni SQL da eseguire, oppure, in maniera più efficiente, secondo un protocollo appositamente studiato.

La soluzione da me proposta si basa essenzialmente su due programmi java:

  • una applicazione che risiede sulla stessa macchina dove si trova la base dati e che attende le connessioni da parte dei client
  • un applet che è scaricato in una pagina web da un client e che spedisce le richieste al server attraverso una connessione via socket

Da notare che l'applicazione server è stata realizzata in JAVA per mia comodità personale di progetto (le socket in java sono abbastanza facili da gestire), ma potrebbe essere realizzata in qualunque linguaggio di programmazione che supporti le socket.

Esistono prodotti commerciali che si basano sullo stesso principio e nei quali l'applicazione server è realizzata in C++ e viene fornita sotto forma di DLL.

Il programma server potrebbe essere tipicamente implementato come servlet.

L'applicazione server

Il server, per prima cosa, crea una nuova socket alla porta 1111 ed entra in un loop infinito in cui attende connessioni.

Quando avviene una connessione, viene chiamata la funzione servicerequest() la quale salva nei vettori netinput e net_output i canali di input ed output del client che si è collegato e che serviranno poi alla comunicazione.

A questo punto viene attivato un Thread (servitore), il quale si occuperà di gestire tutta la comunicazione client - server per tutto il tempo in cui il client rimarrà collegato.

Il Thread attende una istruzione sql dal client ed interroga la base dati tramite la funzione interroga, la quale si preoccupa anche di spedire in rete i risultati.

Quando il client si disconnette, il Thread riceve un messaggio uguale a null e termina la sua vita.

L'applet client

Il client consente di scrivere una istruzione SQL, di inviarla al server e di leggere gli eventuali risultati.

Il significato dei pulsanti è il seguente:

LEGGI: legge dalla rete il messaggio del server

SCRIVI: scrive in rete l'istruzione SQL

INTERROGA: scrive l'istruzione e legge i risultati (equivale alla pressione in successione dei pulsanti SCRIVI e LEGGI).

NUOVO : consente di preparare una nuova istruzione.

Occorre seguire questo piccolo accorgimento: se l'istruzione che si intende eseguire non comporta la comunicazione di risultati, utilizzare il pulsante SCRIVI e non il pulsante INTERROGA (se tento di leggere risultati che non ci sono si pianta).

Non sono gestite situazioni di errore, perché questo client e questo server sono solamente l'embrione di un progetto più esteso che riguarda la realizzazione di un negozio elettronico che è già disponibile in versione beta e sarà scaricabile a breve, nel quale le istruzioni SQL saranno "cablate" nel client.

La comunicazione in rete

La comunicazione in rete è realizzata sia per il server sia per il client attraverso le funzioni readnetinput, readnetinputline, writenet_output.

La funzione readnetinput legge un carattere dalla rete. La funzione readnetinputline legge una stringa di caratteri (legge caratteri fino a quando trova un carattere predefinito di terminazione della stringa - io ho utilizzato il carattere §). La funzione writenetoutput scrive in rete una stringa di caratteri. Attenzione al fatto che a lato server la writenet_output non pone automaticamente alla fine della frase il simbolo di terminazione; questo per consentire di includere più messaggi in una stessa sessione di comunicazione.

L'interrogazione della base dati

L'interrogazione della base dati avviene a lato server tramite jdbc.

La funzione interroga(String query,OutputStream output) esegue la query sulla fonte dati ODBC specificata nella variabile url (in questo caso "jdbc:odbc:Northwind"); dopodiché chiama il metodo dispResultSet (OutputStream output,ResultSet rs,DatabaseMetaData dma) che provvede a spedire i risultati in rete attraverso le funzioni di comunicazione sopra citate.

Listati

Ecco i sorgenti illustrati in questo articolo: prima il client poi il server

Client

//------------------------------------------------------
// Autore: Filippo Bobbi   f.bobbi@agonet.it 1996               |
// Applet per l'inserimento di una istruzione SQL               |
//------------------------------------------------------

import
 java.applet.*;

import
 java.io.*;

import
 java.net.*;

import
 java.sql.*;

import
 java.awt.*;

public
 
class
 InterfacciaDb 
extends
 java.applet.Applet {
        
// DATI PER LA CONNESSIONE IN RETE

        Socket server;
        InputStream net_input;
        OutputStream net_output;
        String username;
        
boolean
 connected = false;

        
// DATI PER L'ASPETTO DELL'APPLET

        TextArea istr;
        TextArea res;
        TextField stato;
        Button intButton,nuoButton,disButton;
        Button legButton,scrButton;
        
        
public
 
void
 init() {
                resize(400,320);
                setBackground(Color.white);
                add(
new
 Label(
" Istruzione SQL:"
));
                istr = 
new
 TextArea(2,50);
                add(istr);
                add(
new
 Label(
" Risultati "
));
                res = 
new
 TextArea(10,50);
                add(res);
                intButton = 
new
 Button(
" Interroga"
);
                add(intButton);
                legButton = 
new
 Button(
" Leggi "
);
                add(legButton);
                scrButton = 
new
 Button(
" Scrivi "
);
                add(scrButton);

                nuoButton = 
new
 Button(
" Nuovo "
);
                add(nuoButton);
                disButton = 
new
 Button(
"Disconnetti"
);
                add(disButton);
                stato = 
new
 TextField(50);
                add(stato);
        }

        
public
 
boolean
 action(Event evt, Object obj) {
                
if
(evt.target.equals(intButton)) {
                        calcolaRisultati();
                } else
                        
if
(evt.target.equals(nuoButton)) {
                        pulisci();
                } else
                        
if
(evt.target.equals(legButton)) {
                        leggi();
                } else
                        
if
(evt.target.equals(scrButton)) {
                        scrivi();
                } else
                        
if
(evt.target.equals(disButton)) {
                        chiudi();
                }

                
return
 true;
        }

        
public
 
void
 stop() {
                chiudi();
        }

        
// SCRIVE IN RETE QUANTO SCRITTO NELLA CASELLA DI TESTO

        
// E LEGGE I RISULTATI INVIATI DAL SERVER

        
public
 
void
 calcolaRisultati() {
                
if
 (!connected) {                               
//SI CONNETTE

                        
if
 (connect()){
                         connected = true;
                        }
                        else{
                                username = 
"* NOT CONNECTED *"
;
                                stato.setText(
"ERRORE NELLA CONNECT"
);
                                repaint();
                                
return
;
                        }
                }
                
//write_net_output(username + "\n");

                write_net_output(istr.getText());                       
//SCRIVE IL MESSAGGIO

                String string = read_net_input_line(net_input);         
//LEGGE RISULTATO

                res.insertText(string + 
"\n"
,0);                        
//LO SCRIVE A VIDEO

                repaint();
        }

        
// SOLAMENTE SCRIVE IN RETE QUANTO SCRITTO NELLA CASELLA DI TESTO

        
public
 
void
 scrivi() {
                
if
 (!connected) {
                        
if
 (connect()){
                         connected = true;
                        }
                        else{
                                username = 
"* NOT CONNECTED *"
;
                                stato.setText(
"ERRORE NELLA CONNECT"
);
                                repaint();
                                
return
;
                        }
                }
                write_net_output(istr.getText());
                repaint();
        }

        
// SOLAMENTE LEGGE I RISULTATI DAL SERVER

        
public
 
void
 leggi(){
                
if
 (!connected){
                        
if
 (connect()){
                         connected = true;
                        }
                        else{
                                username = 
"* NOT CONNECTED *"
;
                                stato.setText(
"ERRORE NELLA CONNECT"
);
                                repaint();
                                
return
;
                        }
                }
                String string = read_net_input_line(net_input);
                res.insertText(string + 
"\n"
,0);
                repaint();
        }

        
// PULISCE LE AREE DI TESTO

        
public
 
void
 pulisci() {
                istr.setText(
""
);
                
int
 i = res.getRows();
                
int
 j = res.getColumns();
                res.replaceText(
""
,0,500);
                repaint();
        }       

        
// SI DISCONNETTE

        
public
 
void
 chiudi(){
                
if
 (connected){
                try
                        server.close();
                        
catch
 (IOException e);
                        connected = false;
                }
        }
        
        
// METODO PER LA CREAZIONE DELLA CONNESSIONE AL SERVER

        
boolean
 connect(){
        String host = getParameter(
"HOST"
);             
                try{
                        stato.setText(
"\n PRIMA TRY NEW SOCKET"
);
                        server = 
new
 Socket(host, 1111);                   
// CREA SOCKET

                        stato.setText(
"\n DOPO TRY NEW SOCKET"
);
                }
                
catch
 (IOException e){
                        stato.setText(
"Catched exception in new socket"
);
                
return
 false;
                }

        username = 
""
 + server.getLocalPort();                  
//PRENDE LA LOCAL PORT

                                                                
//AL QUALE SI è CONNESSO

        try{
                stato.setText(
"\n PRIMA TRY GET INPUT E OUTPUT"
);
                net_input = server.getInputStream();             
//PRENDE CANALE DI INPUT

                net_output = server.getOutputStream ();          
//PRENDE CANALE DI OUTPUT

                        stato.setText(
"\n DOPO TRY GET INPUT E OUTPUT"
);
        }
        
catch
 (IOException e){
                stato.setText(
"Catched exception in get input/output"
);
                
return
 false;
        }

        write_net_output(username);                              
//COMUNICA AL SERVER

                
//stato.setText(read_net_input_line(net_input)); //L'"IDENTIFICATIVO"

                repaint();

        
return
 true;
      }

        
// LEGGE UN BYTE DALLA RETE

      String read_net_input(InputStream input){
        
byte
 bytes[];
        
int
 number_of_bytes;

        try{
            bytes = 
new
 
byte
[1];
            number_of_bytes = input.read(bytes, 0, 1);
            
if
 (number_of_bytes > 0)
                
return
 (
new
 String(bytes, 0, 0, number_of_bytes));
            else
                
return
 null;
        }
        
catch
 (IOException e)
                
return
 null;
        }

        
// scrive una stringa seguita da un §

     
void
 write_net_output(String string){
        
byte
 byte_array[];

        string = string + 
"§"
;
        
int
 length = string.length();
        byte_array = 
new
 
byte
[length];
        string.getBytes(0, length, byte_array, 0);
        try
                net_output.write(byte_array);
        
catch
 (IOException e);
      }

      
// read_net_input_line legge finché non trova §

      String read_net_input_line(InputStream input){
        String line = 
""
;
        String c;

        c = read_net_input(input);
        
while
 (c.charAt(0) != '§'){
            line = line + c;
            c = read_net_input(input);
        }
        
return
 line;
      }

}

Server

//********************************************************************
// serverSql.java
// Applicazione per l'interrogazione di una base dati da client remoto
// Autore: Filippo Bobbi    f.bobbi@agonet.it  1996
// 
//********************************************************************

import
 java.io.*;

import
 java.net.*;

import
 java.sql.*;

class
 serverSql{
        ServerSocket server_socket;
        String user[];
        InputStream net_input[];
        OutputStream net_output[];

        
static
 
int
 client_counter = 0;

        
//----------------------------------------------------------------

        
        
public
 
static
 
void
 main(String args[]){
                
new
 serverSql();
        }

        
//----------------------------------------------------------------

        
//Crea una socket ed aspetta una connessione

        
public
 serverSql(){
                user = 
new
 String[50];                  
//max 50 clienti

                net_input = 
new
 InputStream[50];        
//max 50 clienti

                net_output = 
new
 OutputStream[50];      
//max 50 clienti

                try
                        server_socket = 
new
 ServerSocket(1111);
                
catch
 (IOException e){
                        System.out.println(
"Errore in creazione socket\n"
);
                        System.out.println(
"Eccezione: <"
+e+
">"
);
                        
return
;
                }

                System.out.println(
"Sto aspettando un client..."
);

                
while
 (true){           
//CICLO INFINITO IN CUI SI ATTENDONO

                                        
//CONNESSIONI

                        try{
                                
//QUANDO UN CLIENT SI CONNETTE

                                Socket socket = server_socket.accept();  
                                
//VIENE SERVITA LA RICHIESTA

                                service_request(socket);                 
                        }
                        
catch
 (IOException e)
                {
                System.out.println(
"Exception: <"
 + e + 
">"
);
                
break
;
              }
          }
      }

    
//----------------------------------------------------------------------------- 

    
        
public
 
void
 service_request(Socket socket){
                InputStream input;
                OutputStream output;
        
                try{
                        input = socket.getInputStream();  
//PRENDE CANALI DI I/O

                        output = socket.getOutputStream();
                }
                
catch
 (IOException e){
                    System.out.println(
"Unable to get input/output streams"
);
                    
return
;
                }

                
//se sono già in 50, rifiuta l'accesso

                
if
 (client_counter >= 50){              
                        write_net_output(output, 
                                
"Server occupato... riprova più tardi"
 + 
"§"
);
                        
return
;
                }

                
                
//MEMORIZZA NEL VETTORE I CANALI DEL CLIENT

                
//ED IL SUO IDENTIFICATIVO INVIATOGLI DAL CLIENT

                net_input[client_counter] = input;                  
                net_output[client_counter] = output;                
                user[client_counter] = read_net_input_line(input);  
                                
                System.out.println(user[client_counter] + 
" si e' connesso "
 +
                                                           socket.toString());
                
                
//ATTIVA UN NUOVO THREAD

                (
new
 servitore(
this
,client_counter)).start();      
                System.out.println(
"Client attualmente connessi: "
+ ++client_counter);
      }

      

//----------- PARTE  DI CONNESSIONE SQL ----------------------

        
public
 
void
 interroga(String query,OutputStream output){
                String url =   
"jdbc:odbc:Northwind"
;
                
try
 {
                        
Class
.forName (
"jdbc.odbc.JdbcOdbcDriver"
);
                        Connection con = DriverManager.getConnection (
                                                url, 
"Amministratore"
, 
"pippo"
);
                        DatabaseMetaData dma = con.getMetaData ();
                        Statement stmt = con.createStatement ();
                        ResultSet rs = stmt.executeQuery (query);
                        dispResultSet(output,rs,dma);
                        rs.close();
                        stmt.close();
                        con.close();
                }
                
catch
 (SQLException ex) {
                        System.out.println (
"\n*** SQLException caught ***\n"
);
                        
while
 (ex != null) {
                                System.out.println (
"SQLState: "
 + ex.getSQLState ());
                                System.out.println (
"Message:  "
 + ex.getMessage ());
                                System.out.println (
"Vendor:   "
 + ex.getErrorCode ());
                                ex = ex.getNextException ();
                                System.out.println (
""
);
                        }
                   }
                   
catch
 (java.lang.Exception ex) {
                        ex.printStackTrace ();
                   }
          }

        
private
 
void
 dispResultSet (OutputStream output,ResultSet rs,DatabaseMetaData dma)
                
throws
 SQLException{
                
int
 i;
                
                
// Le prossime 4 righe possono essere tolte

                
// con l'ultimo parametro della fz.

                write_net_output(output,
"\nConnected to "
 + dma.getURL() + 
"\n"
);
                write_net_output(output,
"Driver       "
 + dma.getDriverName() + 
"\n"
);
                write_net_output(output,
"Version      "
 + dma.getDriverVersion() + 
"\n"
);
                write_net_output(output,
"\n"
);

                ResultSetMetaData rsmd = rs.getMetaData ();
                
int
 numCols = rsmd.getColumnCount ();
                
for
 (i=1; i<=numCols; i++) {
                        
if
 (i > 1) write_net_output(output,
","
);
                        write_net_output(output,rsmd.getColumnLabel(i)+
"\n"
);
                }
                System.out.println(
"\n"
);
                
boolean
 more = rs.next ();
                
while
 (more) {
                        
for
 (i=1; i<=numCols; i++) {
                                
if
 (i > 1) System.out.print(
","
);
                                write_net_output(output,rs.getString(i));
                        }
                        write_net_output(output,
"\n"
);
                        more = rs.next ();
                }
                write_net_output(output,
"§"
);
        }

        
//-------------------------------------------------------------------

        
//LE FUNZIONI DI LETTURA E SCRITTURA SONO LE STESSE USATE DAL CLIENT

        
//LA SOLA DIFFERENZA STA NEL FATTO CHE LA FUNZIONE DI SCRITTURA NON

        
//METTE AUTOMATICAMENTE IL CARATTERE DI FINE MESSAGGIO

        
//-------------------------------------------------------------------

        String read_net_input_line(InputStream input){
                String line = 
""
;
                String c;

                c = read_net_input(input);
                
if
(c==null) 
return
 null;        
//restituisce null se non

                
while
 (c.charAt(0) != '§'){     
//legge

                        line = line + c;
                        c = read_net_input(input);
                }
                
return
 line;
        }

    
//----------------------------------------------------------------

        String read_net_input(InputStream input){
                
byte
 bytes[];
                
int
 number_of_bytes;
                try{
                        bytes = 
new
 
byte
[1];
                        number_of_bytes = input.read(bytes, 0, 1);
                
if
 (number_of_bytes > 0)
                        
return
 (
new
 String(bytes, 0, 0, number_of_bytes));
                else
                        
return
 null;
                }
                
catch
 (IOException e)
                        
return
 null;
        }

    
//----------------------------------------------------------------

    
// Attenzione  che non mette § alla fine

    
void
 write_net_output(OutputStream output, String string){
        
byte
 byte_array[];
        
int
 length = string.length();
        byte_array = 
new
 
byte
[length];
        string.getBytes(0, length, byte_array, 0);
        try
                output.write(byte_array);
        
catch
 (IOException e);
    }
  }

//-----------------------------------------------------------------
// THREAD PER LA GESTIONE DELLA COMUNICAZIONE CON UN CLIENT
//-----------------------------------------------------------------

class
 servitore 
extends
 Thread{
                serverSql s;
                
int
 indice;
                String query;

                
public
 servitore(serverSql s,int indice){
                        
this
.s=s;               
//L'INDICE MI SERVER PER UTILIZZARE 

                        
this
.indice=indice;     
//I CANALI DI COMUNICAZIONE CORRETTI 

                }                               
//LEGGENDOLI NEL  VETTORE

                
public
 
void
 run(){
                        
//CICLO IN CUI SI LEGGE UNA QUERY, SI INTERROGA IL DB

                        
//E SI INVIANO RISULTATI, FINO A QUANDO IL CLIENT SI DISCONNETTE

                        
while
(true){                     
                                query = s.read_net_input_line(s.net_input[indice]);
                                
if
(query==null) 
break
;  
//SE SI è DISCONNESSO -> USCITA                 

                                        System.out.println(s.user[indice] + 
": "
 + query);
                                s.interroga(query,s.net_output[indice]);
                        }
                        System.out.println(s.user[indice] + 
" si e' disconnesso"
);
                        System.out.println(
"Client attualmente connessi: "
+ --s.client_counter);
                        stop();
                }
                
//QUANDO IL CLIENT DA GESTIRE SI E' DISCONNESSO IL THREAD MUORE         

}