Search code examples
delphiindy

Upgrading Delphi 7 Indy 9 app to Indy 10 (II)


This is a follow-up question to my previous question here.

A lot of the commands and the responses are coded as delimited strings. In Delphi 7 these are typically coded using chr(166) and chr(167).

procedure TFormMain.IdTCPServer1InsertAccount(
  ASender: TIdCommand);
var
  cmd: String;
  request: String;
  Params: TMyStrings;
  AccountNo, Address, UserName: String;
begin
  cmd := 'InsertAccount';
  request := Copy(ASender.Rawline, Length(cmd) + 2, Length(ASender.RawLine));
  Params := TMyStrings.Create;
  try
    AssignDelimited(chr(166), request, Params);
    AccountNo := Params[0];
    Address := replace(char(167), #13#10, Params[1])
    UserName := Params[2];

It appears that this was done so that parameters can contain spaces. Similarly, commands whose content came from memo controls have their carriage return-line feeds replaced with chr(167) so the memo contents can be sent without terminating the command:

// typical client code
request := edAccountNo.Text + chr(166) + 
  replace(#13, chr(167), replace(#10, '', memoAddress.Lines.Text) + 
  chr(166) + Fusername;

idTCPClient1.WriteLn('InsertAccount' + space + request);

Now in converting this code to Delphi 10.1 with Indy 10, I did a search-and-replace chr(166) with ANSIChar(166), but I soon discovered that Indy 10 doesn't "like" ANSIChars higher than 127. The request appears correct at the client but is received at the server with ?'s

What's the best approach to upgrading this code? Thanks.


Solution

  • Indy 10 is UnicodeString-aware, whereas Indy 9 is not. Delphi 2009 and later versions use UnicodeString for their native string type, whereas Delphi 2007 and earlier versions use AnsiString instead.

    Indy 9 transmits AnsiString data as-is as 8-bit data. Indy 10 converts AnsiString/UnicodeString characters to bytes using charset conversions and then transmits the bytes.

    Indy 10's default charset is ASCII, where any Unicode character above U+007F will get converted to 0x3F. You are using characters greater than U+007F for your parameter delimiters, so the default ASCII charset is converting them to ?, breaking your protocol. It would have been safer to use ASCII control characters < U+0020 instead, such as U+0001.

    To address this issue without changing your protocol, you can set Indy 10 to use its built-in 8bit charset for string <-> byte conversions (as long as you don't ever need to send Unicode characters > U+00FF in your protocol). To do that, you can either:

    1. set the connection's IOHandler.DefStringEncoding property to IndyTextEncoding_8Bit after the client connects to your server. Do this on both the client and server sides of the connection:

      procedure TFormMain.IdTCPServer1Connect(AContext: TIdContext);
      begin
        AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_8Bit;
      end;
      

      idTCPClient1.Connect;
      idTCPClient1.IOHandler.DefStringEncoding := IndyTextEncoding_8Bit;
      
    2. set Indy's global GIdDefaultTextEncoding variable in the IdGlobal unit to enc8Bit.

      procedure TFormMain.FormCreate(Sender: TObject);
      begin
        GIdDefaultTextEncoding := enc8Bit;
      end;
      
    3. When calling IOHandler.WriteLn() on the client side, you can pass IndyTextEncoding_8Bit in its optional AByteEncoding parameter.

      idTCPClient1.IOHandler.WriteLn('InsertAccount' + space + request, IndyTextEncoding_8Bit);
      

      On the server side, assigning the connection's IOHandler.DefStringEncoding property would be best, or at least setting the GIdDefaultTextEncoding variable. But, as an alternative, you could derive a new component from TIdCmdTCPServer (or even use an interposer class) and override its virtual ReadCommandLine() method to call the connection's IOHandler.ReadLn() method specifying IndyTextEncoding_8Bit in its optional AByteEncoding parameter:

      type
        TIdCmdTCPServer = class(IdCommandHandlers.TIdCmdTCPServer)
        protected
          function ReadCommandLine(AContext: TIdContext): string; override;
        end;
      
        TFormMain = class(TForm)
          IdTCPServer1: TIdCmdTCPServer;
          ...
        end;
      
        ...
      
        function TIdCmdTCPServer.ReadCommandLine(AContext: TIdContext): string;
        begin
          Result := AContext.Connection.IOHandler.ReadLn(IndyTextEncoding_8Bit);
        end;
      

    FYI, on a side note, TCommandHandler has a ParamDelimiter property. If you set it to #166 (it is #32 by default) and set ParseParams to True, you can remove your AssignDelimited() function and let TIdCommandHandler parse your delimited parameters into the TIdCommand.Params property before firing its OnCommand event.

    It might even be possible to go a step further by deriving a new class from TIdCommandHandler and override its virtual DoParseParams() method to handle the #167 -> CRLF conversions, instead of doing that manually in each OnCommand event handler.