5. Tło Klient-Serwer

To świat klientów i serwerów, chłopcze. Praktycznie wszystkie operacje w sieci to procesy klientów rozmawiające z procesami serwera i vice-versa. Weźmy za przykład telnet. Kiedy łączysz się ze zdalnym hostem na porcie 23 używając telneta (klient), program na tamtym hoście (nazywany telnetd, serwer) budzi się do życia. Zajmuje się przychącymi połączeniami telnetowymi, wyświetla ci prośbę o zalogowanie, itd.

Rysunek 2. Interakcja Klient-Serwer.

Wymiana informacji pomiędzy klientem i serwerem jest przedstawiona w Figure 2.

Zauważ, że para klient-serwer może rozmawiać używając SOCK_STREAM, SOCK_DGRAM, lub czegokolwiek innego (dopóki rozmawiają tym samym językiem). Dobrymi przykładami par klient-serwer są telnet/telnetd, ftp/ftpd, lub bootp/bootpd. Za każdym razem, gdy używasz programu ftp, po drugiej stronie jest zdalny program, ftpd, który ci służy.

Często będzie tylko jeden serwer na maszynie, i ten serwer bedzie obsługiwał wiele klientów używając fork(). Najprostszym sposobem jest: serwer czeka na połączenia, akceptuje je (accept()) i tworzy proces potomny (fork()), który obsłuży to połączenie. To jest właśnie to, co nasz przykładowy serwer robi w następnej sekcji.

5.1. Prosty strumieniowy serwer

Wszystko co robi ten serwer to wysłanie tekstu "Hello, World\n" przez połączenie strumieniowe. Wszystko co musisz zrobić, by przetestować ten serwer, to uruchomić go w jednym oknie, i połączyć się za pomocą telneta z drugiego okna:

    $ telnet remotehostname 3490

gdzie remotehostname jest nazwą maszyny, na której właśnie pracujesz.

Kod serwera: (Zauważ: znak '\' kończący linię oznacza, że linia jest kontynuowana w następnym wierszu.)

    /*
    ** server.c -- serwer używający gniazd strumieniowych
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/wait.h>
    #include <signal.h>

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

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

    void sigchld_handler(int s)
    {
        while(wait(NULL) > 0);
    }

    int main(void)
    {
        int sockfd, new_fd;  // nasłuchuj na sock_fd, nowe połaczenia 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;
        struct sigaction sa;
        int yes=1;

        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }
        
        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

        if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
                                                                       == -1) {
            perror("bind");
            exit(1);
        }

        if (listen(sockfd, BACKLOG) == -1) {
            perror("listen");
            exit(1);
        }

        sa.sa_handler = sigchld_handler; // zbierz martwe procesy
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGCHLD, &sa, NULL) == -1) {
            perror("sigaction");
            exit(1);
        }

        while(1) {  // głowna pętla accept()
            sin_size = sizeof(struct sockaddr_in);
            if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
                                                           &sin_size)) == -1) {
                perror("accept");
                continue;
            }
            printf("server: got connection from %s\n",
                                               inet_ntoa(their_addr.sin_addr));
            if (!fork()) { // to jest proces-dziecko
                close(sockfd); // dziecko nie potrzebuje gniazda nasłuchującego
                if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
                    perror("send");
                close(new_fd);
                exit(0);
            }
            close(new_fd);  // rodzic nie potrzebuje tego
        }

        return 0;
    } 

Jeśli jesteś tego ciekaw, to umieściłem ten kod w jednej dużej funkcji main() dla przejrzystości. Możesz spokojnie rozbić go na mniejsze funkcje jeśli to ci poprawi humor.

(Również ten cały sigaction() może być nowy dla ciebie -- nic nie szkodzi. Ten kod jest odpowiedzialny za zbieranie martwych procesów, które się pojawią, gdy procesy-dzieci zakończą działanie. Jeśli zrobisz dużo zombie i nie zbierzesz ich, twój administrator systemu będzie trochę wzburzony.)

Dane z serwera możesz pobrać korzystając z klienta umieszczonego w następnej sekcji.

5.2. Prosty strumieniowy klient

Ten gościu jest nawet prostszy niż serwer. Wszystko co robi ten klient, to łączenie się z podanych w linii komend hostem, na port 3490. Pobiera tekst, który serwer wysyła.

Kod klienta:

    /*
    ** client.c -- klient używający gniazd strumieniowych
    */

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

    #define PORT 3490 // port, z którym klient będzie się łączył

    #define MAXDATASIZE 100 // maksymalna ilość dancyh, jaką możemy otrzymać na raz

    int main(int argc, char *argv[])
    {
        int sockfd, numbytes;  
        char buf[MAXDATASIZE];
        struct hostent *he;
        struct sockaddr_in their_addr; // informacja o adresie osoby łączącej się

        if (argc != 2) {
            fprintf(stderr,"usage: client hostname\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // pobierz informacje o hoście
            perror("gethostbyname");
            exit(1);
        }

        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        their_addr.sin_family = AF_INET;    // host byte order 
        their_addr.sin_port = htons(PORT);  // short, network byte order 
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8);  // wyzeruj resztę struktury

        if (connect(sockfd, (struct sockaddr *)&their_addr,
                                              sizeof(struct sockaddr)) == -1) {
            perror("connect");
            exit(1);
        }

        if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
            perror("recv");
            exit(1);
        }

        buf[numbytes] = '\0';

        printf("Received: %s",buf);

        close(sockfd);

        return 0;
    } 

