Network Remote Control

Anforderung:
Eine Windows-Applikation, die mit einer Makrosprache steuerbar ist, soll im Serverbetrieb und im (mehrfachen) Clientbetrieb sowohl auf einem wie auch auf mehreren Rechnern vorliegen können. Dazu soll der Server die Instruktionen des Anwenders befolgen und die entsprechenden Makrobefehle als ASCII-Textstrings an die Clients verteilen. Natürlich muss weiterhin ein Standalone-Betrieb als einfachste Variante ausführbar sein. Alle Betriebsarten sollen selbstverständlich ohne Polling ausgeführt werden.

Implementierungsschritte:

1.) Einrichten des Netzwerks
Sollen die Instanzen der Applikation auf einem Rechner laufen, ist einfach die Default-IP-Adresse 127.0.0.1 zu verwenden.
Sollen die Instanzen auf mehreren Rechnern laufen, so können die IPs z.B. folgendermaßen vergeben werden:

Die IPs müssen eindeutig sein.
Im verwendeten Fall müssen die vorderen drei Stellen der IPs identisch sein und es muss die gleiche entsprechende Subnetzmaske eingestellt werden.

2.) Anwenderentscheidung über die Konfiguration
Wird mit der vom Anwender angegebenen IP kein aktiver Server gefunden, kann er sich entscheiden, ob er einen Server einrichten oder im Standalone-Betrieb bleiben will.


Im anderen Fall besteht die Möglichkeit, sich dem vorhandenen Server als Client anzuschließen.


In allen Fällen muss bei der vorliegenden Implementierung die IP-Adresse des potentiellen Servers angegeben werden.

3.) Timing Diagramm

Die asynchrone Natur der Kommunikation wurde in Form von Threads realisiert.

Soll der MAIN THREAD des Prozesses in den Server Mode verzweigen, wird zuerst ein Accept Thread erzeugt – anschließend werden bei jeder vom Anwender erhaltenen Instruktion dieselbe entsprechend der Liste der Clients an die Clients verteilt.
Der Accept Thread wartet auf die Anmeldung eines (weiteren) Clients. Tritt dieser Fall ein, wird für diesen Client ein Receive Thread angelegt und die Liste der Clients angepasst.
Da im Receive Thread derselbe Code für den Server Mode wie auch den Client Mode verwendet wird, muss dieser sein Verhalten dem jeweils eingestellten Modus anpassen.

4.) Konsistenzschutz
Da der MAIN THREAD und der Accept Thread asynchron auf die Liste der Clients zugreifen, muss ihre Konsistenz gewährleistet werden. Dies wurde folgendermaßen implementiert:
Alle Zugriffe auf die Liste erfolgen mittels einer einzigen Funktion, die in ihren Daten einen statischen vector beinhaltet. In ihrem Code enthält die Funktion tcpVectorControl eine CriticalSection , die nur die tatsächlich kritischen am vector wirkenden Aktivitäten schnellstmöglich abwickelt.

Codeauszüge

// MAIN THREAD OnDraw  in 1st call
// Hauptaufgabe:  Betriebszustand einstellen
//
// Symbole:
// Mbx::MBoxEror Error Handling
// Tcp_Sock_Mode Betriebszustand

typedef enum { 
    TCP_SOCK_NONE = -1,
    TCP_SOCK_SERVER = 1,
    TCP_SOCK_CLIENT = 2 
} TCP_SOCK_MODE;

static TCP_SOCK_MODE Tcp_Sock_Mode;

// TCP_MSG_INIT Anmeldemessage
// TCP_MSG_EXIT Abmeldemessage
// RECEIVETHREADPARAMS ThreadParameterCtrlStruct

typedef struct { 
    SOCKET mySocket; 
    CView*Me;
    char ipAdr[LBSIZE]; 
} RECEIVETHREADPARAMS;
alle weiteren Symbole sind Windows-Bestandteile
oder C(++)StdLib-Bestandteile oder davon abgeleitet

