[Studienarbeiten: Kommunikation zwischen PDAs]

3.3 Implementierung unter WindowsCE

3.3.2 Beschreibung der Menüpunkte

3.3.3 Beschreibung der Implementierung

IRChat wurde mit Hilfe der Microsoft Foundation Classes (MFC) als Single-Document-Interface (SDI) Applikation erstellt. Die MFC bieten ein standardisiertes objektorientiertes Applikationsgerüst und eine objektorientierte Kapselung vieler Funktionen des Windows API (Application-Programmers-Interface). Im folgenden werden grundlegende Kenntnisse über das Applikationsgerüst und die Funktionsweise der MFC vorausgesetzt.

IrChat besitzt als SDI-Applikation ein Hauptfenster (CMainframe) und eine Hauptansicht (CIrchatview). Die Hauptansicht ist ein CFormView, der zwei Edit-Controls enthält. Das erste (im folgenden Empfangsfenster genannt) füllt fast das gesamte Programmfenster aus und dient zum Darstellen der empfangenen Nachrichten und etwaiger Meldungen. Da dieses nur zur Ausgabe von Text bestimmt ist ist es schreibgeschützt. Das zweite (im folgenden Sendefenster genannt) besteht aus einer einzelnen Zeile am unteren Fensterrand und ermöglicht die Eingabe des zu sendenden Textes.

Das Programm verwendet intern die UNICODE Darstellung von Texten. Die Übertragung erfolgt allerdings wie im Kapitel 3.2 Allgemeine Anforderungen definiert in ASCII.

Für die Kommunikation wurde die IrSocket Erweiterung benutzt. Da diese allerdings nur blockierende Funktionen zur Verfügung stellt, wurden für die meisten grundlegenden Programmfunktionen eigene Threads implementiert. Da ein Chat-Programm von Natur aus asynchronen Datentransfer benötigt, war dies vor allem für das Warten auf ankommenden Text von der Gegenseite nötig. Nur so konnte ermöglicht werden, daß eigener Text eingegeben und versendet werden kann, während gleichzeitig auf Text von der Gegenseite gewartet wird. Diese Threads erschweren an dieser Stelle ein durchgehend objektorientiertes Konzept, da auf MFC-Klassen nicht über Threadgrenzen hinweg problemlos zugegriffen werden kann. Die  Threads sind daher als C-Funktionen implementiert, die gemeinsam auf eine Reihe von globalen Variablen zugreifen.

Ein weiteres Problem war in diesem Zusammenhang die nötige Ausgabe der Threads im Empfangsfenster. Die Ausgabe wird in der Funktion DisplayMsg der zur Hauptansicht gehörenden Klasse IrchatView erledigt. Um einen Text auszugeben (egal ob empfangener Text oder Fehlermeldung) wird die Windows-Message IRMSG_DISPLAYMSG an das Hauptansichtsfenster geschickt. Der auszugebende Text wird dabei in den Local-Heap kopiert und der Handle darauf als LParam mitgeschickt. Die DisplayMessage-Funktion im View ist für die Freigabe zuständig.

3.3.4 Die Makros

/////////////////////////////////////////////////////////////////////////////
// Makrodefinitionen
// Die Makros sind so definiert, dass sie von allen Threads benutzt werden
// koennen.
/////////////////////////////////////////////////////////////////////////////
// Kommando versenden (Esc+MSG)
#define SENDCMD(MSG)  {CString str=MSG;char buf[255];buf[0]='\033';for(int i=0;i<str.GetLength();i++)buf[i+1]=(char)str.GetAt(i);buf[i+1]='\0';if(send(SendSock,buf,strlen(buf)+1,0)==SOCKET_ERROR){FEHLER;}}
// Fehlermeldung auslesen und ausgeben (inklusive Filename und Programmzeile)
#define FEHLER    { TCHAR buf[255];for(UINT i=0;i<=strlen(__FILE__);i++)buf[i]=__FILE__[i];CString str;str.Format(_T(" %s Zeile %d "),buf,__LINE__);GETAPP->FehlerMeldung(str);}
// Gibt das Applikationsobjekt zurueck (richtig gecastet)
#define GETAPP    ((CIrchatApp*)AfxGetApp())

3.3.5 Die Threads

