Beej-ov vodič za mrežno programiranje

Prethodno

 

Dalje


5. Pozadina klijent-servera

Ovo je svijet klijent-servera, lutko. Skoro sve na mreži se dešava kao komunikacija klijentskog procesa sa serverom i obrnuto. Uzmi telnet, na primjer. Kad se spojiš sa udaljenim serverom, na nekom portu, program na serveru (tzv. telnetd, server) oživi. On upravlja pridolazećim pozivima, daje ti odzivni znak, itd.

Slika 2. Razgovor između klijenta i servera.

[Client-Server Interaction Diagram]

Razmjena podataka između klijenta i servera se vidi na Slici 2.

Primijeti da klijent-server par može da priča na SOCK_STREAM, SOCK_DGRAM, ili bilo kom drugom jeziku (dok god im je oboma isti jezik.) Neki primjeri klijent-server konverzacije su telnet/telnetd, ftp/ftpd, ili bootp/bootpd. Svaki put kad koristiš ftp, udaljeni program, ftpd, takođe radi – i uslužuje te.

Obično, postoji jedan serverski program na serverskom računaru, i taj server rukuje mnogobrojnim klijentima koristeći funkciju fork(). Osnovna procedura je: server čeka poziv, prima ga (accept()), i onda se grana (fork()) da bi novi proces obradio taj poziv. Upravo takvo nešto radi serverski program iz sledećeg poglavlja.

5.1. Primjer jednostavnog stream servera

Sve što ovaj server radi je da šalje tekst  "Hello, World!\n" preko stream veze. Sve što je potrebno za testiranje ovog servera je da ga otvoriš u jednom prozoru, a onda se "telnetuješ" na njega iz drugog prozora:

    $ telnet remotehostname 3490

gdje je remotehostname ime računara na kom radiš.

Kôd za serverski program: (Primjedba: Obrnuta kosa crta na kraju reda znači da se red nastavlja u sledeći.)

    /*
    ** server.c -- stream socket server
    */
    #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 na koji će se posjetioci povezivati
    #define BACKLOG 10     // koliko pridolazećih poziva može da bude u redu za čekanje
    void sigchld_handler(int s)
    {
        while(wait(NULL) > 0);
    }
    int main(void)
    {
        int sockfd, new_fd;  // čekati na sock_fd, vezu primiti na new_fd
        struct sockaddr_in my_addr;    // informacije o mojoj adresi
        struct sockaddr_in their_addr; // informacije o adresi onog ko zove
        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;         // serversko uređenje bajtova
        my_addr.sin_port = htons(MYPORT);     // kratko, mrežno uređenje bajtova
        my_addr.sin_addr.s_addr = INADDR_ANY; // automatski popuni mojim IP-om
        memset(&(my_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama
        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; // pokupi mrtve procese
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGCHLD, &sa, NULL) == -1) {
            perror("sigaction");
            exit(1);
        }
        while(1) {  // glavna petlja sa funkcijom 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()) { // ovo je dijete-proces
                close(sockfd); // djetetu ne treba soket koji očekuje poziv
                if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
                    perror("send");
                close(new_fd);
                exit(0);
            }
            close(new_fd);  // roditelju ovo ne treba
        }
        return 0;
    } 

U slučaju da si radoznao, ovo je kôd jedne velike funkcije main(), čisto radi sintaksne čistoće. Ti slobodno možeš da je razbiješ u manje funkcije.

(Takođe, može biti da ti je čitava priča o funkciji sigaction() pomalo nova – to je ok. Kôd koji tu stoji je odgovoran da počisti sve zombi-procese koji se pojavljuju kad god se neko od djece-procesa završi. Ako ostavljaš mnogo zombija za sobom i ne čistiš ih, administrator će da se ljuti.)

Možeš da komuniciraš sa ovim serverom pomoću klijenta u sledećoj sekciji.

5.2. Jednostavan stream klijent

Ovaj je program lakši od ovog gore servera. Sve što ovaj klijent radi je da se spoji sa računarom zadatim na komandnoj liniji, na portu 3490. Prima tekst koji pošalje server.

