Перейти к содержанию
Fire Monkey от А до Я
  • 0

IdTCPClient, IdTCPSever - снова о кодировке


Wovan2

Вопрос

Здравствуйте.

Немного предыстории. Нужно соорудить клиент-сервер. Клиент на Android, сервер на Windows. 

Собственно перебираю варианты. 

1. Сделал с помощью DataSnap. Работает. Но очень громоздко и не понятно.

2. Tethering. Так и не смог реализовать обмен данными между сервером и клиентом типа запрос-ответ.

3. IdTCPClient, IdTCPSever. Раньше (D7) опыт работы отрицательный, но в FMX, видимо, альтернативы нет.

Так что сейчас предварительно реализую простой диалог запрос-ответ. Причем все вроде работает. И кода немного и быстро. НО. Уперся в кодировку. На клиенте (Android) не получается прочитать кириллицу. Сначала пробовал общаться Read, Write и передавать просто строки. Пробовал всякие индийские компоненты для кодировки. Но без понимания ничего и не получилось. В интернете где-то наткнулся на предложение передавать потоками. Реализовал. Стало лучше, но проблема с кодировкой не пропала. Скажем так, эта проблема уменьшилась вдвое. На стороне сервера все ОК. На стороне клиента либо знаки "????", либо ромбики с вопросами, либо ошибка "No mapping for the unicode character exists in the target multi-byte code page". Это я бессистемно перебирал варианты кодировок. К сожалению до сих пор так и не понимаю эти перекодировки. Вернее не так. Не понимаю откуда и куда идут какие кодировки. 

В общем помогите пожалуйста советом или кодом. Ниже код с которым экспериментирую. (код не большой)

Сервер

procedure TForm1.IdTCPServerExecute(AContext: TIdContext);
var
 s : TStringStream;
begin
 s := TStringStream.Create;
 mem.Lines.Add(AContext.Connection.Socket.Binding.PeerIP);
 AContext.Connection.IOHandler.ReadStream(s);
 s.Position := 0;
 mem.Lines.Add(Utf8ToAnsi(s.ReadString(s.Size)));
 s.Free;
 s := TStringStream.Create;
 s.WriteString('И тебе привет');//  And You, Hi с этим все нормально, проблема только с русскими буквами
 s.Position := 0;
 AContext.Connection.IOHandler.Write(s,s.Size,true); //здесь s.Size = 13. в 26 никак не получается превратить, может быть тут что-то?
 s.Free;
end;

Клиент

procedure TfrmMainClient.btn1Click(Sender: TObject);
var s:TStringStream;
begin
 s := TStringStream.Create;
 try
 IdTCPClient.Connect;
 if IdTCPClient.Connected
  then mem.Lines.Add('Есть контакт с сервером ' + IdTCPClient.Socket.Binding.PeerIP);
 s.WriteString('Привет');
 s.Position := 0;
  IdTCPClient.IOHandler.Write(s, s.Size, true);//здесь заметил, что s.Size = 12, похоже на unicode
  s.Clear;
  IdTCPClient.IOHandler.ReadStream(s);
  s.Position := 0;
  Mem.Lines.Add(s.ReadString(s.Size)); //ошибка здесь
 finally
  IdTCPClient.Disconnect;
  if Assigned(s)
   then s.Free;
 end;
end;

Этот код приводит на клиента к ошибке  "No mapping for the unicode character exists in the target multi-byte code page". На сервере сообщение "Привет" видно нормально.

Помогите, кто чем может.

ЗЫ. Тему "Как Получить текст по TCP (Indy) в нужной кодировке?" смотрел. Но либо она не актуальна (инди уже изменился), либо я не смог правильно понять. По крайней мере пробовал использовать это в самом начале, когда пытался передавать строки напрямую. В указанном коде IIdTextEncoding не представляю куда и как вставить.

зы2: Извините. Пишу на Delphi 10.2. Indy 10.6.2(в комплекте с Delphi). Windows 10. Android 4 и 5

Изменено пользователем Wovan2
правка
Ссылка на комментарий

Рекомендуемые сообщения

  • 0

Пока докопался до следующего.

В Delphi строка закодирована  в ANSI. (Или в Windows???)

