问题描述
我将 Indy 10.6.2 与 Delphi 7 和 Windows 10 64 位波兰语一起使用。
我运行一个 FTP 服务器:FTPServer: TIdFTPServer
,并且通过使用 FTPClient: TIdFTP
,我试图获取一个在其文件名中包含国家符号的文件。不幸的是,在调用 Get()
函数后,在服务器端的 OnRetrieveFile
事件中,而不是 AFileName
中的国家符号,我以问号结束,这显然会导致其他异常。
为了测试,我在同一台机器和同一个应用程序中同时运行服务器和客户端,以消除任何其他干扰。
在客户端,我已经尝试过:
FTPClient.DefStringEncoding := IndyTextEncoding_UTF8;
FTPClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
FTPClient.IOHandler.DefAnsiEncoding := IndyTextEncoding_UTF8;
在服务器端,我尝试过:
ASender.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
ASender.Connection.IOHandler.DefAnsiEncoding := IndyTextEncoding_UTF8;
它们都没有任何区别。我也尝试过其他编码,但都失败了。
下面是我的测试应用程序,以及客户端和服务器的文本 DFM 值。
unit Glowny_FTP;
interface
uses
Windows,Messages,SysUtils,Variants,Classes,Graphics,Controls,Forms,Dialogs,StdCtrls,IdBaseComponent,IdComponent,IdTCPConnection,IdTCPClient,IdExplicitTLSClientServerBase,IdFTP,ZLibCompression,IdGlobal,IdioHandler,IdioHandlerStream,IdioHandlerSocket,IdioHandlerStack,IdCustomTcpserver,IdTcpserver,IdCmdTcpserver,IdFTPServer,IdContext,IdAntiFreezeBase,IdAntiFreeze;
type
TForm1 = class(TForm)
Button1: TButton;
FTPClient: TIdFTP;
FTPServer: TIdFTPServer;
IdAntiFreeze1: TIdAntiFreeze;
procedure Button1Click(Sender: TObject);
procedure FTPServerUserLogin(ASender: TIdFTPServerContext;
const AUsername,APassword: String; var AAuthenticated: Boolean);
procedure FTPServerRetrieveFile(ASender: TIdFTPServerContext;
const AFileName: WideString; var VStream: TStream);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
FTPServer.DefaultPort := 1350;
FTPServer.Active := True;
FTPClient.Host := '127.0.0.1';
FTPClient.Port := 1350;
FTPClient.Username := 'new';
FTPClient.Password := 'pass';
FTPClient.Connect;
// FTPClient.DefStringEncoding := IndyTextEncoding_UTF8;
// FTPClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
// FTPClient.IOHandler.DefAnsiEncoding := IndyTextEncoding_UTF8;
FTPClient.Get('sample ąęśćłó','C:\węzły.cpy',True,False); // <-- filename containing national symbols
FTPClient.disconnect;
end;
procedure TForm1.FTPServerUserLogin(ASender: TIdFTPServerContext;
const AUsername,APassword: String; var AAuthenticated: Boolean);
begin
if (APassword='pass') then
begin
AAuthenticated := True;
end else AAuthenticated := False;
end;
procedure TForm1.FTPServerRetrieveFile(ASender: TIdFTPServerContext;
const AFileName: WideString; var VStream: TStream);
begin
// ASender.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
// ASender.Connection.IOHandler.DefAnsiEncoding := IndyTextEncoding_UTF8;
VStream := TFileStream.Create(AFileName+'.serv',fmOpenRead); //<-- here I get: "/sample ??????" without UTF8 and "/sample ????" with UTF8
end;
end.
也许还有有趣的 DFM 部分:
object FTPClient: TIdFTP
Passive = True
ConnectTimeout = 0
DataPort = 20
Password = 'TaJnEH1aS2loD3oSt4ePu'
TransferType = ftBinary
NATKeepAlive.UseKeepAlive = False
NATKeepAlive.IdleTimeMS = 0
NATKeepAlive.IntervalMS = 0
ProxySettings.ProxyType = fpcmNone
ProxySettings.Port = 0
Left = 104
Top = 72
end
object FTPServer: TIdFTPServer
Bindings = <>
DefaultPort = 1350
TerminateWaitTime = 100
CommandHandlers = <>
ExceptionReply.Code = '500'
ExceptionReply.Text.Strings = (
'UnkNown Internal Error')
Greeting.Code = '220'
Greeting.Text.Strings = (
'Indy FTP Server ready.')
MaxConnectionReply.Code = '300'
MaxConnectionReply.Text.Strings = (
'Too many connections. Try again later.')
ReplyTexts = <>
ReplyUnkNownCommand.Code = '500'
ReplyUnkNownCommand.Text.Strings = (
'UnkNown Command')
PathProcessing = ftppUnix
AnonymousAccounts.Strings = (
'anonymous'
'ftp'
'guest')
OnUserLogin = FTPServerUserLogin
OnRetrieveFile = FTPServerRetrieveFile
SITECommands = <>
MLSDFacts = []
ReplyUnkNownSITCommand.Code = '500'
ReplyUnkNownSITCommand.Text.Strings = (
'Invalid SITE command.')
Left = 104
Top = 8
end
更新:
经过雷米的帮助还是没有效果。在 FTPClient.Get() 中的 UTF8Encode() 之后,我在服务器端得到了更多的问号。现在我的代码如下所示: (我在 Form1.Caption 上检查 AFileName 只是为了快速调试)
...
FTPClient.Connect;
FTPClient.DefStringEncoding := IndyTextEncoding_UTF8;
FTPClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
FTPClient.IOHandler.DefAnsiEncoding := IndyTextEncoding_8Bit;
FTPClient.Get(UTF8Encode('sample ąęśćłó'),False);
FTPClient.disconnect;
procedure TForm1.FTPServerRetrieveFile(ASender: TIdFTPServerContext;
const AFileName: WideString; var VStream: TStream);
begin
Form1.Caption := AFileName;
VStream := TFileStream.Create('C:\tymczas z węzłami obl.aqr',fmOpenRead);
// VStream := TFileStream.Create(AFileName+'.serv',fmOpenRead);
end;
procedure TForm1.FTPServerConnect(AContext: TIdContext);
begin
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
AContext.Connection.IOHandler.DefAnsiEncoding := IndyTextEncoding_UTF8;
end;
解决方法
背景
IOHandler 的 DefStringEncoding
需要设置为通过套接字传输字符串时要使用的字节编码。 Indy 将通过使用 DefStringEncoding
将其编码为字节来发送一个 Unicode 字符串。相反,Indy 将通过使用 DefStringEncoding
将接收到的字节解码为 Unicode 来读取 Unicode 字符串。
在 Delphi 2007 及更早版本中,涉及一个额外的步骤。 IOHandler 的 DefAnsiEncoding
需要设置为您要与 AnsiString
一起使用的编码。 Indy 将发送一个 AnsiString
,首先使用 DefAnsiEncoding
将其从 ANSI 解码为 Unicode,然后使用 DefStringEncoding
发送该 Unicode。相反,Indy 将首先使用 AnsiString
读取 Unicode 字符串,然后使用 DefStringEncoding
将该 Unicode 编码为 ANSI 来读取 DefAnsiEncoding
。
默认情况下,DefStringEncoding
为 IndyTextEncoding_ASCII
(由于历史原因),而 DefAnsiEncoding
为 IndyTextEncoding_OSDefault
。
在客户端
TIdFTP
如果确定服务器支持 UTF-8,将自动将 IOHandler 的 DefStringEncoding
切换为 IndyTextEncoding_UTF8
。如果没有 UTF-8 扩展,FTP 协议需要使用 ASCII 来代替。因此,您根本不应该弄乱客户的 DefStringEncoding
。
您最初将远程文件名作为波兰语编码的 AnsiString
传递,因此将 DefAnsiEncoding
设置为 IndyTextEncoding_OSDefault
在设置为波兰语区域设置的操作系统上是有意义的。将 DefStringEncoding
设置为 IndyTextEncoding_UTF8
,应将正确的 UTF-8 传输到服务器。
您后来将客户端更改为传入 UTF-8 编码的 AnsiString
,但您将 DefAnsiEncoding
设置为 IndyTextEncoding_8Bit
,因此 AnsiString
不会转换为Unicode 正确,因此后续转换为 UTF-8 格式不正确。在这种情况下,DefAnsiEncoding
需要设置为 IndyTextEncoding_UTF8
。我建议将 DefAnsiEncoding
设置为 IndyTextEncoding_OSDefault
并使用操作系统区域设置编码的 AnsiString
,除非您有令人信服的理由不这样做。
在服务器端
TIdFTPServer
只会在客户端发出 DefStringEncoding
或 IndyTextEncoding_UTF8
命令(OPTS UTF-8 <NLST>
,但其他客户可能不会)。请注意,这不符合 RFC 2640,Indy 尚未完全实现。
您将 OPTS UTF8 ON
和 TIdFTP
都设置为 DefStringEncoding
,这在大多数情况下通常是可以的。在暴露对 DefAnsiEncoding
的访问权限的事件中,您最终会得到 UTF-8 编码的 IndyTextEncoding_UTF8
。
但是,AnsiString
事件在您的 Delphi 版本中使用 AnsiString
作为其 OnRetrieveFile
参数。服务器从套接字中读取文件名作为 WideString
后,它会按原样传递给事件处理程序,使用 RTL 自己的转换将其转换为 AFileName
逻辑,默认情况下使用操作系统的语言环境。因此,在您的情况下,AnsiString
是 UTF-8 编码但操作系统区域设置是波兰语,您最终会得到格式错误的转换。幸运的是,您可以通过预先调用 RTL 的 SetMultiByteConversionCodePage()
函数来使用代码页 65001 (UTF-8) 执行 WideString
AnsiString
转换来缓解该问题。
然而,在 Delphi 2007 及更早版本中,AnsiString
的一些事件使用 WideString
参数,因此 TIdFTPServer
的内部有相当多的区域依赖于 RTL 的默认 WideString
TIdFTPServer
转换。因此,我建议在服务器端也将 AnsiString
设置为 WideString
,除非您有令人信服的理由不这样做。如果您想强制 DefAnsiEncoding
为 IndyTextEncoding_OSDefault
而不管 DefStringEncoding
命令,那没问题(前提是您的客户端只发送 UTF-8 编码的路径)。
话虽如此,试试这个:
IndyTextEncoding_UTF8
或者这个:
OPTS
,
最后我放弃了 D7 下乱七八糟的 TextEncoding。迷失了三天,我还在原地。
无论我选择哪种编码类型,长时间的调查都让我找到了 TIdASCIIEncoding.GetBytes()
。这里任何高于 $007F (127) 的 Char 都变成了“?”这对于 ASCII 很明显,但对于 UTF8 和 ANSI 则不然。
只要我只在两个应用程序之间进行通信,我就会使用更长的路径来解决应该可以解决的问题。我知道它正在修补,但至少有效。
“解决方案”:
在 FTPClient.Get()
中,我将字符串编码为 IdEncoderMIME.EncodeString()
而不是包含国家符号的源字符串,在服务器端 FTPServerRetrieveFile()
中我删除第一个字符“/”然后我的字符串被解码IdDecoderMIME.DecodeString()
,最后我在另一边得到了正确的文件名。
客户端:
S := IdEncoderMIME1.EncodeString('sample ąęśćłó',IndyTextEncoding_UTF8,IndyTextEncoding_UTF8);
FTPClient.Get(S,'C:\węzły.cpy',True,False);
服务器端:
S := Copy(AFileName,2,Length(AFileName));
S := IdDecoderMIME1.DecodeString(S,IndyTextEncoding_UTF8);
丑陋但有效...