Link to home
Start Free TrialLog in
Avatar of werehamster-
werehamster-

asked on

Properly Formatted and Stable Indy TCP Client Thread w/ Reader Thread

I am looking for a really good layout of an Indy TCP client that works without error that uses a reader thread.

I have a button that is supposed to change text from connect to reconnect when the client changes connection status, but it does not work all the time.

Some Specific questions:

-If I have stuff in the Connection events, should I have the client.disconnect in a sycronized procedure or something?  I have a button that is supposed to change from 'connect' to 'disconnect' and vice versa when the event is called, but It doesn't change even though I still get TMemo messages showing that it was called.

-How do I properly end a connection?  From outside a reader thread and within a reader thread?  I know something about Terminate, but if it is waiting for a header, it will not see that portion of the thread to terminate itself.  I saw some examples of just .free ing the thread itself, but I don't know if that is safe.

-Can a thread properly start another thread?

-Can I safely write to a connection from within a thread while still writing to same connection from another thread?  I have some packets that need to be handled autonomousely, and some are generated via commands in an input box.  Is there a chance that, if both write at the same time, that the packets going out will be screwed up?  I am using the FlushBuffer functions.

-I am connecting to a server that every packet has a header that has a length, a packet ID, and data.  There are about 30 or so packet types/events that I need to take care of.  Right now I am using a big WHILE CONNECTED CASE LOOP.  Is there a better way of doing this?

Anyway, looking for stability without going overboard.  Any help would be appreciated.

Please don't just give 2 lines of text, state the obvious, give commonly known links, and expect points for it.  I hate that.  :)
Avatar of Jacco
Jacco
Flag of Netherlands image

Hi there,

I use a TIdTCPServer with a TIdThreadMgr (TServerThreadManager) descendant that creates a TIdPeerThread (TReadThread) descendant when needed. On construction this TReadThread creates a TWriteThread. The first command received on the TReadThread is instructions how the server can "backconnect" to the client. (So two different ports are used and both client and server have both a TIdTCPClient and TIdTCPServer). The TWriteThread uses it own TIdTCPClient to backconnect to the clients TIdTCPServer.  The writethread has a threadsafe queue of messages to be sent and is a state machine which tries to reconnect when a disconnect or exception occurs. The setup is is symetric on both client and server application the only difference being that the server does not initiate a connection but only waits for connection and the client does initiate a connection.

I hope this helps a bit, I know it all must sounds fuzzy if you don't have the code. I use this scheme very succesfully though.

Your questions:
>>should I have the client.disconnect in a sycronized procedure
This depends:
Indy is designed to be synchronized by itself. So call connect/read/write/disconnect all from the same thread. It is best to work with a component only from one thread. Unless you code critical sections around calls this is the only safe way.

>>Connection events
The events will be called in the same thread as the connect/disconnect calls. So if these calls are done fro another thread than the mainthread you need to synchronizse otherwise not.

>>Memo
The memo is updated internally with a SendMessage (bypasses the windows messagequeue) changing the button might cause a postmessage somewhere and might not be handled until the mainthread gets to an Application.ProcessMessages.

>>Terminating
From outside: Use Disconnect
From inside: Use Terminate in combination with read/write instructions with a TimeOut

>>Staring thread from thread
Yes

>>safely write to a connection from within a thread while still writing to same connection
No, unless you install a threadsafe queue or use a critical section on the message sending method. This can be as simple as a TStringList holding the messages to be sent. Protect reading and writing to the queue with a critical section.

>>There are about 30 or so packet
You could try using the TIdTCPServer.CommandHandlers.
I use a mechanism where strings are exchanged. These strings consist of headers and compressed bodies. The string is structured like binary xml form. I designed specialized classes for every messagetype that get instantiated from within the reader thread. All these classes are derived from the same baseclass that takes care of decompressing and populating the properties. This also has the advantage that any actions that should follow receiving a message can be converted to method calls.

Let me know if you think this info sucks ;)

Regards Jacco
Avatar of werehamster-
werehamster-

ASKER

>> >>Terminating
>> From outside: Use Disconnect
>> From inside: Use Terminate in combination with
>> read/write instructions with a TimeOut

I never figured out how to do a TimeOut.

>> Let me know if you think this info sucks ;)

It is all good basic strategy I would guess, but is there any chance of providing some sample source?

