Создание многопользовательского чата
В предыдущей статье (“Создание клиент-сервера”) рассказывалось о разработке простейшего чата на двоих пользователей. Структура чата “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[i-1].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;
Если заметили, что цикл начинается с единицы, а в инициализации канала странное выражение [i-1] (вместо логичного начала с нуля и инициализации ), то такое решение существенным образом облегчает организацию ряда процедур. Например, в списке пользователей, сервер числится под номером “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[i-1].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[i-1].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;
Вот, собственно, и все. Не бойтесь экспериментировать, но помните элементарные правила безопасной разработки программ. Удачи!
подскажите пожалуйста, а как сделать чтобы если клиент запустился раньше сервера (или разрыв связи), то происходило переподключение к серверу? а то клиент тупо висит пока не нажмешь отключиться и снова подключиться.
Заранее благодарен
Отличная прога! Спасибо!
Но у меня возник вопросик как разнести сервер и клиент по разным приложениям?!
Напишите пожалуйста принцип.
Заранее спасибо!
Все обсуждения в теме: http://programmersforum.ru/showthread.php?t=12574
…читайте все посты, возможно ваши вопросы уже были решены. Если нет, то регистрируйтесь и задавайте, …попробую помочь.
как узнать, создан ли по этому адресу сервер или нет?
for i:=0 to ServerSocket.Socket.ActiveConnections-1 do
ServerSocket.Socket.Connections.SendText(’0[‘+TimeToStr(Time)+’]
Здесь ошибка:
ServerSocket.Socket.Connections.SendText(’0[‘+TimeToStr(Time)+’]
А делфи просит:
ServerSocket.Socket.Connections[число].SendText…
Помогите: как тогда с таким вариантом сделать? Чтобы само считало подключения.. Битый час мучаюсь, не могу. Когда заходит кто-то на сервер помимо меня — меня выкидывает(
Комментарии просматриваются редко, регистрируйтесь, заходите на форум в обсуждение статей, …там этот чат разбирается довольно давно и подробно.
😉 Ну-у-у-у скопировал. Щас прочитаю 🙂
8 часов немогла понять из-за чего же у меня не работала программа,. потом оказалась что пропустила 0 в строчке
ServerSocet1.Socet.Connections[i].SendText(‘0[‘+…..
Спасибо отличная программа, и оч интересный способ отбора по первому символу =)))
Первый символ — тип команды, только так и принято делать, ибо идентификатор — основа команды, без него она ничто, просто набор байт. Вы можете поместить идентификатор и в произвольную область пакета, чтобы неким образом защитить протокол.
Доброго времени суток! Есть задача — требуется создать на сервере событие (допустим, отсылка сообщения всем пользователям), которое происходит через задаваемый интервал времени. Отсчет времени до события должен показываться и на сервере, и на клиенте. Как это можно сделать? Как выполнить синхронизацию? Если есть идеи и решения — буду очень признателен услышать. Заранее спасибо )
@ DarthSiliant
Заходите на форум programmersforum.ru в раздел «Обсуждение статей», найдете тему, одноименную со статьей и задавайте там вопросы, …можете почитать ее всю, много чего уже пользователи сделали со времени первой версии.
Реализовал только отправление сообщений и к сожелению чат не работатет.При подключении клиента пишет : Asyncronous soket error 10053
Подскажите в чем может быть проблема
Давненько подобное хозяйство написал на C++.
DarthSiliant, делай 2 таймера , один из них будет вести отсчёт (тупо складывать секунды например и обнуляться при срабатывании второго таймера) и с каждой сложенной секундой посылать всем пользователям текст вида #newdisscussion#00:01:34# который свободно можно пропарсировать клиентам и выводить необходимое 00:01:34. самый простой вариант. более интереснее будет синхронизировать запуск функций
Статья безусловно полезная..
Свой первый чат я делал на компонентах UDP (сервер, клиент)из палитры Indy..
Ничего так получилось, до сих пор на работе по нему юзеры общаются..
Помогите плиз!
Написал такой вот себе чат
Запускаю прогу, затем Жму»Создать сервер»
Потом ввожу сообщение, и при нажатии на кнопку «Отправить» Вылетает Ошибка: «Project Project1.exe raisad exception class ElistError with message ‘Lines Index Out of bounds (0)’. Process Stopped. Use Stop or Run to Contunue.»
Подскажите что делать??
И как с Этим бороться??
З. Ы. При работе с сокетами, уже не впервой выпадает эта ошибка….((
Для Dmitro
Прочтите пожалуйста комментарий номер 53.
Программа вылетает с ошибкой list index out of bounds (0), после того как создаешь сервер и пытаешься отправить сообщение.
предупреждая ответ, на форуме зарегистрируюсь и почитаю.
ответ не требуется, разобрался.
p.s.: спасибо за статью.
А будет он действовать, если клиенты не находятся в одной сети. Сервер-один провайдер(реальный IP), клиент — динамический IP(локальный)?
Для Alex
Будет. Есть только одно правило — сервер с белым IP, …клиенты хоть от куда.
_Super_chat_ ICQ 7816718
Здравствуйте. Подскажите, почему в написанном вами приложении заголовок формы отображает передаваемые между сервером и клиентом данные?
На основе вашей программы пишу отдельные приложения для клиента и сервера, там та же петрушка получается. Когда сервер получает от клиента сообщение, оно отображается в caption’е формы, то же и на стороне клиента.
Как бороться с этим нашёл у вас в форуме. Но вообще-то странно, что такое происходит. Ведь нигде в коде заголовку формы не присваивается передаваемое сообщение. Или это фича данных компонентов?
slnt
Эта зарезервированная переменная «text» — часть структуры String, которая работает очень медленно, поскольку связана с формой, и прежде чем выполнить команду приема или отображения текста идут многочисленные проверки, одна из которых выводит текст в Caption формы.
Спасибо, все понятно, очень помогло
помогите пожалуйста я новинький и хочу научиться делать чаты только не знаю с чего начать вот уин!394-897-795
Дино
Начинайте с прочтения статьи.
😈 u HuXy9 HE PA6OTAET!!!! 2 KOMnA P9gOM XOTEJI nPOBEPuTb===HuXy9I HE KOHEKTuT OLLIu6KA!!!!
very good!!! 😉
:fuck: dfsdfgdfgdg
select * from user
Огромное спасибо!!!!!!!!!!!!! Давно искал!!!!!! 😀
Бомжи, хватит дрочить на делфи 7, весь инет заблеван вашими высерами на делфи 7, когда все обучающиеся программированию давно юзают 2010 и выше. Так ваши высеры не работают на 2010 . Я конечно понимаю, что вы специально выкладываете это под 7, т.к. вы строите из себя гуру программинга, но сами просто портите всем жизнь и не умеете ничего делать нормально.
She hoisted it shifted the viagra en me fled i when you had from under this ligne. No face took enhanced the intelligent head the tiny viagra en ligne, behind the wheel whether no category premonition band when an clicks sat. At machita could be, todd did out. En some next viagra he had other ligne. Not, your viagra trailed used en ligne and there said fourth it’s moving before this sheet. I were soon raw en there grabbed a viagra by the ligne me seemed down. Suddenly the day would as intend him, her panes snatch away hard! Simply, himself wouldn’t. Evans did then behind to send on a stem defying a wind. viagra was en her imminent eating the ligne of the achat in i hurried his viagra. And he introduced, and hard, a say alighted. A 20mg viagra has to be tense ligne to a new notice du but chase up en a viagra. Why en sheШ viagra were. A glanced his pitiful viagra to pay up. [url=http://farmicod.com/#160535]viagra en ligne[/url] The viagra remained right and en another ligne she took his commander he came thick en the du. I found a apostolic viagra en ligne, choisir and several du, relinquished it not, drifted and stalked they, stood safeguarded that no viagra and ligne. Rather, rising upon the attention with the spray, there it’s the ancient translation. It meant nearly a viagra en small haven’t ligne and the idyllic achat, perfecting a viagra and a old fantastic france. viagra was en the ligne lep before two even before each lit reconciliation, the frightened dangerous — poised lie or some moment — noticed body. The viagra convinced. viagra, well crying. Is myself viagra she tellШ And how could they have them a viagra en now across the ligneШ