Главная » Статьи » Программирование в Delphi

Создание многопользовательского чата
В предыдущей статье (“Создание клиент-сервера”) рассказывалось о разработке простейшего чата на двоих пользователей. Структура чата “head-to-head” достаточно проста, ведь есть только один канал, с одной стороны которого сервер, с другой – клиент. Multy-user-структура несколько сложнее. Есть один сервер и множество клиентов. Сервер при этом выполняет обработку входящих сообщений, пересылает их по нужным каналам, регистрирует пользователей и показывает всем, сколько пользователей общаются в текущий момент. В данной статье мы попробуем все это реализовать.

Многопользовательский чат (Multy-user on-line)

Начнем разработку приложения чата с уже готовой формы из предыдущей статьи, или с новой. Вот, что должно быть в форме:

PortEdit (Edit)
HostEdit (Edit)
NikEdit (Edit)
TextEdit (Edit)
ChatMemo (Memo)
ClientBtn (Button)
ServerBtn (Button)
SendBtn (Button)
ServerSocket (ServerSocket)
ClientSocket (ClientSocket)

Компоненты из стандартного пакета Delphi ServerSocket и ClientSocket не всегда могут быть отображены в палитре Internet, и их нужно загрузить следующим образом:

выбрать меню: Component - Install Packages… - Add., далее нужно указать файл …\bin\dclsockets70.bpl.

Добавляются новые компоненты:

UserListView (ListView)
ImageList (ImageList)
ServerTimer (Timer)


UserListView предназначен для вывода списка пользователей, который будет динамически обновляться при подключении или отключении пользователей. Сам компонент ListView настраивается как табличный отчет: свойство ViewStyle = vsReport (стиль таблицы), свойство ShowColumnHeaders = False (не показывать имена столбцов), свойство ReadOnly = True (только отображение), свойство SmallImages = ImageList (массив с иконками). Двойным кликом на компоненте ListView выводится редактор Editing UserListView.Columns. Добавляется один столбец (порядковый номер -0).
В ImageList через Add закидываются иконки, в нашем случае две, с изображением силуэта пользователя: в белом – пометка сервера, в красном – пометка клиента.

Теперь разберем принцип работы сервера. Традиционно в ServerSocket для приема клиентских пакетов используется OnClientRead, но данный способ не очень удобен, ведь для идентификации пакета (кто прислал) потребуется повозиться со структурой “прием\ответ” и решить каким образом будет происходить синхронизация. Гораздо проще и эффективнее использовать цикл по числу пользователей, в котором ведется “прослушивание” всех каналов и обработка пакета, если он пришел на конкретный канал, закрепленный за конкретным пользователем. Процедура “прослушивания” каналов выполняется в теле таймера, интервал (Interval) работы которого можно изменять по необходимости (для чата нормально 500 мс, для игр нужно существенно меньше). Вот так выглядит общая структура процедуры опроса:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
// условие на наличие установленных каналов
if ServerSocket.Socket.ActiveConnections<>0 then
begin
// цикл по существующим каналам
for i:=1 to ServerSocket.Socket.ActiveConnections do
begin
// сохраним пакет (если ничего не прислали, по пакет пустой)
text:=ServerSocket.Socket.Connections.ReceiveText();
// условие, что пакет не пуст
if text<>” then
begin
{тут обработка строки, выделение составляющих кода команд (com) и пр.}
// определение команд
case com of
код: begin
{процедура}
end;
код: begin
{процедура}
end;
…………………………………….
end;
end;
end;
end;
// разрешение на выполнение процедур обновления
if UpdDo=True then
begin
{процедуры}
// блокируем разрешение
UpdDo:=False;
end;
end;

Если заметили, что цикл начинается с единицы, а в инициализации канала странное выражение (вместо логичного начала с нуля и инициализации ), то такое решение существенным образом облегчает организацию ряда процедур. Например, в списке пользователей, сервер числится под номером “0”, а клиенты - начиная с “1”. Так же удобно совмещать количество каналов (ServerSocket.Socket.ActiveConnections) с процедурами определения активности пользователей.
Последнее условие в теле таймера необходимо для задержки выполнения некоторых процедур обновления. Эти процедуры должны выполняться в самом конце “прослушивания” открытых каналов, и не всегда (если будет команда).
Данный алгоритм применим практически к любого рода соединениям Клиент-сервер, в том числе и для игр.

