Wstęp do programowania z użyciem gniazd

Adam Boduch

Artykuł przeznaczony jest dla początkujących, którzy zainteresowani są aspektem programowania z wykorzystaniem sieci. Artykuł stanowi wprowadzenie do tematyki programowania z użyciem gniazd w Delphi. Tekst powstał w oparciu o Delphi 7 i komponenty TClientSocket oraz TServerSocket. Jako przykład użycia gniazd, zaprezentujemy program służący do przesyłania plików przez internet.

1 Przesyłanie tekstu
2 Przesyłanie plików
3 Kod źródłowy

Czym są gniazdka? Z punktu widzenia Windowsa jest to zwykły uchwyt. Dla nas jest to jednak sposób aby komunikować się z innym komputerem za pomocą protokołu TCP/IP.

My w Delphi do wykorzystania mamy dwa komponenty znajdujące się na zakładce Internet. Jest to komponent TClientSocket oraz TServerSocket. Aby dwa komputery mogły się ze sobą komunikować potrzebny jest właśnie serwer (TServerSocket) oraz klient, który łączy się do serwera (TClientSocket). Aby dwa komputery mogły się połączyć potrzebny jest numer IP osoby do której chcemy się podłączyć oraz port. Port jest furtką dzięki której możemy się ze sobą połączyć (jest to liczba z zakresu 0 do 65535 ). Jeżeli na maszynie oznaczonej danym adresem IP działa program to wówczas otwarty jest dany port i dwa komputery mogą się ze sobą połączyć. Jeżeli program jest uruchomiony to działa jako serwer i czeka na przyłączenie jakiegoś klienta. Jeżeli żądamy połączenia się z innym komputerem - automatycznie nasza aplikacja staje się klientem.

Tak więc stwórz nowy projekt i umieść na nim komponenty TClientSocket oraz TServerSocket. Obu tym komponentom nadaj taki sam port - np. 77. O tym, czy gniazdko jest włączone decyduje właściwość Active. Aby połączyć dwa komputery będziesz przede wszystkim potrzebował IP tegoż komputera. Przykładowo:

<tt> Client.Host := '245.87.23.24';
Server.Active := False;
Client.Active := True;</tt>

W tym wypadku nadajemy komponentowi ClientSocket jakiś adres IP z którym chcemy się połączyć. Aktywujemy klienta nadając mu właściwość True. Natomiast serwerowi nadajemy wartość False, gdyż teraz już nasza aplikacja nie robi za serwer. W tym momencie powinniśmy się połączyć z programem na innym komputerze. Aby poinformować o tym użytkownika możesz skorzystać ze zdarzeń tych komponentów.

Przesyłanie tekstu

Dobrze - jak więc komunikować się z komputerem - tj. wysyłać do innego komputera jakiś tekst? Najlepiej jest stworzyć w programie jeszcze jedną zmienną typu Boolean mówiącą o tym, czy aplikacja działa jako serwer, czy klient. Nazwijmy tę zmienną IsServer. Jeżeli łączymy się do innego komputera zmienna ta powinna przybrać wartość False. Jeżeli ktoś łączy się z nami powinniśmy nadać tej zmiennej wartość True. Skorzystajmy na przykład ze zdarzenia OnAccept komponentu TServerSocket. Zdarzenie to występuje tuż po tym jak połączył się z nami jakiś klient. W zdarzeniu tym możemy tej zmiennej nadać właśnie wartość True.

procedure TMainForm.ServerAccept(Sender: TObject; Socket: TCustomWinSocket);
begin
  IsServer := True;
  StatusBar.SimpleText := 'Akceptuje połączenie z: ' + Socket.RemoteAddress;
end;

Socket.RemoteAddress oznacza adres IP komputera, który się do nas podłącza. Jak teraz wysłać tekst do drugiego klienta? Należy rozróżnić, czy jesteśmy serwerem, czy klientem:

 if IsServer then
   Server.Socket.Connections[Server.Socket.ActiveConnections -1].SendText('Witaj')
 else Client.Socket.SendText('Witaj');

Funkcja SendText() wysyła jak łatwo się domyśleć - tekst do drugiego komputera. Jeżeli jesteśmy serwerem to wysyłamy tekst do aktywnego połączenia (serwer może obsługiwać kilka połączeń). Można także skorzystać z funkcji SendBuff, które to wysyła bufor określonych danych.

Dobrze, a co z odbiorem danych? W komponencie TServerSocket wygeneruj zdarzenie OnClientRead, a w komponencie ClientSocket: OnRead. Treści obu zdarzeń mogą być identyczne.

procedure TMainForm.ClientRead(Sender: TObject; Socket: TCustomWinSocket);
begin
  ShowMessage('Otrzymałem tekst: ' + Socket.ReceiveText);
end;

Odbiór takich danych jest równie prosty co wysłanie. Z jedną różnicą - służy do tego polecenie ReceiveText. Jak widzisz korzystanie z gniazdek na tym poziomie nie jest aż tak trudne.

