Problema disconnessione client

Ciao a tutti,
sono da poco alle prese con un Arduino Uno per cercare di sviluppare un piccolo progettino (appena sono a buon punto apro un nuovo thread).
Essendo nuovo del forum vorrei fare i miei più sinceri complimenti al grande Massimo Banzi!

Anticipo che ho un problema con la funzione client.stop()
La mia situazione è la seguente

hardware: Arduino Uno + Ethernet Shield

software:
arduino implementa un server. Senza scendere troppo nei dettagli del protocollo di comunicazione con i client, attualmente le funzioni richiamate dal main sono:

  • waitClient() attende che un client si connetta;
  • clientLogin(“provaPassword”) ritorna true se il client invia una stringa come quella specificata come parametro (nell’esempio “provaPassword”);
  • disconnectClient() disconnette il client attualmente connesso.

Sorgente “main”:

byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; 
byte ip[] = {192, 168, 1, 15};
byte gateway[] = {192, 168, 1, 1};
byte subnet[] = {255, 255, 255, 0};
int  port = 23;

void setup(){
  //Initialize log system
  enableLog();
  //Start up the arduino server
  printLog("Start Server...");
  startServer(mac, ip, gateway, subnet, port);
  printlnLog(" ok");
}

void loop(){
  printlnLog("Waiting client connection...");
  waitClient();                                         //<===
  printLog("Client connect: ");
  if(clientLogin("provaPassword")){                     //<===
    printlnLog("login ok!");
    disconnectClient();                                 //<===
  } else {
      printlnLog("login failed!");
      disconnectClient();                               //<===
   }
}

Evito di inserire il sorgente per le istruzioni di log… attualmente non fanno altro che stampare, correttamente, stringhe su Serial…

