המדריך של ביג' לתכנות רשת
הקודםהבא

5. רקע לשרת - לקוח

זה עולם של לקוח-שרת. כנעט כל דבר ברשת מתעסק עם תהליכי לקוח מדברים לתהליכי שרת, ולהיפך. קח את telnet, לדוגמא. כשאתה מתחבר למארח מרוחק בפתחה 23 (עם telnet) (הלקוח), תוכנה במארח הזה (שנקראת telnetd, השרת) קופצת לחיים. היא מנהלת את חיבור ה telnet הנכנס, מסדרת לך מסך login, וכו'

איור 2.פעולה בין שרת-לקוח.

[איור פעולה בין שרת-לקוח]

תחלופת המידע בין השרת והלקח מתומצתת באיור 2.

שימו לב ש זוג לקוח\שרת יכולים לדבר SOCK_STREAM, SOCK_DGRAM, או כל דבר אחר (כל עוד הם מדברים אותו דבר.) כמה דוגמאות טובות לזוגות לקוח-שרת הם telnet/telnetd, ftp/ftpd, or bootp/bootpd. בכל פעם שאתה משתמש ftp, יש תוכנה בשרת, ftpd, שמשרתת אותך.

לעיתים קרובות, יהיה רק שרת אחד על מחשב אחד, והשרת ינהל כמה לקוחות באמצעות fork(). הרצף הבסיסי הוא: השרת יחכה לחיבור, יקבל אותו ( עםaccept() ) אז יצור (עם fork() )תהליך בן לנהל את החיבור. זה מה השרת לדוגמא שלנו עושה בחלק הבא.

5.1. שרת זורם (Stream) פשוט

כל מה שהשרת הזה עושה הוא לשלוח את המחרוזת "Hello, World!\n" לחיבור זורם. כל מה שתצצרכו לעשות כדי לבחון את שרת זה הוא להריץ אותו בחלון אחון, ו telnet אליו בחלון אחר, עם:


    $ telnet remotehostname 3490

כש remotehostname הוא השם של המחשב שאתה מריץ את השרת עליו.

הקוד של השרת: (שים לב: לוכסן הפוך בסוף שורה אומר שהשורה נמשכת לשורה הבאה.)


    /*
    ** server.c -- a stream socket server demo
    */

    #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    // the port users will be connecting to

    #define BACKLOG 10     // how many pending connections queue will hold

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

    int main(void)
    {
        int sockfd, new_fd;  // listen on sock_fd, new connection on new_fd
        struct sockaddr_in my_addr;    // my address information
        struct sockaddr_in their_addr; // connector's address information
        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; // automatically fill with my IP
        memset(&(my_addr.sin_zero), '\0', 8); // zero the rest of the struct

        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; // reap all dead processes
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGCHLD, &sa, NULL) == -1) {
            perror("sigaction");
            exit(1);
        }

        while(1) {  // main accept() loop
            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()) { // this is the child process
                close(sockfd); // child doesn't need the listener
                if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
                    perror("send");
                close(new_fd);
                exit(0);
            }
            close(new_fd);  // parent doesn't need this
        }

        return 0;
    }

במקרה שאת סקרן, הקוד נמצא ב פונקציית main() גדולה בישביל (מה שאני מרגיש) קריאות התחביר. תרגיש חופשי לפצל את זה לפונקציות קטנות אם זה גורם לך להרגיש יותר טוב.

(בנוסף, ה sigaction() אולי יהיה חדש לך--זה בסדר. הקוד ששם אחראי לטפל בתהליכים זומביים שמופיעים כאשר תהליך בן (שנוצר עם fork()) יוצר. אם אתה עושה הרבה זומביים ולא מטפל בהם, מנהל המערכת של יהיה מוטרד )

אתה יכול להשיג את המידע מהשרת, בעזרת לקוח הנמצא החלק הבא.

5.2. לקוח זורם ( Stream) פשוט

הדבר הזה יותר קל מהשרת. כל מה שזה עושה הוא להתחבר למארח שאתה נותן לו בשורת הפקודה, לפתחה 3490. ומשיג את המחרוזת שהשרת שולח.

הקוד מקור של הלקוח:


    /*
    ** client.c -- a stream socket client demo
    */

    #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 // the port client will be connecting to

    #define MAXDATASIZE 100 // max number of bytes we can get at once

    int main(int argc, char *argv[])
    {
        int sockfd, numbytes;
        char buf[MAXDATASIZE];
        struct hostent *he;
        struct sockaddr_in their_addr; // connector's address information

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

        if ((he=gethostbyname(argv[1])) == NULL) {  // get the host info
            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);  // zero the rest of the struct

        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;
    }

שימו לב שאם אתם לא מריצים את השרת לפני שאתם מריצים את הלקוח, connect() תחזיר "החיבור סורב" ("Connection refused") מאוד מועיל.

5.3. שקעים של חבילות (Datagram Sockets)

אין להרבה מה להגיד פה, אז אני רק אציג זוג תוכניות לדוגמא: talker.c ו listener.c.

listener נמצאת על מחשב מחכה לחבילת מידע בפתחה 4950. התוכנית talker שולחת חבילת מידע לפתחה הזאת, למחשב מסוים, שמכיל את מה שהמשתמש מכניס בשורת הפקודה.

הנה קוד המקור של listener.c:


    /*
    ** listener.c -- a datagram sockets "server" demo
    */

    #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    // the port users will be connecting to

    #define MAXBUFLEN 100

    int main(void)
    {
        int sockfd;
        struct sockaddr_in my_addr;    // my address information
        struct sockaddr_in their_addr; // connector's address information
        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; // automatically fill with my IP
        memset(&(my_addr.sin_zero), '\0', 8); // zero the rest of the struct

        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;
    } 

שימו לב שבקריאה ל socket() אנחנו סוף סוף משתמשים ב SOCK_DGRAM. בנוסף, שימו לב שאיו צורך לקרוא ל listen() או accept(). זה אחד המאפיינים בשימוש בשקעי חבילה חסרי חיבור!

הנה בא המקור של talker.c:


    /*
    ** talker.c -- a datagram "client" demo
    */

    #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    // the port users will be connecting to

    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in their_addr; // connector's address information
        struct hostent *he;
        int numbytes;

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

        if ((he=gethostbyname(argv[1])) == NULL) {  // get the host info
            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); // zero the rest of the struct

        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;
    } 

וזה הכל! הרץ את listener במחשב מסוים, ואז הרץ את talker במחשב אחר. ראה איך הם מתקשרים! כיף והתרגשות לכל המשפחה הגרעינית!

חוץ מפרט אחד קטן שהזכרתי פעמים רבות בעבר: שקעי חבילה מחוברים. אני צריך לדבר על זה כאן מכיוון שאנחנו בחלק של שקעי חבילה. בואו נאמר שהיית ב talker קורא ל connect() ומוסר לה את הכתובת של listener. מנקודה זאת והלאה, התוכנית talker רשאי רק לשלוח ולקבל מהכתובת שנמסרה ל connect(). . מסיבה זאת, אתה לא חייב להשתמש ב sendto() ו recvfrom(); אתה פשוט יכול להשתמש ב send() ו recv().


הקודםביתהבא
קריאות מערכת טכניקות קצת מתקדמות