КОМПЬЮТЕРНЫЕ КУРСЫ "ПОИСК"
[Главная страница] [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.