Prosty komunikator sieciowy

   
Kurs ten został opracowany dla środowiska Borland C++ Builder 6 Profesional, do przerobienia tego kursu niezbędne są komponenty TClientSocket i TServerSocket. Nie będzie to komunikator współpracujący z komunikatorami Gadu-Gadu, Tlen, itp... Celem tego kursu jest pokazania jak należy używać komponentów TClientSocket i TServerSocket w komunikacji sieciowej, nasz komunikator będzie dość prosty i będzie działał podobnie do innych komunikatorów, z tą jednak różnicą, że do jednego serwera będzie mogło być podłączonych kilku użytkowników i będą oni mogli komunikować się ze sobą nawzajem, tak więc nasz komunikator będzie posiadał cechy czatu.   
    Tworzymy nowy projekt i zapisujemy go pod dowolną nazwą, następnie umieszczamy na formularzu komponenty według zamieszczonego rysunku:

Właściwości (Properties):

Form1:

BorderStyle

bsSingle

+BorderIcons - biMinimize

false

 

User1:

+EditLabel - Caption

Użytkownik:

LabelPosition

lpLeft

Text

Nazwa użytkownika

 

MemoKomunikaty i MemoZdarzenia:

DragMode

dmAutomatic

ReadOnly true
WordWrap (tylko MemoKomunikaty) false

 

SpeedButton1:

Down

false

GroupIndex 1

 

SpeedButton2:

Down

true

GroupIndex 1

 

Port1:

Text

1024

 

ServerSocket1:

Active

false

Port 1024

 

ClientSocket1:

Active

false

Port 1024

 

StatusBar1:

Panels

0 - TStatusPanel

 

    W pliku nagłówkowym (np. Unit1.h) w sekcji private tworzymy zmienną IsServer typu bool, ponieważ komunikator będzie mógł pracować jako klient i jako serwer, więc ta zmienna posłuży jako przełącznik, sprawdzający w jakim trybie pracuje program:

// Plik nagłówkowy Unit1.h
//--------------------------------
private:
        bool IsServer;
//--------------------------------

Przechodzimy do pliku źródłowego (np. Unit1.cpp) i w zdarzeniu OnClick dla przycisku Button1 (Połącz) umieszczamy instrukcje odczytujące adres hosta pod który zostanie podłączony klient, a następnie aktywujemy klienta:

// Plik źródłowy Unit1.cpp
//--------------------------------

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"

//--------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"

TForm1 *Form1;
//--------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
}
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 AnsiString tekst = InputBox("Client - Server","Podaj adres:","127.0.0.1");
 ClientSocket1->Active = false;
 ClientSocket1->Host = tekst;
 ClientSocket1->Active = true;
 SpeedButton1->Enabled = false;
}
//--------------------------------

IntputBox jest to okienko dialogowe, które pobiera adres hosta i przepisuje go do zmiennej tekst, jeśli nie zostanie podany żaden adres hosta to okienko zwróci domyślny adres hosta lokalnego, następnie Klient jest wyłączany, przepisywany jest adres hosta do obiektu ClientSocket1 i klient jest aktywowany. W zdarzeniu OnClick dla przycisku Button2 (Rozłącz) dezaktywujemy klienta:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
 ClientSocket1->Active = false;
 StatusBar1->Panels->Items[0]->Text = "Oczekiwanie...";
 SpeedButton1->Enabled = true;
}
//-------------------------------

