Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Brian 'Beej' Hall <beej@piratehaven.org>
Tłumaczenie: Bartosz Zapałowski <bartek@klepisko.eu.org>
Biblioteki C++

Wywołania systemowe

[lekcja] Rozdział 4. Narzędzia do komunikacji sieciowej.
W tej sekcji przyjrzymy się bliżej wywołaniom systemowym, które dają dostęp do funkcjonalności sieci na maszynie Uniksowej. Gdy wywołujesz jedną z tych funkcji, jądro przejmuje wywołanie i załatwia całą sprawę automagicznie.

Miejscem, w którym większość ludzi się zatrzymuje jest prawidłowa kolejność wywoływania tych funkcji. W tej sytuacji podręcznik man nie jest zbyt pomocny, co już zapewne mogłeś zauważyć. Żeby pomóc ci w tej okropnej sytuacji, starałem się umieścić wywołania systemowe w kolejnych sekcjach w takiej samej kolejności, w jakiej będziesz je wywoływał w swoich programach.

Powyższe, połączone z wieloma przykładami kodu umieszczonymi tu i tam, z niewielką ilością mleka i ciasteczek (w które mam nadzieję już się zaopatrzyłeś), jak również odwagą, będziesz przesyłał dane przez Internet jak Son of Jon Postel (od tłumacza: niech to ktoś ładnie przetłumaczy!).

socket() -- Daj deskryptor pliku!

Zapewne nie mogę już dłużej tego odkładać - muszę powiedzieć o wywołaniu systemowym socket(). Oto jej prototyp:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

int socket( int domain, int type, int protocol );
Ale co to za argumenty? Pierwszy, domena powinien być ustawiony na "AF_INET", tak jak w struct sockaddr_in powyżej (od tłumacza: obecnie za właściwe uznaje się wstawianie tutaj wartości PF_INET). Następny parametr, typ, mówi jądru jaki jest to typ gniazda: SOCK_STREAM lub SOCK_DGRAM. W końcu, protokol ustaw na 0, żeby pozwolić funkcji socket() wybrać właściwy protokół bazując na parametrze typ. (Uwagi: jest dużo więcej typów, które mogą być wstawione jako domena niż tutaj napisałem. Masz również większy wybór co do parametru type niż tutaj napisałem. Zobacz stronę podręcznika systemowego funkcji socket(). Jest również dużo lepszy sposób na ustalenie parametru protokol. Zobacz stronę podręcznika systemowego funkcji getprotobyname().)

Funkcja socket() zwraca ci po prostu deskryptor gniazda, który możesz później użyć w innych wywołaniach systemowych, lub -1 w przypadku błędu. W drugim przypadku zmienna globalna errno jest ustawiana na wartość błędu (patrz stronę podręcznika systemowego funkcji perror()).

W niektórych dokumentacjach zobaczysz wzmiankę o tajemniczym "PF_INET. Jest to dziwna, rzadko spotykana bestia, ale może trochę rozjaśnię ją trochę. Dawno, dawno temu, myślano, że prawdopodobnie rodzina adresów (za tym terminem stoi "AF" w "AF_INET") może obsługiwać wiele protokołów, do których odwoływano się za pomocą ich rodziny protokołów (za tym terminem stoi właśnie to "PF" w "PF_INET"). Jednak tak się nie stało. Jednakże prawidłową rzeczą jest użycie AF_INET w struct sockaddr_in oraz PF_INET w wywołaniu socket(). Jednak w praktyce możesz używać AF_INET gdziekolwiek. I ponieważ robi to W. Richard Stevens w swojej książce, tak też będę ja to robił tutaj. (od tłumacza: nie idź tropem Stevensa w tym przypadku, bo wtedy skażesz się na ewentualne godziny pracy potrzebne do zamiany AF_INET na PF_INET w odpowiednich miejscach, gdy tylko ich definicja się zmieni. Pamiętaj też o przenośności - to co jest prawdą pod Linuksem nie musi nią być pod *BSD.)

