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
}