Замена на сервере s.WriteString('И тебе привет');//AnsiToUtf8   And You, Hi

на 
 a := TEncoding.ANSI.GetBytes('И тебе привет');
 s.Write(TEncoding.Convert(TEncoding.ANSI, TEncoding.UTF8, a), Length(a) * 2);

где  a : TBytes;

решает проблему. Только как-то вроде не красиво. Может как можно поизящнее переписать? Другими функциями, которых дофига и больше?

Изменено пользователем Wovan2
Ссылка на комментарий
  • 0
38 минут назад, Wovan2 сказал:

mem.Lines.Add(Utf8ToAnsi(s.ReadString(s.Size)));

Одна из ошибок здесь.

Во-первых, ReadString уже возвращает строку. В той кодировке, которую правильно воспримет memo.
Во-вторых, ReadString читает байты как строку, считая что это заранее заданная кодировка. По умолчанию, емнип, идет ANSI. Во внутренностях ReadString сразу идет преобразование из выбранной кодировки в дефолтную на выбранной платформе.

Требуется: убрать напрочь излишнее UTF8ToAnsi
У каждого StringStream сразу после создания подставить s.Encoding:=TEncoding.UTF8;

Имхо, этого хватит.

Ссылка на комментарий
  • 0
48 минут назад, kami сказал:

Одна из ошибок здесь.

Во-первых, ReadString уже возвращает строку. В той кодировке, которую правильно воспримет memo.

Без Utf8ToAnsi сервер (это серверный код) пишет "Привет".

Свойство s.Encoding только для чтения.

Пока помогает только прямое перекодирование, как в предыдущем моем ответе.

Изменено пользователем Wovan2
Ссылка на комментарий
  • 0
57 минут назад, Wovan2 сказал:

Свойство s.Encoding только для чтения

Зато его можно указать в конструкторе.

 

1 час назад, Wovan2 сказал:

В Delphi строка закодирована  в ANSI. (Или в Windows???)

Нет. Исходная string в версиях от 2009 и выше - это UnicodeString. В большинстве случаев можно считать как WideString или UCS-2. Кодировка по 2 байта на символ. ANSI - это 1 байт на символ.

Действительно, получившийся у Вас код не есть супер-красив. Я продолжаю считать, что всего навсего явное указание Encoding для StringStream снимет эти проблемы. Всякие остальные преобразования, включая явные упоминания конкретных TEncoding  и функций перекодировки не нужны.

Изменено пользователем kami
Ссылка на комментарий
  • 0

Спасибо. Действительно, если при создании потока указать s := TStringStream.Create('', TEncoding.UTF8), то все пляски с перекодировками оказываются не нужными. В общем первоначальный код работает с указанной поправкой корректно.

Ссылка на комментарий
  • 0

Здравствуйте. Хотелось бы поделиться своим пониманием проблемы кодирования строк. Может я что-то не понимаю. Чтобы как-то прояснить для себя.

Тип string в delphi и кодировка символов, это две разные вещи. String (в Delphi 10 по сути это UnicodeString) - это указание компилятору сколько памяти выделить под переменную, заданного типа. Для Delphi 10 это 2 байта на символ. А что конкретно в этих байтах - это уже кодировка. Латиница практически во всех кодовых таблицах имеет они и те же коды (в unicode первый байт заполнен нулями). Поэтому с ней и проблем, практически не бывает. Кириллица же в разных кодовых таблицах имеет разные коды. Из-за этого и все проблемы. Это все понятно.

Мне не понятно, например, в какой кодировке хранится текст в таком коде:
var s : string:
s := 'Привет';

Это зависит, видимо от ОС, в которой запущено приложение. На Android, по видимому, это UTF8. На Windows - ANSI(первый байт 0). Но это по наитию. Как все же правильно работать с кодировками, чтобы всегда знать, в какой кодировке находятся строки в которыми ты работаешь в данный конкретный момент, на данном конкретном компе? Если это понять, то скорее всего и проблемы с "крякозяблами" должны пропасть сами собой. 

Вы согласны? Поделитесь пожалуйста своими соображениями, кто в курсе. Спасибо.

Ссылка на комментарий
  • 0
Цитата

IIdTextEncoding не представляю куда и как вставить.

В событие OnConnect и будут русские буквы.

