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

6. טכניקות קצת מתקדמות

אלה לא באמת מתקדמות, אבל הן יוצאות מהרמות הבסיסיות שכבר כיסינו. למעשה אם הגעת עד כאן, אתה צריך להחשיב את עצמך שהשלמת בצורה יפה את הבסיס של תכנות רשת ביוניקס! ברכותי!

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

6.1. חסימה (Blocking)

חסימה. אתה שמעת על זה--עכשיו מה זה? בקיצור, "לחסום" ("Block") זה ג'רגון טכני ל "לישון" ("sleep"). אתה בטח שמת לב שכאשר אתה מריץ את listener, לעיל, הוא פשוט יושב ומחכה עד שחבילת מידע מגיעה. מה שקרה הוא שכש recvfrom(), נקראה, לא היה מידע, ולכן על recvfrom() נאמר שהיא "חוסמת" (זאת אומרת, ישנה (sleep) שמה ) עד שמידע מגיע.

הרבה פונקציות חוסמות. accept() חוסמת. כל פונקציות ה recv() חוסמות. הסיבה שהן יכולות לעסות זאת היא שמותר להן. כשאתה יוצר את מתאר השקע עם socket(), הגרעין (kernel) קובעת אותו לחסום. אם אתה לא רוצה שהשקע יחסום, אתה צריך לקרוא ל fcntl():


    #include <unistd.h>
    #include <fcntl.h>
    .
    .
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    .
    .

על - ידי שאתה קובע ששקע לא יחסום, אתה יכול באפקטיביות להוציא("poll") משקע מידע . אם אתה מנסה לקרוא משקע שלא חוסם, ואין שם מידע, לא ניתן לו לחסום -- הוא יחזיר -1 ו errno יושם הערך EWOULDBLOCK.

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

6.2. select()--תיאום קלט/פלט רב בו זמנית (Synchronous I/O Multiplexing)

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

אין בעיה אתה אומר, פשוט accept() וכמה recv()-ים. לא כל כך מהר! מה אם אתה חוסם בקריאה accept() איך אתה הולך לקבל (עם recv() ) מידע באותו זמן? "אשתמש בשקעים שלא חוסמים" מה פתאום! אתה לא רוצה לקחת את כל זמן המעבד. אז מה?

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

ללא השתאות, הנה התחביר של select():


       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int numfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

הפונקציה בוחנת קבוצות של מתארי קבצים; וכאן את readfds, writefds, ו exceptfds. אם אתה רוצה לראות אם אתה יכול לקרוא מהקלט הסטדרטי (standart input) ואיזה שהוא מתאר קובץ מסוים, sockfd, פשוט הוסף את מתארי הקובץ 0 ו sockfd לקבוצה readfds. בפרמטר numfds צריך להיות מושם הערך של מתאר הקובץ הגדול ביותר ועוד אחד. בדוגמא זאת, צריך להיות מושם בו sockfd+1, מכיוון שלבטח הוא גדול יותר מהקלט הסטנדרטי (0).

כאשר select() חוזרת, readfds ישתנה בהתאם למתארי הקובץ שבחרת, ועתה מוכנים לקריאה מהם. אתה יכול לבחון אותם עם המקרו FD_ISSET(), למטה.

לפני שנתקדם יותר, אני אדבר על איך עובדים עם הקבוצות האלו. כל קבוצה היא מסוג fd_set. המקרו-ים הבאים פועלים על סוג זה:

לבסוף, מה זה ה struct timevalהמוזר הזה? טוב לעיתים אתה לא רוצה לחכות לנצח למישהו שישלח לך מידע. אולי כל 96 שנוי אתה רוצה להדפיס "עדיין ממשיך" למסוף, אפילו שכלום לא קרה. המבנה הזה של הזמן נותן לך לתת תקופת שהי לזמן מסוים. אם הזמן עבר ו select() עדיין לא מצאה איזהשהו מתאר קובץ מוכן, הפונקציה תחזור כדי שתוכל להמשיך עם התוכנית.

למבנה struct timeval יש את השדות הבאים:


    struct timeval {
        int tv_sec;     // seconds
        int tv_usec;    // microseconds
    };

