Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Leggere e scrivere su un mailslot con delphi

Leggere e scrivere su un mailslot con delphi

E-mail Stampa PDF

Un mailslot è un file residente nella memoria temporanea (RAM) e rappresenta uno dei tanti modi che hanno i processi per comunicare tra loro (Inter Process Comunication = IPC). A differenza degli altri sistemi IPC i mailslot sono unidirezionali, cioè c'è un server che crea il mailslot e ne legge i dati, mentre il client ci si può connettere e può solo scriverci dentro.
Il vantaggio che si ha usando i mailslot è che sono semplici da usare, mentre lo svantaggio più grande è che non danno la conferma della avvenuta ricezione dei dati trasmessi dal client verso il server.

Il server crea un mailslot nel suo spazio di memoria, mentre i client ad esso connessi possono risiedere nello stesso PC o in altre macchine, ma per inviare i dati non è necessario conoscere i vari protocolli di rete, dato che il tutto è gestito dal sistema operativo.
Di fatto il programmatore non fa altre che scrivere e leggere dati da un file. I blocchi di dati scritti o letti sono chiamati record.
Dato che è possibile scrivere simultaneamente sullo stesso mailslot da più client, i record inviati verranno accodati in ordine di arrivo.
Dal punto di vista del server invece i dati potranno essere letti solo nello stesso ordine con cui sono arrivati.
I dati all'interno di un record possono essere in qualsiasi formato, quindi potrebbe trattarsi di un testo, come anche di dati binari.
In pratica i record non sono altro che insiemi di byte, quello che rappresentano riguarda solo il programmatore.
La dimensione massima di un record è stabilita al momento della creazione del mailslot, ma non deve superare i 64000 byte. Il client non dovrà mai eccedere la dimensione massima per un record stabilita dal server.
Una volta chiuso il mailslot, nel server viene liberata tutta l'area di memoria occupata da esso.

Client

Vediamo un semplice esempio di client. La prima operazione che un client deve fare è quella di connettersi al mailslot, per fare questo userà la funzione CreateFile. Il primo parametro è quello che identifica il mailslot a cui connettersi e sarà una stringa di caratteri nel formato \\NomeComputer\mailslot\[percorso]nome
in questo modo si identifica un mailslot su un determinato computer, nel caso questo risieda nello stesso pc al posto del nome del computer ci sarà un punto:

\\.\mailslot\[percorso]nome

è anche possibile collegarsi allo stesso tempo a mailslot che hanno lo stesso nome ma sono su pc diversi e che appartengono allo stesso dominio, per fare ciò basterà usare l'asterisco:

\\*\mailslot\[percorso]nome

Gli altri parametri sono quelli classici per la apertura di un file esistente in scrittura:

function SlotConnect(name:string):boolean;
begin
  result:=false;
  SlotHandle := CreateFile(PAnsiChar('\\.\mailslot\'+name),
    GENERIC_WRITE,
    FILE_SHARE_READ,
    0,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    0);
  if (SlotHandle = INVALID_HANDLE_VALUE) then begin
    writeln( SysErrorMessage(GetLastError) );
  end
  else begin
    result:=true;
  end;
end;
 

L'altra funzione del client è quella che permette di scrivere nel mailslot, per questo viene utilizzata la funzione WriteFile che come primo parametro usa l'handle del mailslot aperto precedentemente; gli altri parametri che vengono passati sono il puntatore ai dati da inserire, la quantità in byte dei dati da inserire, una variabile che verrà restituita col numero di byte effettivamente scritti ed infine un puntatore ad un record di tipo TOverlapped, usata in caso di letture o scritture asincrone, ma che nel nostro esempio non verrà utilizzata.

procedure WriteSlot(testo:string);
var
  BytesWrite:cardinal;
  error:cardinal;
begin
  if (SlotHandle <> 0) then begin
    if ( WriteFile( SlotHandle, PChar(testo)^, Length(testo)+1, BytesWrite, nil ) = False ) then begin
      error:= GetLastError;
      case error of
      87: writeln(format('Error: %u -> ',[error])+SysErrorMessage(error)+': record too big');
      38: begin
        writeln(format('Error: %u -> ',[error])+SysErrorMessage(error));
        CloseHandle( SlotHandle );
        SlotHandle:=0;
      end;
      else
        writeln(format('Error: %u -> ',[error])+SysErrorMessage(error));
      end;
    end;
  end;
end;

E' stato aggiunto un minimo di gestione degli errori, ma la cosa è del tutto opzionale.

Il codice completo per il client è:

program Client;
 
{$APPTYPE CONSOLE}
 
uses
 SysUtils,windows;
 
var
 SlotHandle:THandle;
 testo:string;
 
function SlotConnect(name:string):boolean;
begin
  result:=false;
  SlotHandle := CreateFile(PAnsiChar('\\.\mailslot\'+name),
    GENERIC_WRITE,
    FILE_SHARE_READ,
    0,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    0);
  if (SlotHandle = INVALID_HANDLE_VALUE) then begin
    writeln( SysErrorMessage(GetLastError) );
  end
  else begin
    result:=true;
  end;
end;
 
procedure WriteSlot(testo:string);
var
  BytesWrite:cardinal;
  error:cardinal;
begin
  if (SlotHandle <> 0) then begin
    if ( WriteFile( SlotHandle, PChar(testo)^, Length(testo)+1, BytesWrite, nil ) = False ) then begin
      error:= GetLastError;
      case error of
      87: writeln(format('Error: %u -> ',[error])+SysErrorMessage(error)+': record too big');
      38: begin
        writeln(format('Error: %u -> ',[error])+SysErrorMessage(error));
        CloseHandle( SlotHandle );
        SlotHandle:=0;
      end;
      else
        writeln(format('Error: %u -> ',[error])+SysErrorMessage(error));
      end;
    end;
  end;
end;
 
begin
  writeln('Start client...');
  if SlotConnect('mio') then begin
    writeln('Connected');
    writeln('Ready to write and send to mailslot (Type "exit" to finish)');
    while testo<>'exit' do begin
      Readln(testo);   
      WriteSlot(testo);
      if SlotHandle=0 then begin
        writeln('Mailslot closed');
        testo:='exit';
      end;
    end;
  end
  else begin
    writeln('Not Connected');
    writeln('Press any key to close');
    readln;
  end;
end.
 

Server

Ora vediamo la parte server, la quale avrà il compito di creare il mailslot e di leggere i dati che i vari client ci scriveranno dentro.
Per la creazione useremo la funzione dedicata CreateMailslot che avrà come parametri:

  • Il nome del mailslot, secondo le regole che abbiamo visto in precedenza
  • La dimensione massima in byte dei suoi record, in pratica un client non potrà inviare un record di taglia superiore a quella impostata qui in fase di creazione. Se non si vuole imporre alcun limite basta impostare questo parametro a zero.
  • Il timeout, ovvero il tempo in millisecondi che si desidera attendere per l'arrivo di un record. Questo perché, a seconda delle esigenze, al momento della lettura può essere presente o meno un record da leggere; se ce ne fosse uno pronto la lettura avverrebbe immediatamente, mentre se il mailslot fosse vuoto, potremo impostare un tempo di attesa, dopo il quale la funzione di lettura sarebbe comunque conclusa. I valori possibili sono tra 0 e infinito (MAILSLOT_WAIT_FOREVER = -1).
  • L'ultimo argomento è un puntatore ad un record di tipo SECURITY_ATTRIBUTES, questo record è importante solo se il programma dovrà passare il mailslot ad un altro programma per consentirgli di leggere i messaggi. Se non è necessaria questa funzionalità basta passare un puntatore nullo.

Se la funzione CreateMailslot è riuscita a creare il mailslot, allora verrà restituito un handle che potrà essere passato alle altre funzione per usale lo stesso mailslot. Se invece CreateMailslot fallisce, allora verrà restituito il valore INVALID_HANDLE_VALUE (-1), e per conoscere esattamente il codice di errore basterà chiamare la funzione GetLastError().

const
  MAILSLOT_SIZE = 512;
 
var
  SlotHandle:THandle;
 
procedure CreateSlot(name: string);
var
  sa: TSecurityAttributes;
  SlotName: string;
begin
  sa.nLength := sizeof(sa);
  sa.lpSecurityDescriptor := nil;
  sa.bInheritHandle := false;
 
  SlotName:='\\.\mailslot\'+name;
  SlotHandle := CreateMailslot( PAnsiChar(SlotName), MAILSLOT_SIZE, MAILSLOT_WAIT_FOREVER, @sa );
 
  if ( SlotHandle = INVALID_HANDLE_VALUE ) then begin
    SlotHandle := 0;
    raise Exception.Create( SysErrorMessage(GetLastError) );
  end;
end;

In questo esempio si è scelta una dimensione massima dei record paria a 512 byte, e l'handle del mailslot è conservato da una variabile globale (SlotHandle), per poter leggere i suoi messaggi in seguito.

La seconda prerogativa del server è infatti quella di leggere i record inviati dai vari client.

function ReadSlot(var testo:string):integer;
var
  NumMsg,        //Numero i record nel mailslot
  BytesRead,     //Byte letti
  size:cardinal; //Dimensione record da leggere
  err:boolean;
  buf:array[0..MAILSLOT_SIZE-1] of char;
  i:integer;
begin
  result:=-1;
  if (SlotHandle <> 0) then begin
    // Ricava la dimensione del prossimo record
    err := GetMailslotInfo(SlotHandle, 0, size, @NumMsg, 0);
    if not err then
      writeln('GetInfo error: '+SysErrorMessage(GetLastError))
    else if (size <> MAILSLOT_NO_MESSAGE) then
      writeln(format('%u Records, next is %u bytes', [NumMsg,size]))
    else begin
      exit;
    end;
 
    if ( ReadFile( SlotHandle, buf, size, BytesRead, nil ) = False ) then begin
      testo:='';
      raise Exception.Create( SysErrorMessage(GetLastError) );
    end
    else begin
      testo:=string(buf);
      result:=NumMsg-1;
    end;
  end;
end;
 

Cercando di analizzare questa funzione come prima cosa verica che l'identificativo sia diverso da zero dopo di che recupera il numero di record e la dimensione in byte del primo record da leggere, e facendo questo verifica che il mailslot sia valido e che non sia vuoto.
Il fulcro della lettura è però la funzione ReadFile :

  • Il primo parametro è l'handle restituito da CreateMailslot .
  • Il secondo parametro è un array di caratteri che rappresenta il buffer da riempire con i byte del record. Sarebbe meglio dire che quello che viene passato è l'indirizzo di quell'area di memoria che deve essere riempita dai dati letti, nell'esempio è stato utilizzato un array di caratteri, ma avremmo potuto utilizzare un normalissimo puntatore.
  • Il terzo parametro è il numero massimo di byte da leggere, che abbiamo ricavato precedentemente.
  • Il quarto parametro ci restituisce il numero di byte realmente letti.
  • Il quinto parametro è un puntatore a un record di tipo Toverlapped :
POverlapped = ^TOverlapped;
Toverlapped = record
  Internal: DWORD;
  InternalHigh: DWORD;
  Offset: DWORD;
  OffsetHigh: DWORD;
  hEvent: THandle;
end;

Questo è necessario se il file è stato aperto con l'opzione FILE_FLAG_OVERLAPPED altrimenti deve essere impostato come nullo (nil). Questa struttura viene utilizzata in caso di lettura asincrona dei dati di un file, ma non è questo il caso trattato nell'esempio.

Il programma non deve fare altro creare il mailslot e poi continuare a interrogarlo ciclicamente per verificare la presenza di dati, e in caso ci fosse uno o più record estrarli e usarli in qualche modo. Nel caso specifico verranno solo stampati a video.
Riporto di seguito il codice completo del server:

program Server;
 
{$APPTYPE CONSOLE}
 
uses
  SysUtils,windows;
 
const
  MAILSLOT_SIZE = 512;
 
var
  SlotHandle:THandle;
 
procedure CreateSlot(name: string);
var
  sa: TSecurityAttributes;
  SlotName: string;
begin
  sa.nLength := sizeof(sa);
  sa.lpSecurityDescriptor := nil;
  sa.bInheritHandle := false;
 
  SlotName:='\\.\mailslot\'+name;
  SlotHandle := CreateMailslot( PAnsiChar(SlotName), MAILSLOT_SIZE, MAILSLOT_WAIT_FOREVER, @sa );
 
  if ( SlotHandle = INVALID_HANDLE_VALUE ) then begin
    SlotHandle := 0;
    raise Exception.Create( SysErrorMessage(GetLastError) );
  end;
end;
 
//chiude il mailslot
procedure FreeSlot;
begin
  if (SlotHandle <> 0) then
  CloseHandle( SlotHandle );
 
  SlotHandle := 0;
end;
 
//legge un record dal mailslot
function ReadSlot(var testo:string):integer;
var
  NumMsg,        //Numero i record nel mailslot
  BytesRead,     //Byte letti
  size:cardinal; //Dmensione record da leggere
  err:boolean;
  buf:array[0..MAILSLOT_SIZE-1] of char;
  i:integer;
begin
  result:=-1;
  if (SlotHandle <> 0) then begin
    // Ricava la dimensione del prossimo record
    err := GetMailslotInfo(SlotHandle, 0, size, @NumMsg, 0);
    if not err then
      writeln( 'GetInfo error: '+SysErrorMessage(GetLastError) )
    else if (size <> MAILSLOT_NO_MESSAGE) then
      writeln(format('%u Records, next is %u bytes', [NumMsg,size]))
    else begin
      exit;
    end;
 
    if ( ReadFile( SlotHandle, buf, size, BytesRead, nil ) = False ) then begin
      testo:='';
      writeln( SysErrorMessage(GetLastError) );
    end
    else begin
      testo:=string(buf);
      result:=NumMsg-1;
    end;
  end;
end;
 
var
  testo:string;
  i:integer;
begin
  writeln('Start server');
  CreateSlot('mio');
  while SlotHandle<>0 do begin
    while ReadSlot(testo)>=0 do begin
      if testo <> '' then begin
        writeln(testo);
        if testo='exit' then
          FreeSlot;
        testo:='';
      end;
    end;
    sleep(300);
  end;
  writeln('Mailslot close.');
  readln;
end.

Questo esempio di sistema basilare client-server permette a diversi client di inviare dei messaggi al server, ma per semplicità non è stato implementato alcun protocollo nella comunicazione e quindi il server non distinguerà mai chi è il mittente dei dati, ne potrà dare conferma per aver ricevuto i dati.
Una tecnica per rendere bidirezionale la comunicazione è infatti quella di inserire un protocollo che permetta di identificare i messaggi inviati e di creare anche nel lato client un ulteriore server in modo da poter inviare i dati anche in senso opposto.

Ultimo aggiornamento ( Venerdì 17 Settembre 2010 19:21 )  
Loading

Login




Chiudi