Створення на багато користувачів чату borland delphi

У попередній статті ( "Створення клієнт-сервера") розповідалося про розробку найпростішого чату на двох користувачів. Структура чату "head-to-head" досить проста, адже є тільки один канал, з одного боку якого сервер, з іншого - клієнт. Multy-user-структура дещо складніше. Є один сервер і безліч клієнтів. Сервер при цьому виконує обробку вхідних повідомлень, пересилає їх по потрібним каналах, реєструє користувачів і показує всім, скільки користувачів спілкуються в поточний момент. У даній статті ми спробуємо все це реалізувати.

Розрахований на багато користувачів чат (Multy-user on-line)

Почнемо розробку програми чату з уже готової форми з попередньої статті, або з новою. Ось, що повинно бути в формі:

Компоненти з стандартного пакета Delphi ServerSocket і ClientSocket не завжди можуть бути відображені в палітрі Internet. і їх потрібно завантажити в такий спосіб:

вибрати меню: Component - Install Packages ... - Add. далі потрібно вказати файл ... \ bin \ dclsockets70.bpl.

Додаються нові компоненти:

Тепер розберемо принцип роботи сервера. Традиційно в 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

// визначення команд
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 форми:

Процедура "прослуховування" відкритих каналів сервером, виглядає так:

Переклад програми в режим сервера здійснюється клавішею "Створити сервер" (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):

Процедури на OnConnect. OnDisconnect. OnRead клієнта ClientSocket. Спочатку на читання повідомлення з сервера (OnRead):

Далі все просто, звичайне додавання в 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;

Ось, власне, і все. Не бійтеся експериментувати, але пам'ятайте елементарні правила безпечної розробки програм. Успіхів!

Схожі статті