Izvorni kôd za klijentski program:

    /*
    ** client.c – Primjer stream soket klijenta
    */
    #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 na koji će se klijent povezati
    #define MAXDATASIZE 100 // najveći broj bajtova koji se može primiti odjednom
    int main(int argc, char *argv[])
    {
        int sockfd, numbytes;  
        char buf[MAXDATASIZE];
        struct hostent *he;
        struct sockaddr_in their_addr; // adresa računara na koji se spajamo
        if (argc != 2) {
            fprintf(stderr,"usage: client hostname\n");
            exit(1);
        }
        if ((he=gethostbyname(argv[1])) == NULL) {  // daj podatke o serverskom računaru
            perror("gethostbyname");
            exit(1);
        }
        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }
        their_addr.sin_family = AF_INET;    // serversko ureženje bajtova
        their_addr.sin_port = htons(PORT);  // kratko, mrežno uređenje bajtova 
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8);  // ostatak strukture popuni nulama
        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;
    } 

Primijeti da, ako ne pokreneš server prije klijenta, funkcija connect() vraća "Connection refused (Spajanje odbijeno)". Veoma korisno.

5.3. Datagram soketi

Nemam o ovoj temi mnogo da pričam, pa ću samo predstaviti par programa radi primjera: talker.c i listener.c.

listener ("slušalac") čeka paket koji će da stigne na port 4950. talker ("govornik") šalje paket na taj port, na datu mašinu, a taj paket sadrži sve što korisnik stavi na komandnu liniju.

Evo izvornog kôda za listener.c:

    /*
    ** listener.c – primjer datagram-soket servera    */
    #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 na koji će se povezivati korisnici
    #define MAXBUFLEN 100
    int main(void)
    {
        int sockfd;
        struct sockaddr_in my_addr;    // informacije o mojoj adresi
        struct sockaddr_in their_addr; // informacije o adresi računara koji nas zove
        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;         // serversko uređenje bajtova
        my_addr.sin_port = htons(MYPORT);     // kratko, mrežno uređenje bajtova
        my_addr.sin_addr.s_addr = INADDR_ANY; // automatski popuni mojom IP adresom
        memset(&(my_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama
        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("dobij paket od %s\n",inet_ntoa(their_addr.sin_addr));
        printf("paket je %d bajtova dug\n",numbytes);
        buf[numbytes] = '\0';
        printf("paket sadrži \"%s\"\n",buf);
        close(sockfd);
        return 0;
    } 

Primijeti da pri pozivu funkcije socket() konačno koristimo SOCK_DGRAM. Takođe, vidiš li da nema potrebe za funkcijama  listen() ili accept(). Ovo je jedna od super stvari kod nepovezanih datagram soketa!

Sad ide izvorni kôd za talker.c:

    /*
    ** talker.c – primjer datagram klijenta
    */
    #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 na koji će se korisnici povezivati
    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in their_addr; // informacije o adresi računara kojeg zovemo
        struct hostent *he;
        int numbytes;
        if (argc != 3) {
            fprintf(stderr,"korištenje: talker računar poruka\n");
            exit(1);
        }
        if ((he=gethostbyname(argv[1])) == NULL) {  // daj info za računar koji zovemo
            perror("gethostbyname");
            exit(1);
        }
        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }
        their_addr.sin_family = AF_INET;     // serversko uređenje bajtova
        their_addr.sin_port = htons(MYPORT); // kratko, mrežno urećenje bajtova
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama
        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 je sve! Pokreni listener na nekoj mašini, onda pokreni talker na drugoj. Pogledaj kako komuniciraju! E zar nije super stvar?!

Moram samo da dodam nešto vezano za ovu temu, za datagram sokete, pošto je ovo poglavlje vezano za njih. Recimo da talker pozove connect() i odredi adresu listener-a. Odatle pa nadalje, talker može samo da šalje i prima podatke sa adrese određene funkcijom connect(). Zbog ovoga, ne moraš da koristiš sendto() i recvfrom(); možeš slobodno koristiti samo send() i recv().


Prethodno

Glavna strana

Dalje

Sistemski pozivi

 

Nešto naprednije tehnike