Sorgente per le funzioni di “networking” (commenti “//<===” nel main):

#include <SPI.h>
#include <Ethernet.h>               //Ethernet libraries

#define STRING_END_TOKEN '\\'       //Carattere di fine stringa del client
#define MAX_CLIENT_DELAY 10000      //Massimo ritardo (in millis) tra due caratteri di una stringa ricevuta da un client
#define ACK              "\\ok\\"   //Messaggio di successo in risposta ad una stringa ricevuta da un client
#define NO_ACK           "\\err\\"  //Messaggio di fallimento in risposta ad una stringa ricevuta da un client

Server server = NULL;               //Arduino Server
Client client = NULL;               //Client connected to arduino

long clientLastActivity = 0;            //Time (in millis) al momento della ricezione dell'ultimo carattere di un client

void startServer(byte *mac, byte *ip, byte *gateway, byte *subnet, int port){
  Ethernet.begin(mac, ip, gateway, subnet);
  server = Server(port);
  server.begin();
}

//Attende la connessione di un client
void waitClient(){ 
  for(client = server.available(); !client; client = server.available());
}

//Legge una stringa (terminata dal carattere STRING_END_TOKEN) inviata dal client
String clientReadln(){
  String clientStr = NULL;
  char cTmp;
  
  clientLastActivity = millis();
  
  while ( (cTmp != STRING_END_TOKEN) && 
          ( (millis() - clientLastActivity) < MAX_CLIENT_DELAY) ){

    client = server.available();
    if(client){
      cTmp = client.read();
      if (cTmp != STRING_END_TOKEN){
        if (clientStr == NULL)
          clientStr = cTmp;
        else
          clientStr = clientStr + cTmp;
      }
      clientLastActivity = millis();
    }
  }
  return clientStr;
}

//Invia una stringa al client
void clientWriteln(String msg){
  server.print(msg);
}

//Ritorna true se il client invia una stringa (terminata da STRING_END_TOKEN) uguale al parametro "password"
boolean clientLogin(String password){
  boolean login = clientReadln().equals(password);
  if (login)
    clientWriteln(ACK);
  else {
    clientWriteln(NO_ACK);
    disconnectClient();
  }
  return (login);
}

//Disconnette il client
void disconnectClient(){
  while(client.connected()){
    client.flush();
    client.stop();
    delay(500);
  }
}

Non avendo ancora scritto il software per il client, ho fatto diverse prove tramite Telnet.

  • Il client si connette correttamente ad arduino: OK
  • Se i client invia una stringa diversa da “provaPassword”, terminata dal carattere STRING_END_TOKEN, riceve correttamente il messaggio NO_ACK e viene correttamente disconnesso: OK
  • Se il client invia una stringa uguale a “provaPassword”, terminata dal carattere STRING_END_TOKEN, riceve correttamente il messaggio ACK e viene correttamente disconnesso: OK
  • Se il client impiega troppo tempo (più di MAX_CLIENT_DELAY millisecondi) a inviare un carattere di una stringa riceve il messaggio NO_ACK (OK) ma non viene disconnesso nonostante sulla seriale venga “dichiarato” come tale. In particolare il client ha la possibilità di inviare ulteriori caratteri risultando, al server, come se fosse un nuovo client: PROBLEMA

Spero di essere stato chiaro anche se un po’ prolisso …
Ho cercato e trovato alcune informazioni circa problemi con la funzione client.stop() ma si riferiscono a vecchie versioni e a bug dichiarati fixati (uso l’ide 0021).

Grazie mille in anticipo!

Volevo aggiornare il post con una soluzione temporanea che, anche se un po' sporca, mi risolve temporaneamente e relativamente il problema... nella funzione disconnectClient(), dopo il ciclo while ho pensato di "riavviare" il server... la funzione diventa quindi:

//Disconnette il client
void disconnectClient(){
  while(client.connected()){
    client.flush();
    client.stop();
    delay(500);
  }
  Ethernet.begin(mac, ip, gateway, subnet);
  server = Server(port);
  server.begin
}

In questo modo: lato server il client risulta disconnesso; lato client la socket risulta ancora aperta fin quando il client non invia dati. A quel punto la connessione risulta chiusa anche lato client.

Per ora mi accontento di questo... Se dovessi trovare la reale soluzione alla cosa non mancherò di aggiornare questo post... magari appena posso do uno sguardo ai pacchetti con wireshark per verificare cosa avviene con i flag FIN e FIN-ACK...

Ovviamente qualsiasi aiuto è ben accetto :D non vorrei che fosse qualche stupido errore nel codice o, peggio ancora, l'uso del client telnet...

forse ci sono!!!
in
//Disconnette il client
void disconnectClient(){
while(client.connected()){
client.flush();
client.stop();
delay(500);
}
}
esegui la disconnessione se il client è connesso, ma il client non è connesso, ma in errore.
cambierei
while(client.connected())
in
while(client!=NULL)
la soluzione è un po’ grezza ma dovrebbe funzionare… sta a te vedere qual è il vero stato del socket…in VB ricordo che poteva stare in stato “invio dati”, “ricezione dati”, “timeout” e molti altri

Grazie mille per la risposta lesto! :) Purtroppo anche mettendo la condizione != NULL il risultato non cambia :( Il client resta sempre connesso e al server risulta come se fosse un nuovo client :S davvero non riesco a capire ...

Dando uno sguardo veloce ai pacchetti, solo nel caso in questione (allo scadere del tempo a disposizione del client), il server non invia il pacchetto con il flag FIN :( mentre in tutti gli altri casi (password errata e corretta) si O.O

fai una bella roba, metti una serial prima e dopo il while, dove magari stampi pure lo stato del socket (ad occhio credo esista una funzione client.state() ), se il client è già NULL allora è un bel problema! vuol dire che a far casino è qualche parte del codice che non mi sembra essere in quello che hai postato, e quindi mi viene in mente un'errore della libreria!

attento che //Attende la connessione di un client void waitClient(){ for(client = server.available(); !client; client = server.available()); }

si aspetta che client sia NULL! (!client come condizione) quindi forse basta fare: //Disconnette il client void disconnectClient(){ while(client.connected()){ client.flush(); client.stop(); delay(500); } client=NULL; } :lol

Si ci avevo pensato... cmq niente :-[ sempre uguale...

Ho fatto una prova scrivendo la funzione così:

void disconnectClient(){
  Serial.println("Tentativo disconnessione");
  while(client.connected()){
    Serial.println("provo...");
    client.flush();
    client.stop();
    Serial.println("fatto");
    delay(500);
  }

nel caso di password corretta... su Serial mi da:

Tentativo disconnessione
provo...
fatto

nel caso di password sbagliata... mi da:

Tentativo disconnessione
provo...
fatto

nel caso **stardo in questione ... mi da solo:

Tentativo disconnessione

Allora ho provato con:

void disconnectClient(){ 
   client.flush();
   client.stop();
}

Ma nulla cambia -.-

Edit sorry non ho letto la tua risposta precedente sul fatto del casino altrove ... leggo ora

Provando a fare un "Serial.println(client.status());" mi da sempre (anche nei casi in cui il tutto "funziona") un rigo bianco... visto che client.status() ritorna un uint8_t, sbaglio a dare l'istruzione "Serial.println(client.status());"?

Edit si sbaglio... con un "Serial.println(int(client.status()));" nei casi in cui tutto funziona lo stato è 23, mentre è 0 nel caso del "malfunzionamento" in questione... Dovrei capire quando e perché va in questo stato insomma, giusto? Più tardi vedo di continuare il debugging controllando client.status() :( Grazie per l'aiuto lesto :) aggiorno appena ho novità

Problema risolto ;D alè :smiley:

Allora… il problema era nella funzione String clientReadln(). In particolare:
nel ciclo while od ogni iterazione la variabile client veniva aggiornata con “server.available()” per cui
all’uscita della funzione, nel caso in cui scadeva il tempo a disposizione (senza che il client inviasse dati), client assumeva valore NULL.
Di conseguenza chiamando client.stop() la connessione non veniva interrotta.
Partendo da questa considerazione ho modificato la funzione in questo modo (la variabile client, dopo la connessione del client, non la tocco più se non per chiamare client.stop()):

String clientReadln(){
  String clientStr = NULL;
  char cTmp;
  
  clientLastActivity = millis();

  while ( (cTmp != STRING_END_TOKEN) && 
          ( (millis() - clientLastActivity) < MAX_CLIENT_DELAY) ){
    if(server.available()){
      cTmp = server.available().read();
      if (cTmp != STRING_END_TOKEN){
        if (clientStr == NULL)
          clientStr = cTmp;
        else
          clientStr = clientStr + cTmp;
      }
      clientLastActivity = millis();
    }
  }
  return clientStr;
}

In questo modo tutto fila liscio :smiley: Ragionavo troppo in termini di socket usando l’oggetto client e i suoi metodi :-[
Grazie mille per l’aiuto lesto :slight_smile:

sisi, era proprio un errore bastardello, non ci avevo fatto caso... però ora funziona, bravo!

grazie lesto :) è merito anche tuo che mi hai messo sulla giusta strada :)

Non vorrei scocciare però... il problema della disconnessione così è risolto ma è possibile che un client si connetta mentre è già presente un altro interferendo con il processo di login... Ci penso un po' e vedo se riesco a sistemare :(

Ok… essendo interessato all’ultimo client connesso non dovevo usare i metodi del server… adesso ho le idee un po’ più chiare, scusate
Per chi è curioso o se può essere utile, questo è il codice corretto:

#include <SPI.h>
#include <Ethernet.h>           //Ethernet libraries

#define STRING_END_TOKEN '\\'       //End token for clients requests
#define MAX_CLIENT_DELAY 10000      //Max delay (in millis) for two subsequent chars in a client message
#define ACK              "\\ok\\"   //Acknowledge message 
#define NO_ACK           "\\err\\"  //Unacknowledge message

Server server = NULL;     //Arduino Server
Client client = NULL;     //Client connected to arduino

long clientLastActivity = 0;  //Time (in millis) of last char in a client message

void startServer(byte *mac, byte *ip, byte *gateway, byte *subnet, int port){
  Ethernet.begin(mac, ip, gateway, subnet);
  server = Server(port);
  server.begin();
}

void waitClient(){
  for(client = server.available(); !client; client = server.available());
}

String clientReadln(){
  String clientStr = NULL;
  char cTmp;
  
  clientLastActivity = millis();

  while ( (cTmp != STRING_END_TOKEN) && ( (millis() - clientLastActivity) < MAX_CLIENT_DELAY) ){
    if (client.available() && client.connected()){
      cTmp = client.read();
      if (cTmp != STRING_END_TOKEN){
        if (clientStr == NULL)
          clientStr = cTmp;
        else
          clientStr = clientStr + cTmp;
      }
      clientLastActivity = millis();
    }
  }

  return clientStr;
}

void clientWriteln(String msg){
  client.print(msg + STRING_END_TOKEN);
}

boolean clientLogin(String password){
  boolean login = clientReadln().equals(password);
  if (login)
    clientWriteln(ACK);
  else
    clientWriteln(NO_ACK);
  return (login);
}

void disconnectClient(){
    client.stop();
    client=NULL;
}

uhmm direi che il tuo codice non è stra-ottimizzato per essere asincrono... la waitClient potrebbe essere eseguita vi interrupt, quindi non necessitare del for che pianta il codice, stessa cosa per le read, e probabilmente per le write... per mantenere più connessioni simultanee ti consiglio una lista di client, però occhio alla ram, mi sa che già sui 30/40 superi il limite...

m... ti ringrazio! Per quanto riguarda più connessioni simultanee non l'ho affrontato perché per ora mi è sufficiente così, certamente è ottima l'idea di una lista :) Magari una linked list di strutture dati che mantengono sia i client che relativi contatori per i limiti di tempo e qualche altra info utile... quando sarò più avanti con il progettino cercherò sicuramente di affrontare la cosa. Per la questione del sincronismo pensavo di risolvere giocando sempre con la funzione "millis()" ... fino alla parolina "interrupt" nella tua risposta :D Non riesco ad immaginare, però, in che modo potrei sfruttare gli iterrupt per waitClient, read e write :( In che modo possono essermi utili? :D

il vantaggio di un'iterrupt è che quando arriva, se "attivato", viene bloccata l'esecuzione di codice corrente e attivata quella dell'interrupt. Per esempio, quando arriva un dato via seriale, scatta un'iterrupt che mette il dato in un buffer, poi quando tu vai a fare Serial.avaiable() semplicemente non fai altro che controllare quanto è grande il buffer :-) ora immaginati se anzichè ASPETTARE che un client si connetta, tu faccia le tue cose, poi un client si collega, scatta un'interrupt che esegue il codice della waitClient() (senza for) e mette il client nella linked list "di attesa" (o nel tuo caso in variabile clientBuffer) al prossimo ciclo (o sempre nell'interrupt) il tuo programma si accorge che c'è un client (o un nuovo client) nel buffer e fa ciò che deve fare, ad esempio, nel tuo caso, disconnettere il vecchio client (attento che è una cosa che mi pare tu non faccia quando arriva una nuova connessione! errore! :-)) e sostituire client con clientBuffer, autenticarlo ecc... Ora il punto sta a capire come lanciare questo interrupt... se fosse un segnale ad un pin nessun problema (le ho già usate), ma per beccare proprio il momento Server.avaiable() (che diciamo è uno segnale software) dovresti dare un'occhiata quì: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1162726448 Però in effetti per la tua applicazione potrebbe essere un'over kill

Magari una linked list di strutture dati che mantengono sia i client che relativi contatori per i limiti di tempo e qualche altra info utile

Una info potrebbe essere lo stato del client nel tuo programma: semplicemente connesso, autenticato con password, ecc..

Perfetto :D Si si il concetto di interrupt ce l'ho presente :) qualche volta in ambiente unix in c li ho usati... Pensavo che sull'Arduino fosse possibile gestire solo interrupt hardware su alcuni pin e non lanciare e catturare segnali software... ottimo :D non appena ritorna il sole mi leggo il link che mi hai segnato. Purtroppo per te ... mi sa che non mancherò di aggiornare il thread :D Ancora grazie mille :)

Nella stesura delle funzioni di gestione della linked list per i client mi si è presentata la necessità di confrontare due oggetti Client... Ho la necessità, cioè, di verificare se due oggetti Client si riferiscono effettivamente allo stesso client (scusate il gioco di parole :S).

Ho modificato la libreria Ethernet ed in particolare la classe Client, aggiungendo il seguente metodo:

uint8_t Client::getSocket(){
  return _sock;
}

In questo modo, per verificare se due oggetti Client si riferiscono allo stesso client, ne confronto il numero di socket con l'istruzione:

((int)client1.getSocket()) == ((int)client2.getSocket())

Ovviamente preferirei non modificare la libreria ma... ho alternative?

Come sempre... grazie mille per l'aiuto.

secondo me non è una buona idea perchè confronti due indirizzi... e potrebbero puntare alla stessa cosa anche se sono differenti(son stanco magari sparo catzate) io farei una classe contenitore con un'id univoco assegnato quando fai la server.available() (o una struttura, le classi son più comode ma un pò più bastardelle :-) )

Purtroppo per la programmazione OO ho sempre usato il Java e non mastico molto il C++ :( Avevo pensato di implementare un metodo equals nella classe Client ... a questo punto, se riesco, lo faccio in una wrapper come mi hai consigliato :)

Ho bisogno, tuttavia, che l'eventuale ID sia identificativo del client fisico ... ad esempio il suo ip o la socket (ma non esistono, per la classe Client, metodi per questo scopo :()... Per questo uso gli indirizzi delle socket... se i due indirizzi delle socket sono uguali -> la socket è la stessa -> gli oggetti Client si riferiscono allo stesso client fisico. A tal proposito ho trovato anche una libreria Ethernet modificata dove è disponibile il seguente metodo per Client:

uint8_t *Client::getRemoteIP(uint8_t remoteIP[]){
  getSn_DIPR(_sock, remoteIP);
  return remoteIP;
}

Non so bene, però, come adattare la funzione "getSn_DIPR(_sock, remoteIP);" per l'ultima versione della libreria Ethernet...

Niente... non riesco a capire come usare la libreria W5100 per leggere l'ip del client :( Nella vecchia versione della libreria modificata il metodo "getRemoteIp", della classe Client, richiama questa funzione della sua libreria w5100:

void getSIPR(uint8 * addr)
{
      addr[0] = IINCHIP_READ(SIPR0);
      addr[1] = IINCHIP_READ(SIPR0+1);
      addr[2] = IINCHIP_READ(SIPR0+2);
      addr[3] = IINCHIP_READ(SIPR0+3);
}

dove IINCHIP_READ è usata per leggere i valori dai registri del w5100 e SIPR0 è indicato come indirizzo dell'"Ip Register" (0x8000 + 0x000F). Dovrei modificare anche la W5100 per leggere questi valori ma non riesco a farlo :(