///////////////////////////////////////////////////////////
// Thread-Routinen
///////////////////////////////////////////////////////////
// ConnectThread
// Dieser Thread versucht eine Verbindung zu einem Server
// zu erzeugen. Dazu wird zunaechst nach Geraeten in
//  Reichweite gesucht und dann versucht mit dem ersten
// gefundenen Geraet eine Verbindung zu oeffnen.
UINT ConnectThreadFunction(LPVOID data)
{
 TCHAR  buffer[1024];
 TCHAR  devname[22];
 const  NumRetries=5; // Anzahl der Versuche fuer die Suche nach Geraeten
 SOCKET  sock;
 DEVICELIST devlist[3];
 int   devlistlen=sizeof(devlist);

 int i;

 if( g_bConnected)
  return -1;  // Wenn schon verbunden nichts tun und Thread-beenden
 //Hilfssocket erzeugen. Diese wird fuer die Suche nach Geraeten benoetigt
 sock = socket(AF_IRDA,SOCK_STREAM,0);
 if(sock==INVALID_SOCKET)
 {
  FEHLER;
  return 0;
 }
 devlist[0].numDevice=0;
 PRINT("Looking for Server");
 // Suche starten
 for(int cnt=1;cnt<=NumRetries;cnt++)
 {
  PRINT(_T("Looking for Server..."));
  if(getsockopt(sock,SOL_IRLMP,IRLMP_ENUMDEVICES,(char*)&devlist,&devlistlen)==SOCKET_ERROR)
  {
   FEHLER;
   closesocket(sock);
   return 0;
  }
  if(devlist[0].numDevice==0)
  {
   PRINT(_T("[!!!!!!]  No Server found"));
   continue;
  }
  else
  {
   // Geraet wurde gefunden. Namen Ausgeben.
   //
   for(int j=0;j<22;j++)
    devname[j]=devlist[0].Device[0].irdaDeviceName[j];
   wsprintf(buffer,_T("[!!!!!!]  Found: %x.%x.%x.%x **  %s ** "),
      (BYTE)devlist[0].Device[0].irdaDeviceID[0],
      (BYTE)devlist[0].Device[0].irdaDeviceID[1],
      (BYTE)devlist[0].Device[0].irdaDeviceID[2],
      (BYTE)devlist[0].Device[0].irdaDeviceID[3],
      devname,
      devlist[0].Device[0].Reserved);
   PRINT(buffer);
   break;
  }
 }
 if(cnt>=NumRetries)
 {
  // Es wurde kein Geraet gefunden. Also Abbrechen.
  PRINT(_T("[!!!!!!]  No Device found!"));
  return 0;
 }
 // Jetzt wird die Adresse des gefundenen Geraets kopiert.
 for(i=0;i<4;i++)
 {
  ConnectAdr.irdaDeviceID[i]=devlist[0].Device[0].irdaDeviceID[i];
 }
 
 // Socket an lokale Adresse binden.
 if(bind(RemoteSock,(struct sockaddr*)&ListenerAdr,sizeof(ListenerAdr))==SOCKET_ERROR)
 {
  FEHLER;
  closesocket(RemoteSock);
  return 0;
 }
 if(connect(RemoteSock,(struct sockaddr *)&ConnectAdr,sizeof(SOCKADDR_IRDA))==SOCKET_ERROR)
 {
  FEHLER;
  closesocket(RemoteSock);
  return 0;
 }
 // Verbindung aufgebaut. Flags setzen und ReceiveThread starten
 g_bConnected=TRUE;
 g_bExitRecv=FALSE;
 g_pRecvThread=AfxBeginThread(CRecvThreadFunction,NULL);

 // UserName und Begruessung senden
 SENDCMD(_T("U")+GETAPP->m_MyName);
 SEND(_T("Mit WindowsCE PDA verbunden") );
 return -1;
}