Zauważ, że jeśli nie uruchomisz serwera przed klientem, connect() zwróci "Connection refused" ("Połączenie odrzucone"). Bardzo użytecznie.

5.3. Gniazda datagramowe

Naprawdę nie trzeba tutaj tak dużo mówić, więc po prostu przedstawię parę przykładowych programów: talker.c i listener.c.

listener siedzi na maszynie i czeka na pakiety przychodzące na port 4950. talker wysyła pakiet na ten port, na do podanej maszyny, który zawiera cokolwiek użytkownik wprowadzi w lini poleceń.

Tu jest źródło dla listener.c:

    /*
    ** listener.c -- serwer używający gniazd datagramowych
    */

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

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

    #define MAXBUFLEN 100

    int main(void)
    {
        int sockfd;
        struct sockaddr_in my_addr;    // informacja o moim adresie
        struct sockaddr_in their_addr; // informacja o adresie osoby łączącej się
        int addr_len, numbytes;
        char buf[MAXBUFLEN];

        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        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

        if (bind(sockfd, (struct sockaddr *)&my_addr,
                                              sizeof(struct sockaddr)) == -1) {
            perror("bind");
            exit(1);
        }

        addr_len = sizeof(struct sockaddr);
        if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0,
                           (struct sockaddr *)&their_addr, &addr_len)) == -1) {
            perror("recvfrom");
            exit(1);
        }

        printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
        printf("packet is %d bytes long\n",numbytes);
        buf[numbytes] = '\0';
        printf("packet contains \"%s\"\n",buf);

        close(sockfd);

        return 0;
    } 

Zauważ, że w naszym wywołaniu socket() w końcu używamy SOCK_DGRAM. Zauważ również, że nie ma potrzeby korzystania z funkcji listen() oraz accept(). Jest to jedna z zalet korzystania z niepołączonych gniazd strumieniowych!

Przyszła kolej na kod źródłowy talker.c:

    /*
    ** talker.c -- klient używający gniazd datagramowych
    */

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

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

    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in their_addr; // informacja o adresie osoby łączącej się
        struct hostent *he;
        int numbytes;

        if (argc != 3) {
            fprintf(stderr,"usage: talker hostname message\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // pobierz informacje o hoście
            perror("gethostbyname");
            exit(1);
        }

        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        their_addr.sin_family = AF_INET;     // host byte order
        their_addr.sin_port = htons(MYPORT); // short, network byte order
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8); // wyzeruj resztę struktury

        if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
             (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
            perror("sendto");
            exit(1);
        }

        printf("sent %d bytes to %s\n", numbytes,
                                               inet_ntoa(their_addr.sin_addr));

        close(sockfd);

        return 0;
    } 

I to wszystko co się tego tyczy! Uruchom listener na jednej maszynie, następnie uruchom talker na innej. Patrz jak gadają ze sobą! Fun G-rated excitement for the entire nuclear family!

Poza jednym szczegółem, o którym wspomniałem wiele razy w przeszłości: połączone gniazda datagramowe. Muszę o tym powiedzieć tutaj, ponieważ jesteśmy w sekcji o gniazdach datagramowych. Powiedzmy, że talker wywołuje connect() i podaje adres programu listener. Od tego momentu, talker może wysyłać i odbierać dane tylko z podanego funkcji connect() adresu. Z tego powodu nie musisz używać sendto() ani recvfrom(): możesz po prostu używać send() i recv().