I do not have the option of changing the server code at all, I am making a client that is using an already existent system of packet header with a packet length in it.

I'd love to be able to make an Event that is called when a full packet is recieved if there are any recomendations on this.  Such as how to pass the data and not have memory leaks and stuff.
How is the packet ended? How do you separate header from body? Is it some kind of known protocol?

I could cook up some sample code but need to know better what you want.

Regards Jacco
Basically, each header has an Event ID, followed by a Sub Event ID, followed by the length of the total packet including the header.

0x00 Header
0x00 Sub Header
0x0000 Packet Length
[...] Data

If It wasn't Indy, I would do a loop checking connection, peeking the buffer to check the length and to see if all the data was available, and then, when it is, read it all and create some memory to put it in send it to an event procedure.

If you could make something that works full proof, I would have no problem with somehow giving you like 4000 points or whatever.  As long as this is allowed via E-E policy.

ProcessEvent(header : Byte; SubHeader : Byte; Len : Word; var Data);

Something like that.  Maybe simplified where the event processor is in charge of releasing the memory.  Or works from a pool of TMemoryStreams that get recycled.

A simple chat demo would be cool if you can do it.  :)  Maybe the header would always be $FF and the subheader could be the color or something.  Just a thought.  :)
So there is no termination character for the packets?

How do client and server communicate. Is it always the client initiating? Is it one packet up one down?

This is important since Indy is designed for synchronous communication,meaning that a client must know at any time if it is going to read or write.

A client receiving messages at random times (even times when it is writing) is not considered a client and is harder to implement.

I would gladly make a demo! Is this server you can not alter online somewhere or can I run it here?

Regards Jacco

Packets are the length of the value shown in the header.  SO the next begins when it has read all the data

ReadByte Header
ReadByte SubHeader
TeadWord LengthOfPacket
ReadBuf(Data, LengthOfPacket-4)

Basically how it is done.


Here is my read thread that I am currently using...

An example doesn't have to be this.  I am just looking for framework or well designed code that will disconnect and terminate properly with a disconnect and connect button on the user end and will also detect if the server disconnects it also.  Something that I can work off of.  With timeouts and all that good stuff.

For not, you can do something with old pascal style strings or something in a chat demo or something.  Where there is a header record that just has the length of a string followed by the string data.  Or add to the header a username or something.  Just looking for something simple that will terminate properly and interact with the rest of the program such as adding things to a TMemo.  And allow something outside the tread to send stuff back.  Plus any automatic events such as a PING with a PONG response every few seconds.  Basically an incoming and outgoing packet handler.

procedure TBNETReadThread.Execute;
var
  AByte,AByte2,I : Byte;
  AString : String;
  procedure DoDisplay(S: String);
  begin
    FMessage := S;
    Synchronize(DisplayString);
  end;
