Шаг 31.
Создание Internet-приложений в среде Delphi. Пример использования сокетов

    На этом шаге мы рассмотрим примеры клиентского и серверного приложений, использующих сокеты.

    В этом примере мы создадим на основе компонентов TClientSocket и TServerSocket многопользовательское сетевое приложение, с помощью которого программы-клиенты будут обмениваться друг с другом строковыми сообщениями через программу-сервер. Клиентские программы могут отправлять свои сообщения на сервер, который пересылает их всем участникам этого форума, либо какому-то одному, конкретному получателю, тому, которого выбрал отправитель. Чтобы клиент смог начать работу, ему нужно подключиться к серверному сокету, для чего клиентскому приложению нужно знать адрес или имя серверного компьютера и номер порта, который прослушивает серверный сокет. В нашем примере эти параметры задаются на стадии проектирования: для адреса задается значение "127.0.0.1", номер порта кладется равным 1024. Заметим, что выбранный вариант адреса, означает, что серверное и клиентские приложения будут выполняться на одном компьютере. Если вы хотите тестировать пример в своей локальной сети или через Интернет, то вам нужно будет указать "правильный" адрес или имя компьютера, на котором будет запускаться серверное приложение. Клиентскому сокету нужно задать тот же номер порта - 1024. Внешний вид форм клиентской и серверной программ показан на рисунках 1 и 2.


Рис.1. Внешний вид клиентского приложения


Рис.2. Внешний вид серверного приложения

    Как видите, компонентов здесь использовано немного, особенно в серверной части. На форме серверного приложения располагается список, командная кнопка, метка, строка состояния и компонент TServerSocket. На клиентской форме компонентов немного больше: четыре метки, поясняющие назначение списка и три поля редактирования, три командные кнопки и компонент TClientSocket. В списки на обоих формах попадают сообщения клиентов. Формат сообщений, посылаемых клиентом следующий: "имя отправителя" | "имя получателя" | "строка сообщения". Если сообщение предназначено для всех, формат немного меняется - имя получателя отсутствует - и строка сообщения приобретает следующий вид: "имя отправителя" | "строка сообщения". Имена отправителя и получателя указываются в соответствующих полях редактирования клиентского приложения. В третьем поле набирается строка отправляемого сообщения. Две кнопки Подключиться и Выход выполняют соответствующие своим названиям задачи, как впрочем и кнопка Послать, при нажатии на эту кнопку вызывается метод SendText, отправляющий строку сообщения.

    Для начала работы нужно загрузить серверное приложение. При создании формы приложения серверный сокет будет автоматически активизироваться. Эту задачу выполняет следующая процедура:

procedure TForm1.FormCreate(Sender: TObject);
begin
  ServerSocket1.Active := True;
  num_users := 0;
end;

    Здесь же присваивается нулевое значение глобальной переменной, в которой хранится количество пользователей, работающих в данный момент на форуме. Эта переменная используется совместно с массивами s_users и s_handles, в которые помещаются имена участников и дескрипторы соответствующих им сокетов. Когда нажимается кнопка Выход на форме серверного приложения, выполняется следующая процедура:

procedure TForm1.Button1Click(Sender: TObject);
//Кнопка "Выход".
begin
  ServerSocket1.Active := False;
  ServerSocket1.Close;
  Close;
end;

    Здесь серверный сокет сначала переводится в неактивное состояние, затем вызывается метод Close, который удаляет из очереди клиентские запросы на соединение и закрывает все действующие соединения. После этого закрывается форма приложения. Следующая процедура - обработчик события OnClientConnect.

procedure TForm1.ServerSocket1ClientConnect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
  StatusBar1.SimpleText := 'На связи ' +
    IntToStr(ServerSocket1.Socket.ActiveConnections)+ ' клиентов';
end;

    Здесь просто выводится в строку состояния количество подключенных в данный момент клиентов. Следующая процедура обслуживает "противоположное" событие - OnClientDisconnect.

procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject;
  Socket: TCustomWinSocket);
var
  i, j, k: integer;
begin
  StatusBar1.SimpleText := 'На связи '+
    IntToStr(ServerSocket1.Socket.ActiveConnections-1)+ ' клиентов';
  for i := 0 to num_users-1 do
    if Socket.SocketHandle = s_handles[i] then
    begin
      k := i; break;
    end;
  for j := k+1 to num_users-1 do
  begin
    s_handles[j-1] := s_handles [ j ];
    s_users[j-1] := s_users[j];
  end;
  num_users := num_users-1;
end;

    Поскольку сокет, обслуживающий закрываемое соединение, удаляется только после события OnClientDisconnect, то от значения ServerSocket1.Socket.ActiveConnections нам приходится отнимать единицу и уже это число выводить в строку состояния в качестве количества активных клиентов. Далее в цикле мы просматриваем наш массив сокетных дескрипторов, пока не найдем в нем значение, соответствующее удаляемому сокету. После этого, в следующем цикле, массив переписывается: из него удаляется дескриптор не нужного теперь сокета, соответственно изменяется массив имен клиентов, и уменьшается на единицу переменная num_users.

    Нам осталось рассмотреть обработчик "основного" события OnClientRead.

procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
  Socket: TCustomWinSocket);
var
  str, s_send, s_rec: string;
  p1, p2, hndl, i, j, k: integer;
