КОМПЬЮТЕРНЫЕ КУРСЫ "ПОИСК"
[Главная страница] [Delphi] [Контакты]
На основе компонентов IdTCPClient и IdTCPServer для организации сетевого взаимодействия рассмотрим создание полноценного клиент-серверного приложения — многопользовательского разговорника. Как можно догадаться из названия, это приложение будет позволять обмениваться сообщениями большому количеству пользователей (наподобие чата).
Рассмотрим подробно основные этапы его проектирования, разработки и реализации (начиная с требований и поведения клиента и сервера и заканчивая нюансами реализации приложений).
Требования к клиентскому и серверному приложениям
Пользователи при работе с клиентскими приложениями должны иметь следующие возможности:
Серверное приложение, кроме управления подключением, отключением пользователей, а также доставки сообщений, должно обеспечивать протоколирование событий (подключение, отключение пользователей, от кого и кому послано то или иное сообщение).
При реализации серверного приложения нужно преодолеть некоторые сложности, связанные с тем, что к серверу будут постоянно подключены сразу несколько пользователей, причем информация о каждом пользователе будет постоянно храниться и использоваться сервером. Нужно также обеспечить надежную работу клиентского, а главное — серверного приложения при проблемах, связанных с неисправностями сети.
И, наконец, нужно обеспечить автоматическую рассылку клиентским приложениям следующей информации (клиенты эту информацию специально с сервера не запрашивают):
Формат сообщений клиента и сервера
Клиент и сервер обмениваются только текстовыми сообщениями (не путать с сообщениями, которыми обмениваются пользователи в ходе разговора). Строка любого сообщения состоит из двух частей: префикса и текста сообщения. Префикс отделяется от текста сообщения символом : (двоеточие). По префиксу можно определить, что делать с полученным сообщением.
Возможны следующие сообщения от клиента серверу:
К сообщениям третьего типа относятся все сообщения, принимаемые сервером и не начинающиеся с text: или name:.
В свою очередь, сервер может посылать клиентской программе сообщения следующего вида:
Перед рассмотрением реализации клиентской и серверной частей скажем несколько слов об использовании специальных сообщений клиента (name:имя_пользователя) и сервера (ок: и error:сообщение_об_ошибке). Дело в том,что в предлагаемой реализации сервера присоединение нового пользователя к разговору происходит следующим образом.
Остальные нюансы будут рассмотрены при описании исходного кода клиентского и серверного приложений.
Реализация сервера
Рис. 1.
Форма сервера сообщений
Элемент управления ListBox (имя lstEvents), который можно увидеть на форме, предназначен для вывода списка событий (присоединение, отсоединение клиентов, передача сообщений). Список помещается в рамку GroupBox1. Для списка и рамки задано значение свойства align = client.
Кроме перечисленных элементов управления, на форму также помещены компоненты IdTCPServer (имя TCPServer) и Timer (имя Timer1). Для таймера задаются значения свойств Enabled = True и Intervel = 50. Компонент TCPServer настраиваем на прослушивание порта 12345, а также устанавливаем значение свойства Active = True.
При реализации сервера основной программный код помещен в файле формы (Unit1.pas). Этот модуль условно можно разделить на две части: в первой реализованы специальные функции и процедуры (регистрации пользователей, пересылки текстовых сообщений между пользователями и т. д,), во второй части следуют процедуры-обработчики событий (методы класса TfrmServer).
Сначала рассмотрим процедуры обработки событий, так как они значительно проще, чем остальные функции и процедуры, и их рассмотрение вначале позволит лучше представить функционирование приложения (листинг 1).
Листинг 1. Процедуры обработки событий серверного приложения (Unit1.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;
Первая и последняя из приведенных в листинге 1 процедур не имеют непосредственного отношения к работе TCP-сервера. Процедура TfrmServer. Timer1Timer вызывается только один раз при первом срабатывании таймера Timer1. В ней, исходя из заданного значения глобальной переменной SERVERVISIBLE, происходит (или не происходит) скрытие окна сервера. Значение глобальной переменной SERVERVISIBLE (и переменной REPORT) определяется в момент запуска сервера.
Процедура TfrmServer.FormCreate создает объект синхронизации, используемый остальными функциями и процедурами для предотвращения одновременного доступа к общим данным нескольких потоков (ведь сервер-то у нас многопоточный).
Остальные три процедуры используются непосредственно для организации взаимодействия сервера с клиентами. Как было сказано ранее, сервер хранит информацию о присоединенных к нему клиентах. Хранилищем этой информации является массив структур (подробно он будет рассмотрен немного ниже). Здесь же необходимо сказать, что при присоединении к серверу нового клиента (процедура TfrmServer.TCPServerConnect) предпринимается попытка найти для информации о новом пользователе место в указанном массиве (вызов функции AddClient). Если место нашлось, то функция AddClient возвращает True, и сервер переходит в режим регистрации пользователя. Для регистрации клиентская программа должна передать серверу имя пользователя (сообщение с префиксом name:).
Особенностью реакции сервера на отключение клиентской программы (процедура TfrmServer.TCPServerDisconnect) является то, что, помимо удаления информации об отсоединившемся клиенте (вызов функции DeleteClient), все остальные пользователи уведомляются об отсоединении собеседника (вызовы функции SendAll).
При получении сообщения от клиента (процедура TfrmServer.Execute) происходит всего лишь передача полученной строки функции ProcessMessage, которая и занимается анализом текста сообщения и определением действий, которые сервер должен выполнять.
Теперь рассмотрим функции и процедуры, которые прямо или косвенно используются описанными выше обработчиками событий и на которых по большей части и основывается работа серверного приложения. Часть файла Unit1.pas, содержащая объявление типов данных, переменных и подключения модулей (добавленные вручную), которые нужны для работы сервера, приведена в листинге 2.
Листинг 2. Типы данных и переменные серверного приложения (Unit1.pas)
unit Unit1;
interface
uses
..., SyncObjs;
type
TfrmServer = class(TForm)
lstEvents: TListBox;
...
end;
var
frmServer: TfrmServer;
REPORT: Boolean; //Если = True, то все события
//записываются в ListBox
//окна сервера
SERVERVISIBLE: Boolean; //Если = True, то показывается
//на экране и приложение есть
//на Панели задач
implementation
{$R *.dfm}
//Следующая структура используется для хранения информации
//о пользователе, подключившись к серверу
type
client = record
fUsed: Boolean; {Ячейка свободна}
fNamed: Boolean; {Клиент сообщил свое имя}
strName: String;{Ия пользователя}
strIP: String; {IP-адрес клиента}
Connection: TIdTCPServerConnection; {Соединение клиента с сервером}
end;
const
MAX_CLIENT = 100; //Максимальное количество клиентов
var
clients: array [1..MAX_CLIENT] of client; //Массив со
//сведениями о клиентах
section: TCriticalSection; //Критическая секция
//для синхронизации потоков
Процедура, записывающая событие в журнал (ListBox на форме сервера), приведена в листинге 3.
Листинг 3. Добавление события в журнал
procedure AddEvent(strEvent: string); begin section.Enter; frmServer.lstEvents.Items.Append(strEvent); section.Leave; end;
В листинге 4 приводится процедура, рассылающая текстовое сообщение всем присоединенным к серверу клиентам.
Листинг 4. Рассылка сообщения всем клиентам
procedure SendAll(strMessage: String);
var
i: Integer;
begin
for i := 1 to MAX_CLIENT do
if (clients[i].fNamed) then
begin
try
clients[i].Connection.WriteLn(strMessage);
except
//при возникновении ошибки отключим клиента
//и продолжим рассылку
ErrorCloseConnection(clients[i].Connection);
end;
end;
end;
Далее, в листинге 5, приведена процедура, посылающая текстовое сообщение strMessage клиенту с заданным именем strName.
Листинг 5. Посылка сообщения клиенту с заданным номером
procedure SendTo(strMessage: string; strName: string);
var
i: Integer;
begin
if (clients[i].fNamed) then
if (clients[i].strName = strName) then
//Нашли клиента с заданным именем
try
clients[i].Connection.WriteLn(strMessage);
except
//При возникновении ошибки отключим клиента
//и продолжим рассылку
ErrorCloseConnection(clients[i].Connection);
end;
end;
Процедура, приведенная в листинге 6, находит и помечает как занятую для нового пользователя запись в массиве clients. Если свободных записей в массиве не осталось, то достигнуто максимальное количество пользователей.
Листинг 6. Добавление информации о новом клиенте
function AddClient(Connection: TIdTCPServerConnection): Boolean;
var
i: Integer;
begin
section.Enter;
for i := 1 to MAX_CLIENT do
begin
if (not clients[i].fUsed) then
begin
//Нашли свободную запись - заполним ее
//(клиент пока безымянный)
clients[i].fUsed := True;
clients[i].Connection := Connection;
clients[i].strIP := Connection.Socket.Binding.PeerIP;
AddClient := True;
section.Leave;
Exit;
end;
end;
section.Leave;
AddClient := False;
end;
Процедура DeleteClient, приведенная в листинге 7, освобождает запись заданного пользователя в массиве clients.
Листинг 7. Удаление информации о клиенте
function DeleteClient(Connection: TIdTCPServerConnection): client;
var
i: Integer;
begin
section.Enter;
for i := 1 to MAX_CLIENT do
begin
if (clients[i].fUsed) then
if (clients[i].Connection = Connection) then
begin
//Вот она - запись о нужном клиенте
clients[i].fUsed := False;
clients[i].fNamed := False;
clients[i].Connection := Nil;
DeleteClient := clients[i];
clients[i].strName := '';
clients[i].strIP := '';
section.Leave;
Exit;
end;
end;
end;
Процедура SendClientList, приведенная в листинге 8, отправляет клиентской программе заданного пользователя (только что зарегистрировавшегося) сообщения addclient: с именем каждого зарегистрированного ранее пользователя.
Листинг 8. Посылка списка всех присоединенных клиентов
procedure SendClientList(Connection: TIdTCPServerConnection);
var
i: Integer;
begin
for i := 1 to MAX_CLIENT do
if (clients[i].fNamed) then
if (clients[i].Connection <> Connection) then
try
//Сообщим имя очередного найденного пользователя
Connection.WriteLn('adduser:' + clients[i].strName);
except
//При возникновении ошибки отключим клиента
//и продолжим рассылку
ErrorCloseConnection(clients[i].Connection);
end;
end;
Процедура ЕrrorCloseConnect ion (листинг 9) вызывается при ошибке отправки сообщений пользователям (например, при нарушении сетевого соединения). Она отключает пользователя, соединение с которым работает с ошибками, и сообщает об этом другим пользователям.
Листинг 9. Закрытие соединения с клиентом (при возникновении ошибки)
procedure ErrorCloseConnection(Connection: TIdTCPServerConnection);
var
clError: client; //Информация о пользователе, соединение
//с которым прервалось (толькоимя и IP)
begin
//Отлючим соединение, работающее с ошибками
clError := DeleteClient(Connection);
//Сообщим об отлючении остальным пользователям
SendAll('deluser:' + clError.strName);
SendAll('Нас покинул "' + clError.strName + '".');
//Добавим событие в журнал
if (REPORT) then AddEvent('Из-за ошибки отсоединен клиент "' +
clError.strName + '" на компьютере "' + clError.strIP + '"');
end;
Процедура RegisterClient, приведенная в листинге 10, регистрирует пользователя под указанным в сообщении name: именем (ранее выполнялась функция AddClient, которая нашла для записи этого пользователя место в массиве clients). Если имя, под которым хочет зарегистрироваться пользователь, уже используется , то клиентской программе посылается соответствующее уведомление, после чего соединение разрывается.
Листинг 10. Регистрация нового клиента
procedure RegisterClient(Connection: TIdTCPServerConnection; strName: String);
var
i: Integer;
begin
//Проверим, чтобы имя клиента еще не использовалось
for i := 1 to MAX_CLIENT do
begin
if (clients[i].fNamed) then
if (clients[i].strName = strName) then
begin
//Дублирование имени - придется разрывать соединение
Connection.WriteLn('error:Пользователь с именем "' +
strName + '" уже существует в разговоре.');
DeleteClient(Connection);
Connection.Socket.Close;
Exit;
end;
end;
//Поиск записи о нужном клиенте и присвоение ему имени
for i := 1 to MAX_CLIENT do
begin
if (not clients[i].fNamed and clients[i].fUsed) then
if (clients[i].Connection = Connection) then
begin
//Вот он, наш клиент...
clients[i].fNamed := True;
clients[i].strName := strName;
//Сообщим другим о появлении нового участника
SendAll('adduser:' + strName);
SendAll('text:К нам присоединился "' + strName + '". Поприветствуем!');
//Отсылаем новому клиенту список остальных участников разговора
SendClientList(Connection);
//Разрешим новому клиенту отсылать сообщения
Connection.WriteLn('ok:');
//Если нужно, то добавим событие в список
if (REPORT) then AddEvent('Присоединен клиент "' +
strName + '" на компьютере "' +
Connection.Socket.Binding.PeerIP + '"');
end;
end;
end;
В листинге 11 приведена служебная функция, возвращающая имя пользователя по ссылке на объект TIdTCPServerConnection, соответствующий этому клиенту.
Листинг 11. Определение имени клиента по его соединению с сервером
function GetClientName(Connection: TIdTCPServerConnection): String;
var
i: Integer;
begin
for i := 1 to MAX_CLIENT do
if (clients[i].fNamed) then
if (clients[i].Connection.Socket.Binding.Handle =
Connection.Socket.Binding.Handle) then
begin
GetClientName := clients[i].strName;
Exit;
end;
end;
И, наконец, в листинге 12 приводится главная процедура серверного приложения, обрабатывающая сообщения, полученные от клиентов.
Листинг 12. Обработка сообщения от клиента
procedure ProcessMessage(Connection: TIdTCPServerConnection; strMessage: String);
var
strName: String; //Имя отправителя сообщения
strAction: String; //Строка с обозначением действия (префикс)
len: Integer; //Длина строки strAction
begin
//Определим действие, которое хочет выполнить клиент
len := Pos(':', strMessage);
strAction := Copy(strMessage, 1, len - 1);
Delete(strMessage, 1, len);
if (strAction = 'name') then
begin
//Клиент сообщает свое имя - пытаемся его зарегестрировать
RegisterClient(Connection, strMessage);
end
else if (strAction = 'text') then
begin
//Клиент передает сообщение всем - подпишем соощение и отошлем
strMessage := GetClientName(Connection) + ': ' + strMessage;
SendAll('text:' + strMessage);
//Если надо, то сохраним сообщение в списке событий
if (REPORT) then
AddEvent('Сообщение от ' + strMessage);
end
else
begin
//Клиент передает сообщение определенному собеседнику
//(строка strAction содержим имя собеседника)
strName := GetClientName(Connection);
SendTo('text:' + strName + ': ' + strMessage, strAction);
if (strName <> strAction) then
//Передадим копию сообщения отправителю
Connection.WriteLn('text:' + strName + ' для ' +
strAction + ': ' + strMessage);
//Если надо, то сохраняем сообщения отправителю
if (REPORT) then
AddEvent('Сообщение для ' + strAction + ' от ' + strName +
': ' + strMessage);
end;
end;
Информация о каждом новом пользователе (участнике разговора) хранится в отдельной структуре client:
type
client = record
fUsed: Boolean; {Ячейка свободна}
fNamed: Boolean; {Клиент сообщил свое имя}
strName: String;{Ия пользователя}
strIP: String; {IP-адрес клиента}
Connection: TIdTCPServerConnection; {Соединение клиента с сервером}
end;
Непосредственно к пользователю имеют отношение три последних поля структуры. Самым полезным из них является ссылка на объект TIdTCPServerConnection, с помощью которой сервер может в любое время отправить данные определенному пользователю.
Информация обо всех пользователях хранится в массиве clients. Его размер органичен (константа MAXCLIENT) и определяет максимальное количество пользователей — участников разговора. Так как используется массив с постоянным количеством элементов, то можно применять специальный флаг (поле fUsed) для индикации того, что ячейка массива занята (значение Тrue) или свободна (значение False). Поле fName структуры client используется для фиксации факта сообщения клиентской программой имени пользователя (клиентские программы незарегистрированных пользователей сообщения не получают). Изначально значение поля fNamed равно False и устанавливается в True, только если имя пользователя сообщено серверу и не используется одним из участников разговора.
Одним из самых сложных моментов работы рассматриваемого сервера является обеспечение синхронизации доступа к массиву clients. Для этого используется критическая секция. Она также применяется для синхронизации добавления событий в список lstEvents сервера.
И, наконец, последний момент в реализации сервера. Чтобы сервер можно было запускать с отключенным протоколированием событий, а также чтобы окно сервера не мешало пользователю, можно хранить значения переменных REPORT и SERVERVISIBLE в INI-файле. Так, собственно, и сделано: значения этих переменных хранятся в секции [Common] файла Server.ini. Для считывания значений из INI-файла при запуске сервера код в модуле Server (файл Server.dpr) изменен следующим образом (листинг 13).
Листинг 13. Изменения в модуле Server
program server;
uses
Forms,
Unit1 in 'Unit1.pas',
IniFiles, Dialogs;
{$R *.res}
var
{Переменные из INI-файла}
config: TIniFile;
strPath: string;
begin
//Грузим информацию из INI-файла
strPath := Copy(Application.ExeName, 1, Length(Application.ExeName) - 3) +
'ini';
config := TIniFile.Create(strPath);
SERVERVISIBLE := config.ReadBool('Common', 'ServerVisible', False);
REPORT := config.ReadBool('Common', 'EventReport', False);
config.Free;
try
//Запуск сервера
Application.Initialize;
Application.CreateForm(TfrmServer, frmServer);
Application.Run;
except
MessageDlg('Не удается запустить сервер сообщений.' +
'Возможно, он был ранее запущен.', mtError, [mbOk], 0);
end;
end.
В приведенном листинге код создания формы помещен в блок try. Сделано это только для того, чтобы сервер не "падал" с выдачей всем прекрасно знакомого окна о критической ошибке при попытке ошибочного запуска своей копии.
Соответственно, INI-файл для запуска сервера с видимым окном и включенным протоколированием имеет следующий вид:
[Common] ServerVisible=1 EventReport=1
Реализация клиентского приложения
Проект клиентской программы имеет имя Client. Внешний вид формы клиентского приложения во время его работы представлен на рис. 2.
Рис. 2
Приведенная на рис. 2 форма имеет имя frmClient. Свойства (только существенные для работы приложения) основных элементов управления, помещенных на форму, приведены в табл. 1.
Таблица 1.Свойства элементов управления формы frmClient
| Название (свойство Name) | Описание | Измененные свойства и их новые значения |
|---|---|---|
| TCPClient | Элемент управления IdTCPClient | MaxLineAction = msSplit Port = 12345 ReadTimeout = 1 |
| Timer1 | Таймер (Timer) - используется для проверки прихода уведомления от сервера | Enabled = True Interval = 50 |
| lstUsers | Список участников разговора (ListBox) в левой части формы | Enabled = False Sorted = False |
| txtChat | Элемент Memo с текстом разговора | Enabled = False ReadOnly = True ScrollBars = ssVertical |
| txtMessage | Текстовое поле (Edit) для ввода и редактирования сообщений внизу формы | Enabled = False |
| cmbSend | Кнопка (Button) отправки сообщения, введенного в текстовое поле txtMessage | Caption = >> Default = True Enabled = False Font.Style = [fsBold] |
| txtServer | Текстовое поле (Edit) для указания имени или IP-адреса сервера | Отсутствует |
| txtUser | Текстовое поле (Edit) для указания имени пользователя | Отсутствует |
| cmbConnect | Кнопка (Button) подключения к указанному в txtServer серверу | Caption = Подключиться |
Далее приведены функции и процедуры, не являющиеся обработчиками событий, но имеющие большое значение для работы клиентского приложения.
Приведенная в листинге 14 процедура обновляет форму при удачном подключении к серверу.
Листинг 14. Обновление формы при присоединении к серверу
procedure Connect();
begin
with frmClient do
begin
cmbConnect.Caption := 'Отключиться';
txtUser.Enabled := False;
txtServer.Enabled := False;
Caption := 'Разговорник [' + txtUser.Text + ' подкючен к ' +
txtServer.Text + ']';
lstUsers.Enabled := True;
cmbSend.Enabled := True;
txtMessage.Enabled := True;
txtChat.Enabled := True;
end;
end;
Процедура Disconnect, приведенная в листинге 15, обновляет форму при отключении от сервера (в таком виде форма frmClient предстает первоначально).
Листинг 15. Обновление формы при отсоединении от сервера
procedure Disconnect();
begin
with frmClient do
begin
cmbConnect.Caption := 'Подключиться';
txtUser.Enabled := True;;
txtServer.Enabled := True;
Caption := 'Разговорник';
lstUsers.Enabled := False;
lstUsers.Clear;
cmbSend.Enabled := False;
txtMessage.Enabled := False;
txtChat.Enabled := False;
end;
end;
Процедура ProcessMessage (листинг 16) обрабатывает сообщение, полученное от сервера, аналогично такой же процедуре в серверном приложении (естественно, сообщения и реакция на них отличны от серверных).
Листинг 16. Обработка строки полученной от сервера
procedure ProcessMessage(strMessage: String);
var
strAction: String; //Тип сообщения (префикс сообщения)
len: Integer; //Длина строки strAction
begin
//Определим действие, которое хочет выполнить клиент
len := Pos(':', strMessage);
strAction := Copy(strMessage, 1, len - 1);
Delete(strMessage, 1, len);
if (strAction = 'ok') then
begin
//Регистрация пользователя завершена - можно отправлять
//сообщения
Connect;
end
else if (strAction = 'error') then
begin
//Ошибка!!!
frmClient.TCPClient.Disconnect;
Disconnect;
MessageDlg(strMessage, mtError, [mbOK], 0);
end
else if (strAction = 'adduser') then
begin
//К разговору присоединится новый пользователь
frmClient.lstUsers.Items.Add(strMessage);
end
else if (strAction = 'deluser') then
begin
//Какой-то пользователь отсоединился
frmClient.lstUsers.Items.Delete(
frmClient.lstUsers.Items.IndexOf(strMessage));
end
else
begin
//Покажем принятое сообщение
frmClient.txtChat.Lines.Add(strMessage);
end;
end;
Далее приводятся обработки событий, на которых, собственно, и основана работа клиентской программы. Обработчик нажатия кнопки cmbConnect, приведенный в листинге 17, пытается присоединиться к серверу. Если клиент присоединен к серверу, то эта же кнопка используется для его отсоединения.
Листинг 17. Присоединение/отсоединение от сервера
procedure TfrmClient.cmbConnectClick(Sender: TObject);
begin
if (cmbConnect.Caption = 'Подключиться') then
begin
//Проверим, чтобы были введены имя сервера
//и имя пользователя
if (txtServer.Text = '') then
begin
MessageDlg('Введите имя сервера в текстовом поле.', mtInformation,
[mbOK], 0);
Exit;
end
else if (txtUser.Text = '') then
begin
MessageDlg('Введите имя пользователя в текстовом поле.', mtInformation,
[mbOK], 0);
Exit;
end;
//Пытаемся подключиться к серверу
try
TCPClient.Host := txtServer.Text;
TCPClient.Connect;
except
MessageDlg('Не удается соединиться с сервером', mtError, [mbOK], 0);
end;
end
else
//Отключаемся от сервера
TCPClient.Disconnect;
end;
Обработчик нажатия кнопки cmbSend (листинг 18) отправляет сообщение, которое могут прочесть все пользователи, присоединенные к серверу.
Листинг 18. Отправка сообщения всем собеседникам
procedure TfrmClient.cmbSendClick(Sender: TObject);
begin
if (txtMessage.Text <> '') then
begin
//Тправка сообщения всем собеседникам
TCPClient.WriteLn('text:' + txtMessage.Text);
txtMessage.Text := '';
txtMessage.SetFocus;
end;
end;
При двойном щелчке кнопкой мыши на имени в списке пользователей отправляется сообщение, которое получает только выделенный в списке пользователь (листинг 19).
Листинг 19. Отправка сообщения заданному собеседнику
procedure TfrmClient.lstUsersDblClick(Sender: TObject);
begin
if ((lstUsers.ItemIndex >= 0) and (txtMessage.Text <> '')) then
begin
//Отправим сообщение только для выбранного собеседника
//(сообщение вида "имя_собеседника:текст_сообщения")
TCPClient.WriteLn(lstUsers.Items.Strings[lstUsers.ItemIndex] +
'+' + txtMessage.Text);
txtMessage.SetFocus;
end;
end;
Сразу после соединения с сервером, тоесть в обработчикеТfrmClient.ТСPClientConnected, приведенном в листинге 20, клиентская программа отправляет имя пользователя серверу. При отсоединении от сервера (тот же листинг 20) происходит соответствующее оформление внешнего вида формы frmClient.
Листинг 20. Обработка присоединения/отсоединения от сервера
procedure TfrmClient.TCPClientConnected(Sender: TObject);
begin
//Отправляем на сервер имя пользователя
TCPClient.WriteLn('name:' + txtUser.Text);
end;
procedure TfrmClient.TCPClientDisconnected(Sender: TObject);
begin
//Оформим форму для отсоединения от сервера состояния
Disconnect;
end;
Ключевой обработчик (именно по таймеру проверяется факт прихода сообщения от сервера) приведен в листинге 21. Для элемента управления TCPClient значение тайм-аута установлено для того, чтобы при отсутствии принятых данных клиентская программа не переходила надолго в состояние ожидания, а генерировалось исключение, по которому и можно судить, что данных еще нет (см. блок try в этом обработчике).
Листинг 21. Проверка, есть ли данные от сервера
procedure TfrmClient.Timer1Timer(Sender: TObject);
var
strMessage: String;
begin
//Проверим, нет ли для нас сообщения
if (TCPClient.Connected) then
begin
try
strMessage := TCPClient.ReadLn;
if (strMessage <> '') then
ProcessMessage(strMessage);
except
on EIdReadTimeOut do; //Ошибки тайм-аута игнорируем
else
//При остальных ошибках отсоединяемся от сервера
TCPClient.Disconnect;
end;
end;
end;
![]() |
Примечание Чтобы при запуске клиентского приложения из среды Delphi постоянно не появлялись сообщения об исключениях (возникают при истечении тайм-аута в TfrmClient.Timer1Timer), снимите флажок Stop on Delphi Exceptions на вкладке Language Exceptions диалогового окна Debugger Options (меню Tools > Debugger Options). |
Исходный код здесь. Выполнен на Delphi 7.