Jak natomiast wykryć, że na danej maszynie nie jest otwarty dany port (co oznacza, że program nie jest uruchomiony) lub, że maszyna o danym IP nie jest w sieci? Wygeneruj zdarzenie OnCreate:

procedure TMainForm.ClientError(Sender: TObject; Socket: TCustomWinSocket;
  ErrorEvent: TErrorEvent; var ErrorCode: Integer);
begin
  case ErrorCode of
    10060: ShowMessage('Komputer z którym chcesz się połączyć nie jest podłączony do sieci!');
    10061: ShowMessage('Na danym komputerze port nie jest aktywny');
  end;
  ErrorCode := 0;
end;

Przesyłanie plików

Kolejna bardziej zaawansowana sprawa to przesyłanie plików przez gniazdka. Jest to z pozoru trudne. Tak jak w poprzednich przypadkach tworzymy serwer oraz klienta. Jest on trochę bardziej skompilowany od zaprezentowanych tutaj dwóch procedur. Gotowy kod źródłowy najpierw wysyła do użytkownika zapytanie, czy chce odebrać od kogoś plik taki i taki o takich rozmiarach. Jeżeli użytkownik naciśnie OK to rozpoczyna się przesyłanie pliku.

Jak odbywa się przesyłanie pliku? Ja zrobiłem to tak, że przesyłam plik porcjami po 1 kB każda. Plik zostanie dzielony na poszczególne porcje, po 1 kB każda, a następnie w pętli po kolei wszystko zostaje przesyłane przy pomocy procedury SendBuf. Naprawdę nie jest to trudne. Procedura SendFile posiada jeden parametr, który jest ścieżką do przesyłanego pliku:

procedure TMainForm.SendFile(const FileName: String);
var
  SrcF : File;
  RealSize : Integer;  // realna odczytana wartość z pliku
  Buffer : array[0..Count] of char;  // bufor przesyłanych danych
  TotalSize : Integer;
begin
  TotalSize := 0;
  AssignFile(SrcF, FileName);
  try
  { otwieramy plik źródłowy i określamy jego rozmiar }
    Reset(SrcF, 1);
    lblProgress.Caption := 'Wysyłam plik...';
    ProgressBar.Max := FileSize(SrcF);

    repeat
      Application.ProcessMessages;
      Sleep(10);

      Seek(SrcF, TotalSize); // przesuń na odpowiednia pozycje w pliku
    { tutaj nastepuje odczytanie 1 KB danych i przypisanie ich do bufora }
      BlockRead(SrcF, Buffer, SizeOf(Buffer), RealSize);
     
      if RealSize > 0 then  // jeżeli liczba odczytanych bajtów jest większa od zera...
      begin
      { wyślij pakiet... }
        if IsServer then
          Server.Socket.Connections[0].SendBuf(Buffer, RealSize)
        else Client.Socket.SendBuf(Buffer, RealSize);
        TotalSize := TotalSize + RealSize;
      end;

    until RealSize = 0;

  finally
    CloseFile(SrcF);
  end;
end;

Wysyłając pakiet przy pomocy procedury SendBuf w pierwszym parametrze musimy podać zawartość bufora do przesyłania, a w drugim parametrze rozmiar przesyłanego pakietu.

Zmienna TotalSize oznacza ilość bajtów które aktualnie zostały przesyłane. Jak zapewne widzisz plik jest przesyłany w pętli toteż za każdą jej iteracją trzeba za pomocą procedury Seek przesuwać ustawienie w pliku na podstawie zmiennej TotalSize. Aby zrozumieć ten przykład potrzebna Ci będzie wiedza na temat plików amorficznych.

Tak więc za pomocą funkcji BlockRead następuje odczytanie jakiejś porcji danych do zmiennej (tablicy) Buffer. Tablica posiada 1024 elementy (1 kB), ale to zmienna RealSize zawiera liczbę rzeczywiście odczytanych bajtów. I to właśnie ta liczba jest podawana w procedurze SendBuf jako liczba przesłanych bajtów.

To by było na tyle jeśli chodzi o przesyłanie danych. A co z odbieraniem? Wygeneruj zdarzenie OnClientRead lub OnRead (zależnie, czy dla komponentu ClientSocket, czy ServerSocket). Oto jak wygląda kod tego zdarzenia:

procedure TMainForm.RecieveFile(Sender: TObject; Socket: TCustomWinSocket);
var
  Buffer : array[0..Count] of char;
  RealSize : Integer;
begin
{
   procedura odczytuje przeslane dane i zapisuje do pliku
}

  ProgressBar.Max := RecieveRec.FSize;

