Solved

Properly Coded Indy Reader Threads - Part 2

Posted on 2004-09-13
17
689 Views
Last Modified: 2010-04-05
This is basically a continuation of a privious question...
http://www.experts-exchange.com/Programming/Programming_Languages/Delphi/Q_21116285.html

I've got a decent system of reading the packets and using methods of handling the data, but now I need a good way of handling disconnects from the server end. (abnormal or whatever)

I cannot modify the server in anyway, but every packet begins with...

Record
  StartPacket : Byte; //always $FF
  PacketID : Byte;  //function
  PacketLength : Word; // including header
  PacketData : array[PacketLength-4] of byte

I am now, with good suggestions for jacco, using a consts file that contains all the packet ID's and a variable that holds the names of methods that can be searched searched for and run...

const
  SID_NULL = $00;
  SID_CLIENTID = $05;
[...]
published
[...]
  procedure Process_SID_NULL;
  procedure Process_SID_CLIENTID;
[...]

var
  BNET_HeaderNames: array[Byte] of string;

initialization
  BNET_HeaderNames[SID_NULL] := 'SID_NULL';
  BNET_HeaderNames[SID_CLIENTID] := 'SID_CLIENTID';

It currently has threadsafe methods of writing packets from outside the thread.  I am basically looking for more ideas and suggestions.  

What I really need is a good way to terminate the connection.  Should I just .free the reader thread and indy client or is there a better way?

PS, I don't like just links to commonly known sites.  I like examples and detailed suggestions.

I will be awarding points appropriately.  Some points are owed to a contributor in "Part 1", but I will still give to others who contribute.  Soon as there is enough useful information in this topic, I will make a "Part 3" and continue on from there.
0
Comment
Question by:werehamster-
  • 11
  • 6
17 Comments
 
LVL 10

Accepted Solution

by:
Jacco earned 500 total points
ID: 12051615
Hi there,

First of all thanks for the points!

Now from your last comment in your previous question:

>>I can't use strings as the data packets can contain nulls.