//C++ Builder
void __fastcall TFormPrint::IdTCPServer1Connect(TIdContext *AContext)
{
 AContext->Connection->IOHandler->DefStringEncoding = IndyTextEncoding_UTF8();
}

 

Ссылка на комментарий
  • 0
27 минут назад, Anatoliy сказал:

В событие OnConnect и будут русские буквы.


//C++ Builder
void __fastcall TFormPrint::IdTCPServer1Connect(TIdContext *AContext)
{
 AContext->Connection->IOHandler->DefStringEncoding = IndyTextEncoding_UTF8();
}

 

Спасибо. Все это конечно хорошо, но уж больно далеко и не интуитивно.

Ссылка на комментарий
  • 0

Я бы вообще не парился с кодировкой, а отправлял/принимал буфер как он есть. Какая разница что там внутри? Тут правда будет проблема отсечения блока текста, так как чаще всего каждый второй байт будет нулём. Поэтому перед отсылкой буфера лучше отсылать его размер. Ну или переводить в Ansistring utf8 и отправлять ноль после буфера. Но так обрывы менее очевидны.

Лично я сжимаю сначала весь буфер (у меня xml передаётся в обычном string) через TZCompressionStream, затем отправляю заголовок, в котором содержится тип пакета, размер буфера и пр. и потом сам буфер.

function ZCompressIDBuf(aText: string): TIdBytes;
var
  strInput: TMemoryStream;
  strOutput: TMemoryStream;
  Zipper: TZCompressionStream;
begin
  SetLength(result,0);
  strInput:= TStringStream.Create();
  strInput.Write(aText[1],Length(aText)*2);

  strInput.Position := 0;
  strOutput:= TMemoryStream.Create;
  try
    Zipper:= TZCompressionStream.Create( clFastest ,strOutput);
    try
      Zipper.CopyFrom(strInput, strInput.Size);
    finally
      Zipper.Free;
    end;
    SetLength(result,strOutput.Size);
    strOutput.Position := 0;
    strOutput.Read(result[0],strOutput.Size);
//    Result:= strOutput.DataString;
  finally
    strInput.Free;
    strOutput.Free;
  end;
end;
procedure LetsSendBuf(p:pointer; siz:integer;cli: TIdIOHandlerSocket);
var
   buf : TIdBytes;
begin
   setlength(buf,siz);
   move(p^,buf[0],siz);
   cli.Write(buf);
end;
сама отправка:
var
   head : TProtocolHeader;
   XMLOut:string;
   buf: TIdBytes;
.....
   buf := ZCompressIDBuf(XMLOut);
   head.PacketBodyLen := length(buf);
   LetsSendBuf(@head,sizeof(head),AContext.Connection.Socket);
   AContext.Connection.Socket.Write(buf);

Приём по аналогии.

P.S. String андроида/айос начнёт соответствовать стандартам Delphi, если выключить сверхсекретную опцию, которая по умолчанию почему-то включена.

{$ZEROBASEDSTRINGS OFF}

 

Ссылка на комментарий
  • 0
3 часа назад, Wovan2 сказал:

А что конкретно в этих байтах - это уже кодировка.

Да, это правильно.

3 часа назад, Wovan2 сказал:

На Windows - ANSI(первый байт 0)

Нет, на Windows - UTF-16

3 часа назад, Wovan2 сказал:

Как все же правильно работать с кодировками, чтобы всегда знать, в какой кодировке находятся строки в которыми ты работаешь в данный конкретный момент, на данном конкретном компе?

Внутри приложения не стоит заморачиваться на это. Всё приложение работает в единой кодировке и никаких проблем с передачей из класса в класс, из метода в метод, из модуля в модуль нет и не будет.

Другое дело - общение с "внешним миром". Самые простые случаи - сохранение /загрузка текста в файл, общение по сети.

В этом случае необходимо просто явно указывать кодировку при получении и отправке данных.

Ссылка на комментарий
  • 0
1 час назад, Akad сказал:

Какая разница что там внутри?

Как какая? Android по умолчанию искренне считает, что там UTF8. И в сеть выбрасывает UTF8.

Сделал, на мой взгляд, довольно компактно.

Клиент

function TfrmMainClient.GetFromServer(Command : string) : string;
var
 s : TStringStream;
