Prosta komunikacja TCP i UDP oparta o gniazda

Opublikowany: 31-03-2011 20:36 przez Krystian

W dzisiejszym artykule skupię się na komunikacji między komputerami za pomocą gniazd (Socket BSD, WinSock i takie tam). Co prawda gniazda nie dają wielkiego pola manewru jednak na sam początek z pewnością nam wystarczą. Główna ich zaleta to wygoda użytkowania. W zasadzie większość roboty wykonają za nas biblioteki i system operacyjny. Naturalnie do każdego przykładu pojawi się dzisiaj kod źródłowy z odpowiednim komentarzem.


Prosta komunikacja przy użyciu TCP

Zacznijmy może od najprostszej komunikacji. Pomiędzy linuxowym serwerem i linuxowym klientem za pośrednictwem TCP. W zasadzie tego typu rozwiązanie wraz z kodem pojawia się na wielu stronach wprowadzających do socketów, więc pójdziemy krok dalej żeby bezsensownie nie powielać tego co już jest łatwo dostępne. Dodamy więc komunikację w obie strony, oraz pozbędziemy się blokowania (czyli komunikacji po jednej wiadomości na zmianę). Co pewnie będzie dość istotne dla tych, którzy trafili tutaj, aby znaleźć coś użytecznego do swojego projektu na uczelnie (-:


Zacznijmy od serwera, którego kod znajdziecie w tym miejscu (link). Po skompilowaniu podczas uruchomienia podajemy mu tylko port na jakim serwer będzie nasłuchiwał. Jak widać kodu nie ma zbyt wiele, jednak dzięki wykorzystaniu socketów (#include ) w tych kilkudziesięciu liniach kodu mamy serwer, który czeka na nawiązanie przez klienta połączenia TCP, po czym przechodzi do komunikacji dwukierunkowej.


Kilka ważniejszych spraw z kodu serwera:


sock=socket(AF_INET, SOCK_STREAM,0);

Funkcja socket tworzy nasze podstawowe gniazdo, z którego będziemy korzystać. Pierwszy parametr określa protokół IP z jakiego będziemy korzystać (AF_INET dla IPv4 AF_INET6 dla IPv6), drugi parametr pozwala nam wybrać czy chcemy korzystać z datagramów UDP (SOCK_DGRAM) czy pakietów TCP (SOCK_STREAM), trzeci parametr pozwala wybrać protokół warstwy transportowej, ale skoro wcześniejszy wybór jasno określa co chcemy wysyłać to pole można zostawić w trybie domyślnym nadając mu wartość 0 (zostanie wybrany domyślny protokół pasujący do wybranych wcześniej dwóch wartości). Wynik przypisujemy do zmiennej sock, która przyda się później, ponieważ zwrócony zostanie nam deskryptor utworzonego gniazda.


status=bind(sock, (struct sockaddr*)&myAddr, sizeof (myAddr));

Funkcja bind przypisuje do utworzonego wcześniej gniazda adres i port. Pierwszym parametrem jest oczywiście numer deskryptora, dalej podajemy strukturę określonej wielkości, którą znajdziecie w kodzie. W strukturze tej pojawia się pole myAddr.sin_port=htons(port); gdzie wrzucamy podany podczas uruchomienia port, na którym nasz serwer będzie nasłuchiwał. Inne pole w strukturze myAddr.sin_addr.s_addr=INADDR_ANY; spowoduje, że sam system przypisze nasze gniazdo do odpowiedniego lokalnego adresu IP (można rzecz jasna ręcznie ustawić adres).


sluchanie=listen(sock,5);

Funkcja listen uruchamia nasłuch na gnieździe przypisanym do podanego deskryptora. Druga podawana wartość to rozmiar kolejki. W naszym przypadku wartość jest nieistotna, ponieważ będziemy łączyli się tylko z jednym klientem.


sock2=accept(sock,(struct sockaddr*)&clientaddr,&clientaddrlen);

Kiedy już doczekamy się nowego klienta funkcja accept wrzuci jego dane do struktury przeznaczonej dla klienta i zwróci nam numer nowego deskryptora za pomocą, którego będziemy mogli się komunikować z klientem.


W tym momencie jesteśmy gotowi do nadawania i odbierania. Kod programu zaś wchodzi do pętli głównej, gdzie mamy dwa podobne fragmenty kodu. Pierwsza część zaczyna się od "wyzerowania" obserwacji deskryptorów FD_ZERO(&rfds); Dalej wybieramy deskryptor przypisany do naszego klienta FD_SET(sock2, &rfds); i poleceniem rv = select(sock2+1, &rfds, NULL, NULL, &timer); sprawdzamy co u niego słychać (czy coś dla nas ma). Polecenie select może sprawdzać deskryptor pod względem odczytu, zapisu, lub błędów, ostatni przyjmowany parametr to wskaźnik do struktury zawierające ustawienia czasu (w końcu nie ma sensu zbyt często sprawdzać, dla celów testowych możemy ustawić timer.tv_sec na wartość 1.0, ustawić kilka "flag" i obserwować powoli (taki slow motion) co się dzieje w programie.


Jeśli select ma coś dla nas zwróci nam coś innego niż 0, wtedy wchodzimy do pętli, za pomocą recv(sock2, buforodczyt, 1024, MSG_DONTWAIT); wrzucamy czekające nas dane do bufora, a następnie wrzucamy je na ekran zwykłym printf’em.


Druga pętla jest podobna tyle tylko, że zamiast deskryptora gniazda sprawdzamy wejście z klawiatury STDIN_FILENO i jeśli coś podaliśmy to zbieramy to fgets(buforzapis, 1024, stdin); do bufora i wysyłamy do klienta send(sock2, buforzapis, strlen(buforzapis), MSG_NOSIGNAL);


To w zasadzie tyle. Nic nie stoi na przeszkodzie dorzucić jeszcze jednego selecta, dla funkcji accept i dzięki temu łączyć się jednocześnie z wieloma klientami. Accept ma to do siebie, że zwraca przewidywalne wartości, jeśli jest pusto to zwraca 3, jeśli pojawia się nowy "interesant" zwraca przypisany do niego nowy deskryptor, wartość to kolejne stałe, więc pierwszy dostanie 4, drugi 5 i tak dalej. Oczywiście wtedy trzeba będzie zmodyfikować fragment odpowiadający za wysyłanie wiadomości przez serwer, tak aby pisał do wszystkich deskryptorów. Można również pobawić się z analizą danych wprowadzanych z ekranu i przykładowo ustalić własne polecenia sterujące naszym serwerem. Wymyślać można do woli i ogranicza nas tylko wyobraźnia. Macie tutaj jednak podstawę, na której resztę można spokojnie oprzeć (-:


Chwilkę poświęcimy na analizę kodu klienta, który znajdziecie tutaj (podczas uruchamiania podajemy mu adres IP serwera oraz jego port). Chwilkę ponieważ różnic nie ma wiele. Pierwsza to brak funkcji bind i listen. Zamiast nich mamy funkcję connect, która próbuje podłączyć się do wskazanego w strukturze portu i adresu ip. Jeśli wszystko jest w porządku connect zwróci nam 0 jeśli coś poszło źle -1. O adres ip i port klienta zatroszczy się sam system. Teraz wystarczy sprawdzać wejście i wysyłać dane na deskryptor zwrócony przez funkcję socket. Lub odbierać sprawdzając również ten właśnie deskryptor.


Całość może nie powala, ale stosunkowo niewielkim nakładem pracy mamy sprawnie działający prosty komunikator internetowy (kilka zmienionych linijek i zadziała również na IPv6). Nic nie stoi na przeszkodzie, żeby komunikować się z Windowsami, w końcu one też mają biblioteki do tworzenia i korzystania z gniazd.


Prosta komunikacja przy użyciu UDP

Skoro można to zrobić po TCP to można też po UDP. W sumie UDP będzie lepsze do takiej prostej komunikacji. Tym razem potrzebować będziemy tylko jednego programu (kod macie tutaj) na obu komputerach (czyli bez podziału na klienta i serwer). Podczas uruchomienia podajemy port, na którym będziemy prowadzić nasłuch (przy okazji będzie też domyślnym zdalnym portem naszego partnera do komunikacji), oraz adres IP, na którym nasłuchiwać będzie nasz drugi program. Do dyspozycji mamy coś w rodzaju menu i kilka prostych poleceń. Polecenie "menu" wyświetli listę komend, zaś "exit" zakończy program. Do komunikacji posłużą następujące dwa polecenia. "sendserv wiadomość" wyśle data gram UDP z podaną zawartością na adres i port podany przy uruchomieniu. "send IP:port wiadomość" - to polecenie wyśle na dowolny podany adres i port naszą wiadomość.


Przejdźmy może do kodu. Na wstępie przeproszę za jego dziwność, ponieważ wyciągnąłem go ze swojego większego programu (-; Przepraszam też za wszystkie głupie, nielogiczne, nieoptymalne rozwiązania, czasami tak mam (-;


Ważniejsze fragmenty:

sock_udp = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

Podobnie jak w komunikacji TCP tworzymy sobie gniazdo, tym razem oczywiście nie TCP tylko UDP, stąd odpowiednie zmiany.


bind(sock_udp,(struct sockaddr *)&sa, sizeof sa)

Bindujemy odpowiednią strukturę do wcześniej utworzonego socket. W strukturze sa przechowujemy informacje o naszym gnieździe, w sa2 trzymamy dane podane na wejściu programu, które wykorzystujemy przy poleceniu "sendserv", zaś sa3 jest każdorazowo tworzone przy wysyłaniu datagramu poprzez polecenie "send".



Przy odbieraniu wiadomości podobnie jak w komunikacji TCP korzystamy z selecta (używamy go tak jak przy TCP więc nie będę się już powtarzał) oraz funkcji:

recsize = recvfrom(sock_udp, (void *)buffer, 1024, 0, (struct sockaddr *)&sa, &fromlen);

która przypisuje do zmiennej recsize zawartość bufora wejściowego naszego gniazda. Jeśli nic tam ciekawego nie ma, zwraca 0, w przeciwnym wypadku przyjmuje pewną wartość i zapisuje treść datagramu do zmiennej buffer.


Przy wysyłaniu datagramów korzystamy z funkcji:

bytes_sent = sendto(sock_udp, buforeksp, strlen(buforeksp), 0,(struct sockaddr*)&sa3, sizeof sa3);

Która pcha w świat zawartość bufora (buforeksp) o danej długości (strlen(buforeksp)) na adres umieszczony w strukturze sa3 (lub sa2, w zależności od polecenia, z którego skorzystaliśmy).


Tak oto mamy trywialny kliento-serwer UDP z prościutkim menu, w sam raz do niezobowiązującej wymiany myśli (-;


bro
Brak komentarzy