Dobra, dobra, ale co jest dobrego w gnieździe? Tak naprawdę gniazdo samo w sobie nie jest zbyt dobre. Musisz jeszcze czytać dalej i użyć kolejnych wywołań systemowych, żeby nabrało to sensu.

bind() -- Na jakim jestem porcie?

Jak już masz gniazdo, możesz potrzebować je przypisać pewnemu portowi na maszynie lokalnej. (jest to często wykonywane, jeśli zamierzasz wywoływać listen() dla umożliwienia przychodzenia połączeń na danym porcie -- MUDy często to robią, kiedy ci mówią "zatelnetuj się na x.y.z na port 6969".) Numer portu jest używany przez jądro dla określenia, który pakiet wchodzący powinien pójść do którego deskryptoru gniazda. Jeśli zamierzasz wywoływać tylko connect(), ta funkcja może być zbędna. Mimo to, przeczytaj ten rozdział, przynajmniej tak dla szpanu.

Oto prototyp dla funkcji bind():
C/C++
#include <sys/types.h>
#include <sys/socket.h>

int bind( int sockfd, struct sockaddr * my_addr, int addrlen );
sockfd to deskryptor gniazda zwrócony przez socket(). my_addr to wskaźnik na struct sockaddr, który zawiera informacje o twoim adresie, a dokładniej porcie i adresie IP. addrlen może być ustawiony na wartość sizeof(struct sockaddr).

Ugh. To trochę dużo do przyswojenia w tak krótkim czasie. Weźmy przykład:
C/C++
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490