פשוט שים ב tv_sec למספר השניות לחכות, וב tv_usec את מספר המיקרו-שניות לחכות. כן זה מיקרו-שניות לחכות. כן זה מיקרו-שניות, לא מילי-שניות. יש 1000 מיקרו-שניות במילי-שניה, ו 1000 מילי-שניות בשניה. זאת אומרת יש 1,000,000 מיקרו-שניות בשניה. אז למה זה "usec"? ה "u" אמרוה להארות כמו האות היוונית ?¼ (מיו) שאנו משתמשים עבור "מיקרו". בנוסף שהפונקציה חוזרת, timeout אולי יעודכן ויכיל את הזמן שעדיין נותר. זה תלוי בסוג היוניקס שאתה מריץ.

יש! יש לנו טיימר ברולוציה יש מיקרו-שניה! טוב, אל תסמכו על זה. חתיכחת זמן סטנדרטית של יוניקס היא בסביבות 100 מילי-שניות, אז אתה כנראה תצטרך לחכות כמות זאת של זמן ללא תלות בכמה קטן כיוונת את struct timeval להיות.

דברים אחרים מעניינים: אם אתה שם בשדות struct timeval את 0, select() תחזור מיידית, וכך תוכל באפקטיביות לבדוק את מצב השקעים שבקבוצות. אם תשים בפרמטר timeout להיות NULL , אף פעם לא יגמר לה הזמן, והיא תחכה עד שיהיה לה מתאר קובץ מוכן. לבסוף, אם לא אכפת לך על לחכות לקבוצה מסוימת, אתה פשוט יכול לשים NULL במקומה, בקריאה ל select().

קטע הקוד הבא מחכה מחכה 2.5 שניות שמשהו יופי בקלט הסטנדרטי:


    /*
    ** select.c -- a select() demo
    */

    #include <stdio.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>

    #define STDIN 0  // file descriptor for standard input

    int main(void)
    {
        struct timeval tv;
        fd_set readfds;

        tv.tv_sec = 2;
        tv.tv_usec = 500000;

        FD_ZERO(&readfds);
        FD_SET(STDIN, &readfds);

        // don't care about writefds and exceptfds:
        select(STDIN+1, &readfds, NULL, NULL, &tv);

        if (FD_ISSET(STDIN, &readfds))
            printf("A key was pressed!\n");
        else
            printf("Timed out.\n");

        return 0;
    } 

אם אתם בטרמיל עם אוגר זכרון(buffer), המקש שעליהם להקיש הוא RETURN (אנטר) או שהפונקציה תחזור שיגמר לה הזמן בכל מקרה.

עכשיו, חלק מכם עלולים לחשוב שזאת דרך מצוינת לחכות למידע בשקעי חבילה חסרי חיבור--ואתם צודקים: זאת יכולה להיות חלק ממערכות היוניקס יכולות לשהתמש ב select באופן זה, וחלק לא יכולות. אתה צריך לבדוק מה עומדי המדריך המקומיים אומרים על העניין, אם אצה רוצה לנסותו..

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

מה קורה אם שקע בקבוצה של הקריאה סוגר את החיבור? במקרה הזה, select() תחזיר את מתאר השקע הזה כמוכן לקריאה. כשאתה תבצע recv() ממנו, recv() תחזיר 0. ככה אתה יודע שהלקוח סגר את החיבור.

עוד דבר מעניין על select(): אם יש לך שקע שאזין לחיבורים (עם listen() ), אתה יכול לבדוק אם יש חיבור חדש ע"י כך שתשים את מתאר השקע שלו בקבוצה readfds .

וזה, חברי, היה תיאור מהיר של הפונקציה select() החזקה.

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