///////////////////////////////////////////////////////////
// ReceiveThread
// Dieser Thread wartet in einer Schleife auf ankommende Daten
// von der Gegenseite. Je nachdem, ob es sich bei den Daten um
// Text oder um Kommandos handlet, wird entweder der Text im
// Empfangsfenster ausgegeben oder das Kommando bearbeitet.
// Der Thread wird beendet, durch setzen des Flags gbExitRecv.
// Dieser Thread steuert sowohl den Textempfang, als auch den
// Empfang von Dateien.
UINT CRecvThreadFunction(LPVOID data)
{
 char inbuf[2550];
 TCHAR inbufU[2550];
 int ncount;
 CString strRemoteFileName;
 CString strLocalFileName;
 CFile file;
 BOOL bFileOpen=FALSE;
 UINT FileLen;
 
 // Am Anfang ist der Zustand CHAT, kein Filetransfer
 ReceiveState=CHAT;
 CIrchatApp* app=(CIrchatApp*)data;
 if(!g_bConnected)
 {
  PRINT(_T("[!!!!!!]  NOT CONNECTED RECEIVE FAILED!"));
  return -1;
 }
 PRINT(_T("[!!!!!!]  Starting RecvThread!"));
 // Hauptschleife
 while(!g_bExitRecv)
 {
  memset( inbuf, 0, 255);
  // Warte auf Daten von der Gegenseite!
  if((ncount=recv(RemoteSock,inbuf,sizeof(inbuf),0))==SOCKET_ERROR)
  {
   FEHLER;
   GETAPP->Disconnect();  // Bei einem Fehler Verbindung abbrechen
   return -2;
  }
  // Wenn 0 Bytes empfangen wurden, hat die Gegenseite die Verbindung beendet.
  if(ncount==0)
  {
   PRINT(_T("[!!!!!!]  Remote has closed the connection!"));
   g_bExitRecv=TRUE;
   closesocket(RemoteSock);
   g_bConnected=FALSE;
   break;
  }
  // Umwandeln in UNICODE
  for(int i=0;i<=sizeof(inbuf);i++)
   inbufU[i]=inbuf[i];
  inbufU[i]=0;

  // Waren die Daten ein Kommando?
  if(inbufU[0]=='\033')
  {
   CString ps=_T("RECEIVE COMMAND: ");
   ps+=inbufU[1];
   //AfxMessageBox(ps);
   ps.Format(_T("%d Bytes"),ncount);
   // Die Daten waren ein Kommando, fragt sich nur welches.
   switch(inbufU[1])
   {
    case 'U':
     // USER -> Den Namen der Gegenseite neu setzen
     GETAPP->m_RemoteName=(const TCHAR*)&inbufU[2];
     PRINT(_T("[!!!!!!] Remote User changed Name to: ")+GETAPP->m_RemoteName);
     break;
    case 'N':
     // FILENAME -> Die Gegenseite will eine Datei uebertragen
     //    und sendet nun zunaechst einmal den Dateinamen.
     {
     strRemoteFileName=(const TCHAR*)&inbufU[2];
     // Nun wird ein "Datei speichern"-Dialog geoeffnet, mit dem man
     // Position und Namen der zu empfangenden Datei festlegen kann.
     CFileDialog dlg(FALSE,NULL,strRemoteFileName);
     if(dlg.DoModal()!=IDOK)
     {
      // Benutzer hat Dialog abgebrochen => Dateitransfer abbrechen
      bFileOpen=FALSE;
      ReceiveState=CHAT;
      SENDCMD(_T("C")); // Transfer abbrechen durch senden von CANCEL
     }
     strLocalFileName=dlg.GetPathName();
     // Alles OK. Datei erzeugen und oeffnen.
     if(!file.Open(strLocalFileName, CFile::modeCreate | CFile::modeWrite | CFile::shareExclusive))
     {
      // Fehler beim Erzeugen=> Abbruch
      PRINT(_T("[!!FILE!!]  Datei konnte nicht erzeugt werden!"));
      bFileOpen=FALSE;
      ReceiveState=CHAT;
      SENDCMD(_T("C"));
     }
     // Flag setzen, dass das file-Objekt gueltig ist.
     bFileOpen=TRUE;
     break;
     }
    case 'R':
     // FILEREADY -> Datei ist zu senden bereit.
     if(bFileOpen)
     {
      // Zustand ist jetzt FILEREADY
      // Gegenseite wartet auf ACK...
      ReceiveState=FILEREADY;
      SENDCMD(_T("A")); // ACK senden
     }
     else
     {
      // Wenn Datei nicht erzeugt wurde. Abbrechen!
      ReceiveState=CHAT;
      SENDCMD(_T("C"));
     }
     break;
    case 'A':
     // ACK -> Gegenseite hat ein Acknowledge gesendet.
     //   Falls wir selber eine Datei senden und im Zustand
     //   FILEWAITACK sind, bedeutet das, dass wir mit der
     //   Uebertragung beginnen koennen. Das erledigt
     //   allerdings der SendFileThread, sobald der Zustand
     //   auf FILEACK steht.
     //   Falls eine Datei empfangen, bedeutet das, dass alle
     //   Bloecke uebertragen wurden.
     if(ReceiveState==FILEWAITACK)
      ReceiveState=FILEACK;
     else if(ReceiveState==FILEREADY)
     {
      // Uebertragung beendet. Zustand wieder auf CHAT und Datei schliessen
      ReceiveState=CHAT;
      bFileOpen=FALSE;
      file.Close();
      PRINT(_T("[!!FILE!!] Filetransfer complete!"));
     }
     break;
    case 'C':
     // FILECANCEL -> Uebertraggung abbrechen und wieder in Zustand CHAT.
     ReceiveState=CHAT;
     if(bFileOpen)
      file.Close();
     bFileOpen=FALSE;
     break;
    case 'B':
     // BLOCK -> Hinter diesem Kommando folgt ein Datenblock
     //   Die Daten werden in das erzeugte File geschrieben.
     //   Die Anzahl der Daten betraegt ncount-2
     //   (empfangene Bytes-2 Bytes fuer den Befehl)
     if((bFileOpen)&&(ReceiveState==FILEREADY))
     {
      CString ps;
      ps.Format(_T("BLOCK mit %d Bytes geschrieben"),ncount);
      file.Write(&inbuf[2],(UINT)ncount-2);
     }
     else
      PRINT(_T("[!!FILE!!]  Protokollfehler!"));
     break;
    default:
     PRINT(_T("[!!!!!!]  UNKNOWN COMMAND !"));
     break;
   }
  }
  else
  {
   // Es war kein Kommando. Also gesendeten Text ausgeben.
   CString str=_T("[")+GETAPP->m_RemoteName+_T("]  ");
   str+=inbufU;
   PRINT(str);
  }
 }
 PRINT(_T("[!!!!!!]  Stopping RecvThread!"));
 return 0;
}