ВНИМАНИЕ! Если вы не уверены в корректности написанного вами кода обработки команд в цикле “прослушивания” открытых каналов, то ВСЕГДА применяйте функцию Try..Except..End;, которая позволит избежать серьезных ошибок, ведь повторяемость цикла может быть очень быстрой. Пример:

Try
{процедура}
Except
// остановка таймера и сопутствующие действия
End;

Перейдем непосредственно к нашему приложению чата и его процедурам. Как и прежде, проверок на корректность ввода значений в поля не будет.
Создадим новый тип, для использования массива объектов, так гораздо удобнее:

Type
TUserList = object
Status: Byte; // 1 - сервер, 2 - клиент
Rec: Boolean; // отметка записи пользователя в список
Name: String; // имя (ник)
Image: Byte; // индекс иконки
end;

Вот переменные, которые понадобятся в программе:

var
Form1: TForm1;
i, j, com, ContList: Byte;
len, pos, x: Word;
text, StrUserList: String;
UpdDo: Boolean;
Buf: array[0..3] of Byte;
UserMas: array[0..255] of TUserList; //массив объектов
UItems: TListItem;

Опишем процедуру OnCreate формы:

procedure TForm1.FormCreate(Sender: TObject);
begin
// заголовок формы
Caption:='Многопользовательский чат';
Application.Title:=Caption;
// предложенное значения порта
PortEdit.Text:='7777';
// адрес при проверке программы на одном ПК ("сам на себя")
HostEdit.Text:='127.0.0.1';
// введем ник по-умолчанию, остальные поля просто очистим
NikEdit.Text:='Ананим';
TextEdit.Clear;
ChatMemo.Lines.Clear;
end;

Процедура “прослушивания” открытых каналов сервером, выглядит так:

procedure TForm1.ServerTimerTimer(Sender: TObject);
begin
// условие на наличие установленных каналов
if ServerSocket.Socket.ActiveConnections<>0 then
begin
// цикл по существующим каналам
for i:=1 to ServerSocket.Socket.ActiveConnections do
begin
// сохраним пакет (если ничего не прислали, по пакет пустой)
text:=ServerSocket.Socket.Connections.ReceiveText();
// условие, что пакет не пуст
if text<>” then
begin
// получим код команды, длину строки
com:=StrToInt(Copy(text,1,1));
len:=Length(text)-1;
// определение команд
case com of
// код приема сообщения
0: begin
// добавим в ChatMemo сообщение клиента
ChatMemo.Lines.Add(Copy(text,2,len));
// разошлем сообщение пользователям (кроме того, кто прислал)
for j:=0 to ServerSocket.Socket.ActiveConnections-1 do
begin
if (j+1)<>i then ServerSocket.Socket.Connections[j].SendText(’0′+Copy(text,2,len));
end;
end;
// код приема ника клиента
1: begin
// запишем в массив полученный ник
UserMas.Name:=Copy(text,2,len);
// отметим, что пользователь записан в список
UserMas.Rec:=True;
// обновляем список
UpdateUserList;
end;
end;
end;
end;
end;
// разрешение на выполнение процедур обновления
if UpdDo=True then
begin
// обновляем массив пользователей
UpdateUserMas;
// обновляем список пользователей
UpdateUserList;
// блокируем разрешение
UpdDo:=False;
end;
end;

Перевод программы в режим сервера осуществляется клавишей “Создать сервер” (ServerBtn). Вот так выглядит процедура на нажатие клавиши ServerBtn (OnClick):