התוכנה הזאת מתנהגת כמו צאט רב משתמשים פשוט. התחילו אותה בחלון אחד, ואז בצעו telnet אליה ("telnet hostname 9034") מכמה חלונות אחרים. כשאתה מקיש משהו בחלון telnet אחד, הוא צריך להופיע גם בכל החלונות האחרים.


    /*
    ** selectserver.c -- a cheezy multiperson chat server
    */

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

    #define PORT 9034   // port we're listening on

    int main(void)
    {
        fd_set master;   // master file descriptor list
        fd_set read_fds; // temp file descriptor list for select()
        struct sockaddr_in myaddr;     // server address
        struct sockaddr_in remoteaddr; // client address
        int fdmax;        // maximum file descriptor number
        int listener;     // listening socket descriptor
        int newfd;        // newly accept()ed socket descriptor
        char buf[256];    // buffer for client data
        int nbytes;
        int yes=1;        // for setsockopt() SO_REUSEADDR, below
        int addrlen;
        int i, j;

        FD_ZERO(&master);    // clear the master and temp sets
        FD_ZERO(&read_fds);

        // get the listener
        if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        // lose the pesky "address already in use" error message
        if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes,
                                                            sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }

        // bind
        myaddr.sin_family = AF_INET;
        myaddr.sin_addr.s_addr = INADDR_ANY;
        myaddr.sin_port = htons(PORT);
        memset(&(myaddr.sin_zero), '\0', 8);
        if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) {
            perror("bind");
            exit(1);
        }

        // listen
        if (listen(listener, 10) == -1) {
            perror("listen");
            exit(1);
        }

        // add the listener to the master set
        FD_SET(listener, &master);

        // keep track of the biggest file descriptor
        fdmax = listener; // so far, it's this one

        // main loop
        for(;;) {
            read_fds = master; // copy it
            if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
                perror("select");
                exit(1);
            }

            // run through the existing connections looking for data to read
            for(i = 0; i <= fdmax; i++) {
                if (FD_ISSET(i, &read_fds)) { // we got one!!
                    if (i == listener) {
                        // handle new connections
                        addrlen = sizeof(remoteaddr);
                        if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr,
                                                                 &addrlen)) == -1) {
                            perror("accept");
                        } else {
                            FD_SET(newfd, &master); // add to master set
                            if (newfd > fdmax) {    // keep track of the maximum
                                fdmax = newfd;
                            }
                            printf("selectserver: new connection from %s on "
                                "socket %d\n", inet_ntoa(remoteaddr.sin_addr), newfd);
                        }
                    } else {
                        // handle data from a client
                        if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) {
                            // got error or connection closed by client
                            if (nbytes == 0) {
                                // connection closed
                                printf("selectserver: socket %d hung up\n", i);
                            } else {
                                perror("recv");
                            }
                            close(i); // bye!
                            FD_CLR(i, &master); // remove from master set
                        } else {
                            // we got some data from a client
                            for(j = 0; j <= fdmax; j++) {
                                // send to everyone!
                                if (FD_ISSET(j, &master)) {
                                    // except the listener and ourselves
                                    if (j != listener && j != i) {
                                        if (send(j, buf, nbytes, 0) == -1) {
                                            perror("send");
                                        }
                                    }
                                }
                            }
                        }
                    } // it's SO UGLY!
                }
            }
        }

        return 0;
    }

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

הסיבה שיש לי את הקבוצה master היא משום ש select() למעשה משנה את הקבוצה שאתה מעביר לה, כדי להראות איזה שקעים מוכנים ל'קריאה מהם. מכיוון שאני צריך לעקוב על החיבורים מקריאה אחת של select() לבאה, אני חייב לאחסן אותם במקום אחר. בדקה האחרונה, אני מעתיק את master לתוך read_fds, ואז קורא ל select().

אבל רגע, זה לא אומר שבכל פעם שאני מקבל חיבור חדש, עלי להוסיף אותו לקבוצה master ? כן! ובכל פעם שחיבור נסגר, אני צריך להסיר אותו מהקבוצה master ? כן, זה אומר.

שימו לב שאני בודק לראות מתי השקע listener מוכן לקריאה. כשהוא מוכן לקריאה, זה אומר שיש לי חיבור חדש בתור של החיבורים, ואני מקבל (עם accept()) אותו למצרף אותו לקבוצה master באופן דומה, כשחיבור של לקוח מוכן לקריאה, ו recv() מחזירה 0, אני יודע שהלקוח סגר את החיבור, ואני חייב להסירו מהקבוצה master .