///////////////////////////////////////////////////////////
// ListenerThread
// Dieser Thread wartet in einer Schleife auf ankommende
// Verbindungswuensche und akzeptiert diese.
UINT CListenerThreadFunction(LPVOID data)
{
 const   NumPending=1;
 SOCKET tmpSock;

 closesocket(ListenerSock);
 ListenerSock=socket(AF_IRDA,SOCK_STREAM,0);
 if(ListenerSock==INVALID_SOCKET)
 {
  FEHLER;
  goto fehler;
 }
 // ListenerSocket binden
 if(bind(ListenerSock,(struct sockaddr*)&ListenerAdr,sizeof(ListenerAdr))==SOCKET_ERROR)
 {
  FEHLER;
  closesocket(ListenerSock);
  goto fehler;
 }
 // Listenbefehl
 if(listen(ListenerSock,NumPending)==SOCKET_ERROR)
 {
  FEHLER;
  closesocket(ListenerSock);
  goto fehler;
 }

 g_bServerRunning=TRUE;
 PRINT(_T("Server gestartet"));
 // Warten auf Verbindung
 while(!g_bExitServer)
 {
  // Accept
  if((tmpSock=accept(ListenerSock,NULL,NULL))==INVALID_SOCKET)
  {
   FEHLER;
   closesocket(ListenerSock);
   goto fehler;
  }
  if(!g_bConnected)
  {
   RemoteSock=tmpSock;
   // Verbindung aufgebaut!!
   //  Jetzt Identifikation schicken, Meldung ausgeben und
   //  ReceiveThread starten
   PRINT(_T("Connection established"));
   SENDCMD(_T("U")+GETAPP->m_MyName);
   SEND(_T("Mit WindowsCE PDA verbunden"));
   g_bConnected=TRUE;
   g_bExitRecv=FALSE;
   g_pRecvThread=AfxBeginThread(CRecvThreadFunction,NULL);
  }
  else
  {
   closesocket(tmpSock);
   PRINT(_T("Connect from Remote failed!!"));
  }
 }
 g_bServerRunning=FALSE;
 PRINT(_T("[!!!!!!]  Server exiting."));
 closesocket(ListenerSock);
 return -1;
fehler:
 g_bServerRunning=FALSE;
 PRINT(_T("[!!!!!!]  Server failed ! Server exiting."));
 closesocket(ListenerSock);
 return 0;
}

