Sockety.
Witam. Przedstawiam jak utworzyć dwa programy – serwer, używający komponen TServerSocket oraz klient, używający TClientSocket. Za pośrednictwem klienta będziemy łączyli się z serwerem. Umożliwimy przesyłanie tekstu. Po kolei będziemy ulepszać oba programy, dodając możliwość wysyłania plików oraz możliwość rozmowy z wieloma klientami itp.
Krótki opis TCP/IP oraz połączenia komputerów.
Do łączenia komputerów posłużymy się protokołem TCP/IP. Gwarantuje on dostarczenie oraz wykrycie utraty spójności danych. Jest przez to wolniejszy od UDP, ale za to bezpieczniejszy, może służyć do przesyłania bardziej wartościowych danych. Jednak, jak łatwo się domyślić, jeśli ktoś będzie chciał oraz posiadał odpowiednią wiedzę, uda mu się zaszkodzić. Do połączenia potrzebne będą nam dwa programy – serwer, które oczekuje na połączenie, oraz klient, który będzie się do niego łączył. Przed połączeniem będziemy musieli w obu programach określić porty, na których nastąpi połączenie. Port jest to liczba z zakresu 1..65535. Są to tak jakby drzwi, do których klient puka chcąc podłączyć się do serwera. Gdyby portów nie było, komputer nie wiedziałby, do której aplikacji podłączyć klienta. Dzięki portom, nie dojdzie do pomyłki. Potem, w kliencie, podamy numer IP serwera bądź jego nazwę (host). IP jest to identyfikator komputera w sieci np. 192.168.0.1, podobnie jak nazwa komputera, np. komp_iskara. Gotowe, teraz wystarczy uaktywnić serwer oraz podłączyć się do niego klientem. Możemy teraz wymieniać dane pomiędzy serwerem a klientem. Oprogramujemy także przydatne zdarzenia, informujące o stanie połączenia.
Nie mam socketów!
Jeżeli w Twojej wersji Delphi nie ma socketów, musisz je zainstalować.
Budujemy serwer.
W serwerze użyjemy komponentu TServerSocket z zakładki Internet. Dodaj komponenty, które wypisałem poniżej na formę oraz ustaw odpowiednie wartości ich właściwości. Później dodamy obsługę odpowiednich zdarzeń.
1. Na początek dodaj TButton, nazwij go BtnAktywuj. Będzie on służył do aktywacji/dezaktywacji serwera. Caption ustaw na Aktywuj.
2. Następnie, dodaj TEdit, do którego wpisywany będzie port. Nazwij go EdtPort. Skasuj tekst, jaki zawiera, wpisz domyślnie 12345.
3. Dodaj jeszcze jeden przycisk o nazwie BtnPrzeslij. Po jego kliknięciu do serwera wysłana zostanie wiadomość tekstowa. Ustaw Enabled na false. Na początku działania programu nie będzie do nas podłączony żaden klient, więc przycisk ten powinien być nieaktywny.
4. Dodaj na formę dwa TRichEdit. Pierwszy z nich nazwij RDoWys i umieść pod przyciskiem BtnPrzeslij. W tym komponencie umieszczać będziemy tekst, który chcemy przesłać do klienta. Drugi TRichEdit nazwij ROdebrane. W nim umieścimy otrzymane wiadomości. Ustaw jego właściwość ReadOnly na true oraz ScrollBars na ssVertical.
5. Dodaj na formę komponent TStatusBar, na którym będziemy informować użytkownika o stanie połączenia. Nazwij go StatBar.
6. Na końcu, dodaj TServerSocket. Nazwij go Serwer.
To tyle. U mnie program ten wygląda następująco:
Może być 😉
Teraz zajmiemy się oprogramowaniem odpowiednich zdarzeń.
Na początek OnClick przycisku BtnAktywuj:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
procedure TForm1.BtnAktywujClick(Sender: TObject); begin Serwer.Port := StrToInt(EdtPort.Text) ; // 1 Serwer.Active := not Serwer.Active ; // 2 if Serwer.Active then // 3 begin StatBar.SimpleText := 'Serwer oczekuje na klientów.' ; BtnAktywuj.Caption := 'Dezaktywuj' ; end else // 4 begin StatBar.SimpleText := 'Serwer nieaktywny.' ; BtnAktywuj.Caption := 'Aktywuj' ; end ; end; |
1. Pobieramy z Edita port, na którym serwer będzie oczekiwał klientów.
2. Zmieniamy stan serwera na aktywny/nieaktywny.
3. Jeżeli serwer jest aktywny, na StatusBar informujemy użytkownika, że serwer oczekuje na klientów oraz zmieniamy napis na BtnAktywuj na ‘Dezaktywuj’.
4. Analogicznie, jeżeli serwer jest nieaktywny.
Teraz zajmiemy się zdarzeniami naszego TServerSocket.
OnAccept – występuje, gdy podłącza się do nas klient. W parametrze Socket zawarte są informacje o kliencie. Na StatusBar wyświetlimy adres oraz nazwę komputera klienta oraz udostępniamy przycisk odpowiadający za wysłanie wiadomości.
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.SerwerAccept(Sender: TObject; Socket: TCustomWinSocket); begin StatBar.SimpleText := 'Akceptuje połączenie z ' + Socket.RemoteAddress + '(' + Socket.RemoteHost + ')' ; BtnPrzeslij.Enabled := true ; end; |
W RemoteAddress znajduje się adres klienta, a w RemoteHost nazwa jego komputera.
OnClientDisconnect – zachodzi, gdy dany klient odłącza się od serwera.
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.SerwerClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); begin StatBar.SimpleText := 'Odłączył się: ' + Socket.RemoteAddress + '(' + Socket.RemoteHost + ')' ; BtnPrzeslij.Enabled := false ; end; |
OnClientRead – wywołane zostaje, gdy klient wysyła do nas dane.
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.SerwerClientRead(Sender: TObject; Socket: TCustomWinSocket); begin ROdebrane.Lines.Add(Socket.ReceiveText) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; end; |
Do TRichEdit o nazwie ROdebrane dodajemy tekst otrzymany od klienta. W ReceiveText znajduje się string wysłany przez klienta. Kolejna linijka jest dla wygody czytania kolejnych wiadomości. Wysyłamy komunikat do komponentu, aby zawarty w nim tekst został przewinięty na dół, dzięki czemu będziemy widzieli kolejno dodawane wiadomości (w przeciwnym razie musielibyśmy ciągle przesuwać ScrollBar na dół).
Został nam już tylko przycisk BtnWyslij.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
procedure TForm1.BtnPrzeslijClick(Sender: TObject); begin if RDoWys.Text = '' then Exit ; // 1 if Serwer.Socket.ActiveConnections > 0 then // 2 begin ROdebrane.Lines.Add(RDoWys.Text) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText(RDoWys.Text) ; // 3 RDoWys.Text := '' ; end ; end; |
1. Jeżeli nie wpisaliśmy żadnej wiadomości, to wychodzimy.
2. ActiveConnections jest liczbą podłączonych do nas klientów. Wysyłamy wiadomość tylko wtedy, gdy podłączony jest przynajmniej jeden klient.
3. Connections jest tablicą podłączonych klientów. Chcąc odnieść się do danego, podłączonego klienta, podajemy jego indeks. W tym przypadku, poprzez Serwer.Socket.ActiveConnections – 1 nastąpi wysłanie do ostatnio podłączonego klienta. Służy do tego metoda SendText.
Serwer mamy gotowy. Po włączeniu i kliknięciu przycisku BtnAktywuj, serwer będzie oczekiwał na klientów.
Piszemy klienta.
Na początku umieść odpowiednie komponenty na formie.
1. Dodaj przycisk, nazwij go BtnPolacz, jego Caption ustaw na ‘Połącz’. Za jego pomocą będziemy łączyć/rozłączać się z serwerem.
2. Umieść na formie 3 TEdit, nazwij je kolejno EdtIP, EdtHost, EdtPort. Nazwy mówią same za siebie. W EdtPort umieść tekst 12345.
3. Następnie, dodaj kolejny przycisk, nazwij go BtnPrzeslij. Będzie on służył do przesyłania wiadomości do serwera. Na razie powinien zostać nieaktywny, ustaw więc Enabled na false.
4. Czas na dwa TRichEdit, jeden z nich nazwij RDoWys a drugi ROdebrane. Właściwości (ROdebrane) ReadOnly ustaw na true, a ScrollBars na ssVertical. Służą do tego samego, co w serwerze 😉
5. Nie zapomnij o StatusBar, na którym będziemy informować użytkownika o stanie połączenia. Nazwij go StatBar.
6. Na końcu dodaj TClientSocket z zakładki Internet, nazwij go Klient.
Komponenty mamy rozłożone. U mnie wygląda to w ten sposób:
Oprogramujmy teraz odpowiednie zdarzenia.
Zacznijmy od OnClick przycisku BtnPolacz.
Jeżeli klient nie jest aktywny, sprawdzamy czy podano numer IP serwera bądź jego nazwę. Jeżeli nie, wyświetlamy na StatusBar odpowiednią wiadomość. Jeżeli wszystko w porządku, przypisujemy do Address (numer IP serwera) lub Host (nazwa serwera) odpowiednie wartości pobrane z Edit’ów. Następnie przypisujemy port. Jeżeli go nie podaliśmy, ujrzymy odpowiedni komunikat. Jeżeli podaliśmy odpowiednie dane, następuje aktywowanie socketa Klient i próba połączenia z serwerem. Na czas łączenia Dezaktywujemy przycisk BtnPolacz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
procedure TForm1.BtnPolaczClick(Sender: TObject); begin if not Klient.Active then begin if EdtIP.Text <> '' then Klient.Address := EdtIP.Text else if EdtHost.Text <> '' then Klient.Host := EdtHost.Text else begin StatBar.SimpleText := 'Musisz podać numer IP serwera lub jego nazwę.' ; Exit ; end ; if EdtPort.Text = '' then begin StatBar.SimpleText := 'Musisz podać numer portu.' ; Exit ; end ; Klient.Port := StrToInt(EdtPort.Text) ; BtnPolacz.Enabled := false ; Klient.Active := true ; end else begin Klient.Active := false ; BtnPolacz.Caption := 'Połącz' ; end ; end; |
Jeżeli jesteśmy już połączeni i klikniemy na przycisk BtnPolacz, to następuje dezaktywacja klienta i odłączenie od serwera. Na przycisku znowu pojawia się napis ‘Połącz’ (po udanym połączeniu zmienimy napis na ‘Rozłącz’, patrz dalej).
Teraz zdarzenia naszego TClientSocket.
OnConnect – zachodzi, gdy uda się nam połączyć z serwerem.
Na StatusBar pokazujemy odpowiednią wiadomość, udostępniamy przycisk do przesyłania wiadomości, na przycisku BtnPolacz zmieniamy napis na ‘Rozłącz’ i udostępniamy go.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
procedure TForm1.KlientConnect(Sender: TObject; Socket: TCustomWinSocket); begin StatBar.SimpleText := 'Połączono z serwerem.' ; BtnPrzeslij.Enabled := true ; BtnPolacz.Caption := 'Rozłącz' ; BtnPolacz.Enabled := true ; end; |
OnConnecting – zachodzi podczas łączenia się z serwerem.
1 2 3 4 5 6 7 8 9 |
procedure TForm1.KlientConnecting(Sender: TObject; Socket: TCustomWinSocket); begin StatBar.SimpleText := 'Trwa łączenie...' ; end; |
OnDisconnect – zachodzi, gdy z jakichś przyczyn połączenie zostanie przerwane.
Na StatusBar pokazujemy odpowiednią wiadomość, dezaktywujemy przycisk do przesyłania oraz zmieniamy napis oraz stan przycisku BtnPolacz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TForm1.KlientDisconnect(Sender: TObject; Socket: TCustomWinSocket); begin StatBar.SimpleText := 'Rozłączono z serwerem.' ; BtnPrzeslij.Enabled := false ; BtnPolacz.Caption := 'Połącz' ; BtnPolacz.Enabled := true ; end; |
OnError – jest to dla nas ważne zdarzenie. Jeżeli np. serwer nie byłby aktywny, zostałby wyświetlony błąd. Dzięki OnError, możemy rozpoznać błąd oraz sami powiadomić użytkownika o zaistniałym błędzie. Jeżeli do ErrorCode wstawimy wartość 0, nie zostanie wyświetlony żaden komunikat o błędzie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
procedure TForm1.KlientError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); begin case ErrorCode of 10060 : StatBar.SimpleText := 'Komputer-serwer nie jest w sieci.' ; 10061 : StatBar.SimpleText := 'Podany port nie jest otwarty na danym komputerze.' ; else StatBar.SimpleText := 'Wystąpił błąd, jego kod to: ' + IntToStr(ErrorCode) ; end ; ErrorCode := 0 ; BtnPolacz.Enabled := true ; end; |
OnRead – zachodzi, gdy serwer wysyła do nas dane.
W ReceiveText znajduje się wysłana przez serwer wiadomości tekstowa. Wstawiamy ja do odpowiedniego TRichEdit. Potem każemy ‘przescrollować tekst na dół’ – wyjaśniłem tą sprawę podczas tworzenia serwera.
1 2 3 4 5 6 7 8 9 |
procedure TForm1.KlientRead(Sender: TObject; Socket: TCustomWinSocket); begin ROdebrane.Lines.Add(Socket.ReceiveText) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; end;I na końcu, OnClick przycisku BtnPrzeslij: |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TForm1.BtnPrzeslijClick(Sender: TObject); begin if RDoWys.Text = '' then Exit ; ROdebrane.Lines.Add(RDoWys.Text) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; Klient.Socket.SendText(RDoWys.Text) ; RDoWys.Text := '' ; end; |
Już chyba nie trzeba omawiać co i jak. Powiem tylko, że SendText przesyła do serwera wiadomość tekstową.
Nareszcie, obie aplikacje gotowe ;] Uruchom teraz na swoim komputerze aplikacje-serwer oraz klient. Aktywuj serwer. W Kliencie, możesz albo wpisać swoje IP lub 127.0.0.1, albo jako Host podać localhost bądź nazwę swojego komputera. Podanie IP 127.0.0.1 lub Hosta localhost oznacza, że chcesz się połączyć z samym sobą. Potestuj, sprawdź czy wszystko działa.
Być może nie wiesz, jakie masz IP. Przedstawiam w jaki sposób można programowo sprawdzić swoje IP oraz nazwę komputera.
Posłużymy się w tym celu odpowiednimi funkcjami z biblioteki Winsock. Otwórz projekt aplikacji-serwera. Dodaj do uses Winsock. Umieść na formie dodatkowy przycisk, nazwij go BtnIP. Jego OnClick powinno wyglądać następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
procedure TForm1.Button1Click(Sender: TObject); var wVersionRequested : WORD ; wsaData : TWSAData ; P : PHostEnt ; S : array [0..128] of Char ; P2 : PChar ; begin // ładujemy bibliotekę Winsock wVersionRequested := MAKEWORD(1, 1); WSAStartup(wVersionRequested, wsaData); // pobieramy nazwę komputera GetHostName(@s , 128) ; p := GetHostByName(@s) ; ROdebrane.Lines.Add(P^.h_Name) ; // pobieramy adres IP P2 := iNet_ntoa(PInAddr(p^.h_addr_list^)^) ; ROdebrane.Lines.Add(p2) ; // zwalniamy Winsock WSACleanup ; end; |
Kod jest trochę bardziej zaawansowany. Najpierw ładujemy bibliotekę Winsock, poźniej, za pomocą GetHostName, otrzymujemy nazwę naszego komputera. Potem używamy GetHostByName aby otrzymać wskaźnik do struktury zawierającej dane o komputerze o nazwie przesłanej jako argument. Dodajemy do TRichEdit nazwę komputera (ROdebrane.Lines.Add(P^.h_Name); ). Funkcja iNet_ntoa konwertuje podany adres sieciowy do formatu znanego nam, czyli np. 192.168.0.1. Dodajemy adres do TRichEdit. Na końcu zwalniamy bibliotekę.
Po uruchomieniu i kliknięciu na przycisk, w TRichEdit pojawi się nasze IP oraz nazwa komputera.
Przesyłanie plików.
Zajmiemy się teraz przesyłaniem plików. Przedstawię kod, który będzie prawie identyczny dla serwera i klienta, więc nie ma sensu omawiać go krok po kroku w obu aplikacjach. Jak będzie się odbywać wysyłanie pliku? Na początek wskażemy plik, który chcemy wysłać, wyślemy do odbiorcy informacje i jego nazwie oraz rozmiarze. Jeżeli druga strona zechce odebrać plik, odpowie nam odpowiednim komunikatem, a wtedy zaczniemy przesyłać plik w częściach po 1 KB. Poza tym, zadbamy o to, ażeby w razie czego nie wystąpiły żadne błędy. Będziemy musieli zmodyfikować też trochę wcześniejszy kod. Od razu uprzedzam, że kod jest lekko zawiły 😉 Do dzieła!
Na początku dodaj na formę odpowiednie komponenty.
1. Przycisk, nazwij go BtnWyslijPlik. Ustaw jego właściwość Enabled na false.
2. TOpenDialog, który będzie nam służył do wybierania pliku. Nazwij go OpenDlg.
3. Na końcu wrzuć na formę TSaveDialog, nazwa: SaveDlg.
Potrzebnych będzie nam też kilka zmiennych globalnych:
1 2 3 4 5 6 7 8 9 |
var Form1: TForm1; F : TFileStream ; RozmiarPliku : Integer ; OdbierzPlik , WyslijPlik : Boolean ; |
F jest strumieniem, który będzie czytał/zapisywał kolejne bajty do pliku.
W RozmiarPliku przechowamy rozmiar odbieranego pliku.
Jeżeli zgodzimy się odebrać plik, to zmienna OdbierzPlik przybierze wartość true, dzięki czemu będziemy wiedzieli, że następne przesłane do nas bajty należy wpisać do pliku. Jeżeli w trakcie przesyłu będziemy musieli przerwać operację, nadamy tej zmiennej wartość false.
Przed rozpoczęciem wysyłania pliku WyslijPlik przyjmuje wartość true. Jeżeli w trakcie przesyłania trzeba będzie przerwać operację, to nadamy tej zmiennej wartość false, na przykład gdy wyłączymy program czy odłączy się od nas klient.
Przejdźmy do kodu. W OnClick przycisku BtnPrzeslijPlik wpisz kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
procedure TForm1.BtnWyslijPlikClick(Sender: TObject); begin if OpenDlg.Execute then begin F := TFileStream.Create(OpenDlg.FileName , fmOpenRead) ; Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText('/m:p' + IntToStr(F.Size) + ';' + ExtractFileName(OpenDlg.FileName)) ; BtnWyslijPlik.Enabled := false ; end ; end; |
Jeżeli wybierzesz plik, zostaje on otwarty do odczytu przez strumień F. Jeżeli nie znasz strumieni, nie martw się, ich obsługa jest dość prosta. W kolejnej linijce przesyłamy wiadomość do klienta w specjalny sposób: założyłem, że pewne informacje przesyłamy w postaci odpowiedni sformatowanych stringów, na początku stawiamy ‘/m:p’, co klient zinterpretuje jako chęć przesyłu do niego pliku. Możesz zmienić to, na co chcesz, np. /uwaga_komenda:przesylam_plik, ale najlepiej żeby był to krótki tekst. Będziemy wykorzystywali jeszcze kilka innych komend. Nie zamykamy pliku, bo nie ma to sensu – jeżeli użytkownik zechce odebrać plik, to od razu zaczniemy wysyłać jego kolejne części, a gdy skończymy, zamkniemy plik. Na czas wysyłania pliku nie będziemy mogli używać tego przycisku, więc uniedostępniamy go.
Teraz przejdźmy do sedna sprawy. Zmodyfikujemy zdarzenie OnClientRead (w kliencie: OnRead), tak, by zależności od tego, co się dzieje, odpowiednio reagowało. W OnClientRead musimy dodać szereg zmiennych, które będą nam potrzebne:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
procedure TForm1.SerwerClientRead(Sender: TObject; Socket: TCustomWinSocket); var Odczytane : Integer ; Buf : array [1..1024] of Char ; Wiadomosc : String ; NazwaPliku : String ; |
Odczytane – oznacza ilość wysłanych/odczytanych bajtów w rzeczywistości.
Buf – do tej tablicy wczytujemy kolejny bajty pliku, a następnie je wysyłamy, lub, w przypadku gdy to do nas jest wysyłany plik, przechowujemy w niej wysłane bajty, które później wpisujemy do pliku.
Wiadomosc – dane, które wysyła do nas druga strona.
NazwaPliku – jest to nazwa pliku, który odbieramy.
Na razie może się to wydawać skomplikowane, postaram się wyjaśnić wszystko przystępnie po kolei, rozbije kod na poszczególne fragmenty.
Na początku umieszczamy w zmiennej Wiadomosc dane do nas przeslane:
1 |
Wiadomosc := Socket.ReceiveText ; |
Potem sprawdzamy, jaką komenda zawarta jest tej wiadomości. Jeżeli wysłaliśmy już do użytkownika rozmiar oraz nazwę pliku, czekamy na jego rekcję. Jeżeli odeśle nam wiadomość ‘/m:y’, oznaczać to będzie, że chce odebrać plik i powinniśmy zacząć go wysyłać.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
if Wiadomosc = '/m:y' then // 1 begin WyslijPlik := true ; // 2 while F.Position < F.Size do // 3 begin Application.ProcessMessages ; // 4 if not WyslijPlik then Break ; // 5 Sleep(10) ; Odczytane := F.Read(Buf , SizeOf(Buf)) ; // 6 Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendBuf(Buf , Odczytane) ; // 7 StatBar.SimpleText := 'Przesyłam plik, postęp: ' + FormatFloat('0.00' , (F.Position / F.Size) * 100) + '%' ; // 8 end ; if F.Position < F.Size then StatBar.SimpleText := 'Wysyłanie pliku przerwane.' // 9 else BtnWyslijPlik.Enabled := true ; WyslijPlik := false ; F.Free ; end |
1. Tutaj sprawdzamy, czy w wiadomości zawarta jest odpowiednia komenda, jeżeli tak, to należy wysłać plik.
2. Ustawiamy wartość na true, bo tak długo, jak wartość jest true, będzie wysyłany plik. Jeżeli z pewnych powodów, o czym pisałem wcześniej, trzeba będzie przerwać wysyłanie – nadamy tej zmiennej wartość false.
3. Pętla wykonuje się dopóki pozycja w pliku, na której się znajdujemy, jest mniejsza od całego rozmiaru pliku.
4. Pozwalamy aplikacji obsłużyć komunikaty, które do niej napłynęły. Gdyby nie ta linijka, program nie reagował by na nasze działania podczas wysyłania pliku, wyglądało by to na zawieszenie.
5. Jeżeli z jakiegoś powodu WyslijPlik = false, to wyskakujemy z pętli i tym samym przerywamy przesyłanie pliku.
6. W tej linijce odczytujemy z pliku bajty, które zostają wpisane do tablicy Buf. Liczbę bajtów, które mają zostać odczytane, przysyłamy w drugim argumencie. Funkcja Read zwraca ilość odczytanych bajtów. Wartość tą wpisujemy do Odczytane.
7. W tej linijce wysyłamy część pliku do odbiorcy. Używamy SendBuf, podając w parametrze źródło danych, a w drugim – liczbę bajtów do wysłania z tego źródła.
Być może zapytasz, dlaczego potrzebna jest nam tam zmienna Odczytane? Przecież wczytujemy tyle bajtów, ile zmieście się tablicy Buf: SizeOf(Buf), a potem, w następnej tyle samo powinniśmy wysłać. Otóż trzeba przewidzieć, że na końcu zostanie do odczytania np. 120 bajtów, a przecież nasza tablica Buf pomieści ich 1024. Jeżeli podalibyśmy, że znowu należy wysłać wszystko, co mieści się w Buf, to przecież do pliku u odbiorcy dodane zostałby by bajty niepotrzebne, a nawet niechciane, przez które odczyt pliku byłby niemożliwy. Właśnie do tego używamy zmiennej Odczytane – funkcja Read zwraca ilość bajtów, które zostały odczytane w rzeczywistości, czyli np. 120, a potem tyle też bajtów wysyłamy.
8. Informujemy o postępie na StatusBar. Obliczamy, jakim procentem całej objetośći pliku (F.Size) jest pozycja (w bajtach) na której się znajdujemy. Wynik formatujemy za pomocą FormatFloat.
9. Gdy przesyłanie zostanie skończone, sprawdzamy, czy plik nie został wysłany w całości – pozycja na której skończyliśmy będzie różna od rozmiaru pliku – wtedy powiadamiamy na StatBar, że wysyłanie zostało przerwane. W przeciwnym razie, z powrotem udostępniamy plik BtnWyslijPlik. Potem przypisujemy do WyslijPlik wartość false i zamykamy plik.
To by było na tyle, jeśli chodzi o wysyłanie.
Jeżeli jednak użytkownik nie zechciał odebrać pliku, to wysyła do nas komendę /m:n, a my odpowiednio reagujemy:
1 2 3 4 5 6 7 8 9 10 11 |
else if Wiadomosc = '/m:n' then begin StatBar.SimpleText := 'Użytkownik nie chce odebrać pliku.' ; BtnWyslijPlik.Enabled := true ; F.Free ; end |
Pokazujemy odpowiedni tekst na StatBar, z powrotem udostępniamy przycisk do wysyłania plików i zamykamy wcześniej otwarty plik.
Dalej sprawdzamy, czy to do nas nie zechciano wysłać pliku – wtedy w wiadomość powinna zawierać komendę ‘/m:p’ oraz nazwę i rozmiar pliku:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
else if Pos('/m:p' , Wiadomosc) = 1 then // 1 begin // 2 RozmiarPliku := StrToInt(Copy(Wiadomosc , 5 , Pos(';' , Wiadomosc) - 5)) ; NazwaPliku := Copy(Wiadomosc , Pos(';' , Wiadomosc) + 1 , Length(Wiadomosc) - Pos(';' , Wiadomosc)) ; SaveDlg.FileName := NazwaPliku ; // 3 // 4 if (MessageBox(0 , PAnsiChar('Czy chcesz pobrać plik ' + NazwaPliku + ' (rozmiar: ' + IntToStr(RozmiarPliku div 1024) + ' KB) od użytkownika ' + Socket.RemoteAddress + '(' + Socket.RemoteHost + ')?') , 'Pobieranie pliku' , MB_YESNO + MB_ICONINFORMATION) = ID_YES) and (SaveDlg.Execute) then begin F := TFileStream.Create(SaveDlg.FileName , fmCreate) ; // 5 OdbierzPlik := true ; // 6 StatBar.SimpleText := 'Odbieram plik' ; Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText('/m:y') ; // 7 BtnWyslijPlik.Enabled := false ; // 8 end else Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText('/m:n') ; // 9 end |
1. Sprawdzamy, czy w wiadomości na początku znajduje się komenda ‘/m:p’.
2. Następne dwie linijki, jak pewnie pomyślałeś, są straszne. Wycedzamy z nich rozmiar oraz nazwę pliku.
3. Jako domyślną nazwę pliku w SaveDialog podajemy nazwę pliku, który ma być do nas wysłany.
4. Komponujemy wiadomość, w której pytamy czy chcemy pobrać plik o nazwie NazwaPliku o rozmiarze RozmiarPliku od danego użytkownika. Wywołanie MessageBox umieszczamy w warunku, bo jeżeli użytkownik wciśnie Tak i jeżeli potem wybierze nazwę dla pliku w SaveDialog (pokazujemy go za pomocą SaveDlg.Execute, funkcja ta zwraca true jeżeli użytkownik podał nazwę pliku i nacisnął OK), to powinniśmy odesłać wiadomość, że chcemy odebrać plik – komenda ‘/m:y’.
5. Otwieramy (a w razie, gdy nie istnieje, tworzymy go) plik do zapisu, w którym będą zapisywane kolejne przesłane do nas bajty.
6. Ustawiamy OdbierzPlik na true, co oznacza, że gdy następnym razem dojdą do nas jakieś dane, to należy je będzie wpisać do pliku.
7. Wysyłamy odpowiedź twierdzącą – chcemy odebrać plik.
8. Na czas odbierania pliku, uniedostępniamy przycisk odpowiedzialny za rozpoczęcie wysyłania.
9. Jeżeli jednak nie zgodziliśmy się odebrać pliku, to wysyłamy odpowiedź przeczącą – komenda ‘/m:n’.
Jeżeli w zmiennej Wiadomosc nie występuje żadna z powyższych komend, oznacza to, że po prostu wysyłany jest do nas zwykły tekst, czyli druga strona chce pogadać 😉 W tym przypadku dodajemy do RichEdit odebrany tekst:
1 2 3 4 5 6 7 8 9 10 11 |
else begin Delete(Wiadomosc , 1 , 4) ; ROdebrane.Lines.Add(Wiadomosc) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; end ; |
Znasz ten kod, używaliśmy go wcześniej, gdy nasze programy służyły jedynie do komunikacji. Jest jednak dodatkowa linijka: Delete(Wiadomosc , 1 , 4) ; Dlaczego musieliśmy ją dodać, dowiesz się za chwile.
Odbieranie pliku.
Jeżeli to do nas chciano wysłać plik i zgodziliśmy się na jego odebranie, należy kolejne wysłane dane wpisywać do pliku.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
if OdbierzPlik then begin Odczytane := Socket.ReceiveBuf(Buf , SizeOf(Buf)) ; // 1 Wiadomosc := Buf ; // 2 if Pos('/m:m' , Wiadomosc) = 1 then begin Delete(Wiadomosc , 1 , 4) ; ROdebrane.Lines.Add(StrPas(PChar(Wiadomosc))) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; Exit ; end ; StatBar.SimpleText := 'Odbieram plik, postęp: ' + FormatFloat('0.00' , (F.Position / RozmiarPliku) * 100) + '%' ; // 3 F.Write(Buf , Odczytane) ; // 4 if F.Position >= RozmiarPliku then // 5 begin F.Free ; StatBar.SimpleText := 'Przesyłanie zakończone sukcesem.' ; OdbierzPlik := false ; BtnWyslijPlik.Enabled := true ; end ; end |
1. Do tablicy Buf wpisujemy wysłane do nas dane, a w zmiennej Odczytane zapisujemy, ile tak naprawdę tych danych było (opisałem co i jak wcześniej).
3. Na StatBar wypisujemy postęp w odbieraniu pliku, dzielimy aktualną pozycją pliku przez cały jego rozmiar. Rozmiar otrzymaliśmy w wiadomości od drugiej strony (wraz z nazwą pliku).
4. Wpisujemy do pliku odpowiednią liczbę bajtów z tablicy Buf.
5. Jeżeli osiągnęliśmy pozycję w pliku równą rozmiarowi pliku, to znaczy, że odebraliśmy już cały plik. Zamykamy plik, wyświetlamy odpowiednią wiadomość na StatBar, ustawiamy OdbierzPlik na false oraz umożliwiamy wysłanie pliku, poprzez udostępnienie przycisku BtnWyslijPlik.
Jak widzisz, ominąłem jeden punkt – 2. Po co jest tamten kod? Zastanów, co by się stało, gdyby podczas odbierania pliku, osoba go do nas wysyłająca napisałaby jakąś wiadomość tekstową i zechciała nam przesłać. Wtedy, z racji tego, że należy odebrać plik, wysłany tekst został by zamiast do TRichEdit wpisany do pliku, co byłoby katastrofalne w skutkach – plik byłby niemożliwy do odczytu. Musimy więc w jakiś sposób sprawdzić, czy wysyłana jest zwykła wiadomość tekstowa, czy szereg bajtów pliku, który odbieramy. Dlatego w zdarzeniu OnClick przycisky BtnPrzeslij (to ten, którego używamy do przesłania właśnie wiadomości tekstowej) zmieniliśmy linijkę odpowiedzialną za wysłanie tekstu. Dodaliśmy na początek ‘/m:m’, co ma oznaczać, że wysyłane dane są wiadomością tekstową i nie należy ich wpisywać do pliku, lecz do naszego TRichEdit. Tak wygląda to zdarzenie po zmodyfikowaniu, wprowadź u siebie odpowiednie zmiany:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
procedure TForm1.BtnPrzeslijClick(Sender: TObject); begin if RDoWys.Text = '' then Exit ; if Serwer.Socket.ActiveConnections > 0 then begin ROdebrane.Lines.Add(RDoWys.Text) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText('/m:m' + RDoWys.Text) ; RDoWys.Text := '' ; end ; end; |
Dlatego we wcześniejszym kodzie musieliśmy dodać Delete(Wiadomosc , 1 , 4) ; aby usunąć niepotrzebną komendę ‘/m:m’.
Wróćmy do części kodu oznaczonej jako 2. Na początku sprawdzamy, czy na początku zmiennej Wiadomos znajduje się komenda ‘/m:m’, jeżeli tak, to usuwamy z niej niepotrzebną cześć ‘/m:m’, potem za pomocą StrPas obcinamy niepotrzebne znaki i dodajemy to co zostało do TRichEdit, a na końcu wyskakujemy ze zdarzenia za pomocą Exit.
Trzeba jeszcze zmodyfikować dwa zdarzenia serwera: OnAccept oraz OnClientDisonnect, a w klienci: OnConnect i OnDisconnect.
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.SerwerAccept(Sender: TObject; Socket: TCustomWinSocket); begin StatBar.SimpleText := 'Akceptuje połączenie z ' + Socket.RemoteAddress + '(' + Socket.RemoteHost + ')' ; BtnPrzeslij.Enabled := true ; BtnWyslijPlik.Enabled := true ; end; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
procedure TForm1.SerwerClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); begin StatBar.SimpleText := 'Odłączył się: ' + Socket.RemoteAddress + '(' + Socket.RemoteHost + ')' ; BtnPrzeslij.Enabled := false ; BtnWyslijPlik.Enabled := false ; if OdbierzPlik then begin OdbierzPlik := false ; StatBar.SimpleText := 'Odbieranie pliku przerwane.' ; F.Free ; end ; WyslijPlik := false ; end; |
Jeżeli klient się odłączy, a odbieraliśmy od niego plik, musimy zmienić wartość OdbierzPlik na false, umieścić stosowny komunikat na StatBar oraz zamknąć plik. Jeżeli to my wysyłaliśmy plik, to dzięki ustawieniu WyslijPlik na false, przerwie dalszy przesył.
Na końcu pozostaje dodanie zdarzenia OnClose formy:
1 2 3 4 5 6 7 |
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin WyslijPlik := false ; end; |
Co zapewni przerwanie wysyłania w razie zamknięcia programu.
—
Oto pełny kod OnClientRead serwera:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
procedure TForm1.SerwerClientRead(Sender: TObject; Socket: TCustomWinSocket); var Odczytane : Integer ; Buf : array [1..1024] of Char ; Wiadomosc : String ; NazwaPliku : String ; begin if OdbierzPlik then begin Odczytane := Socket.ReceiveBuf(Buf , SizeOf(Buf)) ; Wiadomosc := Buf ; if Pos('/m:m' , Wiadomosc) = 1 then begin Delete(Wiadomosc , 1 , 4) ; ROdebrane.Lines.Add(StrPas(PChar(Wiadomosc))) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; Exit ; end ; StatBar.SimpleText := 'Odbieram plik, postęp: ' + FormatFloat('0.00' , (F.Position / RozmiarPliku) * 100) + '%' ; F.Write(Buf , Odczytane) ; if F.Position >= RozmiarPliku then begin F.Free ; StatBar.SimpleText := 'Przesyłanie zakończone sukcesem.' ; OdbierzPlik := false ; BtnWyslijPlik.Enabled := true ; end ; end else begin Wiadomosc := Socket.ReceiveText ; if Wiadomosc = '/m:y' then begin WyslijPlik := true ; while F.Position < F.Size do begin Application.ProcessMessages ; if not WyslijPlik then Break ; Sleep(10) ; Odczytane := F.Read(Buf , SizeOf(Buf)) ; Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendBuf(Buf , Odczytane) ; StatBar.SimpleText := 'Przesyłam plik, postęp: ' + FormatFloat('0.00' , (F.Position / F.Size) * 100) + '%' ; end ; if F.Position < F.Size then StatBar.SimpleText := 'Wysyłanie pliku przerwane.' else BtnWyslijPlik.Enabled := true ; WyslijPlik := false ; F.Free ; end else if Wiadomosc = '/m:n' then begin StatBar.SimpleText := 'Użytkownik nie chce odebrać pliku.' ; BtnWyslijPlik.Enabled := true ; F.Free ; end else if Pos('/m:p' , Wiadomosc) = 1 then begin RozmiarPliku := StrToInt(Copy(Wiadomosc , 5 ,Pos(';' , Wiadomosc)-5)) ; NazwaPliku := Copy(Wiadomosc , Pos(';' , Wiadomosc) + 1 , Length(Wiadomosc) - Pos(';' , Wiadomosc)) ; SaveDlg.FileName := NazwaPliku ; if (MessageBox(0 , PAnsiChar('Czy chcesz pobrać plik ' + NazwaPliku + ' (rozmiar: ' + IntToStr(RozmiarPliku div 1024) + ' KB) od użytkownika ' + Socket.RemoteAddress + '(' + Socket.RemoteHost + ')?') , 'Pobieranie pliku' , MB_YESNO + MB_ICONINFORMATION) = ID_YES) and (SaveDlg.Execute) then begin F := TFileStream.Create(SaveDlg.FileName , fmCreate) ; OdbierzPlik := true ; StatBar.SimpleText := 'Odbieram plik' ; Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText('/m:y') ; BtnWyslijPlik.Enabled := false ; end else Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText('/m:n') ; end else begin Delete(Wiadomosc , 1 , 4) ; ROdebrane.Lines.Add(Wiadomosc) ; SendMessage(ROdebrane.Handle , WM_VSCROLL , SB_BOTTOM , 0) ; end ; end ; end; |
Jeżeli dodałeś już wszystko w serwerze i zmodyfikowałeś zdarzenia, które tego wymagały, zajmij się teraz aplikacją – klientem. OnRead klienta będzie identyczne z OnClientRead serwera poza linijkami, w których coś wysyłamy, np:
1 |
Serwer.Socket.Connections[Serwer.Socket.ActiveConnections - 1].SendText('/m:y') ; |
Takie linijki zamień na:
1 |
Klient.Socket.SendText('/m:y') ; |
To by było na tyle, jeśli chodzi o wysyłanie plików. Na tym też zakończę artykuł o Socketach. Można by oczywiście bardziej rozbudować oba programy, aby umożliwić np. wysyłanie kilku plików na raz, wysyłanie i odbieranie plików w tym samym czasie czy dalsze ściąganie pliku np. od połowy po wcześniejszym przerwanym połączeniu. Można by umożliwić podłączanie się z wieloma klientami, rozmawiania z nimi pojedynczo oraz w grupach itp. itd. – jak to w programowaniu, możliwości są nie ograniczone :> ale to już zostawiam do zrobienia we własnym zakresie. Podpowiem tylko, że można kolejni ‘klienci’ są ‘przechowywani’ w tablicy Serwer.Socket.Connections, jeżeli chcielibyśmy na przykład wysłać do każdego z nich wiadomości, moglibyśmy skorzystać z poniższego kodu:
1 2 3 |
for i := 0 to Serwer.Socket.ActiveConnections - 1 do Serwer.Socket.Connections.SendText(‘Tekst do wszystkich klientow’) ; |
W razie zauważonych błędów piszcie w komentarzach/na forum w dziale Serwis Unit1 i forum, a najlepiej, przesyłajcie PW na forum, pozdro.