Solved

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

Posted on 2004-09-01
13
1,397 Views
Last Modified: 2010-10-05
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.  :)
0
Comment
Question by:werehamster-
  • 7
  • 6
13 Comments
 
LVL 10

Expert Comment

by:Jacco
ID: 11992157
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
0
 

Author Comment

by:werehamster-
ID: 12028960
>> >>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.
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12029435
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
0
 

Author Comment

by:werehamster-
ID: 12032521
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.  :)
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12033334
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

0
 

Author Comment

by:werehamster-
ID: 12033485
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;
0
What Is Threat Intelligence?

Threat intelligence is often discussed, but rarely understood. Starting with a precise definition, along with clear business goals, is essential.

 
LVL 10

Accepted Solution

by:
Jacco earned 500 total points
ID: 12033827
Here is a first draft of the ReaderThread:

unit oReadThread;

interface

uses
  Classes, SyncObjs, IdTCPClient;

type
  TClientState = (csDisconnected, csConnected);

  TMessageHandler = procedure(const aHeader, aSubHeader: Byte; const aData: string) of object;

  TReaderThread = class(TThread)
  private
    fClient: TIdTCPClient;
    fState: TClientState;
    fQueueIn: TStringList;
    fQueueOut: TStringList;
    fReceived: string;
    fQueueInSection: TCriticalSection;
    fQueueOutSection: TCriticalSection;
    fOutAvail: Boolean;
    fInAvail: Boolean;
    procedure ProcessReceived;
    function FindAutoHandler(const aHeader: Byte): TMessageHandler;
  public
    constructor Create(const aServerIP: string; const aServerPort: Integer);
    destructor Destroy; override;
    procedure Execute; override;
    function Connect: TClientState;
    function Process: TClientState;
    procedure AddMessageOut(const aHeader, aSubHeader: Byte; const aData: string; const aPriority: Boolean = False);
    procedure SendMessageOut;
    procedure AddMessageIn(const aHeader, aSubHeader: Byte; const aData: string);
    function GetMessageIn(var aHeader, aSubHeader: Byte; var aData: string): Boolean;
    property InAvail: Boolean read fInAvail;
  published
    procedure Process_PING(const aHeader, aSubHeader: Byte; const aData: string);
  end;

const
  SID_PING = 1;
  SID_CHAT = 2;

var
  HeaderNames: array[Byte] of string;

function CreateMessage(const aHeader, aSubHeader: Byte; const aData: string): string;

implementation

uses
  Windows;

{ TReaderThread }

function CreateMessage(const aHeader, aSubHeader: Byte; const aData: string): string;

  function MsgLen(const aMessage: string): string;
  var
    liLen: Word;
  begin
    liLen := Length(aMessage) + 2;
    Result := Char(liLen mod 256) + Char(liLen div 256);
  end;

begin
  Result := Char(aHeader) + Char(aSubHeader) + aData;
  Insert(MsgLen(Result), Result, 3);
end;

constructor TReaderThread.Create(const aServerIP: string; const aServerPort: Integer);
begin
  inherited Create(False);
  fClient := TIdTCPClient.Create(nil);
  fClient.Host := aServerIP;
  fClient.Port := aServerPort;
  fQueueIn := TStringList.Create;
  fQueueOut := TStringList.Create;
  fQueueInSection := TCriticalSection.Create;
  fQueueOutSection := TCriticalSection.Create;
end;

destructor TReaderThread.Destroy;
begin
  fClient.Free;
  fQueueIn.Free;
  fQueueOut.Free;
  fQueueInSection.Free;
  fQueueOutSection.Free;
  inherited Destroy;
end;

procedure TReaderThread.Execute;
begin
  while not Terminated do
  begin
    case fState of
      csDisconnected: fState := Connect;
      csConnected   : fState := Process;
    end;
    Sleep(10);
  end;
end;

function TReaderThread.Connect: TClientState;
begin
  try
    fClient.Connect(500);
  except
  end;
  if fClient.Connected then
    Result := csConnected
  else
    Result := csDisconnected;
end;

function TReaderThread.FindAutoHandler(const aHeader: Byte): TMessageHandler;
var
  lMethod: TMethod;
begin
  lMethod.Data := Self;
  lMethod.Code := MethodAddress('Process_' + HeaderNames[aHeader]);
  if Assigned(lMethod.Code) then
    Result := TMessageHandler(lMethod)
  else
    Result := nil;
end;

procedure TReaderThread.ProcessReceived;
var
  liDataLength: Word;
  liHeader, liSubHeader: Byte;
  lsData: string;
  lHandler: TMessageHandler;