///////////////////////////////////////////////////////////
// SendFileThread
// Dieser Thread wird gestartet, um eine Datei zur
// Gegenstelle zu schicken. Er wird automatisch beendet,
// wenn die Uebertragung (erfolgreich oder durch Fehler/Abbruch)
// beendet wurde.
UINT SendFileThreadFunction(LPVOID data)
{
 CFileDialog dlg(TRUE);
 int ncount;
 const ncountMax=250;
 char buffer[ncountMax+2];

 if((!g_bConnected)||(ReceiveState!=CHAT))
 {
  PRINT(_T("[!!FILE!!]  NOT CONNECTED OR TRANSFER IN PROGRESS !!"));
  return 0;
 }
 // File-Dialog anzeigen, zur Auswahl des zu übertragenden Files
 if(dlg.DoModal()!=IDOK)
  return 0;
 CString strPathName=dlg.GetPathName();
 CFile file(strPathName, CFile::modeRead);
 CString strFileName=file.GetFileName();
 // FileNamen uebertragen. Dies signalisiert gleichzeitig der Gegenseite,
 // dass eine Datei uebertragen werden soll.
 SENDCMD(_T("N")+strFileName);
 CString str;
 // FILEREADY senden
 SENDCMD(_T("R"));
 ReceiveState=FILEWAITACK;
 // Warten, bis Gegenseite ein ACK zuruecksendet
 while(ReceiveState==FILEWAITACK)
  Sleep(100);
 if(ReceiveState!=FILEACK)
  return 0;
 buffer[0]='\033';  // Esc-Zeichen
 buffer[1]='B';
 // Nun werden nacheinander alle Bloecke uebertragen
 do
 {
  ncount=file.Read(&buffer[2],ncountMax);
  CString s;
  s.Format(_T("%d"),ncount);
  if(ncount>0)
  {
   if(send(RemoteSock,buffer,ncount+2,0)==SOCKET_ERROR) // ncount +2 wegen ESCB
    {
     FEHLER;
     SENDCMD(_T("C"));
     return 0;
    }
  }
 }while ((ncount>0)&&(ReceiveState==FILEACK));
 // Zum Schluss noch ein ACK, um der Gegenseite das Uebertragungsende mitzuteilen
 SENDCMD(_T("A"));
 // Zustand wieder auf CHAT setzen
 ReceiveState=CHAT;
 return -1;
}

3.3.6 Die Ausgabe und Sendefunktionen

 ///////////////////////////////////////////////////////////
// Globale-Funktionen
///////////////////////////////////////////////////////////
// PRINT
// Dies Funktion gibt den übergebenen Text im Empfangsfenster aus.
// Dazu wird der Text als UNICODE-String auf dem lokalen Heap erzeugt.
// Der Handle wird als lParam mit der Message verschickt.
// Der Message-Handler ist für die Freigabe des Speichers zuständig.
void PRINT(CString MSG)
{
 HLOCAL hmem;
 LPTSTR ptr;
 if((hmem=LocalAlloc(LPTR,MSG.GetLength()*2+2))!=NULL)
 {
  ptr=(LPTSTR)LocalLock(hmem);
  wcscpy(ptr,(LPCTSTR)MSG);
  AfxGetMainWnd()->PostMessage(IRMSG_SENDMSG,0,(long)hmem);
  LocalUnlock(hmem);
 }
 else
 {
  AfxMessageBox(_T("Could not allocate Memory for Send!!"));
 }
}

///////////////////////////////////////////////////////////
// SEND
// Dies Funktion sendet den übergebenen Text an die Gegenstelle
// Dazu wird der Text als UNICODE-String auf dem lokalen Heap erzeugt.
// Der Handle wird als lParam mit der Message verschickt.
// Der Message-Handler ist für die Freigabe des Speichers zuständig.
void SEND(CString MSG)
{
 HLOCAL hmem;
 LPTSTR ptr;
 if((hmem=LocalAlloc(LPTR,MSG.GetLength()*2+2))!=NULL)
 {
  ptr=(LPTSTR)LocalLock(hmem);
  wcscpy(ptr,(LPCTSTR)MSG);
  AfxGetMainWnd()->PostMessage(IRMSG_DISPLAYMSG,0,(long)hmem);
  LocalUnlock(hmem);
 }
 else
 {
  AfxMessageBox(_T("Could not allocate Memory for ErrorMsg!!"));
 }
}
 

 

[Studienarbeiten: Kommunikation zwischen PDAs]