begin
  If not terminated and FIndyClient.Connected then
    Begin
      DoDisplay('<BNET Thread Started>');
      Synchronize(ConnectBNLS);
      AByte := 1; // Protocol Byte
      FIndyClient.OpenWriteBuffer();
      FIndyClient.WriteBuffer(AByte,1,True);
      FIndyClient.FlushWriteBuffer();
      FIndyClient.OpenWriteBuffer();
      If Form1.tr2.FVersionByte = 0 then
        Begin
          DoDisplay('BNET Waiting: BNLS_REQUESTVERSIONBYTE');
          while Form1.IdTCPClient2.Connected and (Form1.tr2.FVersionByte = 0) do
            Windows.Sleep(0);
          DoDisplay('BNET Waiting: DONE!');
        End;
      If not Form1.IdTCPClient2.Connected then
        Begin
          DoDisplay('BNET Terminating: Requires BNLS connection.');
          Terminate;
          Exit;
        End;
      For I := $00 to $0F do
        FIndyClient.WriteBuffer(SID_AUTH_INFO_Data[I],1);
      AByte := Form1.tr2.FVersionByte;
      FIndyClient.WriteBuffer(AByte,1);
      For I := $11 to $39 do
        FIndyClient.WriteBuffer(SID_AUTH_INFO_Data[I],1);
      FIndyClient.FlushWriteBuffer();

      DoDisplay('Sent: SID_AUTH_INFO');
    End;

  while not Terminated and FIndyClient.Connected do
  try
    FIndyClient.ReadBuffer(FStartPacket,1);
    FIndyClient.ReadBuffer(FPacketID,1);
    FIndyClient.ReadBuffer(AByte,1);
    FIndyClient.ReadBuffer(FPacketLength,1);
    FPacketLength := (FPacketLength SHL 8) + AByte;
    FIndyClient.ReadBuffer(FPacketData,FPacketLength-4);
    Case FStartPacket of
      $FF :
        Case FPacketID of
          SID_NULL : DoDisplay('BNET Recv: SID_NULL');
          SID_CLIENTID : DoDisplay('BNET Recv: SID_CLIENTID');
          SID_STARTVERSIONING : DoDisplay('BNET Recv: SID_STARTVERSIONING');
          SID_REPORTVERSION : DoDisplay('BNET Recv: SID_REPORTVERSION');
          SID_GETADVLISTEX  : DoDisplay('BNET Recv: SID_GETADVLISTEX');
          SID_ENTERCHAT : DoDisplay('BNET Recv: SID_ENTERCHAT');
          SID_GETCHANNELLIST : DoDisplay('BNET Recv: SID_GETCHANNELLIST');
          SID_CHATEVENT : DoDisplay('BNET Recv: SID_CHATEVENT');
          SID_FLOODDETECTED : DoDisplay('BNET Recv: SID_FLOODDETECTED');
          SID_UDPPINGRESPONSE : DoDisplay('BNET Recv: SID_UDPPINGRESPONSE');
          SID_MESSAGEBOX : DoDisplay('BNET Recv: SID_MESSAGEBOX');
          SID_PING :
            Begin
              DoDisplay('BNET Recv: SID_PING');
              FIndyClient.OpenWriteBuffer();
              FIndyClient.WriteBuffer(FStartPacket,1);
              FIndyClient.WriteBuffer(FPacketID,1);
              AByte := FPacketLength;
              FIndyClient.WriteBuffer(AByte,1);
              AByte := 0;
              FIndyClient.WriteBuffer(AByte,1);
              FIndyClient.WriteBuffer(FPacketData,FPacketLength-4);
              FIndyClient.FlushWriteBuffer();
              DoDisplay('BNET Sent: SID_PING');
            End;
          SID_READUSERDATA : DoDisplay('BNET Recv: SID_READUSERDATA');
          SID_LOGONCHALLENGE : DoDisplay('BNET Recv: SID_LOGONCHALLENGE');
          SID_LOGONRESPONSE : DoDisplay('BNET Recv: SID_LOGONRESPONSE');
          SID_CREATEACCOUNT : DoDisplay('BNET Recv: SID_CREATEACCOUNT');
          SID_CHANGEPASSWORD : DoDisplay('BNET Recv: SID_CHANGEPASSWORD');
          SID_CDKEY2 : DoDisplay('BNET Recv: SID_CDKEY2');
          SID_CREATEACCOUNT2 : DoDisplay('BNET Recv: SID_CREATEACCOUNT2');
          SID_LOGONREALMEX : DoDisplay('BNET Recv: SID_LOGONREALMEX');
          SID_AUTH_INFO :
            Begin
              DoDisplay('BNET Recv: SID_AUTH_INFO');
              If not Form1.IdTCPClient2.Connected then
                Begin
                  DoDisplay('BNET: Cannot Continue further without BNLS.');
                  FIndyClient.Disconnect;
                  Terminate;
                  Exit;
                End;
              //---BNLS_CHOOSENLSREVISION---
              // FPacketData[01..00] = NLS revision
              with Form1.IdTCPClient2 do
                Begin
                  OpenWriteBuffer();
                  Abyte := 7; //Length header
                  WriteBuffer(AByte,1);
                  AByte := 0;
                  WriteBuffer(AByte,1);
                  AByte := BNLS_CHOOSENLSREVISION;

                  WriteBuffer(AByte,1);
                  WriteBuffer(FPacketData[0],2);
                  AByte := 0;
                  WriteBuffer(AByte,1);
                  WriteBuffer(AByte,1);
                  FlushWriteBuffer();
                  DoDisplay('BNLS Send: BNLS_CHOOSENLSREVISION');
                End;
              form1.tr2.FSessionKey[0] := FPacketData[8];
              form1.tr2.FSessionKey[1] := FPacketData[9];
              form1.tr2.FSessionKey[2] := FPacketData[10];
              form1.tr2.FSessionKey[0] := FPacketData[11];

              DoDisplay('BNET Info: Session Key = '
                +IntToHex(form1.tr2.FSessionKey[0],2)
                +IntToHex(form1.tr2.FSessionKey[1],2)
                +IntToHex(form1.tr2.FSessionKey[2],2)
                +IntToHex(form1.tr2.FSessionKey[3],2));

              AByte := $11;
              AString := '';
              AByte2 := 0;
              While FPacketData[AByte] <> 0 do
                Begin
                  AString := AString + Char(FPacketData[AByte]);
                  If Char(FPacketData[AByte]) = '.' then
                    AByte2 := StrToInt(Char(FPacketData[AByte-1]));
                  AByte := AByte + 1;
                End;
              DoDisplay('BNET Info: DLL Revision number = '+IntToStr(AByte2));
              AByte := AByte + 1;
              AString := '';
              While FPacketData[AByte] <> 0 do
                Begin
                  AString := AString + Char(FPacketData[AByte]);
                  AByte := AByte + 1;
                End;
              DoDisplay('BNET Info: Checksum = "'+AString+'"');
              //---BNLS_VERSIONCHECK---
              with Form1.IdTCPClient2 do
                Begin
                  OpenWriteBuffer();
                  AByte := 11; //header + id + ver
                  AByte := AByte + Length(AString) + 1;
                  WriteBuffer(AByte,1);
                  AByte := 0;
                  WriteBuffer(AByte,1);
                  AByte := BNLS_VERSIONCHECK;
                  WriteBuffer(AByte,1);
                  AByte := PRODUCT_WARCRAFT3;
                  WriteBuffer(AByte,1);
                  AByte := 0;
                  WriteBuffer(AByte,1);
                  WriteBuffer(AByte,1);
                  WriteBuffer(AByte,1);
                  WriteBuffer(AByte2,1);
                  WriteBuffer(AByte,1);
                  WriteBuffer(AByte,1);
                  WriteBuffer(AByte,1);
                  WriteBuffer(AString[1],Length(AString));
                  WriteBuffer(AByte,1);
                  FlushWriteBuffer();
                  DoDisplay('BNLS Send: BNLS_VERSIONCHECK');
                End;
            End;
          SID_AUTH_CHECK :
            Begin
              DoDisplay('BNET Recv: SID_AUTH_CHECK');
              If (FPacketData[0] = 0) and (FPacketData[1] = 0) then
                Begin
                  DoDisplay('BNET Info: Passed version and CD-key check.');
                End
                else
                Begin
                  DoDisplay('BNET Info: Did *NOT* pass authentification process.  Halting.  ('
                    +IntToHex(FPacketData[1],2)+IntToHex(FPacketData[0],2)+')');
                  FIndyClient.Disconnect;
                  Terminate;
                  Exit;
                End;
            End;
          SID_FRIENDLIST : DoDisplay('BNET Recv: SID_FRIENDLIST');
          SID_FRIENDUPDATE : DoDisplay('BNET Recv: SID_FRIENDUPDATE');
          SID_FRIENDADDED : DoDisplay('BNET Recv: SID_FRIENDADDED');
          SID_FRIENDREMOVED : DoDisplay('BNET Recv: SID_FRIENDREMOVED');
          SID_FRIENDMOVED : DoDisplay('BNET Recv: SID_FRIENDMOVED');
          SID_FINDCLANCANDIDATES : DoDisplay('BNET Recv: SID_FINDCLANCANDIDATES');
          SID_INVITEMULTIPLEUSERS : DoDisplay('BNET Recv: SID_INVITEMULTIPLEUSERS');
          SID_DISBANDCLAN : DoDisplay('BNET Recv: SID_DISBANDCLAN');
          SID_CLANINFO : DoDisplay('BNET Recv: SID_CLANINFO');
          SID_CLANREQUEST : DoDisplay('BNET Recv: SID_CLANREQUEST');
          SID_CLANINVITE : DoDisplay('BNET Recv: SID_CLANINVITE');
          SID_CLANMOTD : DoDisplay('BNET Recv: SID_CLANMOTD');
          SID_CLANMEMBERLIST : DoDisplay('BNET Recv: SID_CLANMEMBERLIST');
          SID_CLANMEMBERUPDATE : DoDisplay('BNET Recv: SID_CLANMEMBERUPDATE');
          SID_CLANPROMOTION : DoDisplay('BNET Recv: SID_CLANPROMOTION');
        end;
      Else
        Begin
          DoDisplay('BNET Recv: Unknown Packet $'+IntToHex(FPacketID,2));
        End;
    End;
  except on E: Exception do Form1.Memo1.Lines.Add('BNET Thread Error: '+E.Message);
  end;
  DoDisplay('<BNET Thread Ended>');