אם בקבלה (עם recv()) מהלקוח מחזירה ערך חיובי, אני יודע שמידע התקבל. אז אני משיג אותו, ואז עובר דרך השקעים ש ב master ושולח את המידע הזה לכל הקליינטים המחוברים האחרים.

וזה היה, חברי, דוגמא קצת-לא-פשוטה של הפונקציה הכל-יכולה select() .

6.3. ניהול שליחה חלקית של send()

זוכרים שבחלק על send(), לעיל, כשאמרתי ש send() עלולה לא לשלוח את כל המידע שביקשתם ממנה? זאת אומרת, שאם רציתם לשלוח 512 בתים, אבל היא החזירה 412 מה קרה ל 100 בתים שנשארו?

טוב הם עדיין במאגר הקטן שלך מחכים להשלח. עקב נסיבות שלא בשליתתך, גרעין המערכת החליט שלא לשלוח את כל המידע בחתיכה אחת, ועכשיו, זה תלוי בכם לדאוג ששאר המידע יגיע.

בנוסף, אתם יכולים לכתוב פונקציה כמו זאת:


    #include <sys/types.h>
    #include <sys/socket.h>

    int sendall(int s, char *buf, int *len)
    {
        int total = 0;        // how many bytes we've sent
        int bytesleft = *len; // how many we have left to send
        int n;

        while(total < *len) {
            n = send(s, buf+total, bytesleft, 0);
            if (n == -1) { break; }
            total += n;
            bytesleft -= n;
        }

        *len = total; // return number actually sent here

        return n==-1?-1:0; // return -1 on failure, 0 on success
    } 

בדוגמא זאת, s הוא השקע שאתם רוצים לשלוח את המידע אליו, buf הוא המאגר שמכיל את המידע, len הוא מצביע למספר שלם (int) המכיל את מספר הבתים במאגר (אורך המאגר).

הפונקציה מחזירה -1 בשגיאה (ו errno יהיה מכוון מהקריאה ל send().) בנוסף, מספר הבית שבאמת נשלחו יוחזר ב len. זה יהיה זהה למספר הבתים שרצית שהוא ישלח, אלא אם הייתה שגיאה. sendall() תעשה ככל יכולת, ותתאמץ לשלוח את המידע החוצה, אבל אם נתקלה שגיאה, היא תחזור מיד.

לשם השלמות,הנה קריאה לדומא לפונקציה:


    char buf[10] = "Beej!";
    int len;

    len = strlen(buf);
    if (sendall(s, buf, &len) == -1) {
        perror("sendall");
        printf("We only sent %d bytes because of the error!\n", len);
    }

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

6.4. כימוס מידע (Data Encapsulation)

מה זה אומר לכמס מידע? במקרה הכי פשוט, אתה תשים כותרת בהתחלה, עם מידע מזהה, או אורך המידע, או שניהם.

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

ואו, זה לא כל כך ברור.

טוב, בוא נאמר שיש לך תוכנת צאט רבת משתמשים, שמשתמשת ב שקעים זורמים ("SOCK_STREAM"). כשמשתמש מקיש ("אומר") משהו, שני חתיכות מידע צריכות להיות מועברות לשרת: מה הוא אמר, ולמי הוא אמר.

עד כאן הכל טוב, "מה הבעיה?" אתה שואל.

הבעיה היא שהודעה היא באורך משתנה. אדם אחד בשם "tom" יאמר "Hi" , ואדם אחר בשם "Benjamin" יאמר, "Hey guys what is up?"

אז אתה שולח (עם send()) את כל הדבר הזה ללקוחות כשהוא מגיע, זרם המידע החוצה יראה כך:


    t o m H i B e n j a m i n H e y g u y s w h a t i s u p ?

וכו. אז איך הקליינט יודע שהודעה אחת מתחילה ואחרת נגמר? אתה יכול, אם רצית, לעשות את ההודעות כולן באותו אורך ואז פשוט לקרוא ל sendall() שממשנו, למעלה. אבל זה בזבוז של רוחב פס! אנחנו לא רוצים לשלוח (עם send()) ברשת 1024 בתים רק כדי ש "tom" יוכל לאמר "Hi".

