IdTcpServer послать первым сообщение клиенту Delphi

ArtGrek13

Подскажите по нажатию кнопки, какой командой сервер может отправить клиенту сообщение, по TCP

Delphi XE8, Indy 10, IdTcpServer

1 ответ

ArtGrek13

где то так

IdTCPClient1.Host := '127.0.0.1';
IdTCPClient1.Connect;// подключились
IdTCPClient1.Socket.WriteLn('command'); // отправили команду  command и перевод строки
//Ожидаем ответ и закрываем соединение
txtResults.Lines.Append(IdTCPClient1.Socket.ReadLn);
IdTCPClient1.Disconnect;

в данном случае команда - это просто текст с переводом строки. Это сильно упрощает прием команды с другой стороны (просто ReadLn). В общем же случае нужно придумывать (или использовать готовый) протокол.

upd

выше это был клиент. А теперь сервер. С сервером все немного сложнее. Понятное дело, что для сервера нормально обслуживать не одного клиента, многих. И для этого есть несколько "схем".

  • Классическая - один клиент - один поток. Схема проста в кодировании, интуитивно понятна. Хорошо распаралеливается по ядрам. Недостаток - обычно очень сложно создать много потоков, а это ограничивает кол-во клиентов. Для 32битных программ верхний предел где то в районе 1500 (полторы тысячи) потоков на процесс. Но в этом случае накладные расходы на их переключение могут "скушать" весь проц. Именно эта схема используется в indy.

  • Вторая классическая - все клиенты на один поток. Эта схема часто более сложна в кодировании, но при правильном подходе позволяет держать 20-30к "медленных пользователей" практически не нагружая ядро. Сильный плюс этой схемы - можно обойтись без мютексов и других примитивов синхронизации. Эту схему использует NodeJS и стандартные классы для работы с сетью в Qt.

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

Как это сделано в Indy. Indy tcp сервер для каждого подключения создает отдельный поток (TThread) и дальнейшая работа с клиентом идет в нем. Indy красиво это прячет, оставляя для пользователя только необходимость реализовать метод IdTCPServer.onExecute. Но, как я сказал выше, этот метод запускается в отдельном треде и он у каждого клиента свой личный. Это значит следующее:

  • в этом методе можно позвать sleep и только один клиент будет ждать. Все остальные будут работать (а вот если в обработчике нажатия кнопки позвать sleep, то результат известен)
    • обращаться к глобальным переменным лучше только через примитивы синхронизации.
    • обращаться к элементам gui нужно аккуратно и правильно. Напрямую лучше не делать (некоторые компоненты разрешают обращаться к ним с других потоков, но нужно внимательно читать доки).
    • обращаться к другим клиентам нужно через блокировку (потому что если два потока захотят писать одному и тому же пользователю - ничего хорошего с этого не выйдет).

Рассмотрим очень простой пример. На любой запрос клиента отвечаем тем же и закрываем соединение (такой себе echo сервер).

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  strText: String;
begin
  //Принимаем от клиента строку
  strText := AContext.Connection.Socket.ReadLn;
  //Отвечаем
  AContext.Connection.Socket.WriteLn(strText);

  //Закрываем соединение с пользователем
  AContext.Connection.Disconnect;
end;

AContext это специальный объект, который содержит всю необходимую информацию о клиенте. Сам idTcpServer содержит список этих контекстов и к нему можно обратиться. Рассмотрим более сложный бродкаст. То есть, разослать одно сообщение всем

Var
  Clients : TList;
  i : integer;
begin
  // защита от дурака:)
  if not Assigned(IdTCPServer1.Contexts) then exit;

  // получим список клиентов, и заблокируем его
  Clients:=IdTCPServer1.Contexts.LockList;
  try
    for i := 0 to Clients.Count-1 do
      try
        //LBuffer имеет тип TBytes и содержит подготовленные данные для отправки
        // но можно и WriteLn использовать.
        TIdContext(Clients[i]).Connection.IOHandler.Write(LBuffer);
      except
        // тут нужно добавить логику. Клиент может отключиться в процессе
      end;
  finally
    // важно! список нужно разблокировать, иначе другие методы не смогут пройти дальше Contexts.LockList
    IdTCPServer1.Contexts.UnlockList;
  end;

end;

инди содержит BytesToString() и ToBytes() для предобразования String и TIdBytes друг в дружку.

Список блокируется, что бы другие не могли его модифицировать. Иначе сам цикл сильно усложняется. И главное, не забывать разблокировать!

Остался последний вопрос. Как отправить сообщение какому то определенному клиенту. Для этого нужно научиться идентифицировать соединение. Это можно сделать несколькими способами - посмотреть в айпи/порт. Но есть лучше. У IdContext (точнее у его предка idTask) есть свойство Data типа TObject. В него можно записать свой объект и хранить там все нужные данные. Типичный пример использования будет следующий. Когда клиент только подключился - это поле пустое. Когда он прошел проверку имени-пароля - создаем объект (свой), сохраняем имя туда и прописываем в свойство Data. А потом, когда нужно делать цикл по подключенным клиентам, просто вычитываем его. Конечно, если пользователей тысячи, каждый раз просматривать всех пользователей будет накладно. Но как делать это более оптимально - тема другой большой статьи.

licensed under cc by-sa 3.0 with attribution.