end;
ASKER CERTIFIED SOLUTION
Avatar of Jacco
Jacco
Flag of Netherlands image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Here is some explanation:

The thread is a state machine that does two things. When csDiconnected it tries to connect, when connected it first reads the buffer handles a message that if there is anything in the QueueOut it sends one of those.

Two things can happen to incoming messages from the server if there is a Handler (a published method named Process_XXXX where XXXX is the HeaderName) if found this handler is call with the info of the message, when no handler is found the message is added to QueueIn.

Now you can add a message to the QueueOut from outside this thread using the threadsafe method AddMessageOut. (SendMessageOut sends one message out, if an exception occurs the message is put back on the Queue). Both QueueIn and QueueOut are protected with a TCriticalSection to prevent resource conflicts.

  fReader.AddMessageOut(SID_PING, 0, 'ClientPing!');

So messages that are not automatically handled are on QueueIn. The main application can fetch one message from thing Queue in a threadsafe manner using GetMessageIn. (It can use a timer to fetch them)

  if fReader.InAvail then
    if fReader.GetMessageIn(liHeader, liSubHeader, lsData) then
     ...

As an example I implemented the automatic message PING. The handler responds by putting a Message on the QueueOut with priority (this means before any other messages). The main application should never use priority since it is meant for responding immediately to server requests.