W zdarzeniu OnClick dla przycisku Button3 (Wyślij) umieszczamy instrukcję wysyłającą wiadomość do serwera, podobną instrukcję umieszczamy w zdarzeniu OnKeyDown dla obiektu EditKomunikat, chodzi o to żeby po wpisaniu komunikatu i wciśnięciu przycisku Enter wiadomość została wysłana bez konieczności klikania na przycisku Button3:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
 String time = " (" + TimeToStr(Now().FormatString("hh:nn:ss")) + ")";
 if(IsServer)
 {
  MemoKomunikaty->Lines->Add(User1->Text + time + ":\n" +
  EditKomunikat->Text + "\n====================");
  for(int x = 0; x < ServerSocket1->Socket->ActiveConnections; x++)
  {
   ServerSocket1->Socket->Connections[x]->SendText(User1->Text + time + ":\n" +
   EditKomunikat->Text + "\n====================");
  }
 }
 else
 {
  ClientSocket1->Socket->SendText(User1->Text + time + ":\n" +
  EditKomunikat->Text + "\n====================");
 }
 EditKomunikat->Text = "";
 MemoKomunikaty->SetFocus();
 Application->ProcessMessages();
 EditKomunikat->SetFocus();
}
//--------------------------------
void __fastcall TForm1::EditKomunikatKeyDown(TObject *Sender, WORD &Key,
        TShiftState Shift)
{
 String time = " (" + TimeToStr(Now().FormatString("hh:nn:ss")) + ")";
 if(Key == VK_RETURN)
 {
  if(IsServer)
  {
   MemoKomunikaty->Lines->Add(User1->Text + time + ":\n" +
   EditKomunikat->Text + "\n====================");
   for(int x = 0; x < ServerSocket1->Socket->ActiveConnections; x++)
   {
    ServerSocket1->Socket->Connections[x]->SendText(User1->Text + time + ":\n" +
    EditKomunikat->Text + "\n====================");
   }
  }
  else
  {
   ClientSocket1->Socket->SendText(User1->Text + time + ":\n" +
   EditKomunikat->Text + "\n====================");
  }
  EditKomunikat->Text = "";
  MemoKomunikaty->SetFocus();
  Application->ProcessMessages();
  EditKomunikat->SetFocus();
 }
}
//--------------------------------

Oprócz samej treści wiadomości, wysyłane są jeszcze dwie dodatkowe linie pierwsza zawierająca nazwę użytkownika pobraną z obiektu User1 + czas wysłania wiadomości, ostatnia linia jest po prostu rozdzielająca. Funkcja ActiveConnections sprawdza ilu użytkowników jest podłączonych do serwera, a pętla for przesyła wszystkie komunikaty wychodzące z serwera, do wszystkich klientów podłączonych do niego. Widzimy tutaj zastosowanie zmiennej IsServer, sprawdza ona czy komunikaty wysyła serwer czy klient.
W zdarzeniu OnClientRead dla obiektu ServerSocket1 umieszczamy instrukcje przechwytujące komunikaty od klientów, a następnie te komunikaty są przesyłane do wszystkich klientów podłączonych do serwera wewnątrz pętli for:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::ServerSocket1ClientRead(TObject *Sender,
        TCustomWinSocket *Socket)
{
 MemoKomunikaty->Lines->Add(Socket->ReceiveText());
 MemoKomunikaty->SetFocus();
 if(IsServer)
 {
  int i = MemoKomunikaty->Lines->Count;
  String tekst1 = MemoKomunikaty->Lines->Strings[i - 3];
  String tekst2 = MemoKomunikaty->Lines->Strings[i - 2];
  String tekst3 = MemoKomunikaty->Lines->Strings[i - 1];

  for(int x = 0; x < ServerSocket1->Socket->ActiveConnections; x++)
  {
   ServerSocket1->Socket->Connections[x]->SendText(tekst1 + tekst2 + tekst3);
  }
 }
}
//-------------------------------

W zdarzeniu OnAccept dla obiektu ServerSocket1 zmieniamy status zmiennej IsServer, a w zdarzeniu OnClientConnect dla tego samego obiektu umieszczamy odpowiedni komunikat:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::ServerSocket1Accept(TObject *Sender,
        TCustomWinSocket *Socket)
{
 IsServer = true;
 StatusBar1->Panels->Items[0]->Text = "Podłączono do: " + Socket->RemoteAddress;
}
//--------------------------------
void __fastcall TForm1::ServerSocket1ClientConnect(TObject *Sender,
        TCustomWinSocket *Socket)
{
 MemoZdarzenia->Lines->Add("Podłączono klienta: " + Socket->RemoteAddress + " " + Socket->RemoteHost);
 MemoKomunikaty->Lines->Clear();
}
//--------------------------------