אז אנחנו מכמסים את המידע בכתורת קטנה, ומבנה חבילה. גם השרת וגם הקליינט יודעים איך ל"ארוז" ול"פרוק" (באנגלית pack ו unpack, וגם יכול להיות שתתקלו במילים marshal ו unmarshal) את המידע הזה. אל תביט עכשיו, אבל אנחנו מתחילים להגדיר protocol שיתאר את קליינט ושרת ומתקשרים!

במקרה הזה, בוא נניח כי שם המשתמש הוא באורך קבוע של 8 תוים, מרופד ב '\0'. ובוא נניח כי המידע הוא בגודל משתנה, עד לגודל תקרה של 128 תוים. בןא מביט במבנה חבילה לדוגמא שאנו יכולים לשתמש בו במצב זה:

  1. אורך (בית אחד - בלי סימן) -- האורך הכולל של החבילת המידע, סופר את ה 8 בתים שם משתמש ואת המידע של הצאט.

  2. שם (8 בתים) -- שם המשתמש, מרופד ב NUL אם צריך.

  3. מידע הצאט (nבתים) -- המידע עצמו, לא יותר מ 128 בתים. האורך של החבילה צריך להיות מחושב ע"י האורך של מידע זה ועוד 8 (האורך של השדה של השם-למעלה).

למה בחרתי להגביל את השדות ב 8 בתים ו 128 בתים? שלפתי אותם ,בהנחה שהם יהיו מספרים. אולי 8 בתים הוא לא מספק את צרכיך , ואתה רוצה שדה שם בארוך 30 בתים, או כל דבר. הבחירה היא שלך.

בהשתמש בהגדרות לחבילה לעיל, החבילה הראשונה תכיך את המידע הבא (בהקסדצימלי ו ב ASCII):


      0A     74 6F 6D 00 00 00 00 00      48 69
   (length)  T  o  m    (padding)         H  i

והשניה באופן דומה:


      14     42 65 6E 6A 61 6D 69 6E      48 65 79 20 67 75 79 73 20 77 ...
   (length)  B  e  n  j  a  m  i  n       H  e  y     g  u  y  s     w  ...

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

כשאתה שולח במידע, אתה צריך להיות זהיר ולהשתמש בפקודה דומה ל sendall(), שלעיל. כך שאתה תדע שכל המידע נשלח, גם אם נדרשו כמה קריאת ל send() כדי להוציא את כולו החוצה.

באותו אופן, שאתה מקבל מידע, אתה תצטרך לעשות עוד קצת עבודה. כדי להיות בטוח, אתה צריך להניח שאתה עלול לקבל חבילה חלקית ( אולי נקבל "00 14 42 65 6E" מ Benjamin, למעלה , אבל זה כל מה שנקבל בקריאה הזאת ל recv()). אנחנו צריכים לקרוא ל recv() עוד פעם ועוד פעם, עד שהחבילה התקבלה בשלמותה.

אבל איך? טוב אנחנו יודעים את מספר הבתים שאנחנו צריכים לקבל סך הכל כדי שהחבילה תיהיה שלמה.מכיוון שמספר זה נמצא בראש חבילת המידע. אנחנו גם יודע את האורך המקסימלי של חבילה : 1+8+128 או 137 בתים (זה בגלל שככה הגדרנו אותה.)

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

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

ברגע שחבילה היא שלמה, אתה יכול לעשות איתה מה שתרצה, להשתמש בה,ולהסיר אותה מהמערך לעבודה שלך.

ואו! אתה מריץ את זה בראש שלך עדיין? טוב הנה עוד משהו: יכול להיות שקראת מעבר לחבילה אחת והמשכת לחבילה השניה, ב קריאה אחת ל recv(). זאת אומרת, מערך העבודה שלך מכיל חבילה שלמה אחת, וחלק לא גמור של החבילה הבאה! (אבל זה למה עשית את המערך לעבודה שלך גדול מספיק להחזיק שני חבילות--במקרה שזה קרה!)

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

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

אף פעם לא אמרתי שזה יהיה קל. טוב, כן אמרתי שזה יהיה קל. וזה באמת קל; אתם רק צריכים להתאמן, ובקרוב זה יבוע בטבעיות!


אחורהביתהבא
רקע לשרת - לקוח הפניה נוספת