Can you do something with this?

I have a sample client app here that uses the same FindHandler method for non-automatic messages.

Regards Jacco
Oh yeah. Exception handling still needs to be fine tuned in a way that the state-machine can never hang.

Illegaly formatted messages can make it hang because the data will never be complete.

Additional checks for disconnect still need to be added. (I didn't do so because it involves a lot of testing)

Regards Jacco
Looks cool.  I will see if I can implement it.  Main thing though, how do I keep it from hanging if it does not recieve a full packet?  I'd rather it timeout and disconnect and give me some kind of event to work with so that I could go thru a reconnect sequence again for example.

And what happens if the user clicks on the 'X' button?
>>Main thing though, how do I keep it from hanging if it does not recieve a full packet?

It will not hang on receiving part of a packet. It will just continue to go round sending/receiving until the packet is full. Only when there is an error in the packet length it will wait until all bytes are in (and this might be as much as 64K of legal packets).

>>And what happens if the user clicks on the 'X' button?

The application should then free the thread which in turn free the TIdTCPClient which in turn closes the connection with the server.

This loop:

function TReaderThread.Process: TClientState;
var
  lsMessage: string;
begin
  if fClient.Connected then
  begin
    // read messages
    fClient.ReadFromStack(False, 0, False);
    fReceived := fReceived + fClient.InputBuffer.Extract(fClient.InputBuffer.Size);
    if Length(fReceived) > 4 then
      ProcessReceived;
    // write a message
    SendMessageOut;
    Result := csConnected;
  end else
    Result := csDisconnected;
end;

Has virtually no wait in it.

    // read messages
    fClient.ReadFromStack(False, 0, False);
    fReceived := fReceived + fClient.InputBuffer.Extract(fClient.InputBuffer.Size);

The "0" in ReadFromStack means no timeout.

Um, been trying to implement this...

got a few problems though.

I can't use strings as the data packets can contain nulls.  I was thinking of using an FPacket instead.  I am only going to have the thread handle the packets so I will be modifying it so that it will ignore packets that don't have a "process_" method.  I think this would be safe enough to use a single FPacket data structure instead of passing it to the methods...

TBNET_Packet = record
  StartPacket : Byte; // always $ff
  PacketID : Byte;
  PacketLength : Word;
  PacketData : array[Word] of Byte;
end;

Another problem I am having is that I want to display some messages in TMemo's, guess that is what syncronize is for.  Well I am going to work on this to see what I can do.  I'll be accepting your answers and forwarding this question to another question topic with future questions if you are still willing to help and it will enable me to give you more points...
Continuing on to another question topic with more points to give...

https://www.experts-exchange.com/questions/21129083/Properly-Coded-Indy-Reader-Threads-Part-2.html