Reputation: 2287
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.
Upvotes: 0
Views: 1321
Reputation: 595319
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:
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;
set Indy's global GIdDefaultTextEncoding
variable in the IdGlobal
unit to enc8Bit
.
procedure TFormMain.FormCreate(Sender: TObject);
begin
GIdDefaultTextEncoding := enc8Bit;
end;
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.
Upvotes: 1