Code:
/* [...] do all the init stuff */
WSADATA wsadata; // »rižArtë TcpRemoteCtrl INIT«
if(::WSAStartup(MAKEWORD(1,1),&wsadata))
{
    TcpActive=false; // failed to get network  access
    Mbx::MBoxEror("::WSAStartup  failed","Remote  Control"); 
} else {
    Tcp_Sock_Mode = TCP_SOCK_NONE;
    TcpActive=true;
    mySocket=::socket(AF_INET,SOCK_STREAM,0);
    if(mySocket==INVALID_SOCKET) // failed to get socket
        Mbx::MBoxEror("::socket failed","Remote Control");
    else {  /* [...] get the TcpPar_IPAddress from registry, if exists */
        SOCKADDR_IN sckAddress;
        memset(&sckAddress,0,sizeof(sckAddress));
        sckAddress.sin_port = htons(TCP_IP_PORT);
        sckAddress.sin_family = AF_INET;
        sckAddress.sin_addr.s_addr = inet_addr(TcpPar_IpAddress); //  sckAddress.sin_addr.s_addr = inet_addr("127.0.0.1");
        if(!::connect(mySocket,(SOCKADDR*)&sckAddress, sizeof(sckAddress))) // 0==SUCCEss^client? ||  SOCKET_ERROR^server?
        {  
            int len=(int)(1+strlen(TCP_MSG_INIT));
            if(len!=::send(mySocket,TCP_MSG_INIT,len,NULL))
            {
                ::closesocket(mySocket); // no connection
                Mbx::MBoxEror("::send(TCP_INIT_MSG) failed", "Remote Control");
            } else  // connected 2 TcpServer {
                bool res; 
                /* [...] ask user */
                if(res) // user decided to attach  to server as client {
                    Tcp_Sock_Mode =  TCP_SOCK_CLIENT;
                    { 
                        static  RECEIVETHREADPARAMS params;
                        DWORD myThreadId;
                        params.Me = this;
                        params.mySocket = mySocket;
                        CreateThread(NULL,100000, // Crea ReceiveThread 
                            (LPTHREAD_START_ROUTINE)&tcpReceiveThread,
                            &params,NULL,&myThreadId);
                    }
                } else // message to user {
                    Tcp_Sock_Mode =  TCP_SOCK_NONE;
                    ::send(mySocket,TCP_MSG_EXIT,
                        (int)(strlen(TCP_MSG_EXIT)),NULL);
                    Sleep(100); closesocket(mySocket);
                }
            }
        } else // connect failed => establish the server? {
            ::closesocket(mySocket);
            bool res; 
            /* [...] ask user */
            if(!res) // user: StandaloneMode 
            {
                Tcp_Sock_Mode = TCP_SOCK_NONE;
                return; 
            }
            mySocket=::socket(AF_INET,SOCK_STREAM,0);
            if(mySocket==INVALID_SOCKET)
                Mbx::MBoxEror("::socket failed","Remote Control");
            else {
                Tcp_Sock_Mode = TCP_SOCK_SERVER;
                memset(&sckAddress,0,sizeof(sckAddress));
                sckAddress.sin_port  = htons(TCP_IP_PORT);
                sckAddress.sin_family  = AF_INET;
                sckAddress.sin_addr.s_addr = INADDR_ANY;
                if(::bind(mySocket,(SOCKADDR*)&sckAddress,
                    sizeof(sckAddress)))
                    Tcp_Sock_Mode=TCP_SOCK_NONE;
                if(Tcp_Sock_Mode!=TCP_SOCK_NONE&&
                    ::listen(mySocket,SOMAXCONN))
                    Tcp_Sock_Mode=TCP_SOCK_NONE;
                if(Tcp_Sock_Mode==TCP_SOCK_NONE) // error->user 
                    Mbx::MBoxEror("::bind||::listen failed",
                    "Remote Control");
                else { 
                    DWORD acceptThreadId; // create AcceptThread
                    CreateThread(NULL,100000,
                        (LPTHREAD_START_ROUTINE)&tcpAcceptThread,
                        this,NULL,&acceptThreadId);
                } 
            }
        }
    }
}
Accept Thread tcpAcceptThread
Hauptaufgabe: Clients erfassen
Symbol:
tcpVectorControl Funktion für Liste der Clients

Code:
int CriztestView::tcpAcceptThread(CView*Me)
{
    tcpVectorControl(CLEAR,NULL,NULL,NULL); // reset the vector
    while(true)
    {
        fd_set fds; FD_ZERO(&fds); FD_SET(mySocket,&fds);
        if(!::select(0,&fds,NULL,NULL,NULL)) continue;
        if(FD_IssET(mySocket,&fds))
        {
            SOCKET newSocket = ::accept(mySocket,NULL,0); 
            //  LEAVE THE THREAD ON WM_DESTROY!!
            if((int)newSocket<0) return 0;
            RECEIVETHREADPARAMS params;
            memset(&params,0,sizeof(params));
            params.Me = Me;
            params.mySocket = newSocket;
            {
                SOCKADDR name;
                int len = sizeof(name); 
                //  try to get ip-address
                getpeername(newSocket,&name,&len);
                unsigned char*p=((unsigned char*)&name)+4;
                sprintf(params.ipAdr,"%d.%d.%d.%d", 
                    p[0],p[1],p[2],p[3]); 
            }
            DWORD myThreadId;
            CreateThread(NULL,100000,
                (LPTHREAD_START_ROUTINE)&tcpReceiveThread,
                &params,NULL,&myThreadId);
            tcpVectorControl(ADD,newSocket,myThreadId, 
                params.ipAdr);  // add the new client -> the vector
        }
    } return 0;
}

Receive Thread tcpReceiveThread
Hauptaufgabe: Messages empfangen

Symbole:
TCPVECTORCONTROL_ERASE Löschen in der Liste der Clients
TEST auskommentierte Debugfunktionalitäten
ExecInternalMacro Ausführen der Instruktion (vom Serv)
UpdateStatText Anzeige updaten

Code:

int CriztestView::tcpReceiveThread(RECEIVETHREADPARAMS*params)
{
    RECEIVETHREADPARAMS par = *params;
    while(true)
    {
        char buf[10*LBSIZE]; 
        memset(buf,0,sizeof(buf));
        if(::recv(par.mySocket,buf,10*LBSIZE,0))
        {  
            if(!*buf) 
            // empty message:
            // If  the connection has been gracefully closed,
            // the return value  is zero
            {  
                if(Tcp_Sock_Mode==TCP_SOCK_SERVER)
                {  
                    TCPVECTORCONTROL_ERASE; // remove client
                } else { 
                    closesocket(par.mySocket);
                    Tcp_Sock_Mode=TCP_SOCK_NONE; 
                }
                return  0;
            }
            if(Tcp_Sock_Mode==TCP_SOCK_SERVER)
            {  
                if(!strcmp(buf,TCP_MSG_INIT)) // client connects
                {  /* [...] par.Me->MessageBox(buf); */
                    continue; 
                } else if(!strcmp(buf,TCP_MSG_EXIT)) // client closes {
                    /* [...] par.Me->MessageBox(buf); */
                    TCPVECTORCONTROL_ERASE;
                    return 0;
                } else // illegal instr from client {
                     /* [...] create information */
                     Mbx::MBoxEror(buf,"Remote Control"); 
                }
            } else // (Tcp_Sock_Mode==TCP_SOCK_CLIENT) {
                /* [...] par.Me->MessageBox(buf); */
                sprintf(statClient, // info -> display
                    "client[%d] received instr: %s",
                    params->mySocket,buf);
                    strcat(buf,"\n");
                    ((CriztestView*)(par.Me))->
                    ExecInternalMacro(buf,statClient);
                    ((CriztestView*)(par.Me))->UpdateStatText();
            }
        }
    }   return  0;
}
tcpVectorControl
Hauptaufgabe:Liste der Clients hantieren
Code:
int CriztestView::tcpVectorControl( THREADVECTORMODE mode,SOCKET socket,DWORD  threadId,
    char*ipAdr,CView*Me,char*instr,CString*statist)
{
    static CRITICAL_SECTION myCritSect;
    static bool first = true;
    CString stat; 
    char stLine[LBSIZE]; 
    int i;

    if(mode==STATE)
    {
        if(!TcpActive) 
        { 
            /* [...] */
            Mbx::MBoxInfo(stLine,"Remote Control");
            return  0; 
        }
        switch(Tcp_Sock_Mode)
        {  
            case TCP_SOCK_NONE: /* [...] */
                Mbx::MBoxInfo(stLine,"Remote Control");
                return  0;
            case  TCP_SOCK_CLIENT: /* [...] */
                Mbx::MBoxInfo(stLine,"Remote Control");
                return  0;
        }  
    }
    static vector< RECEIVETHREADCLIENT>clientThreadVector;
    RECEIVETHREADCLIENT client;
    client.socket = socket;
    client.threadId  = threadId;
    if(ipAdr)
        strcpy(client.ipAdr,ipAdr);
    vector <RECEIVETHREADCLIENT>::iterator Iter;
    if(first) 
    { 
        InitializeCriticalSection(&myCritSect); // This function does not return a value.
        first = false; 
    }
    int retval = 0;
    // ENTRY  CRITICAL SECTION
    EnterCriticalSection(&myCritSect);
    switch(mode)
    {  
        case  CLEAR: clientThreadVector.clear(); break;
        case  ADD:   clientThreadVector.push_back(client); break;
        case  ERASE:
            for(Iter = clientThreadVector.begin();
                Iter != clientThreadVector.end(); Iter++)
            {
                if(Iter->socket == socket) break;
            }
            if(Iter != clientThreadVector.end())
            {
                if(clientThreadVector.erase(Iter) == clientThreadVector.end())
                    retval = 1;
            } else {
                retval=2; 
                break;
            }
        case  STATE:
            for(i = 0, Iter = clientThreadVector.begin();
                Iter != clientThreadVector.end(); i++, Iter++)
            {  
                sprintf(stLine,"client[%d]:\tthread=%d\tsocket=%u\tIP=%s\n",
                    i+1,Iter->threadId,Iter->socket,Iter->ipAdr);
                stat += stLine; 
            }
            break;
        case  SEND:
            for(Iter = clientThreadVector.begin();
                Iter != clientThreadVector.end(); Iter++)
            {
                ::send(Iter->socket,instr,(int)strlen(instr),NULL);
                break;
            }
        }
        int numClients = (int)clientThreadVector.size();
        // EXIT  CRITICAL SECTION
        LeaveCriticalSection(&myCritSect);
        if(retval)
            return retval;
        switch(mode)
        {  
            case ERASE: closesocket(socket); break;
            case  STATE:
                sprintf(stLine,
                    "This instance of rižArtë runs as a Controlling  Server [IP:»%s«]\n"
                    "for  %d Displaying Client(s):\n\n",TcpPar_IpAddress,numClients);
                stat=stLine+stat;
                if(statist)
                    *statist = stat;
                else 
                    DisplayFile(stat.GetBuffer(3),"Remote Control",false,Me);
                
                break;
        }
    } return 0;
}
Bild das den Remotebetrieb von rižArtë darstellt:
öö

Zurück