{ odczytaj odebrana porcje danych - RealSize oznacza ilość rzeczywiście 
  odczytanych bajtów }
  RealSize := Socket.ReceiveBuf(Buffer, SizeOf(Buffer));
  {$I-}
  BlockWrite(F, Buffer, RealSize); // bufor zapisz do pliku
  {$i+}

  TotalSize := TotalSize + RealSize; // określ realna porcje odczytanych już danych
  ProgressBar.Position := TotalSize;  // oznacz na komponencie

  { wyswietl informacje dla uzytkownika }
  lblProgress.Caption := 'Odbieram... ' + CurrToStr(TotalSize / 1024) + '/' + 
  CurrToStr(RecieveRec.FSize / 1024) + ' kB';
end;

To zdarzenie będzie występowało za każdym razem, gdy przesłana zostanie porcja danych. Toteż nie praktyczne jest, aby za każdym razem otwierać i zamykać plik. W tej procedurze będziemy jedynie dopisywać do pliku kolejne odebrane bajty za pomocą procedury BlockWrite. Funkcja RecieveBuf służy do odbierania bufora. W naszym przypadku będziemy odczytywać dane do zmiennej buffer. Funkcja zwraca ilość rzeczywiście odczytanych bajtów. W tym wypadku TotalSize jest zmienną, która będzie zawierać liczbę faktycznie odebranych bajtów.

Kod źródłowy

Naturalnie kod nie jest kompletny bo trzeba jeszcze obsłużyć tworzenie nowego pliku oraz zamykanie go w przypadku, gdy nastąpi koniec przesyłania danych.

Kompletny kod źródłowy możesz znaleźć w dziale Kody źródłowe:

18 komentarzy

Czy można przesyłać komendy za pomocą tych gniazd?

omg
to juz jest w delphi tylko trzeba dodac na wielu stronach jest brak socketów w delphi7 w innych wyglada to tak samo tylko ze numerki sa inne

Czemu Sockety działają w LAN-ie a nie w NECIE CO trzeba zrobic, albo jakich komponentów uzyc zeby mozna bylo grac przez neta??

omg
to juz jest w delphi tylko trzeba dodac na wielu stronach jest brak socketów w delphi7 w innych wyglada to tak samo tylko ze numerki sa inne

Mam prosby..moglby ktos podac link(dzialajacy) do strony z tymi Tserversocket itp.... i napisac jak to sie instaluje ale DOKLADNIE..
juz pol dnia tego szukam
Bardzo prosze o pomoc...
z Góry dziekuje!

mam delphi 7 pe i nie ma tam zakladki internet. czy kos moglby mi powiedziec jak ja dodac?

w Delphi 7 jest tylko TcpServer i TcpClient :(

A jak zrobić by server obsługiwał kilka klientów na raz? Np na zasadzie czata?
[email protected]

Działa przesyłanie plików, kod potrzebował małej zmiany. Ale mam pytanie. Jaki kod dołączyć do tego, aby pokazywać szybkość przesyłania pliku? Teoretycznie wielkość pliku trzeba podzielić przez czas przesyłania, ale praktycznie nie wiem jak to zrobić. Prosiłbym o dokładny opis.

A jak to zrobić w delphi 7 enterprice - mam tylko TcpServer i TcpClient!

Masz Delphi 7 ENTERPRISE ?! Skąd?????????????????????????

a jak przesyłać inforamcje między komponentami?

Jak za pomocą tego komponentu wysłać maila
wiem ze trzeba wyslac cus takiego
procedure TForm1.SendEMLBySocket(Server, Port, FileName, UserName,
Password: String);
var email ,
eml_file : TStringList;
i : Integer;
begin

email := TStringList.Create;
eml_file := TStringList.Create;

try
eml_file.LoadFromFile(FileName);
email.Add('HELO');
email.Add('MAIL FROM');
email.Add('DATA');
email.AddStrings(eml_file);
email.Add('.');
email.Add('QUIT');

TCPClient.RemoteHost := Server;
TCPClient.RemotePort := Port;

TcpClient.Connect;

if not TcpClient.Connected then
begin
MessageDlg('Nie udało się nawiązać połączenia',mtError,[mbOK],0);
exit;
end;

for i := 0 to email.Count -1 do
begin
TcpClient.Sendln(email[i]);
Application.ProcessMessages;
end;

if TcpClient.Connected then TcpClient.Disconnect;

finally
email.Free;
eml_file.Free;

end;
end;

Ale cos chyba z autentykacja bo tak sie składa ze ten mail nie dochodzi
server := 'smtp.wp.pl' a Port to oczywiscie '25' plik eml pobieram z pliku jest zrobiony w outlooku expresie

A ja mam pytanko:
w jaki sposób pobrać nume IP tej osoby z którą chcemy rozmawiać???
[email protected]

jak zrobic cos takiego:
program laczy sie z serwerem, sprawdza czy to jego serwer, jesli tak to kontynuuje dzialanie, jesli nie to odlacza sie??

W jaki sposób sprawdzić IP wszystkich użytkowników sieci lokalnej.

Bardzo fajny artykuł :) Moglo by tu jeszcze byc napisane jak np klient ma odroznic czy serwer wysyla do niego bufor czy zwykly tekst...