begin
  str  := Socket.ReceiveText;
  hndl := Socket.SocketHandle;
  ListBox1.Items.Add(str);
  p1 := pos ('|', str);
  s_send := copy ( str, 1, p1-1);
  str := copy( str, p1+1, length(str)-p1);
  p2 := pos( '|', str);
  s_rec := copy(str,1,p2-1);
  j:= -1;
  //Если послание от нового клиента, запоминаем его данные.
  for i:=0 to num_users-1 do
  begin
     if s_handles[i]=hndl then
     begin
       j :=i;
       break;
     end;
  end;
  if j = -1 then
  //Обнаружен новый участник конференции.
  begin
    s_handles[num_users] := hndl;
    s_users[num_users]:=s_send;
    num_users := num_users+1;
  end;
  k :=  -1;
  if p2<>1 then
  //Получено именное сообщение -  ищем получателя.
  for i := 0 to num_users-1 do
  begin
    if CompareText(s_users[i],s_rec)=0   then
    begin
      k:=i;
      break;
    end;
  end;
  str := s_send+'|'+copy (str,p2+1,length(str)-p2);
  if p2=1 then
  begin
    //Имя получателя не указано - широковещательное сообщение.
    for i := 0 to ServerSocket1.Socket.ActiveConnections-1 do
      ServerSocket1.Socket.Connections[i].SendText(str);
  end
  else
    //Имя получателя указано - именное сообщение.
    if k = -1 then
    begin
      for i := 0 to ServerSocket1.Socket.ActiveConnections-1 do
        if ServerSocket1.Socket.Connections[i].SocketHandle = hndl then
             ServerSocket1.Socket.Connections[i].SendText('Получатель '
                   +s_rec+' не обнаружен');
    end
    else
    begin
      //Имя получателя указано - именное сообщение.
      for i := 0 to ServerSocket1.Socket.ActiveConnections-1 do
        if ServerSocket1.Socket.Connections[i].SocketHandle =
                    s_handles [k] then
            ServerSocket1.Socket.Connections[i].SendText(str);
    end;
end;

    Основная часть кода здесь обслуживает вариант, когда сообщение правляется конкретному пользователю. Сначала разбирается полученная строка по разделителям "|" и в переменные s_send и s_rec помещаются имена отправителя и получателя. Далее в цикле дескриптор сокета, отправившего сообщение, сравнивается с дескрипторами из массива клиентов. Если его там не оказалось, то массивы клиентских дескрипторов и имен дописываются соответствующими значениями. Далее, с оператора if p2 <>1 начинается фрагмент, в котором ищется индекс элемента массива имен получателей, соответствующего имени получателя. Потом по этому индексу и дескриптору из массива дескрипторов s_handles мы выберем сокет получателя. Затем, "по новой" собирается строка str.

    str := s_send+'|'+copy (str,p2+1,length(str)-p2);

    Из строки удаляется имя получателя - в таком виде она будет отправляться адресату. Дальше происходит отправка сообщения. Здесь предусмотрено три варианта: сообщение отправляется всем участникам; если указано неизвестное имя получателя, отправитель получает сообщение об ошибке, а само сообщение не отправляется никому; если указано верное имя, то сообщение отправляется адресату. В первом случае в цикле по количеству соединений вызывается метод SendText, для каждого активного соединения. Для двух других случаев, выполняется поиск сокета, отправившего сообщение и сокета, который должен получить сообщение, и метод SendText вызывается уже один раз.

    На этом разбор серверного приложения мы заканчиваем и вернемся к клиентской части. О компонентах формы и заданных начальных значениях адреса и порта сервера мы уже говорили. После загрузки клиентского приложения, пользователю обязательно нужно нажать на кнопку Подключиться, что вызовет запуск следующей процедуры.

procedure TForm1.Button2Click(Sender: TObject);
//Кнопка "Подключиться"
begin
  if not ClientSocket1.Active then
           ClientSocket1.Active := True;
end;

    Здесь после проверки значения свойства Active, если сокет не активен, он переводится в активное состояние - свойству Active присваиваете значение True.

    При нажатии на клавишу Выход выполняется следующий код:

procedure TForm1.Button3Click(Sender: TObject);
//Кнопка "Выход"
begin
  ClientSocket1.Active := False;
  ClientSocket1.Close;
  Close;   
end;

    Здесь сокет переводится в пассивное состояние, соединение закрывается и приложение завершает свою работу. Ниже приводится обработчик нажатия на клавишу Отправить. Если имя отправителя не указано, выводится сообщение "Укажите имя отправителя". Если указано имя получателя, то на сервер отправляется строка с заданным именем. Если имя получателя не указано, то отправляется строка с пропущенным именем получателя.

procedure TForm1.Button1Click(Sender: TObject);
//Кнопка "Послать"
begin
  if  length(trim(Edit2.Text))>0 then
    if  length(trim(Edit3.Text))>0 then
       ClientSocket1.Socket.SendText(Edit2.Text+ '|'+Edit3.Text+ '|'+Edit1.Text)
    else
       ClientSocket1.Socket.SendText(Edit2.Text+'||'+Edit1.Text)
  else ShowMessage('Укажите имя отправителя');
end;

    Остается рассмотреть обработчик события OnRead, вызываемого, когда клиенту пришли данные и он должен их прочитать. В обработчике полученная строка просто прибавляется к строкам списка.

procedure TForm1.ClientSocket1Read(Sender: TObject;
  Socket: TCustomWinSocket);
var
  str: string;
begin
  str:=Socket.ReceiveText;
  ListBox1.Items.Add(str);
end;

    Последняя небольшая процедура изменяет заголовок клиентской формы, в который добавляется имя отправителя, если это имя было изменено пользователем.

procedure TForm1.Edit2Exit(Sender: TObject);
begin
  Form1.Caption := 'Клиент "'+Edit2.Text+'"';
end;

    Текст серверного приложения можно взять здесь.

    Текст клиентского приложения можно взять здесь.

    Со следующего шага мы начнем рассматривать компоненты вкладки InternetExpress.




Предыдущий шаг Содержание Следующий шаг