String variables can contain nulls (#0 characters as well) the only thing you have to take care of is when using string functions that expect a PChar with these strings. Now I used strings so I could just append the ip-buffer to it and then check if a complete message is in it by validating its length with DataLength. There is no wait state involved then. If you do a Read(DataLength, 2) the thread will wait infinitely for those two bytes even if there are lots of outgoing messages waiting on the queue. Further more strings are automatically allocated/deallocated (and reference counted, and copied on writing) and the fixed size record you propose is always 64KB big (but you use only one so that OK).

What we could do is put the receivced string with a complete record into FPacket. That would look like this:

procedure TReaderThread.ProcessReceived;
var
  liDataLength, liDiscard: Word;
  lHandler: TMessageHandler;
begin
  liDiscard := 1;
  while (liDiscard <= Length(fReceived)) and (fReceived[liDiscard] <> #255) do
    Inc(liDiscard);
  if liDiscard > 1 then
    Delete(fReceived, 1, liDiscard-1);
  if Length(fReceived) >= 4 then
  begin
    liDataLength := Ord(fReceived[4]) shl 8 + Ord(fReceived[3]);
    if Length(fReceived) >= liDataLength then
    begin
      FillChar(fPacket.PacketData, 65536, 0);
      fPacket.PacketLength := liDataLength;
      fPacket.StartPacket  := Ord(fReceived[1]);
      fPacket.PacketID     := Ord(fReceived[2]);
      Move(fReceived[5], fPacket.PacketData[0], liDataLength-4);
      Delete(fReceived, 1, liDataLength);
      lHandler := FindAutoHandler(fPacket.PacketID);
      if Assigned(lHandler) then
        lHandler;
    end;
  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);
    ProcessReceived;
    // write a message
    SendMessageOut;
    Result := csConnected;
  end else
    Result := csDisconnected;
end;

I also added some stability by discarding anything that doesn't start with $FF. This way when illegal messages are received the ReaderThread will automatically resync.

>>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...

>>Another problem I am having is that I want to display some messages in TMemo's, guess that is what syncronize is for.

If the packet contains information that need some handling (displaying, updating lists) in the client you can use Synchronize and then read the information fro FPacket in the thread. The action taken by the main thread will have to be swift because it will block any further reading/writing by the thread during the action. (That is why I used the QueueIn). Another thing with synchronize is that it will only be handled when the main thread is idle.

Synchronize works like this:
- it puts a message on the main threads message queue (this message contains the method to be called)
- the thread starts waiting for this message to be handled
- the main thread handles messages and idles after each message
- when the synchronize message come it calls the method in the message and idles again
- then the thread will continue its rounds

I always try to never mix thread things with UI things, and I always stay away from Synchronize. If, for example, you do a ShowMessage in the synchonized method the thread can be blocked for a few minutes and not respond to server PINGs. That is why I implemented a timer and let the UI grab anything for it.

Here is a samply of how to use synchronize:

procedure TReaderThread.Process_CHAT;
begin
  Synchronize(Form1.Display);
end;

procedure TForm1.Display;
var
  lsMessage: string;
begin
  SetLength(lsMessage, fReader.Packet.PacketLength-4);
  Move(fReader.Packet.PacketData[0], lsMessage[1], fReader.Packet.PacketLength-4);
  Form1.Memo1.Lines.Add(lsMessage);
end;

And here is some extra stability for the state machine:

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

Let me know what you can use of this info.

Regards Jacco

0
 
LVL 10

Expert Comment

by:Jacco
ID: 12051631
Now terminating the thread would normally be just:

fReader.Terminate and then fReader.Free. The Terminate would brake from the while loop in Execute this is done after one buffer read and one message write (if available).

But as you can see from the destructor of TThread Terminate is also called automatically:

destructor TThread.Destroy;
begin
  if (FThreadID <> 0) and not FFinished then
  begin
    Terminate;
    if FCreateSuspended then
      Resume;
    WaitFor;
  end;
  if FHandle <> 0 then CloseHandle(FHandle);
  inherited Destroy;
  FFatalException.Free;
  RemoveThread;
end;

Now one pitfall is when you use fClient.Read(....) because there the thread will wait for IP input infinitly. That is why I used ReadLn with a timeout in the past and this time I found an even better way of reading using the ReadFromStack/Extract where there is no wait.

Regards Jacco
0
 

Author Comment

by:werehamster-
ID: 12077330
Ok.  So what if I want to disconnect from the client side manually instead of waiting for the server to disconnect?  Do I just use MyReaderThread.free?
0
 

Author Comment

by:werehamster-
ID: 12077546
>> String variables can contain nulls (#0 characters as well) the only thing you have to take care of is when using string functions that expect a PChar with these strings

I don't get what you mean.  Do you mean function that convert strings to PChar?

Anyway, do I have to change any of the sample code you origonally gave me at all?

This is what I have re-written so far...

unit BotFrm;

interface

uses
  SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs, IdTCPClient, SyncObjs,
  QStdCtrls, QExtCtrls, QMenus, QTypes, QComCtrls, MainFrm, inifiles, PacketConsts;

type
  TBNETReader = class;

  TBotForm = class(TForm)
    OutputMemo: TMemo;
    ListBox1: TListBox;
    InputMemo: TMemo;
    Splitter1: TSplitter;
    fReader : TBNETReader;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  public
    INI : TIniFile;


  end;

  TClientState = (csDisconnected, csConnected);
  TMessageHandler = procedure of object;

  TBNETReader = class(TThread)
    fClient: TIdTCPClient;
    fForm : TBotForm;
    fState: TClientState;
    fInPacket : TBNETPacket;
    fOutPacket : TBNETPacket;
    fQueueOutSection: TCriticalSection;
    fReadPos : Word;
    constructor Create(AForm : TBotForm);
    destructor Destroy; override;
    procedure Execute; override;
    procedure DoMessage;
    procedure DoCaption;
  published
    //procedure Process_SID_PING;
    procedure Process_SID_OTHER;
  end;

implementation

{$R *.xfm}

constructor TBNETReader.Create(AForm : TBotForm);
begin
  inherited Create(False);
  fClient := TIdTCPClient.Create(nil);
  fForm := AForm;
  fReadPos := 0;
  fQueueOutSection := TCriticalSection.Create;
  synchronize(DoCaption);
end;

destructor TBNETReader.Destroy;
begin
  fClient.Free;
  fQueueOutSection.Free;
  inherited Destroy;
end;

procedure TBNETReader.Execute;
begin
  while not Terminated do
  begin
//    fClient.ReadFromStack(False,0,False);
    Sleep(10);
  end;
end;

procedure TBNETReader.DoMessage;
begin
end;

procedure TBNETREader.DoCaption;
const
  cConnected : array[Boolean] of string = ('Not Connected','Connected');
begin
  FForm.Caption := FForm.INI.ReadString('Main','Uername','Werebot')
    +' - '+cConnected[fClient.Connected];

end;

procedure TBNETReader.Process_SID_OTHER;
begin
end;

procedure TBotForm.FormCreate(Sender: TObject);
begin
  If not assigned(INI) then
    INI := TIniFile.Create(ExtractFilePath(Application.ExeName)+'config.ini');
  If INI.ReadString('Main','ConnectOnStartup','N') = 'Y' then
    Begin
      OutputMemo.Lines.Add('Connecting...');
    End;
end;

procedure TBotForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
end;

end.

----------------------------------------------------------------

unit MainFrm;

interface

uses
  SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,
  QExtCtrls, QComCtrls, QTypes, QMenus, QStdCtrls, QImgList, QActnList,
  QStdActns,inifiles;

type
  TMainForm = class(TForm)
    StatusBar1: TStatusBar;
    MainMenu: TMainMenu;
    SaveDialog: TSaveDialog;
    OpenDialog: TOpenDialog;
    ActionList: TActionList;
    actnWinClose: TWindowClose;
    actnWinCascade: TWindowCascade;
    actnWinTile: TWindowTile;
    actnWinMinimizeAll: TWindowMinimizeAll;
    actnExit: TAction;
    actnConnect: TAction;
    ImageList: TImageList;
    Connect1: TMenuItem;
    Timer1: TTimer;
    procedure actnConnectExecute(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  public
    INI : TIniFile;

    procedure CreateMDIChild(const Name: string);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.xfm}

uses
   BotFrm;

resourcestring
  sUntitled = 'Untitled';

procedure TMainForm.CreateMDIChild(const Name: string);
var
  Child: TBotForm;
begin
  { create a new MDI child window }
  Child := TBotForm.Create(Application);
  Child.Caption := Name;
  Child.INI := TIniFile.Create(Name)
end;


procedure TMainForm.actnConnectExecute(Sender: TObject);
var
  ABotForm : TBotForm;
begin
  if not Assigned(ActiveMDIChild) then
    Begin
      ABotForm := TBotForm.Create(Self);
      ABotForm.WindowState := wsMaximized;
      ABotForm.INI := TIniFile.Create(ExtractFilePath(Application.ExeName)+'config.ini');
      ABotForm.Caption := ABotForm.INI.ReadString('Main','Username','Werebot');
    End
  else if ActiveMDIChild is TBotForm then
    Begin
      TBotForm(ActiveMDIChild).OutPutMemo.Lines.Add('Connect Click')
    End;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Ini     := TIniFile.Create( ExtractFilePath(Application.ExeName)+'config.ini' );

  Top     := Ini.ReadInteger( 'Position', 'Top',    50  );
  Left    := Ini.ReadInteger( 'Position', 'Left',   100 );
  Height  := Ini.ReadInteger( 'Position', 'Height', 400 );
  Width   := Ini.ReadInteger( 'Position', 'Width',  540 );

  Timer1.Interval := 100;
  Timer1.Enabled := True;

end;


procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ini.WriteInteger('Position','Top',Top);
  ini.WriteInteger('Position','Left',Left);
  ini.WriteInteger('Position','Height',Height);
  ini.WriteInteger('Position','Width',Width);
  ini.Free;
end;


procedure TMainForm.Timer1Timer(Sender: TObject);
begin
  If Ini.ReadString('Main','ConnectOnStartup','N') = 'Y' then
    Begin
      MainForm.actnConnectExecute(Self);
    End;
  Timer1.Enabled := False;
end;

end.

---------------------------------------------------

unit PacketConsts;

interface

const
  SID_NULL                             = $00;
  SID_CLIENTID                         = $05;
  SID_STARTVERSIONING           = $06;
  SID_REPORTVERSION                 = $07;
  SID_GETADVLISTEX                 = $09;
  SID_ENTERCHAT                         = $0A;
  SID_GETCHANNELLIST           = $0B;
  SID_JOINCHANNEL                   = $0C;
  SID_CHATCOMMAND                   = $0E;
  SID_CHATEVENT                         = $0F;
  SID_FLOODDETECTED                 = $13;
  SID_UDPPINGRESPONSE           = $14;
  SID_MESSAGEBOX                   = $19;
  SID_PING                             = $25;
  SID_READUSERDATA                 = $26;
  SID_WRITEUSERDATA                 = $27;
  SID_LOGONCHALLENGE           = $28;
  SID_LOGONRESPONSE                 = $29;
  SID_CREATEACCOUNT                 = $2A;
  SID_CHANGEPASSWORD           = $31;
  SID_CDKEY2                       = $36;
  SID_LOGONRESPONSE2           = $3A;
  SID_CREATEACCOUNT2           = $3D;
  SID_LOGONREALMEX                 = $3E;
  SID_AUTH_INFO                         = $50;
  SID_AUTH_CHECK                   = $51;
  SID_FRIENDLIST               = $65;
  SID_FRIENDUPDATE                 = $66;
  SID_FRIENDADDED                   = $67;
  SID_FRIENDREMOVED                 = $68;
  SID_FRIENDMOVED                   = $69;
  SID_FINDCLANCANDIDATES   = $70;
  SID_INVITEMULTIPLEUSERS  = $71;
  SID_DISBANDCLAN                   = $73;
  SID_CLANINFO                         = $75;
  SID_CLANREQUEST                   = $77;
  SID_CLANINVITE                   = $79;
  SID_CLANMOTD                         = $7C;
  SID_CLANMEMBERLIST           = $7D;
  SID_CLANMEMBERUPDATE         = $7F;
  SID_CLANPROMOTION                 = $81;
var
  SID_HeaderNames: array[Byte] of string;

const
  MCP_STARTUP                           = $01;
  MCP_CHARCREATE                   = $02;
  MCP_CHARLOGON                         = $07;
  MCP_CHARDELETE                   = $0A;
  MCP_MOTD                             = $12;
  MCP_CHARLIST                         = $17;
  MCP_CHARUPGRADE                   = $18;

const
  PACKET_IDLE                           = $00;
  PACKET_LOGON                         = $01;
  PACKET_STATSUPDATE               = $02;
  PACKET_DATABASE                   = $03;
  PACKET_MESSAGE                   = $04;
  PACKET_CYCLE                         = $05;
  PACKET_USERINFO                   = $06;
  PACKET_USERLOGGINGOFF             = $07;
  PACKET_COMMAND                   = $08;
  PACKET_CHANGEDBPASSWORD       = $09;
  PACKET_BOTNETVERSION             = $0A;
  PACKET_BOTNETCHAT                 = $0B;
  PACKET_ACCOUNT                   = $0D;

const
  PKT_SERVERPING                    = $05;
  PKT_KEEPALIVE                         = $07;
  PKT_CONNTEST2                         = $09;


const
  EID_SHOWUSER             = $01;
  EID_JOIN                 = $02;
  EID_LEAVE                = $03;
  EID_WHISPER              = $04;
  EID_TALK                 = $05;
  EID_BROADCAST            = $06;
  EID_CHANNEL              = $07;
  EID_USERFLAGS                    = $09;
  EID_WHISPERSENT          = $0A;
  EID_CHANNELFULL          = $0D;
  EID_CHANNELDOESNOTEXIST  = $0E;
  EID_ERROR                = $13;
  EID_INFO                 = $12;
  EID_EMOTE                = $17;
  MAX_EVENT                                 = $18;

const
  BNLS_NULL                             = $00;
  BNLS_CDKEY                           = $01;
  BNLS_LOGONCHALLENGE               = $02;
  BNLS_LOGONPROOF                   = $03;
  BNLS_CREATEACCOUNT               = $04;
  BNLS_CHANGECHALLENGE             = $05;
  BNLS_CHANGEPROOF                 = $06;
  BNLS_UPGRADECHALLENGE             = $07;
  BNLS_UPGRADEPROOF                 = $08;
  BNLS_VERSIONCHECK                 = $09;
  BNLS_CONFIRMLOGON                 = $0A;
  BNLS_HASHDATA                         = $0B;
  BNLS_CDKEY_EX                         = $0C;
  BNLS_CHOOSENLSREVISION       = $0D;
  BNLS_AUTHORIZE                   = $0E;
  BNLS_AUTHORIZEPROOF               = $0F;
  BNLS_REQUESTVERSIONBYTE       = $10;
  BNLS_VERIFYSERVER                 = $11;
  BNLS_RESERVESERVERSLOTS       = $12;
  BNLS_SERVERLOGONCHALLENGE= $13;
  BNLS_SERVERLOGONPROOF             = $14;

const
  PRODUCT_STARCRAFT        = $01;
  PRODUCT_BROODWAR         = $02;
  PRODUCT_WAR2BNE          = $03;
  PRODUCT_DIABLO2          = $04;
  PRODUCT_LORDOFDESTRUCTION= $05;
  PRODUCT_JAPANSTARCRAFT   = $06;
  PRODUCT_WARCRAFT3        = $07;
  PRODUCT_THEFROZENTHRONE  = $08;

const
  SID_AUTH_INFO_Data : array[$00..$39] of byte = (
$FF,$50,$3A,$00,$00,$00,$00,$00,$36,$38,$58,$49,$50,$58,$33,$57, // .P:.....68XIPX3W
$10,$00,$00,$00,$53,$55,$6E,$65,$18,$BA,$00,$A6,$F0,$00,$00,$00, // ....SUne........
$09,$04,$00,$00,$09,$04,$00,$00,$55,$53,$41,$00,$55,$6E,$69,$74, // ........USA.Unit
$65,$64,$20,$53,$74,$61,$74,$65,$73,$00                          // ed States.
);

type
  TByteArray = array[Word] of Byte;
  TBNETPacket = Record
    StartPacket  : Byte; // Always $FF
    PacketID     : Byte;
    PacketLength : Word; // Including Header
    PacketData   : TByteArray;
  End;


implementation

initialization
  SID_HeaderNames[SID_NULL]                := 'SID_NULL';
  SID_HeaderNames[SID_CLIENTID]            := 'SID_CLIENTID';
  SID_HeaderNames[SID_STARTVERSIONING]     := 'SID_STARTVERSIONING';
  SID_HeaderNames[SID_REPORTVERSION]       := 'SID_REPORTVERSION';
  SID_HeaderNames[SID_GETADVLISTEX]        := 'SID_GETADVLISTEX';
  SID_HeaderNames[SID_ENTERCHAT]           := 'SID_ENTERCHAT';
  SID_HeaderNames[SID_GETCHANNELLIST]      := 'SID_GETCHANNELLIST';
  SID_HeaderNames[SID_JOINCHANNEL]         := 'SID_JOINCHANNEL';
  SID_HeaderNames[SID_CHATCOMMAND]         := 'SID_CHATCOMMAND';
  SID_HeaderNames[SID_CHATEVENT]           := 'SID_CHATEVENT';
  SID_HeaderNames[SID_FLOODDETECTED]       := 'SID_FLOODDETECTED';
  SID_HeaderNames[SID_UDPPINGRESPONSE]     := 'SID_UDPPINGRESPONSE';
  SID_HeaderNames[SID_MESSAGEBOX]          := 'SID_MESSAGEBOX';
  SID_HeaderNames[SID_PING]                := 'SID_PING';
  SID_HeaderNames[SID_READUSERDATA]        := 'SID_READUSERDATA';
  SID_HeaderNames[SID_WRITEUSERDATA]       := 'SID_WRITEUSERDATA';
  SID_HeaderNames[SID_LOGONCHALLENGE]      := 'SID_LOGONCHALLENGE';
  SID_HeaderNames[SID_LOGONRESPONSE]       := 'SID_LOGONRESPONSE';
  SID_HeaderNames[SID_CREATEACCOUNT]       := 'SID_CREATEACCOUNT';
  SID_HeaderNames[SID_CHANGEPASSWORD]      := 'SID_CHANGEPASSWORD';
  SID_HeaderNames[SID_CDKEY2]              := 'SID_CDKEY2';
  SID_HeaderNames[SID_LOGONRESPONSE2]      := 'SID_LOGONRESPONSE2';
  SID_HeaderNames[SID_CREATEACCOUNT2]      := 'SID_CREATEACCOUNT2';
  SID_HeaderNames[SID_LOGONREALMEX]        := 'SID_LOGONREALMEX';
  SID_HeaderNames[SID_AUTH_INFO]           := 'SID_AUTH_INFO';
  SID_HeaderNames[SID_AUTH_CHECK]          := 'SID_AUTH_CHECK';
  SID_HeaderNames[SID_FRIENDLIST]          := 'SID_FRIENDLIST';
  SID_HeaderNames[SID_FRIENDUPDATE]        := 'SID_FRIENDUPDATE';
  SID_HeaderNames[SID_FRIENDADDED]         := 'SID_FRIENDADDED';
  SID_HeaderNames[SID_FRIENDREMOVED]       := 'SID_FRIENDREMOVED';
  SID_HeaderNames[SID_FRIENDMOVED]         := 'SID_FRIENDMOVED';
  SID_HeaderNames[SID_FINDCLANCANDIDATES]  := 'SID_FINDCLANCANDIDATES';
  SID_HeaderNames[SID_INVITEMULTIPLEUSERS] := 'SID_INVITEMULTIPLEUSERS';
  SID_HeaderNames[SID_DISBANDCLAN]         := 'SID_DISBANDCLAN';
  SID_HeaderNames[SID_CLANINFO]            := 'SID_CLANINFO';
  SID_HeaderNames[SID_CLANREQUEST]         := 'SID_CLANREQUEST';
  SID_HeaderNames[SID_CLANINVITE]          := 'SID_CLANINVITE';
  SID_HeaderNames[SID_CLANMOTD]            := 'SID_CLANMOTD';
  SID_HeaderNames[SID_CLANMEMBERLIST]      := 'SID_CLANMEMBERLIST';
  SID_HeaderNames[SID_CLANMEMBERUPDATE]    := 'SID_CLANMEMBERUPDATE';
  SID_HeaderNames[SID_CLANPROMOTION]       := 'SID_CLANPROMOTION';
end.

------------------------------------------------------
You can download my project folder via...
www.datazap.net/ftp/werehamster/MyBot/MyBot.zip

also contains 2 semi-functional prototypes called project1 and project2.  current one is werebot.
0
 

Author Comment

by:werehamster-
ID: 12078083
Anyway, I don't have a need for any other process to read the packets outside the thread so I took out the critical section for the reading part.

Basically what I want to do is that if I don't have a process for it, to call process_other which will display a packet dump of the data to the screen.  I have some code to this already (though a little buggy).

Unhandled Packet: Header = 0000, Length = 0020, Name = Unknown
0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Would put Name = SID_Something if there is a const for it, but no process handler.  Anyway, no real question involved here, just explaining my next goal so you have a better understanding what I am doing.

Heres a couple of questions...

If I call each process_xxx, do I have to pass parameters for any particular reason?  I figured I would just use the fData and fDataLength and stuff instead.  Calling these process procedures holds up the rest of the thread anyway right?

Also, I've been messing with something in the past unsuccessfully.  Can I use something like a TMemoryStream on memory that has already been allocated without writing my own TCustomStream?  I suppose I could make fData a TMemoryStream itlsef and work with it from there, but would it be safe to treat String(fData.memory) as a string?

Actually, if there was a component that allowed using an existing pointer/string, to be allowed to do things like ReadInt, ReadByte, ReadBoolean, etc, it would be extremely useful for this.  I've written my own one in the past for a customized TCP client I made about 5 years ago that I could dig up I guess if I can find it.  Any thoughts on this?  Basically in each process_XXX I'd like to ideally just do something like this...

procedure TBNETReader.Process_SID_XXX
var
  X : Integer;
  B : Byte;
begin

//X := fInPacket.ReadInteger;
//B := fInPacket.ReadByte;
X := ReadInteger;
B := ReadByte;
//fOutPacket.Header := SID_Something
//fOutPacket.Data.WriteByte(B+1)
//fOutPacket.Send;
PacketStart(SID_Something)
WriteByte(B+1);
PacketSend;
end;

Which could be simplified via WriteByte(ReadByte+1) if I wanted to.

Any suggestions?  I looked up and found TReader and TWriter and stuff, but looks too confusing for me.
0
 

Author Comment

by:werehamster-
ID: 12078700
Here is a link to a hex dump question I put up asking for source incase you have any.

http://www.experts-exchange.com/Programming/Programming_Languages/Delphi/Q_21134147.html
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12081706
>>I don't get what you mean.  Do you mean function that convert strings to PChar?

Yep, they exist. Here is a sample:

function AnsiPos(const Substr, S: string): Integer;
var
  P: PChar;
begin
  Result := 0;
  P := AnsiStrPos(PChar(S), PChar(SubStr));
  if P <> nil then
    Result := Integer(P) - Integer(PChar(S)) + 1;
end;

AnsiPos is also used by:

function StringReplace(const S, OldPattern, NewPattern: string; Flags: TReplaceFlags): string;

Which I use a lot. That why I said you have to take care :)

>>Ok.  So what if I want to disconnect from the client side manually instead of waiting for the server to disconnect?  Do I just use MyReaderThread.free?

You could. Free while call Terminate (set fTerminate to True) Execute will break from the while loop (in a short time if you use the ReadFromStack) and if you want you can that call fClient.Disconnect before exiting the Execute method (would be clean).

The thing is, this scheme will not work if you wait in any of the ReadXXX functions (except ReadFromStack).

>>If I call each process_xxx, do I have to pass parameters for any particular reason?  I figured I would just use the fData and fDataLength and stuff instead.  Calling these process procedures holds up the rest of the thread anyway right?

If you use private packet members you do not need to pass anything. And yep, calling the procedures hold up the thread.

>>Also, I've been messing with something in the past unsuccessfully.  Can I use something like a TMemoryStream on memory that has already been allocated without writing my own TCustomStream?  I suppose I could make fData a TMemoryStream itlsef and work with it from there, but would it be safe to treat String(fData.memory) as a string?

What should work is the following:

Add this empty decalration:

type
  TAllocatedMemoryStream = class(TMemoryStream);

With it you can call protected functions so in you code you can create and use an ordinary TMemoryStream and assign it you memory just by using a typecast:

...
  TAllocatedMemoryStream(MS).SetPointer(@fData[0], 65536);
...

I am not sure about the [0] maybe leave it away. And you have to make sure you don't call Write, change the Size or anything, just read.

I still think it would be easier to have fData as a string, you could use is as a stream as simple as a call to TStringStream.Create(fData).

>>Actually, if there was a component that allowed using an existing pointer/string, to be allowed to do things like ReadInt, ReadByte, ReadBoolean, etc, it would be extremely useful for this.  I've written my own one in the past for a customized TCP client I made about 5 years ago that I could dig up I guess if I can find it.  Any thoughts on this?  Basically in each process_XXX I'd like to ideally just do something like this...

When I can determine the format myself I always use TReader/TWriter from the classes unit for that purpose. You can hook them to a stream and read all kinds of data types from it.
 
You can easily make a descendant of TStringStream that supports reading Integers/Bytes from it:

type
  TMyStringStream = class(TStringStream)
  public
    function ReadByte: Byte;
  end;

function TMyStringStream.ReadByte: Byte;
begin
  Read(@Result, 1);
end;

I have downloaded the project, can look at it tonight.

Regards Jacco
0
 

Author Comment

by:werehamster-
ID: 12081896
I suppose I could just use it as a string.  Just going to require a little reconditioning on my part.  I'm still in the mentality of getting the length of a string from checking astring[0] and stuff from back in turbo pascal.  Never really developed my string manipulating skills, but now isn't the worst time to force myself into it.

Actually, what I am thinking of is making a couple of helper functions that do something like...

function getByteAt(var String; Pos: Word) : Byte;
function getWordAt(var String; Pos: Word) : Word;

...and of course checking the string length and stuff so it isn't out of bounds and stuff.
0
Do You Know the 4 Main Threat Actor Types?

Do you know the main threat actor types? Most attackers fall into one of four categories, each with their own favored tactics, techniques, and procedures.

 

Author Comment

by:werehamster-
ID: 12081915
As far as the packet dump thing, I got that pretty much figured out with the help of a couple other fellows.  Nothing I coudn't do on my own, just like to see how others do things and very frequently find better ways of doing things.  The second guy gave me the idea of converting to a string instead of directly to a memo file which would let me reused the doMessage procedure.  Anyway, enough with that.  :)
0
 

Author Comment

by:werehamster-
ID: 12082964
Anyway, I did some work and got something functional now...

www.datazap.net/ftp/werehamster/MyBot/MyBot2.zip

I just can't get my disconnect button working without getting errors.
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12084517
Reverse the destructor order:

destructor TBNETReader.Destroy;
begin
  inherited Destroy;
  fClient.Free;
  fQueueOutSection.Free;
  fQueueOut.Free;
end;

This way the thread will first be terminated and then you resources will be freed.

I have no server so for me it stuck at the Connect with does much longer that 500 milliseconds...

I have IdSocketError added in Tools\Debugger Options\Language Exceptions to ignore it. This measure might be to extreme for you but Indy developers indicate in the sources to at least add EIdSilentException to the ignored ones.

Regards Jacco
0
 

Author Comment

by:werehamster-
ID: 12086193
well if you have config.ini in the folder, you have a server to connect to.  It is a server that is up now.  Just have SID_PING coded right now.  I'll give it a shot when I get home.
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12090650
I have tested you app here and I never get an error (even without my change).

I changed a one small thing:

procedure TMainForm.actnConnectExecute(Sender: TObject);
var
  ABotForm : TBotForm;
begin
  if not Assigned(ActiveMDIChild) then Begin
    ABotForm := TBotForm.Create(Self);
    ABotForm.WindowState := wsMaximized;
    ABotForm.INI := TIniFile.Create(ExtractFilePath(Application.ExeName)+'config.ini');
    ABotForm.Caption := ABotForm.INI.ReadString('Main','Username','Werebot');
    ABotForm.fReader := TBNETReader.Create(TBotForm(ActiveMDIChild));
  End
  else if ActiveMDIChild is TBotForm then
    if not Assigned(TBotForm(ActiveMDIChild).fReader) then begin
      TBotForm(ActiveMDIChild).fReader := TBNETReader.Create(TBotForm(ActiveMDIChild));
      MainForm.actnConnect.Caption := 'Disconnect';
    end
    else begin
      FreeAndNil(TBotForm(ActiveMDIChild).fReader);
      MainForm.actnConnect.Caption := 'Connect';
    end;
end;

FreeAndNil the thread in stead of terminating. This action is in balance the construction fo a thread when connecting.

Regards Jacco
0
 

Author Comment

by:werehamster-
ID: 12090798
www.datazap.net/ftp/werehamster/MyBot/MyBot3.zip

has been expanded to include a 2nd thread to a second server that has a different header.  I used the same techniques.

Only thing is this, how do I stop the thing from contantly trying to reconnect a million times if the server refuses the connection (temp IP ban) ?

If your curious what I am doing, goto...
http://bnetdocs.valhallalegends.com/

Anyway, I am going to close this topic soon and give ya these points and start a new topic again.  

So far things seem to work fine, but like to implement a reconnection timer or somehow informing the main form that one of the clients disconnected.

Also, I need to slow down the outgoing packets some how so that it transmits packets at a delay between them.  The server I am connecting to has flood control that IP bans the client if it detects spam.  Maybe some way of guaging the chars per second for the last 10 seconds and adjust the delay between packets accordingly or something.  Is this something I would use a timer for and have the timer insert a string into the queue on an interval?

Anyway, I'm just babbling.  It's 4am right now and I should be in bed.  I always lose track of time when I am writing a program.  Small flaw in me I guess.

Thanks for the help so far.
-Steve
0
 
LVL 10

Expert Comment

by:Jacco
ID: 12091007
Maybe you can see in the Exception that occurs in the connect what the reason is. The code in the EIdSocketError might be of interest.

In the exception handler of the connect part you could check for a code in the exception:

on E: EIdSocketError do
begin
  case LastError of
    10041: ...
    else raise;
  end;
end;

You could for example add an extra state to the state machine csConnectionTimeOut which just sleeps a timeout and goes to csDisconnected again.

>>Also, I need to slow down the outgoing packets some how so that it transmits packets at a delay between them.

Maybe you can use one of Indy IOHandlers which does a speed limit: TIdIOHandlerThrottle. You can limit the speed to a number of bytes/bits per second for a connection. You could create one in your thread. I haven't experimented with them but I think you can just connect is to the TIdTCPClient.IOHandler before opening it.

>>Anyway, I am going to close this topic soon and give ya these points and start a new topic again.  

Thanks! I think this one will push me over the 100.000 points overall. Do they still give out T-Shirts?

>>Anyway, I'm just babbling.  It's 4am right now and I should be in bed.  I always lose track of time when I am writing a program.  Small flaw in me I guess.

I know the problem. Come with the hobby I guess.

Regards Jacco
0
 

Author Comment

by:werehamster-
ID: 12094335
0
 

Author Comment

by:werehamster-
ID: 12094337
Anyway, I did a re-write again, getting away from CLX as I think it is quite buggy and I don't have any immediate plans to use Kylix and I wanted to use a TRichEdit component to get some color.
0

Featured Post

IT, Stop Being Called Into Every Meeting

Highfive is so simple that setting up every meeting room takes just minutes and every employee will be able to start or join a call from any room with ease. Never be called into a meeting just to get it started again. This is how video conferencing should work!

Join & Write a Comment

The uses clause is one of those things that just tends to grow and grow. Most of the time this is in the main form, as it's from this form that all others are called. If you have a big application (including many forms), the uses clause in the in…
This article explains how to create forms/units independent of other forms/units object names in a delphi project. Have you ever created a form for user input in a Delphi project and then had the need to have that same form in a other Delphi proj…
This tutorial demonstrates a quick way of adding group price to multiple Magento products.
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.

762 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

22 Experts available now in Live!

Get 1:1 Help Now