procedure TForm1.ServerBtnClick(Sender: TObject);
begin
if ServerBtn.Tag=0 then
begin
// клавишу ClientBtn и поля HostEdit, PortEdit, NikEdit заблокируем
ClientBtn.Enabled:=False;
HostEdit.Enabled:=False;
PortEdit.Enabled:=False;
NikEdit.Enabled:=False;
// запишем указанный порт в ServerSocket
ServerSocket.Port:=StrToInt(PortEdit.Text);
// запускаем сервер
ServerSocket.Active:=True;
// добавим в ChatMemo сообщение с временем создания
ChatMemo.Lines.Add('['+TimeToStr(Time)+'] Сервер создан.’);
// изменяем тэг
ServerBtn.Tag:=1;
// меняем надпись клавиши
ServerBtn.Caption:=’Закрыть сервер’;
// включаем таймер сервера
ServerTimer.Enabled:=True;
// вписываем параметры сервера
UserMas[0].Status:=1;
UserMas[0].Rec:=True;
UserMas[0].Name:=NikEdit.Text;
UserMas[0].Image:=1;
// разрешаем обновление
UpdDo:=True;
end
else
begin
// выключаем таймер сервера
ServerTimer.Enabled:=False;
// стираем параметры сервера
UserMas[0].Status:=0;
UserMas[0].Rec:=False;
UserMas[0].Name:=’Неизвестный’;
UserMas[0].Image:=0;
// разрешаем обновление
UpdDo:=True;
// очищаем список клиентов
UserListView.Items.Clear;
// клавишу ClientBtn и поля HostEdit, PortEdit, NikEdit разблокируем
ClientBtn.Enabled:=True;
HostEdit.Enabled:=True;
PortEdit.Enabled:=True;
NikEdit.Enabled:=True;
// закрываем сервер
ServerSocket.Active:=False;
// выводим сообщение в ChatMemo
ChatMemo.Lines.Add(’['+TimeToStr(Time)+'] Сервер закрыт.’);
// возвращаем тэгу исходное значение
ServerBtn.Tag:=0;
// возвращаем исходную надпись клавиши
ServerBtn.Caption:=’Создать сервер’;
end;
end;

Далее идут события, которые должны происходить при определенном состоянии ServerSocket’а. Напишем процедуру, когда клиент подсоединился к серверу (OnClientConnect):

procedure TForm1.ServerSocketClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение с временем подключения клиента
ChatMemo.Lines.Add('['+TimeToStr(Time)+'] Подключился клиент.’);
// разрешаем обновление
UpdDo:=True;
end;

Напишем процедуру, когда клиент отключается (OnClientDisconnect):

procedure TForm1.ServerSocketClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение с временем отключения клиента
ChatMemo.Lines.Add('['+TimeToStr(Time)+'] Клиент отключился.’);
// разрешаем обновление
UpdDo:=True;
end;

Отправка сообщений. У нас она осуществляется нажатием клавиши “Отправить” (SendBtn), но необходима проверка режима программы сервер или клиент. Напишем ее процедуру (OnClick):

procedure TForm1.SendBtnClick(Sender: TObject);
begin
// проверка, в каком режиме находится программа
if ServerSocket.Active=True then
// отправляем сообщение с сервера всем пользователям
for i:=0 to ServerSocket.Socket.ActiveConnections-1 do
ServerSocket.Socket.Connections.SendText(’0['+TimeToStr(Time)+'] ‘+NikEdit.Text+’: ‘+TextEdit.Text)
else
// отправляем сообщение с клиента
ClientSocket.Socket.SendText(’0['+TimeToStr(Time)+'] ‘+NikEdit.Text+’: ‘+TextEdit.Text);
// отобразим сообщение в ChatMemo
ChatMemo.Lines.Add(’['+TimeToStr(Time)+'] ‘+NikEdit.Text+’: ‘+TextEdit.Text);
// очищаем TextEdit
TextEdit.Clear;
end;

Режим клиента. При нажатии клавиши “Подключиться” (ClientBtn), блокируется ServerBtn и активируется ClientSocket. Вот процедура ClientBtn (OnClick):

procedure TForm1.ClientBtnClick(Sender: TObject);
begin
if ClientBtn.Tag=0 then
begin
// клавишу ServerBtn и поля HostEdit, PortEdit заблокируем
ServerBtn.Enabled:=False;
HostEdit.Enabled:=False;
PortEdit.Enabled:=False;
// запишем указанный порт в ClientSocket
ClientSocket.Port:=StrToInt(PortEdit.Text);
// запишем хост и адрес (одно значение HostEdit в оба)
ClientSocket.Host:=HostEdit.Text;
ClientSocket.Address:=HostEdit.Text;
// запускаем клиента
ClientSocket.Active:=True;
// изменяем тэг
ClientBtn.Tag:=1;
// меняем надпись клавиши
ClientBtn.Caption:='Отключиться';
end
else
begin
// клавишу ServerBtn и поля HostEdit, PortEdit разблокируем
ServerBtn.Enabled:=True;
HostEdit.Enabled:=True;
PortEdit.Enabled:=True;
// закрываем клиента
ClientSocket.Active:=False;
// очищаем список клиентов
UserListView.Items.Clear;
// выводим сообщение в ChatMemo
ChatMemo.Lines.Add('['+TimeToStr(Time)+'] Сессия закрыта.’);
// возвращаем тэгу исходное значение
ClientBtn.Tag:=0;
// возвращаем исходную надпись клавиши
ClientBtn.Caption:=’Подключиться’;
end;
end;

Процедуры на OnConnect, OnDisconnect, OnRead клиента ClientSocket. Сначала на чтение сообщения с сервера (OnRead):

procedure TForm1.ClientSocketRead(Sender: TObject;
Socket: TCustomWinSocket);
begin
// получим текст, код комманды, длину строки
text:=Socket.ReceiveText();
com:=StrToInt(Copy(text,1,1));
len:=Length(text)-1;
// определение комманд
case com of
// добавим в ChatMemo сообщение с сервера
0: ChatMemo.Lines.Add(Copy(text,2,len));
// отошлем свой ник на сервер
1: ClientSocket.Socket.SendText('1'+NikEdit.Text);
// примем строку списка пользователей
2: begin
// очищаем список клиентов
UserListView.Items.Clear;
// добавим ключ конца строки (т.к. вырезка символов с задержкой)
text:=text+Chr(152);
// укажем начальный символ
pos:=2;
// обнулим счетчик символов
x:=0;
// пробегаем по длине строки списка
for j:=2 to len+1 do
begin
// записываем в счетчик сдвиг
x:=x+1;
// если найден ключ (отделение ников в строке)
if Copy(text,j,1)=Chr(152) then
begin
// добавим в UserListView строку
UItems:=UserListView.Items.Add;
UItems.Caption:=Copy(text,pos,x-1);
// укажем соответствующую иконку пользователя
if pos>2 then UItems.ImageIndex:=0 else UItems.ImageIndex:=1;
// изменим текущую позицию в строке списка
pos:=j+1;
// обнулим счетчик символов
x:=0;
end;
end;
end;
end;
end;

Дальше все просто, обычное добавление в ChatMemo определенного сообщения:

procedure TForm1.ClientSocketConnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение о соединении с сервером
ChatMemo.Lines.Add('['+TimeToStr(Time)+'] Подключение к серверу.’);
end;

procedure TForm1.ClientSocketDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение о потере связи
ChatMemo.Lines.Add('['+TimeToStr(Time)+'] Сервер не найден.’);
end;

Хранителем информации о пользователях у нас выступает массив, процедура его заполнения и обновления выглядит так:

procedure TForm1.UpdateUserMas;
begin
// очищаем массив с информацией
for i:=1 to 255 do
begin
UserMas.Status:=0;
UserMas.Rec:=False;
UserMas.Name:=’Неизвестный’;
UserMas.Image:=0;
end;
// заполняем данные пользователей
if ServerSocket.Socket.ActiveConnections<>0 then
begin
for i:=1 to ServerSocket.Socket.ActiveConnections do
begin
UserMas.Status:=2;
UserMas.Name:=’Неизвестный’;
UserMas.Image:=0;
// запрашиваем имя (ник) пользователя по его каналу (код команды - 1)
ServerSocket.Socket.Connections.SendText(’1′);
end;
end;
end;

Список UserListView обновляется в следующей процедуре:

procedure TForm1.UpdateUserList;
begin
// очищаем список клиентов
UserListView.Items.Clear;
// очищаем переменную
StrUserList:='';
// обнуляем пометку записи
ContList:=0;
// пробегаем по диапазону каналов
for i:=0 to 255 do
begin
// если запись не пустая
if UserMas.Status<>0 then
begin
// добавим в UserListView строку
UItems:=UserListView.Items.Add;
UItems.Caption:=UserMas.Name;
UItems.ImageIndex:=UserMas.Image;
// если пользователь не записан
if UserMas.Rec=False then ContList:=1;
// составляем строку пользователей
StrUserList:=StrUserList+UserMas.Name+Chr(152);
end;
end;
// если все пользователи отметились, и есть хоть один канал
if (ContList=0) and (ServerSocket.Socket.ActiveConnections<>0) then
begin
// пробегаем по всем открытым каналам
for i:=0 to ServerSocket.Socket.ActiveConnections-1 do
begin
// отправим строку списка пользователей (код команды - 2)
ServerSocket.Socket.Connections.SendText(’2′+StrUserList);
end;
end;
end;

Исходники здесь.

Вот, собственно, и все. Не бойтесь экспериментировать, но помните элементарные правила безопасной разработки программ. Удачи!

Категория: Программирование в Delphi | Добавил: Nikol05 (23.07.2009)
Просмотров: 4324 | Рейтинг: 5.0/2
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]