определить, что делать с полученным сообщением.
Возможны следующие сообщения от клиента серверу:
• name: имя_пользователя – при помощи этого сообщения клиентская программа сообщает серверу, под каким именем зарегистрировать пользователя (это имя будут видеть другие пользователи);
• text: текст – при получении этого сообщения сервер должен разослать текст всем участникам разговора (включая отправителя);
• имя_адресата: текст – при получении этого сообщения сервер должен отправить текст только заданному префиксом пользователю имяадресата, а также должен отправить копию автору сообщения.
К сообщениям третьего типа относятся все сообщения, принимаемые сервером и не начинающиеся с text: или name:.
В свою очередь, сервер может посылать клиентской программе сообщения следующего вида:
• ok: – означает, что пользователь зарегистрирован и может вступать в разговор;
• error: сообщение_об_ошибке – означает, что по каким-то причинам пользователь не может участвовать в разговоре. При получении этого сообщения клиентская программа должна показать окно с текстом сообщение_об_ошибке и разорвать соединение с сервером;
• adduser: имя_пользователя – при получении такого сообщения клиентская программа должна добавить строку имя_пользователя в список участников разговора;
• deluser: имя_пользователя – при получении такого сообщения клиентская программа должна удалить строку имя_пользователя из списка участников разговора;
• text: текст – клиентская программа должна добавить текст к тексту разговора.
Перед рассмотрением реализации клиентской и серверной частей скажем несколько слов об использовании специальных сообщений клиента (name: имя пользователя) и сервера (ok: и error: сообщение_об_ошибке). Дело в том, что в предлагаемой реализации сервера присоединение нового пользователя к разговору происходит следующим образом.
1. Клиентское приложение присоединяется к серверу (количество пользователей ограничено, поэтому сервер может послать лишнему пользователю сообщение error: с соответствующим текстом, описывающим ошибку, и тут же разорвать установленное соединение).
2. Клиентское приложение посылает серверу сообщение с именем пользователя (префикс name:).
3. Если имя, под которым хочет зарегистрироваться новый пользователь, используется, то клиентскому приложению отправляется сообщение error: с пояснением ошибки.
4. Если имя свободно, то сервер сохраняет его (и рассылает его всем остальным клиентским приложениям), а также посылает приложению присоединенного пользователя список всех остальных пользователей, и только после этого дает новому пользователю возможность участвовать в разговоре (сообщение ok:).
Остальные нюансы будут рассмотрены при описании исходного кода клиентского и серверного приложений.
Реализация сервера
Серверное приложение реализовано с оконным интерфейсом. Форма f rmServer приложения во время работы представлена на рис. 11.6.
Рис. 11.6. Форма сервера сообщений
Элемент управления ListBox (имя IstEvents), который можно увидеть на форме, предназначен для вывода списка событий (присоединение, отсоединение клиентов, передача сообщений). Список помещается в рамку GroupBoxl. Для списка и рамки задано значение свойства align = client.
Кроме перечисленных элементов управления, на форму также помещены компоненты IdTCPServer (имя TCPServer)n Timer (имя Timerl). Для таймера задаются значения свойств Enabled = True и Intervel = 50. Компонент TCPServer настраиваем на прослушивание порта 12345, а также устанавливаем значение свойства Active = True.
При реализации сервера основной программный код помещен в файле формы (Unitl. pas). Этот модуль условно можно разделить на две части: в первой реализованы специальные функции и процедуры (регистрации пользователей, пересылки текстовых сообщений между пользователями и т. д.), во второй части следуют процедуры-обработчики событий (методы класса TfrmServer).
Сначала рассмотрим процедуры обработки событий, так как они значительно проще, чем остальные функции и процедуры, и их рассмотрение вначале позволит лучше представить функционирование приложения (листинг 11.9).Листинг 11.9.
Процедуры обработки событий серверного приложения (Unitl.pas)
procedure TfrmServer.Timer1Timer(Sender: TObject);
begin
//Если нужно, то скроем окно сервера
if (not SERVERVISIBLE) then
begin
frmServer.Visible := False;
ShowWindow(Application.Handle,SW_HIDE);
end;
//Таймер больше не нужен
Timer1.Enabled := False;
end;
procedure TfrmServer.TCPServerExecute (AThread: TIdPeerThread);
begin
//Обработаем сообщение, пришедшее от клиента
ProcessMessage(AThread.Connection, AThread.Connection.ReadLn);
end;
procedure TfrmServer.TCPServerConnect(AThread: TIdPeerThread);
begin
//Попытаемся добавить нового пользователя
if (AddClient (AThread.Connection)) then
//Пользователь должен прислать свое имя
ProcessMessage (AThread.Connection, AThread.Connection.ReadLn)
else
begin
//Нет места для нового пользователя
AThread.Connection.WriteLn('error:Достигнуто максимальное
количество ' + 'пользователей. Извините, невозможно принять вас
в разговор. ');
AThread.Connection.Socket.Close;
end;
end;
procedure TfrmServer.TCPServerDisconnect(AThread: TIdPeerThread);
var clDisconnected: client; //Структура с информацией об
//отсоединенном клиенте (заполнены
//только поля strName и strIP)
begin
//Удалим информацию об отсоединенном клиенте
clDisconnected := DeleteClient(AThread.Connection);
if (clDisconnected.strName <> '') then
begin
//Сообщим о событии остальным клиентам
SendAll('deluser:' + clDisconnected.strName);
SendAll('Нас покинул «' + clDisconnected.strName + '».’);
//Добавим событие в журнал
if (REPORT) then AddEvent('Отсоединился клиент '' +
clDisconnected.strName + '' на компьютере '' +
clDisconnected.strIP + ''');
end;
end;
procedure TfrmServer.FormCreate(Sender: TObject);
begin
//Создаем критическую секцию
section := TCriticalSection.Create;
end;
Первая и последняя из приведенных в листинге 11.9 процедур не имеют непосредственного отношения к работе TCP-сервера. Процедура Tf rmServer. TimerlTimer вызывается только один раз при первом срабатывании таймера Timer 1. В ней, исходя из заданного значения глобальной переменной SERVERVISIBLE, происходит (или не происходит) скрытие окна сервера. Значение глобальной переменной SERVERVISIBLE (и переменной REPORT) определяется в момент запуска сервера.
Процедура Tf rmServer. FormCreate создает объект синхронизации, используемый остальными функциями и процедурами для предотвращения одновременного доступа к общим данным нескольких потоков (ведь сервер-то у нас многопоточный).
Остальные три процедуры используются непосредственно для организации взаимодействия сервера с клиентами. Как было сказано ранее, сервер хранит информацию о присоединенных к нему клиентах. Хранилищем этой информации является массив структур (подробно он будет рассмотрен немного ниже). Здесь же необходимо сказать, что при присоединении к серверу нового клиента (процедура Tf rmServer. TCPServerConnect) предпринимается попытка найти для информации о новом пользователе место в указанном массиве (вызов функцшФ^СНеп^. Если место нашлось, то функция AddClient возвращает True, и сервер переходит в режим регистрации пользователя. Для регистрации клиентская программа должна передать серверу имя пользователя (сообщение с префиксом name:).
Особенностью реакции сервера на отключение клиентской программы (процедура Tf rmServer. TCPServerDisconnect) является то, что, помимо удаления информации об отсоединившемся клиенте (вызов функции DeleteClient), все остальные пользователи уведомляются об отсоединении собеседника (вызовы функции SendAll).
При получении сообщения от клиента (процедура Tf rmServer. Execute) происходит всего лишь передача полученной строки функции ProcessMessage, которая и занимается анализом текста сообщения и определением действий, которые сервер должен выполнять.
Теперь рассмотрим функции и процедуры, которые прямо или косвенно используются описанными выше обработчиками событий и на которых по большей части и основывается работа серверного приложения. Часть файла Unitl.pas, содержащая объявление типов данных, переменных и подключения модулей (добавленные вручную), которые нужны для работы сервера, приведена в листинге 11.10.
Листинг 11.10.
Типы данных и переменные серверного приложения (Unitl.pas)
unit Unit1;
interface
uses
…, SyncObjs;
type
TfrmServer = class(TForm)