begin
  liDataLength := Ord(fReceived[4]) shl 8 + Ord(fReceived[3]);
  if Length(fReceived) >= liDataLength then
  begin
    liHeader    := Ord(fReceived[1]);
    liSubHeader := Ord(fReceived[2]);
    lsData      := Copy(fReceived, 5, liDataLength-4);
    Delete(fReceived, 1, liDataLength);
    lHandler := FindAutoHandler(liHeader);
    if Assigned(lHandler) then
      lHandler(liHeader, liSubHeader, lsData)
    else
      AddMessageIn(liHeader, liSubHeader, lsData);
  end;
end;

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;

procedure TReaderThread.Process_PING(const aHeader, aSubHeader: Byte; const aData: string);
begin
  AddMessageOut(aHeader, aSubHeader, aData, True);
end;

procedure TReaderThread.AddMessageIn(const aHeader, aSubHeader: Byte; const aData: string);
begin
  fQueueInSection.Enter;
  try
    fQueueIn.Add(Chr(aHeader) + Chr(aSubHeader) + aData);
    fInAvail := True;
  finally
    fQueueInSection.Leave;
  end;
end;

function TReaderThread.GetMessageIn(var aHeader, aSubHeader: Byte; var aData: string): Boolean;
var
  lsMessage: string;
begin
  Result := False;
  if fInAvail then
  begin
    fQueueInSection.Enter;
    try
      lsMessage := fQueueIn[0];
      fQueueIn.Delete(0);
      fInAvail := fQueueIn.Count > 0;
    finally
      fQueueInSection.Leave;
    end;
    if Length(lsMessage) >= 2 then
    begin
      aHeader    := Ord(lsMessage[1]);
      aSubHeader := Ord(lsMessage[2]);
      aData      := Copy(lsMessage, 3, Length(lsMessage)-2);
      Result := True;
    end else
      Result := False;
  end;
end;

procedure TReaderThread.AddMessageOut(const aHeader, aSubHeader: Byte; const aData: string; const aPriority: Boolean = False);
begin
  fQueueOutSection.Enter;
  try
    if aPriority then
      fQueueOut.Insert(0, CreateMessage(aHeader, aSubHeader, aData))
    else
      fQueueOut.Add(CreateMessage(aHeader, aSubHeader, aData));
    fOutAvail := True;
  finally
    fQueueOutSection.Leave;
  end;
end;

procedure TReaderThread.SendMessageOut;
var
  lsMessage: string;
begin
  if fOutAvail then
  begin
    lsMessage := '';
    fQueueOutSection.Enter;
    try
      if fQueueOut.Count > 0 then
      begin
        lsMessage := fQueueOut[0];
        fQueueOut.Delete(0);
      end;
      fOutAvail := fQueueOut.Count > 0;
    finally
      fQueueOutSection.Leave;
    end;
    if lsMessage > '' then
    begin
      try
        fClient.Write(lsMessage);
      except
        // if an error occurs just put the message back
        fQueueOutSection.Enter;
        try
          fQueueOut.Insert(0, lsMessage);
          fOutAvail := True;
        finally
          fQueueOutSection.Leave;
        end;
      end;
    end;
  end;
end;

initialization
  HeaderNames[SID_PING] := 'PING';
  HeaderNames[SID_CHAT] := 'CHAT';
end.
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12033869
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
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12033872
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
0
 

Author Comment

by:werehamster-
ID: 12036929
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?
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12037382
>>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.

0
 

Author Comment

by:werehamster-
ID: 12045503
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...
0
 

Author Comment

by:werehamster-
ID: 12045739
Continuing on to another question topic with more points to give...

http://www.experts-exchange.com/Programming/Programming_Languages/Delphi/Q_21129083.html
0

Featured Post

Free Trending Threat Insights Every Day

Enhance your security with threat intelligence from the web. Get trending threat insights on hackers, exploits, and suspicious IP addresses delivered to your inbox with our free Cyber Daily.

Join & Write a Comment

Suggested Solutions

Title # Comments Views Activity
System restore point 4 77
How to fill array with TArray.Create? 14 68
Downloading email attachments 2 54
Delphi inherited method 6 36
Introduction The parallel port is a very commonly known port, it was widely used to connect a printer to the PC, if you look at the back of your computer, for those who don't have newer computers, there will be a port with 25 pins and a small print…
Introduction Raise your hands if you were as upset with FireMonkey as I was when I discovered that there was no TListview.  I use TListView in almost all of my applications I've written, and I was not going to compromise by resorting to TStringGrid…
Excel styles will make formatting consistent and let you apply and change formatting faster. In this tutorial, you'll learn how to use Excel's built-in styles, how to modify styles, and how to create your own. You'll also learn how to use your custo…
This video explains how to create simple products associated to Magento configurable product and offers fast way of their generation with Store Manager for Magento tool.

707 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

12 Experts available now in Live!

Get 1:1 Help Now