main()
{
    int sockfd;
    struct sockaddr_in my_addr;
   
    sockfd = socket( AF_INET, SOCK_STREAM, 0 ); // zrób sprawdzanie błędów!
   
    my_addr.sin_family = AF_INET; // host byte order
    my_addr.sin_port = htons( MYPORT ); // short, network byte order
    my_addr.sin_addr.s_addr = inet_addr( "10.12.110.57" );
    memset( &( my_addr.sin_zero ), '\0', 8 ); // wyzeruj resztę struktury
   
    // don't forget your error checking for bind():
    bind( sockfd,( struct sockaddr * ) & my_addr, sizeof( struct sockaddr ) );
    //...
Jest tu kilka rzeczy wartych uwagi: my_addr.sin_port jest w Network Byte Order, tak jak my_addr.sin_addr.s_addr. Następną warto uwagi rzeczą jest to, że dla włączane pliki nagłówkowe mogą się różnić w zależności od systemu. Żeby być pewnym, powinieneś sprawdzić lokalne strony podręcznika systemowego man.

W końcu na temat bind()a mogę powiedzieć, że część procesu pobierania własnego adresu IP i/lub portu może być zautomatyzowane:
C/C++
my_addr.sin_port = 0; // wybierz losowo nieużywany port
my_addr.sin_addr.s_addr = INADDR_ANY; // użyj mojego adresu IP
Jak widzisz, ustawiając my_addr.sin_port na zero, pozwalasz funkcji bind() wybrać port za ciebie. Podobnie, ustawiając my_addr.sin_addr.s_addr na INADDR_ANY, pozwalasz automatycznie wypełnić adres IP maszyny, na której wykonuje się proces.

Jeśli byłeś wystarczająco uważny, zapewne rzucił ci się w oczy fakt, że nie umieściłem INADDR_ANY w Network Byte Order! Ale jestem niedobry, co? Jednak, mam coś na swoje usprawiedliwienie: INADDR_ANY jest tak naprawdę zerem. Zero jest nadal zerem nawet jak przestawisz kolejność bitów. Jednakże, puryści językowi zauważą, że INADDR_ANY na różnych maszynach może przyjmować różne wartości, takie jak np. 12, i że mój kod nie będzie tam działał. Nie stanowi to dla mnie problemu:
C/C++
my_addr.sin_port = htons( 0 ); // wybierz losowo nieużywany port
my_addr.sin_addr.s_addr = htonl( INADDR_ANY ); // użyj mojego adresu IP
No, teraz jesteśmy tak przenośni, że pewnie nie możesz w to uwierzyć. Chciałem tylko zwrócić uwagę na fakt, że w większości kawałkach kodu nie spotkasz się z sytuacją owijania INADDR_ANY w funkcję htonl(). (od tłumacza: ten pogląd skontrastuję z bardzo ciekawym zdaniem: "Jedzcie gówno - miliony much nie mogą się mylić".)

bind() również zwraca -1 w przypadku błędu i ustawia odpowiednio errno na wartość błędu.

Inną rzeczą, wartą zauważenia jest to, żebyś przy wywoływaniu bind() nie schodził za bardzo z numerem portu. Wszystkie porty poniżej 1024 są ZAREZERWOWANE (chyba, że jesteś superużytkownikiem)! Możesz używać tylko portów powyżej tej liczby, aż do 65535 (zakładając, że nie są one już zajęte przez inny program).

Czasami możesz zauważyć, że przy próbie ponownego uruchomienia serwera bind() zwraca błąd stwierdzając "Podany adres jest już w użyciu" ("Address already in use.") Co to oznacza? Część gniazda, która była połączona ciągle się szlaja w jądrze i zajmuje ten port. Możesz poczekać, żeby zasoby się zwolniły (minutę lub trochę dłużej), albo dodać kawałek kodu do twojego programu, sprawiając, że ten ponownie wykorzysta dany port. Przykład:
C/C++
int yes = 1;
//char yes='1'; // programiści Solaris używają tego

// zgub denerwujący komunikat błędu "Address already in use"
if( setsockopt( listener, SOL_SOCKET, SO_REUSEADDR, & yes, sizeof( int ) ) == - 1 ) {
    perror( "setsockopt" );
    exit( 1 );
}
Jeszcze jedna mała uwaga na koniec o bind(): są sytuacje, w których kompletnie nie będziesz potrzebował tej funkcji wywoływać. Jeśli łączysz się ze zdalnym serwerem (connect()) i nie obchodzi cię z jakiego portu będziesz nadawał (tak jak to ma miejsce w programie telnet, gdzie dbasz tylko o numer portu zdalnego), możesz po prostu wywołać connect(). Ta funkcja sama sprawdzi, czy gniazdo nie jest przypisane, i użyje nieużywanego portu jeśli potrzeba.

connect() -- Ej, ty!

Poudawajmy przez chwilę, że jesteś programem telnet. Twój użytkownik rozkazuje ci (dokładnie tak samo jak w filmie TRON), żebyś wziął deskryptor gniazda. Spełniasz prośbę i wywołujesz socket(). Następnie, użytkownik mówi ci, żebyś nawiązał połączenie z "10.12.110.57" na porcie "23 (jest to standardowo port telneta). I co teraz robisz?

Na szczęście dla ciebie, programie, właśnie dokładnie przeglądasz sekcję o connect() -- jak połączyć się ze zdalnym hostem. Więc czytaj dalej! Nie ma czasu do stracenia!

Funkcja connect() ma następujący prototyp:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

int connect( int sockfd, struct sockaddr * serv_addr, int addrlen );
sockfd to nasz przyjazny sąsiad - deksryptor gniazda zwrócony przez wywołanie socket(). serv_addr to struct sockaddr zawierający docelowy port i adres IP, a addrlen może mieć wartość sizeof(struct sockaddr).

Czyż nie zaczyna to nabierać sensu? Pozwól, że przytoczę następujący przykład:
C/C++
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define DEST_IP   "10.12.110.57"
#define DEST_PORT 23

main()
{
    int sockfd;
    struct sockaddr_in dest_addr; // tu będzie przechowywany adres docelowy
   
    sockfd = socket( AF_INET, SOCK_STREAM, 0 ); // zrób sprawdzanie błędów!
   
    dest_addr.sin_family = AF_INET; // host byte order
    dest_addr.sin_port = htons( DEST_PORT ); // short, network byte order
    dest_addr.sin_addr.s_addr = inet_addr( DEST_IP );
    memset( &( dest_addr.sin_zero ), '\0', 8 ); // wyzeruj resztę struktury
   
    // nie zapomnij o sprawdzeniu błędów dla connect()!
    connect( sockfd,( struct sockaddr * ) & dest_addr, sizeof( struct sockaddr ) );
    //...
Ponownie, pamiętaj o sprawdzeniu wartości zwracanej przez connect() -- zwróci ona -1 w przypadku błędu i odpowienio ustawi zmienną errno.

Zauważ również, że nie wywoływaliśmy funkcji bind(). Po prostu nie musimy się troszczyć o numer portu lokalnego: obchodzi nas tylko to gdzie idziemy (zdalny port). Jądro wybierza za nas port lokalny, a strona, z którą się łączymy pobierze tą informację automatycznie. Bez obaw.

listen() -- Proszę dzwonić...

No dobra, czas odwórcić role. Co jeśli nie chcesz się łączyć z innymi hostami. Powiedzmy, że tak dla jaj, chcesz czekać na połączenia przychodzące i obsługiwać je w jakiś sposób. Ten proces jest dwustopniowy: najpierw nasłuchujesz (listen(), następnie przyjmujesz połączenia (accept()) (patrz niżej.)

Ropoczęcie nasłuchiwania jest bardzo proste, jednak wymagą odrobiny tłumaczenia:
C/C++
int listen( int sockfd, int backlog );
sockfd jak zwykle jest deksryptorem gniazda zwróconym przez funkcję socket(). backlog jest ilością dozwolonych połączeń w kolejce połączeń przychodzących. Co to oznacza? Połączenia przychodzące będą czekały w tej kolejce, dopóiki każdego z osobna nie zaakceptujesz ( accept()), a backlog określa ile może być oczekujących połączeń. Większość systemów po cichu ogranicza tą liczbę do 20. Prawdopodobnie i tak będziesz ustawiał tą wartość na 5 lub 10.

Jak zwykle, listen() zwraca -1 i ustawia errno w przypadku błędu.

Tak jak się zapewne domyślasz, musimy wywołać bind() przed listen() - w przeciwnym razie jądro wybierze dla nas losowy port. Fuj! Więc jeśli zamierzasz nasłuchiwać na połączenia przychodzące, prawidłową kolejnością wywołań systemowych jest:
C/C++
socket();
bind();
listen();
/* tutaj accept() wkracza do akcji */
Powyższe pozostawiam jako przykładowy kod źródłowy, ponieważ mówi on sam za siebie (kod z sekcji o accept(), poniżej, jest bardziej kompletny). Pewne trudności z całego tego bałagnu może sprawiać jedynie wywołanie accept().

accept() -- "Dziękujemy za wybranie portu 3490."

Przygotuj się -- wywołanie accept() jest trochę dziwne! Oto co się zaraz stanie: ktoś z bardzo daleka spróbuje połączyć się (connect()) z twoją maszyną na port, na którym nasłuchujesz (listen()). Jego połączenie zostanie umieszczone w kolejce i będzie czekało na zaakceptowanie. Wywołujesz accept() i mówisz jądru, żeby przyjąć jedno połączenie oczekujące. Zostanie ci zwrócony całkiem nowy deskryptor gniazda dla tego jednego połączenia! Zgadza się, nagle masz dwa deskryptory gniazd za cenę jednego! Ten oryginalny ciągle nasłuchuje na twoim porcie, a nowo utworzony jest w końcu gotowy do wysyłania (send()) i odbierania danych (recv()). W końcu tu dotarliśmy!

Wywołanie jest następujące:
C/C++
#include <sys/socket.h>

int accept( int sockfd, void * addr, int * addrlen );
sockfd jest deskryptorem nasłuchującego gniazda. Całkiem proste, nie? addr jest zazwyczaj wskaźnikiem do lokalnej struktury struct sockaddr_in. To właśnie tutaj się znajdą informacje o oczekującym połączeniu (i dzięki temu możesz stwierdzić, który host się do ciebie dobija i z jakiego portu). addrlen jest lokalną zmienną całkowitą, która powinna być ustawiona na sizeof(struct sockaddr_in) zanim adres jest przekazany do accept(). Ta funkcja nie umieści więcej bajtów danych w addr niż określa to zmienna addrlen. Jeśli umieści mniej bajtów, zmienna addrlen zostanie odpowiednio zmieniona, tak by odpowiadała rozmiarowi struktury.

Zgadnij co? accept() zwraca -1 i ustawia zmienną errno, jeśli wystąpi błąd. Założę się, że nie wiedziałeś.

Tak jak przedtem, jest tego trochę dużo do wchłonięcia na raz, więc teraz pokaże przykładowy kawałek kodu, żebyś to lepiej zrowumiał:
C/C++
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490    // port, z którym użytkownicy będą się łączyli

#define BACKLOG 10     // jak dużo może być połączeń oczekujących

main()
{
    int sockfd, new_fd; // nasłuchuj na sock_fd, nowe połączenia na new_fd
    struct sockaddr_in my_addr; // informacja o moim adresie
    struct sockaddr_in their_addr; // informacja o adresie osoby łączącej się
    int sin_size;
   
    sockfd = socket( AF_INET, SOCK_STREAM, 0 ); // zrób sprawdzanie błędów!
   
    my_addr.sin_family = AF_INET; // host byte order
    my_addr.sin_port = htons( MYPORT ); // short, network byte order
    my_addr.sin_addr.s_addr = INADDR_ANY; // uzupełnij moim adresem IP
    memset( &( my_addr.sin_zero ), '\0', 8 ); // wyzeruj resztę struktury
   
    // nie zapomnij o sprawdzeniu błędów dla poniższych wywołań!
    bind( sockfd,( struct sockaddr * ) & my_addr, sizeof( struct sockaddr ) );
   
    listen( sockfd, BACKLOG );
   
    sin_size = sizeof( struct sockaddr_in );
    new_fd = accept( sockfd,( struct sockaddr * ) & their_addr, & sin_size );
    //...
Ponownie zauważ, że będziemy używali deksryptora gniazda new_fd dla wszystkich funkcji send() oraz recv(). Jeśli chcesz odebrać tylko jedno połączenie, możesz zamknąć (close()) nasłuchujące gniazdo sockfd, żeby zapobiec kolejnym połączeniom przychodzącym na ten sam port, jeśli sobie tylko tego życzysz.

send() i recv() -- Mów do mnie, Misiu!

Te dwie funkcje służa do komunikacji przez gniazda strumieniowe lub połączone gniazda datagramowe. Jeśli chcesz użyć normalnych, niepołączonych gniazd datagramowych, będzie musiał się bliżej przyjżeć sekcji o sendto() i recvfrom() poniżej.

Wywołanie sendto():
C/C++
int send( int sockfd, const void * msg, int len, int flags );
sockfd jest deskryptorem gniazda, do którego chcesz wysłać dane (niezależnie od tego czy jest to gniazdo zwrócone przez socket() czy też accept()). msg jest wskaźnikiem do danych, które chcesz wysłać, a len jest długością tych danych w bajtach. flags po prostu ustaw na 0 (zobacz stronę podręcznika systemowego o send(), jeśli chcesz wiedzieć więcej o flagach).

Przykładowym kodem może być:
C/C++
char * msg = "Beej was here!";
int len, bytes_sent;
//...
len = strlen( msg );
bytes_sent = send( sockfd, msg, len, 0 );
//...
send() zwraca ilość bajtów, które udało się wysłać -- może to być mniejsza wartość od tej, którą podałeś funkcji! No bo widzisz, czasami chcesz wysłać ogromną ilość danych i jądro po prostu nie może sobie z tym poradzić. Spróbuje wysłać najwięcej ile może, ufając, że wyślesz resztę później. Pamiętaj, jeśli wartosć zwrócona przez send() nie zgadza się z wartością znajdującą się w zmiennej len, wszystko zależy od ciebie, czy wyślesz resztę danych. Dobra wiadomość: jeśli pakiet jest mały (mniej niż 1KB) prawdopodobnie zostanie on wysłany w całości. Znowu, -1 jest zwracane w przypadku wystąpienia błedu, a zmienna errno jest ustawiana na wartość tego błedu.

Wywołanie recv() jest podobne pod wieloma aspektami:
C/C++
int recv( int sockfd, void * buf, int len, unsigned int flags );
sockfd jest deksryptorem gniazda, z którego dane maja być odczytane. buf jest buforem, w którym zostaną umieszczone odczytane dane o długości podanej za pomocą parametru len -- jest to maksymalna długość bufora. flags znowu może być ustawiona na 0 (zobacz stronę podręcznika systemowego o recv(), żeby uzyskac informacje o flagach).

recv() zwraca ilość odczytanych danych, lub -1 w przypadku błędu (i odpowiednio ustawia errno).

Ale czekaj! recv() może zwrócić 0. To może oznaczać tylko jedno: druga strona zamknęła połączenie! Zwrócona wartość 0 jest sposobem mówienia recv(), że to właśnie się stało.

To było łatwe, prawda? Możesz teraz wysyłać i odbierać dane używając gniazd strumieniowych! Faaajnie! Jesteś Uniksowym Programistą Sieciowym!

sendto() i recvfrom() -- Mów do mnie w stylu DGRAM

To wszystko jest cacy," już słyszę jak mówicie, "ale co mam zrobić z niepołączonymi gniazdami datagramowymi?" No problemo, amigo. Jesteś we właściwym miejscu.

Poniważ gniazda datagramowe nie są połączone ze zdalnym hostem, zgadnij jaką informację musimy podać zanim możemy wysłać pakiet? Dokładnie! Adres docelowy! Oto przepis:
C/C++
int sendto( int sockfd, const void * msg, int len, unsigned int flags,
const struct sockaddr * to, int tolen );
Jak już pewnie zauważyłeś, to wywołanie jest praktycznie takie samo jak wywołanie send() z dwoma dodatakowymi parametrami. to jest wskaźnikiem na strukturę struct sockaddr (którą i tak pewnie będziesz przechowywał jako struct sockaddr_in i wykonywał odpowiednie rzutowanie w ostatniej chwili), któr zawiera docelowy adres IP i port. tolen może być po prostu ustawione na sizeof(struct sockaddr).

Tak jak to było z send(), sendto() zwraca ilość wysłanych danych (ta ilość może być inna od tej, którą podałeś!), lub -1 w przypadku błędu.

Odpowiednio podobne są recv() i recvfrom(). Wywołanie recvfrom() jest następujące:
C/C++
int recvfrom( int sockfd, void * buf, int len, unsigned int flags,
struct sockaddr * from, int * fromlen );
Ponownie, jest to praktycznie to samo co recv() z dodatkiem pewnych pozycji. from jest wskaźnikiem do lokalnej struktury struct sockaddr, która będzie wypełniona adresem IP i portem maszyny, od której pochodzi pakiet. fromlen jest wskaźnikiem do lokalnej zmiennej typu int, która przed wywołaniem tej funkcji powinna być ustawiona na sizeof(struct sockaddr). Gdy funkcja powróci, fromlen będzie zawierało długość adresu przechowywanego w from.

recvfrom() zwraca ilość odebranych danych lub -1 w przypadku błedu (ustawiając odpowiednio zmienną errno).

Pamiętaj, że jesli połączysz (connect()) gniazdo datagramowe, możesz używać po prostu funkcji send() i recv() dla wszystkich operacji. Gniazdo samo w sobie jest nadal gniazdem datagramowych, a pakiety nadal korzystają z protokołu UDP, ale interfejs gniazda automatycznie doda adres docelowy i informacje o źródle za ciebie.

close() i shutdown() -- Wynocha!

Ale jazda! Wysyłałeś (send()) i odbierałeś (recv()) dane przez cały dzień i już masz dość. Jesteś gotowy, by zamknąć połączenie na twoim deksryptorze gniazda. To jest calkiem proste. Możesz po prostu użyć standardowej funkcji Uniksowej do zamykania deskryptorów plików - funkcji close():
C/C++
close( sockfd );
To zapobiegnie jakimkolwiek dalszym odczytom bądź zapisom do tego gniazda. Ktokolwiek próbujący odczytać bądź zapisać do gniazda po drugiej stronie otrzyma błąd.

W przypadku gdybyś chciał mieć większą kontrolę nad zamykaniem gniazda, możesz użyć funkcji shutdown(). Ta funkcja pozwala ci uciąc połączenie w określonym kierunku lub w obu kierunkach (tak jak to robi close()). Wywołanie:
C/C++
int shutdown( int sockfd, int how );
sockfd jest deksryptorem gniazda, które chcesz zamknąć, a how jest jedną z poniższych wartości (od tłumacza: dla zachowania przenośności używaj stałych podanych w nawiasach zamiast podanych tu wartości):
  • 0 (SHUT_RD)-- dalszy odbiór danych jest zabroniony
  • 1 (SHUT_WR) -- dalszy zapis danych jest zabroniony
  • 2 (SHUT_RDWR) -- dalszy odbiór i zapis danych jest zabroniony (tak jak przy close())
shutdown() zwraca 0, gdy wywołane się powiedzie lub -1 w przypadku błedu (ustawiając odpowiednio zmienną errno).

Jeśli użyjesz shutdown() na niepołączonym gnieździe datagramowym, sprawisz, że gniazdo będzie niedostępne dla dalszym wywołań send() i recv() (pamiętaj, że możesz ich używać, jeśli połączysz (connect()) twoje gniazdo datagramowe).

Należy tu zauważyć, że shutdown() nie zamyka deskryptora pliku, ale zmienia jego stan używalności. Żeby zwolnić deksryptor gniazda, musisz użyć close().

Nic więcej.

getpeername() -- Kim jesteś?

Ta funkcja jest baardzo prosta.

Jest tak prosta, że prawie nie dałem jej własnej sekcji. Ale jednak ją ma.

Funkcja getpeername() powie ci, kto jest na drugim końcu połączonego gniazda strumieniowego. Wywołanie:
C/C++
#include <sys/socket.h>

int getpeername( int sockfd, struct sockaddr * addr, int * addrlen );
sockfd jest deskryptorem połączonego gniazda strumieniowego, addr jest wskaźnikeim do struktury struct sockaddr (lub struct sockaddr_in), która będzie przechowywała informacje o drugiej stronie połączenia, a addrlen jest wskaźnikeim do zmiennej typu int, która przed wywołaniem funkcji powinna mieć wartość sizeof(struct sockaddr).

Funkcja zwraca -1 w przypadku błędu i odpowiednio ustawia zmienną errno.

Jak już masz ten adres, możesz użyć funkcji inet_ntoa() lub gethostbyaddr(), by wyświetlić lub pobrać więcej informacji. Nie, nie możesz pobrać nazwy użytkownika (no dobra, jeśli na tamtym komputerze działa demon ident, to jest możliwe. Jednak to wykracza poza ramy tego dokumentu. Patrz RFC-1413 po więcej informacji).

gethostname() -- Kim ja jestem?

Jeszcze prostszą funckją od getpeername() jest funckja gethostname(). Zwraca ona nazwę komputera, na którym działa twój program. Ta nazwa może być później użyta przez funkcję gethostbyname(), poniżej, w celu ustalenia adresu IP twojej maszyny lokalnej.

Co mogłoby nam sprawić większą przyjemność? Przychodzi mi na myśl wiele rzeczy, ale nie mają one związku z programowaniem gniazd. W każdym razie oto wywołanie funkcji:
C/C++
#include <unistd.h>

int gethostname( char * hostname, size_t size );
Argumenty są proste: hostname jest wskaźnikiem do ciągu znaków, w którym będzie przechowywana nazwa hosta przy powrocie z funkcji, a size jest długością w bajtach danych zawartych w ciągu znaków hostname.

Funkcja zwraca 0 przy pomyślnym zakończeniu oraz -1 w przypadku błedu (jak zwykle odpowiednio ustawiając zmienną errno).
DNS -- Mówisz "whitehouse.gov", odpowiadam "198.137.240.92"
W przypadku gdybyś nie wiedział co to jest DNS - jest to "Domain Name Service" Krótko mówiąc, mówisz mu jaki jest czytelny dla człowieka adres dla danego serwisu, a on ci poda jego adres IP (którego możesz używać z bind(), connect(), sendto() lub czegokolwiek innego, do czego jest ci to potrzebne). Tym sposobem, gdy ktoś wpisze:
$ telnet whitehouse.gov
telnet wie, że potrzebuje połączyć się z adresem "198.137.240.92".

Ale jak to działa? Będziesz używał funkcji gethostbyname():
C/C++
#include <netdb.h>

struct hostent * gethostbyname( const char * name );
Jak widzisz, ta funkcja zwraca wskaźnik do struktury struct hostent, której zawartość jest następująca:
C/C++
struct hostent {
    char * h_name;
    char ** h_aliases;
    int h_addrtype;
    int h_length;
    char ** h_addr_list;
};
#define h_addr h_addr_list[0]
A tu są opisy poszczególnych pól w struct hostent:
  • h_name -- oficjalna nazwa hosta
  • h_aliases -- ciąg znaków, zakończony znakiem NULL, zawierający alternatywne nazwy dla hosta
  • h_addrtype -- typ zwróconego adresu - zazwyczaj AF_INET.
  • h_length -- długosc adresu w bajtach
  • h_addr_list -- ciąg znaków, zakończony znakiem NULL, określający adresy sieciowe dla hosta. Adresy hosta są w sieciowej kolejności bajtów.
  • h_addr -- pierwszy adres z h_addr_list.
gethostbyname() zwraca wskaźnik do wypełnionej struktury struct hostent, lub NULL w przypadku błędu (w tym przypadku errno nie jest ustawiane -- h_errno jest ustawiana w zamian. Zobacz herror(), poniżej.)

Ale jak się tego używa? Czasami (czego możemy się dowiedzieć czytając podręczniki komputerowe), zasypanie czytelnika informacja nie wystarcza. Ta funkcja jest z pewnością łatwiejsza w użytcie niż się wydaje.

Tu jest przykładowy program:
C/C++
/*
    ** getip.c -- jak rozwiązywać nazwy hostów
    */

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main( int argc, char * argv[] )
{
    struct hostent * h;
   
    if( argc != 2 ) { // sprawdź poprawność lini poleceń!
        fprintf( stderr, "usage: getip address\n" );
        exit( 1 );
    }
   
    if(( h = gethostbyname( argv[ 1 ] ) ) == NULL ) { // pobierz informacje o hoście
        herror( "gethostbyname" );
        exit( 1 );
    }
   
    printf( "Host name  : %s\n", h->h_name );
    printf( "IP Address : %s\n", inet_ntoa( *(( struct in_addr * ) h->h_addr ) ) );
   
    return 0;
}
Korzystając z gethostbyname() nie możesz używać perror() do wyświetlania komunikatów o błędach (ponieważ errno nie jest używane). Zamiast tego wywołuj herror().

Jest to całkiem proste. Po prostu podajesz tekst, który zawiera nazwę maszyny ("whitehouse.gov") funkcji gethostbyname(), a później zbierasz informacje ze zwróconej struktury struct hostent.

Jedyną dziwną rzeczą może być wyświetlanie adresu IP pobrane powyższym sposobem. h->h_addr jest typu char *, natomiast inet_ntoa() chce typu struct in_addr. W tym celu robię odpowiednie rzutowanie h->h_addr na struct in_addr*, a później stosuję dereferencję, żeby dostać dane.
Poprzedni dokument Następny dokument
Struktury i przetwarzanie danych Tło Klient-Serwer