begin
 s := TStringStream.Create('', TEncoding.UTF8);
 try
 //запрос на сервер
    s.WriteString(Command);
 s.Position := 0;
    IdTCPClient.IOHandler.Write(s, s.Size, True);
    s.Clear;
 //читаем ответ сервера
 IdTCPClient.IOHandler.ReadStream(s);
 s.Position := 0;
 Result := s.ReadString(s.Size);
 finally
  if Assigned(s)
   then s.Free;
 end;
end;

Сервер

procedure TfrmMainServer.IdTCPServerExecute(AContext: TIdContext);
var
 s : TStringStream;
 Command, Param1, Param2 : string;
 Response : string;
procedure ResponseClient(Response : string);
begin
 s.WriteString(Response);
 s.Position := 0;
 AContext.Connection.IOHandler.Write(s, s.Size, True);
end;
begin
 s := TStringStream.Create('', TEncoding.UTF8);
 mem.Lines.Add(AContext.Connection.Socket.Binding.PeerIP);
 AContext.Connection.IOHandler.ReadStream(s);
 s.Position := 0;
 Command := s.ReadString(s.Size);
 mem.Lines.Add(Command); //Utf8ToAnsi
 SeparateNameValue(Command, ':', Command, Param1);
 s.Clear;
//реакция на разные команды клиента
 case AnsiIndexStr(Command, ['GETISPOOLEXISTS', 'UPDATEISSET', 'GETCELLS']) of
 0:begin
//тут код получения строки Response
    ResponseClient(Response);
   end;
 1:begin
//тут код получения строки Response
    ResponseClient(Response);
   end;
 2:begin
//тут код получения строки Response
    ResponseClient(Response);
   end;
  end;
 s.Free;
end;

С кодировкой проблем нет. Спасибо всем, кто откликнулся и помог. Объекты перегоняю через JSON.

59 минут назад, kami сказал:

Нет, на Windows - UTF-16

Спасибо. Вот не знал.

Изменено пользователем Wovan2
правка
Ссылка на комментарий
  • 0
1 час назад, Wovan2 сказал:

Как какая? Android по умолчанию искренне считает, что там UTF8. И в сеть выбрасывает UTF8.

Почему у меня не считает? Почему я без проблем на любой OS отправляю и получаю то, что отправил? Даже код отправки выложил.

P.S. Если ты осознанно отправляешь AnsiString, и его же и пытайся принять (web стандарт так сказать). Или забивай на это, что внутри, и шли буфер. А в твоём примере ты отправляешь UTF-16, а принимаешь UTF-8. Естественно ничего не работает. Delphi работает с UTF 16 всегда, если мы про string. Отправь  AnsiString в uft8, и получи его-же.
 

Ссылка на комментарий
  • 0
15 часов назад, Wovan2 сказал:

Android по умолчанию искренне считает, что там UTF8.

Пусть хоть китайской традиционной считает. Повторюсь - абсолютно без разницы, что за кодировка внутри приложения. Главное, чтобы общение (ввод/вывод) с внешним миром шло с явным указанием кодировки, поскольку автоопределение и/или считание кодировкой по умолчанию рано или поздно приведет к фейлу.

Ссылка на комментарий
  • 0
17 часов назад, Akad сказал:

P.S. Если ты осознанно отправляешь AnsiString

В том то и дело, что до настоящего времени я это делал неосознанно. Понятно, что каждая сторона канала должна работать на понятной ей кодировке. Сейчас, с Вашей помощью, я достиг некоторого понимания вопроса, и проблема рассосалась. Спасибо. Тот код, что выложил я тоже вполне себе работоспособен.

 

3 часа назад, kami сказал:

Пусть хоть китайской традиционной считает. Повторюсь - абсолютно без разницы, что за кодировка внутри приложения. Главное, чтобы общение (ввод/вывод) с внешним миром шло с явным указанием кодировки, поскольку автоопределение и/или считание кодировкой по умолчанию рано или поздно приведет к фейлу.

Ну, собственно, что-то подобное я и сделал.  Автоопределение, согласен, крайне ненадежная штука.

Изменено пользователем Wovan2
Ссылка на комментарий

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.

Гость
Ответить на вопрос...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...