Programowanie gniazd w C
Programowanie gniazd to sposób łączenia dwóch węzłów w sieci w celu wzajemnej komunikacji. Jedno gniazdo (węzeł) nasłuchuje na określonym porcie pod adresem IP, podczas gdy drugie gniazdo łączy się z drugim, tworząc połączenie. Serwer tworzy gniazdo nasłuchiwania, podczas gdy klient łączy się z serwerem.
Programowanie gniazd jest szeroko stosowane w aplikacjach do przesyłania wiadomości błyskawicznych, strumieniowaniu binarnym i współpracy nad dokumentami, platformach strumieniowego przesyłania danych online itp.
Przykład
W tym programie w języku C wymieniamy jedną wiadomość powitalną pomiędzy serwerem a klientem, aby zademonstrować model klient/serwer.
serwer.c
C #include #include #include #include #include #include #define PORT 8080 int main ( int argc char const * argv []) { int server_fd new_socket ; ssize_t valread ; struct sockaddr_in address ; int opt = 1 ; socklen_t addrlen = sizeof ( address ); char buffer [ 1024 ] = { 0 }; char * hello = 'Hello from server' ; // Creating socket file descriptor if (( server_fd = socket ( AF_INET SOCK_STREAM 0 )) < 0 ) { perror ( 'socket failed' ); exit ( EXIT_FAILURE ); } // Forcefully attaching socket to the port 8080 if ( setsockopt ( server_fd SOL_SOCKET SO_REUSEADDR | SO_REUSEPORT & opt sizeof ( opt ))) { perror ( 'setsockopt' ); exit ( EXIT_FAILURE ); } address . sin_family = AF_INET ; address . sin_addr . s_addr = INADDR_ANY ; address . sin_port = htons ( PORT ); // Forcefully attaching socket to the port 8080 if ( bind ( server_fd ( struct sockaddr * ) & address sizeof ( address )) < 0 ) { perror ( 'bind failed' ); exit ( EXIT_FAILURE ); } if ( listen ( server_fd 3 ) < 0 ) { perror ( 'listen' ); exit ( EXIT_FAILURE ); } if (( new_socket = accept ( server_fd ( struct sockaddr * ) & address & addrlen )) < 0 ) { perror ( 'accept' ); exit ( EXIT_FAILURE ); } // subtract 1 for the null // terminator at the end valread = read ( new_socket buffer 1024 - 1 ); printf ( '%s n ' buffer ); send ( new_socket hello strlen ( hello ) 0 ); printf ( 'Hello message sent n ' ); // closing the connected socket close ( new_socket ); // closing the listening socket close ( server_fd ); return 0 ; }
klient.c
C #include #include #include #include #include #define PORT 8080 int main ( int argc char const * argv []) { int status valread client_fd ; struct sockaddr_in serv_addr ; char * hello = 'Hello from client' ; char buffer [ 1024 ] = { 0 }; if (( client_fd = socket ( AF_INET SOCK_STREAM 0 )) < 0 ) { printf ( ' n Socket creation error n ' ); return -1 ; } serv_addr . sin_family = AF_INET ; serv_addr . sin_port = htons ( PORT ); // Convert IPv4 and IPv6 addresses from text to binary // form if ( inet_pton ( AF_INET '127.0.0.1' & serv_addr . sin_addr ) <= 0 ) { printf ( ' n Invalid address/ Address not supported n ' ); return -1 ; } if (( status = connect ( client_fd ( struct sockaddr * ) & serv_addr sizeof ( serv_addr ))) < 0 ) { printf ( ' n Connection Failed n ' ); return -1 ; } // subtract 1 for the null // terminator at the end send ( client_fd hello strlen ( hello ) 0 ); printf ( 'Hello message sent n ' ); valread = read ( client_fd buffer 1024 - 1 ); printf ( '%s n ' buffer ); // closing the connected socket close ( client_fd ); return 0 ; }
Kompilowanie
gcc client.c -o clientgcc server.c -o server
WyjścieClient:Hello message sentHello from serverServer:Hello from clientHello message sentSkładniki programowania gniazdowego
1. Gniazda
Gniazda to jeden z podstawowych komponentów używanych przez program do uzyskiwania dostępu do sieci w celu komunikowania się z innymi procesami/węzłami za pośrednictwem sieci. Jest to po prostu kombinacja adresu IP i numeru portu, który działa jako punkt końcowy komunikacji.
Przykład: 192.168.1.1:8080 gdzie dwie części oddzielone dwukropkiem reprezentują Adres IP (192.168.1.1) i numer portu (8080).Typy gniazd:
- Gniazdo TCP (gniazdo strumieniowe): Zapewnia niezawodną komunikację opartą na połączeniach (tj. Protokół TCP ).
- Gniazdo UDP (gniazdo datagramu): Zapewnia komunikację bezpołączeniową szybszą, ale zawodną (tj. Protokół UDP ).
2. Model klient-serwer
The modelu klient-serwer odnosi się do architektury używanej w programowaniu gniazd, w której klient i serwer współdziałają ze sobą w celu wymiany informacji lub usług. Architektura ta umożliwia klientowi wysyłanie żądań usług, a serwerowi przetwarzanie i wysyłanie odpowiedzi na te żądania usług.
Diagram stanu dla modelu serwera i klienta
Diagram stanu dla modelu serwera i klienta Socket Programowanie gniazd w języku C to skuteczny sposób obsługi komunikacji sieciowej.
Tworzenie procesu po stronie serwera
Serwer jest tworzony w następujący sposób:
1. Tworzenie gniazda
Ten krok polega na utworzeniu gniazda za pomocą funkcji Socket().
Parametry:
- skarpetka: deskryptor gniazda liczba całkowita (jak uchwyt pliku)
- domena: liczba całkowita określa domenę komunikacyjną. Używamy AF_LOCAL zgodnie z definicją w standardzie POSIX do komunikacji między procesami na tym samym hoście. Do komunikacji pomiędzy procesami na różnych hostach połączonych przez IPV4 używamy AF_INET i AF_I NET 6 dla procesów połączonych przez IPV6.
- typ: typ komunikacji
SOCK_STREAM: TCP (niezawodny zorientowany na połączenie)
SOCK_DGRAM: UDP (niewiarygodny brak połączenia) - protokół: Wartość protokołu dla protokołu internetowego (IP), która wynosi 0. Jest to ten sam numer, który pojawia się w polu protokołu w nagłówku IP pakietu. (więcej szczegółów można znaleźć w protokołach man)
sockfd = socket ( domain type protocol )
2. Ustaw opcję gniazda
Pomaga to w manipulowaniu opcjami gniazda, do którego odnosi się deskryptor pliku sockfd. Jest to całkowicie opcjonalne, ale pomaga w ponownym wykorzystaniu adresu i portu. Zapobiega błędom takim jak: adres już używany.
C setsockopt ( sockfd level optname optval socklen_t optlen );
3. Zwiąż
Po utworzeniu gniazda funkcja bind() wiąże gniazdo z adresem i numerem portu określonymi w adresie (niestandardowa struktura danych). W przykładowym kodzie łączymy serwer z hostem lokalnym, dlatego używamy INADDR_ANY do określenia adresu IP.
C++ bind ( sockfd sockaddr * addr socklen_t addrlen );
Parametry:
- skarpetka : deskryptor pliku gniazda utworzony przy użyciu funkcji Socket().
- adres : wskaźnik do struktury sockaddr zawierającej adres IP i numer portu do powiązania z gniazdem.
- adres : długość struktury adresu.
4. Słuchaj
Na tym etapie serwer używa funkcji Listen(), która ustawia gniazdo serwera w tryb pasywny, w którym oczekuje, aż klient zbliży się do serwera w celu nawiązania połączenia. Backlog określa maksymalną długość, do której może urosnąć kolejka oczekujących połączeń dla sockfd. Jeśli żądanie połączenia nadejdzie, gdy kolejka jest pełna, klient może otrzymać błąd ze wskazaniem ECONNREFUSED.
C listen ( sockfd backlog );
Parametry :
- skarpetka : deskryptor pliku gniazda utworzony przy użyciu funkcji Socket().
- zaległości : liczba reprezentująca rozmiar kolejki oczekujących połączeń, podczas gdy serwer czeka na przyjęcie połączenia.
5. Zaakceptuj
W tym kroku serwer wyodrębnia pierwsze żądanie połączenia z kolejki oczekujących połączeń dla gniazda nasłuchującego. sockfd tworzy nowe połączone gniazdo za pomocą przyjąć() funkcję i zwraca nowy deskryptor pliku odnoszący się do tego gniazda. W tym momencie zostaje nawiązane połączenie pomiędzy klientem a serwerem i serwer jest gotowy do przesyłania danych.
C new_socket = accept ( sockfd sockaddr * addr socklen_t * addrlen );
Parametry:
- skarpetka : deskryptor pliku gniazda zwracany przez gniazdo() i bind().
- adres : wskaźnik do struktury sockaddr, w której będzie przechowywany adres IP i numer portu klienta.
- adres : wskaźnik do zmiennej określającej długość struktury adresu.
6. Wyślij/Odbierz
Na tym etapie serwer może wysyłać lub odbierać dane od klienta.
Wysłać(): do przesłania danych do klienta
C send ( sockfd * buf len flags );
Parametry:
- skarpetka : deskryptor pliku gniazda zwrócony przez funkcję Socket().
- buf : wskaźnik do bufora zawierającego dane do wysłania.
- tylko : liczba bajtów danych do wysłania.
- flagi : liczba całkowita określająca różne opcje sposobu wysyłania danych. Zazwyczaj 0 jest używane jako zachowanie domyślne.
Odbierać() : do odbioru danych od klienta.
C recv ( sockfd * buf len flags );
Parametry:
- skarpetka : deskryptor pliku gniazda zwrócony przez funkcję Socket().
- buf : wskaźnik do bufora zawierającego dane, które mają być przechowywane.
- tylko : liczba bajtów danych do wysłania.
- flagi : liczba całkowita określająca różne opcje sposobu wysyłania danych. Zazwyczaj 0 jest używane jako zachowanie domyślne.
6. Zamknij
Po zakończeniu wymiany informacji serwer zamyka gniazdo przy pomocy funkcji close() i zwalnia zasoby systemowe.
C close ( fd );
Parametry:
- fd: deskryptor pliku gniazda.
Tworzenie procesu po stronie klienta
Wykonaj poniższe kroki, aby utworzyć proces po stronie klienta:
1. Podłączenie do gniazda
Ten krok polega na utworzeniu gniazda, które odbywa się w taki sam sposób, jak tworzenie gniazda serwera
2. Połącz
Wywołanie systemowe connect() łączy gniazdo określone przez deskryptor pliku sockfd z adresem określonym przez adres. Adres i port serwera podano w adresie.
C++ connect ( sockfd sockaddr * addr socklen_t addrlen );
Parametry
- skarpetka : deskryptor pliku gniazda zwrócony przez funkcję Socket().
- adres : wskaźnik do struktury sockaddr zawierającej adres IP serwera i numer portu.
- adres : wielkość adresu
3. Wyślij/Odbierz
Na tym etapie klient może wysyłać lub odbierać dane z serwera, co odbywa się za pomocą funkcji send() i recieve() podobnych do sposobu, w jaki serwer wysyła/odbiera dane od klienta.
4. Zamknij
Po zakończeniu wymiany informacji klient musi również zamknąć utworzone gniazdo i zwolnić zasoby systemowe za pomocą funkcji close() w taki sam sposób, jak robi to serwer.
Typowe problemy i ich poprawki w programowaniu gniazd
- Błędy połączenia: Aby uniknąć awarii połączenia powinniśmy upewnić się, że klient próbuje połączyć się z właściwym Adres IP i port .
- Błędy wiązania portów: Te błędy występują, gdy port jest już używany przez inną aplikację. W tym scenariuszu powiązanie z tym portem nie powiedzie się. Spróbuj użyć innego portu lub zamknij poprzednią aplikację korzystającą z portu.
- Blokowanie gniazd: Domyślnie gniazda są blokowane. Oznacza to, że wywołania takie jak Accept() lub recv() będą czekać w nieskończoność, jeśli nie ma połączenia z klientem lub danych. W razie potrzeby możesz ustawić gniazdo w tryb nieblokujący.