W zdarzeniu OnError dla obiektu ClientSocket1 umieszczamy komunikaty o ewentualnych błędach w komunikacji między klientem i serwerem:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::ClientSocket1Error(TObject *Sender,
        TCustomWinSocket *Socket, TErrorEvent ErrorEvent, int &ErrorCode)
{
 String error;
 if(ErrorEvent == eeConnect)
 {
  SpeedButton1->Enabled = true;
  error = "Błąd w połączeniu z serwerem";
 }
 if(ErrorEvent == eeSend)
  error = "Błąd w wysyłaniu";

 if(ErrorEvent == eeReceive)
  error = "Błąd w odbieraniu danych";

 if(ErrorEvent == eeDisconnect)
  error = "Błąd w rozłączaniu";

 if(ErrorEvent == eeAccept)
  error = "Błąd przy próbie akceptacji połączenia klienta";

 if(ErrorEvent == eeGeneral)
  error = "Wystąpił nieokreślony błąd w komunikacji";

 ShowMessage(error);
 MemoZdarzenia->Lines->Add(TimeToStr(Now().FormatString("hh:nn:ss")) + " " + error);
 ErrorCode = 0;
}
//-------------------------------

W zdarzeniu OnClientConnect dla obiektu ClientSocket1 umieszczamy dwa komunikaty, a w zdarzeniu OnRead umieszczamy instrukcje przechwytujące komunikaty przychodzące z serwera:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::ClientSocket1Connect(TObject *Sender,
        TCustomWinSocket *Socket)
{
 StatusBar1->Panels->Items[0]->Text = "Podłączono do: " + Socket->RemoteHost;
 MemoZdarzenia->Lines->Add("Podłączono klienta: " + Socket->RemoteAddress + " " + Socket->RemoteHost);
}
//--------------------------------
void __fastcall TForm1::ClientSocket1Read(TObject *Sender,
        TCustomWinSocket *Socket)
{
 MemoKomunikaty->Lines->Add(Socket->ReceiveText());
 
 MemoKomunikaty->SetFocus();
 Application->ProcessMessages();
 EditKomunikat->SetFocus();
}
//--------------------------------

W zdarzeniu OnClick dla przycisków SpeedButton1 i SpeedButton2 umieszczamy odpowiednio instrukcje włączające i wyłączające serwer:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
 try
 {
  ClientSocket1->Active = false;
  ServerSocket1->Active = true;
  StatusBar1->Panels->Items[0]->Text = "Oczekiwanie...";
  Button1->Enabled = false;
 }
 catch(...)
 {
  SpeedButton1->Down = false;
  SpeedButton2->Down = true;
  Button1->Enabled = true;
 }
}
//--------------------------------
void __fastcall TForm1::SpeedButton2Click(TObject *Sender)
{
 ClientSocket1->Active = false;
 ServerSocket1->Active = false;
 StatusBar1->Panels->Items[0]->Text = "Rozłączone...";
 Button1->Enabled = true;
}
//--------------------------------

W zdarzeniu OnClick dla przycisku Button5 pobieramy numer portu, który powinien być zawsze wysoki, natomiast w zdarzeniu OnClick dla przycisku Button4 sprawdzamy ilu klientów jest podłączonych do serwera:

// Plik źródłowy Unit1.cpp
//--------------------------------
void __fastcall TForm1::Button5Click(TObject *Sender)
{
 ServerSocket1->Port = StrToInt(Port1->Text);
 ClientSocket1->Port = StrToInt(Port1->Text);
}
//--------------------------------
void __fastcall TForm1::Button4Click(TObject *Sender)
{
 ShowMessage("Do serwera jest podłączonych " +
  IntToStr(ServerSocket1->Socket->ActiveConnections) +
  " klientów.");
}
//--------------------------------

I to byłoby już wszystko, program jest gotowy do pracy. W przypadku gdy serwer znajduje się za zaporą klienci mogą mieć problem z podłączeniem się do niego:

Pliki źródłowe projektu, archiwum RAR - rozmiar 258 kb.

Opracował: Cyfrowy Baron