בפוסט הזה נציג מימוש של מערכת client/server. בפוסט קודם שדן ב-IPC Sockets כבר הוצגה דוגמא של client/server כך שמומלץ לקרוא אותה קודם. הדוגמא הנוכחית לא מתקדמת בהרבה מהקודמת, אך היא בכל זאת מעט יותר מורכבת.
מה הדוגמא מבצעת?
ה-client שולח הודעות ל-server. ה-server שולח הודעה חזרה - echo.
איך נשלחת ההודעה?
ההודעה מוקלדת ע"י המשתמש.
מה תוכן ההודעות?
ה-client שולח ל-server שני מספרים. ה-server מסכם אותם ושולח חזרה את הסכום. ה-client מציג את המספרים שנשלחו ואת סכומם.
מה לגבי הודעות לא חוקיות?
אם ה-client מזהה שההודעה שהוקלדה אינה חוקית, כלומר אינה זוג מספרים, תודפס הודעת שגיאה וכלום לא ישלח ל-server.
מהן הפעולות שה-client מבצע?
הוא מחכה להקלדת הודעה מהמקלדת, שולח אותה ל-socket, ואח"כ עובר להמתנה להודעה מה-server.
מהן הפעולות שה-server מבצע?
כשנוצר connection, ה-server יוצר child process עבור ה-client ומשם מטפל בהודעות ה-client. ה-server תומך בהרבה clients.
נציג את הקוד של ה-server ואחריו את הקוד של ה-client, אבל לפני כן, חייב לציין שאת הדוגמא לקחתי מהספר
זהו ספר יסוד בנושא. ספר מומלץ כמובן.
הנה הקוד של ה-server.
שם הקובץ: tcpserv09.c
התוכנית כוללת שלוש פונקציות עקריות:
main - הפונקציה הראשית.
str_echo (בכתום) - פונקציית עזר שקוראת את ההודעה מה-client ומחזירה לו echo.
פרוט בהמשך.
sig_chld (בטורקיז) - זהו ה-handler שמטפל ב-signal ששולחת המערכת כשה-child process מפסיק להתקיים. פרוט בהמשך.
והנה הכנת הפרמטרים ל-bind:
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
שורה 19 מכניסה את 3 הקונפיגורציות הנ"ל למערכת.
הנה ה-prototype של accept:
נמשיך עם התוכנית: עם הווצרות connection ל-client חדש, התוכנית תתקדם מהפונקציה accept, וננגיע לשורה 72.
לפני סיום ה-server, נעיף מבט על ה-signal handler - שורות 12-21 למעלה.
הנה הפונקציה שוב:
הסברים על sig_chld:
זהו כאמור ה-handler של ה-SIGCHLD. כשה-Client מסיים, הוא שולח הודעת FIN. כתוצאה מכך ה-child process שקשור לאותו client יפסיק לפעול. כעת ישלח ה-SIGCHILD. מטרתו - לגרום לכך שה-child process יחוסל, ולא ישאר כ-zombie.
אם כך, למה צריך את ה-while loop? האם sig_chld לא יקרא בנפרד עבור כל סיגנל? התשובה היא - לא תמיד. במצב בו כמה child process יפסיקו לעבוד באותו זמן, יתכן שישלח רק סיגנל אחד, ולפעמים גם אם ישלחו מספר סיגנלים, יתכן שלא כולם יטופלו כי הם לא מחכים ב-queue.
איך פותרים את הבעיה? הפתרון מוצג בשורות 7-9 למעלה.
נראה את ה-prototype של waitpid:
עד כאן על ה-server.
מה הדוגמא מבצעת?
ה-client שולח הודעות ל-server. ה-server שולח הודעה חזרה - echo.
איך נשלחת ההודעה?
ההודעה מוקלדת ע"י המשתמש.
מה תוכן ההודעות?
ה-client שולח ל-server שני מספרים. ה-server מסכם אותם ושולח חזרה את הסכום. ה-client מציג את המספרים שנשלחו ואת סכומם.
מה לגבי הודעות לא חוקיות?
אם ה-client מזהה שההודעה שהוקלדה אינה חוקית, כלומר אינה זוג מספרים, תודפס הודעת שגיאה וכלום לא ישלח ל-server.
מהן הפעולות שה-client מבצע?
הוא מחכה להקלדת הודעה מהמקלדת, שולח אותה ל-socket, ואח"כ עובר להמתנה להודעה מה-server.
מהן הפעולות שה-server מבצע?
כשנוצר connection, ה-server יוצר child process עבור ה-client ומשם מטפל בהודעות ה-client. ה-server תומך בהרבה clients.
נציג את הקוד של ה-server ואחריו את הקוד של ה-client, אבל לפני כן, חייב לציין שאת הדוגמא לקחתי מהספר
Unix Network Programming Volume 1, Third Edition: The Sockets Networking API | ||
By W. Richard Stevens, Bill Fenner, Andrew M. Rudoff |
זהו ספר יסוד בנושא. ספר מומלץ כמובן.
הנה הקוד של ה-server.
שם הקובץ: tcpserv09.c
התוכנית כוללת שלוש פונקציות עקריות:
main - הפונקציה הראשית.
str_echo (בכתום) - פונקציית עזר שקוראת את ההודעה מה-client ומחזירה לו echo.
פרוט בהמשך.
sig_chld (בטורקיז) - זהו ה-handler שמטפל ב-signal ששולחת המערכת כשה-child process מפסיק להתקיים. פרוט בהמשך.
- #include "unp.h"
- struct args {
- long arg1;
- long arg2;
- };
- struct result {
- long sum;
- };
- void
- sig_chld(int signo)
- {
- pid_t pid;
- int stat;
- while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
- printf("child %d terminated\n", pid);
- return;
- }
- void
- str_echo(int sockfd)
- {
- ssize_t n;
- struct args args;
- struct result result;
- for ( ; ; ) {
- if ( (n = Readn(sockfd, &args, sizeof(args))) == 0)
- return; /* connection closed by other end */
- result.sum = args.arg1 + args.arg2;
- Writen(sockfd, &result, sizeof(result));
- }
- }
- int
- main(int argc, char **argv)
- {
- int listenfd, connfd;
- pid_t childpid;
- socklen_t clilen;
- struct sockaddr_in cliaddr, servaddr;
- void sig_chld(int);
- listenfd = Socket(AF_INET, SOCK_STREAM, 0);
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(SERV_PORT);
- Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
- Listen(listenfd, LISTENQ);
- Signal(SIGCHLD, sig_chld);
- for ( ; ; ) {
- clilen = sizeof(cliaddr);
- if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
- if (errno == EINTR)
- continue; /* back to for() */
- else
- err_sys("accept error");
- }
- if ( (childpid = Fork()) == 0) { /* child process */
- Close(listenfd); /* close listening socket */
- str_echo(connfd); /* process the request */
- exit(0);
- }
- Close(connfd); /* parent closes connected socket */
- }
- }
4 הפונקציות הראשיות ב-main מודגשות בצהוב:
-Socket
-Bind
-Listen
-accept
שימו לב לעובדה ששמות הפונקציות (Socket, Bind, Listen) מתחילים באותיות גדולות. הסיבה - נעשה כאן שימוש בפונקציות עוטפות (wrapper). הפונקציה Socket למשל עוטפת את socket.
עיקר התוספת של הפונקציות העוטפות הן הודעות שגיאה.
הנה לדוגמא המימוש של Bind:
void
Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (bind(fd, sa, salen) < 0)
err_sys("bind error");
}
בפוסט הקודם הוצג תרשים בלוקים שמתאר את תהליך יצירת התקשורת עם ה-client. לשם הנוחות אציג את התרשים גם כאן:
הסבר נוסף על התהליך אפשר למצוא כאמור בפוסט של IPC-Socket. עדין יש הבדל בין שני המימושים: שלא כמו ב-IPC - Socket' כאן לא השתמשנו ב-getaddrinfo להכנת הפרמטרים עבור socket ו-bind. מומלץ להשתמש ב-getaddrinfo.
נעקוב אחר הכנת הפרמטרים והקריאה לפונקציות Socket ו- Bind, היות שהכנת הפרמטרים שונה מזו שהכרנו בפוסט הקודם.
שורה 50:
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
ה-prototype של socket:
int socket(int domain, int type, int protocol);
ה-socket הוא אמצעי לתקשורת בין process. הוא מוגדר ע"י 3 פרמטרים: domain, type, protocol. הפונקציה מחזירה file descriptor שישמש לצורך גישה ל-socket.
3 הפרמטרים שהפונקציה מקבלת:
domain: הערך AF_INET מציין IPv4.
type: הערך SOCK_STREAM מציין מערכת full duplex' בד"כ TCP.
protocol: הערך 0 מציין בחירת ה-default protocol עבור ה-type. במקרה שלנו, TCP.
והנה הכנת הפרמטרים ל-bind:
שורות 52-57:
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
הסבר: 4 השורות הראשונות מכינות את הפרמטרים עבור Bind. Bind כזכור מחברת בין ה-Socket לבין הפורט הלוקאלי.
הנה ה-prototype שלה:
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
הפרמטרים מסודרים ב- struct sockadd.
bzero - מאפסת את ה-structure. זוהי פונקציה שמאפסת מספר bytes לפי addrelen החל מהכתובת my_addr.
וכעת יש להכניס 3 פרמטרים ל-structure:
שורה 2: AF_INET זה IPv4.
שורה 3: כאן נקבע שה-socket יעשה bind לכל הכתובות הקיימות במחשב הלוקלי.
שורה 4: קביעת הפורט לו ה-server יאזין. SERV_PORT מוגדר בקובץ unp.h:
#define SERV_PORT 9877 /* TCP and UDP client-servers */
עד כאן על Bind.
שורה 59:
Listen(listenfd, LISTENQ);
כאשר LISTENQ הוא מספר ה-CONNECTION המקסימלי שיאופשר.
הוא מוגדר ב-UNP.H:
#define LISTENQ 1024 /* 2nd argument to listen() */
שורה 61:
Signal(SIGCHLD, sig_chld);
כאשר child process מפסיק לפעול, המערכת שולחת signal. עפ"י סיגנל זה, תוכל התוכנית לחסל את ה-process. אחרת - הוא ישאר כ-zombie ועלול תפוס משאבי מערכת.
איך פועל המנגנון?
צריך לבצע רגיסטרציה של פונקציה-handler, שתופעל כשהסיגנל יופיע. אצלנו ה-handler היא הפונקציה sig_chld (נמצאת למעלה בצבע טורקיז). הרגיסטרציה של sig_chld כ-handler עבור הסיגנל SIGCHLD, מבוצעת ע"י הפונקציה Signal.
הנה signal:
- Sigfunc *
- signal(int signo, Sigfunc *func)
- {
- struct sigaction act, oact;
- act.sa_handler = func;
- sigemptyset(&act.sa_mask);
- act.sa_flags = 0;
- if (signo == SIGALRM) {
- #ifdef SA_INTERRUPT
- act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
- #endif
- } else {
- #ifdef SA_RESTART
- act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
- #endif
- }
- if (sigaction(signo, &act, &oact) < 0)
- return(SIG_ERR);
- return(oact.sa_handler);
- }
- Sigfunc *
- Signal(int signo, Sigfunc *func) /* for our signal() function */
- {
- Sigfunc *sigfunc;
- if ( (sigfunc = signal(signo, func)) == SIG_ERR)
- err_sys("signal error");
- return(sigfunc);
- }
נתרכז בשורות בצהוב. מתבצעים כאן 3 דברים:
שורה 7: הכנסת המצביע ל-handler.
שורה 8: קביעת sa_mask - האם לחסום סיגנלים אחרים בזמן הטיפול ב-SIGCHLD? היות שאיננו מעונינים בחסימה, מפעילים את sigemptyset שקובע שה-mask יהיה ריק.
sigemptyset(&act.sa_mask);
שורה 15: ה-flag קובע את ההתנהגות כשחוזרים לאחת מפונקציות הספריה כגון open, read, write, אחרי שה-signal handler קטע את פעולתה. במצב זה יתכנו שתי תוצאות:
1. פונקצית הספריה תמשיך לאחר החזרה מה-handler. זה יקרה אם הדגל SA_RESTART פועל.
2. פונקצית הספריה תכשל ותחזור עם קוד שגיאה EINTR. זה במקרה שהדגל SA_RESTART לא דולק.
אנו בוחרים באופציה הראשונה:
אנו בוחרים באופציה הראשונה:
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
שורה 19 מכניסה את 3 הקונפיגורציות הנ"ל למערכת.
if (sigaction(signo, &act, &oact) < 0)
עד כאן ההכנה לטיפול בסיגנל SIGCHLD. את ה-handler כלומר את הפונקציה sig_chld נסקור בהמשך.
כעת נסתכל על הקריאה ל-accept:
כעת נסתכל על הקריאה ל-accept:
שורה 65-70:
- if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
- if (errno == EINTR)
- continue; /* back to for() */
- else
- err_sys("accept error");
- }
כאן ה-server נתקע בהמתנה להווצרות connection.
הנה ה-prototype של accept:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd - כרגיל, ה-descriptor שמחזירה socket.
addr - זהו ה-struct אליו יכנסו הנתונים של ה-host שהתחבר. נראה מיד את השימוש בהם.
addrlen - הגודל של struct addr.
שורות 2-7: טיפול בשגיאה. הסבר:
במקרה של חזרה מה-signal handler לפונקציה accept שנחתכה על ידו, יכולים לקרות אחד מהשניים:
1. הפונקציה accept תחזור לפעולה רגילה. זה גם בהתאם ל- SA_RESTART שקבענו קודם - ראה למעלה.
2. במערכות מסוימות הפונקציה אמורה להכשל ולחזור עם קוד שגיאה EINTR - למרות ה-SA_RESTART. שורה 2 מטפלת במצב זה ודואגת להפעלת accept מחדש במקרה זה.
נמשיך עם התוכנית: עם הווצרות connection ל-client חדש, התוכנית תתקדם מהפונקציה accept, וננגיע לשורה 72.
שורות 72-76:
- if ( (childpid = Fork()) == 0) { /* child process */
- Close(listenfd); /* close listening socket */
- str_echo(connfd); /* process the request */
- exit(0);
- }
פרוט:
שורה 1: יצירת process חדש ע"י fork. ה-process יטפל ב-client.
שורות 2-5 פועלות מתוך ה-child process, היות ש- childpid=0.
הסבר:
childpid=0: זה ה-child process.
childpid >0: זה ה-father process.
childpid<0: זה error.
הסבר:
childpid=0: זה ה-child process.
childpid >0: זה ה-father process.
childpid<0: זה error.
שורה 2: סגירת ה-socket המאזין. אין בו צורך אצל ה-child! נזכיר כי פעולת ה-fork משכפלת הכל, כולל ה-descriptors וה-sockets.
שורה 3: קריאה ל-פונקציה str_echo. זוהי הפונקציה שתטפל ב-client ומיד נעבור להסתכל עליה. היא כמובן רצה ב-process של הclient.
שורה 4: (exit(0 לסיום ה-child process. יגרום לשליחת סיגנל SIGCHLD.
שורה 66 -
Close(connfd); /* parent closes connected socket */
השורה מתבצעת ב-context של ה-father process. היות שכך, צריך לסגור את הקישור ל-client. ה-client מטופל ע"י ה-child process.
נעבור לפונקצה str_echo. היא כאמור למעלה מופעלת ב-child process ומטפלת בהודעות מה-client ואליו.
בעיקרה הפונקציה היא לולאה אינסופית בה קוראים הודעה מה-client, מבצעים עיבוד קטן, ושולחים הודעה חזרה. הנה היא:
- for ( ; ; ) {
- if ( (n = Readn(sockfd, &args, sizeof(args))) == 0)
- return; /* connection closed by other end */
- result.sum = args.arg1 + args.arg2;
- Writen(sockfd, &result, sizeof(result));
- }
שורה 2: Readn. קריאה מה-socket. הפונקציה כמובן תקועה עד שה-buffer מכיל הודעה.
שורה 3: במקרה שה-client מודיע הודעת סיום - FIN - ה-buffer יכיל 0, וכתוצאה מזה יהיה return.
שורה 5: ה-client שולח שני מספרים. ה-server מחבר אותם ושולח חזרה.
שורה 6: כתיבת ההודעה ל-socket.
Readn ו- Writen שתיהן פונקציות שעוטפות את read ו- write בהתאמה. ליתר דיוק הן עוטפות את readn ואת writen. האחרונות הן אלה שעוטפות את read ו-write. מה הן עושות? הן מבצעות את לולאת הקריאה והכתיבה מה-socket ואליו.
נראה תחילה את פונקציות ה-readn - נמצאות בקובץ readn.c:
- ssize_t /* Read "n" bytes from a descriptor. */
- readn(int fd, void *vptr, size_t n)
- {
- size_t nleft;
- ssize_t nread;
- char *ptr;
- ptr = vptr;
- nleft = n;
- while (nleft > 0) {
- if ( (nread = read(fd, ptr, nleft)) < 0) {
- if (errno == EINTR)
- nread = 0; /* and call read() again */
- else
- return(-1);
- } else if (nread == 0)
- break; /* EOF */
- nleft -= nread;
- ptr += nread;
- }
- return(n - nleft); /* return >= 0 */
- }
- /* end readn */
- ssize_t
- Readn(int fd, void *ptr, size_t nbytes)
- {
- ssize_t n;
- if ( (n = readn(fd, ptr, nbytes)) < 0)
- err_sys("readn error");
- return(n);
- }
מספר דגשים:
שורות 10-21: זוהי הלולאה מסביב לפונקציה read.
הסבר: הפונקציה read פונה ל-kernel ומבקשת לקרוא מספר מסוים של בתים. במקרה שלנו, מדובר בשמונה בתים - זהו גודלו של struct args. אם הקרנל מספק רק חלק ממספר הבתים שביקשנו, נמשיך ונקרא ממנו, עד שיגיעו כל הבתים להם ציפינו. זו מהות ה- while.
שורות 12-15: טיפול בשגיאה מ-read. אמנם קבענו למעלה ש-SA_RESTART ידאג לחזרה לפעולה של read אחרי חזרה מה-signal handler, אבל במערכות מסוימות תתכן שגיאה עם EINTR בכל זאת. במקרה זה, ה- while ימשיך לפעול.
שורה 27-34: הפונקציה Readn. היא עוטפת את readn ומטפלת במקרה של שגיאה.
והנה באופן דומה, writen מהקובץ writen.c:
- ssize_t /* Write "n" bytes to a descriptor. */
- writen(int fd, const void *vptr, size_t n)
- {
- size_t nleft;
- ssize_t nwritten;
- const char *ptr;
- ptr = vptr;
- nleft = n;
- while (nleft > 0) {
- if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
- if (nwritten < 0 && errno == EINTR)
- nwritten = 0; /* and call write() again */
- else
- return(-1); /* error */
- }
- nleft -= nwritten;
- ptr += nwritten;
- }
- return(n);
- }
- /* end writen */
- void
- Writen(int fd, void *ptr, size_t nbytes)
- {
- if (writen(fd, ptr, nbytes) != nbytes)
- err_sys("writen error");
- }
גם כאן writen מבצעת while loop סביב write עד סיום הכתיבה.
גם כאן מזוהה קוד השגיאה EINTR, שמופיע בגלל הפרעה של signal handler.
לפני סיום ה-server, נעיף מבט על ה-signal handler - שורות 12-21 למעלה.
הנה הפונקציה שוב:
- void
- sig_chld(int signo)
- {
- pid_t pid;
- int stat;
- while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
- printf("child %d terminated\n", pid);
- return;
- }
הסברים על sig_chld:
זהו כאמור ה-handler של ה-SIGCHLD. כשה-Client מסיים, הוא שולח הודעת FIN. כתוצאה מכך ה-child process שקשור לאותו client יפסיק לפעול. כעת ישלח ה-SIGCHILD. מטרתו - לגרום לכך שה-child process יחוסל, ולא ישאר כ-zombie.
אם כך, למה צריך את ה-while loop? האם sig_chld לא יקרא בנפרד עבור כל סיגנל? התשובה היא - לא תמיד. במצב בו כמה child process יפסיקו לעבוד באותו זמן, יתכן שישלח רק סיגנל אחד, ולפעמים גם אם ישלחו מספר סיגנלים, יתכן שלא כולם יטופלו כי הם לא מחכים ב-queue.
איך פותרים את הבעיה? הפתרון מוצג בשורות 7-9 למעלה.
נראה את ה-prototype של waitpid:
pid_t waitpid (pid_t pid, int *statloc, int options);
הפונקציה מקבלת 3 פרמטרים:
pid - ה-process id שאותו רוצים לחסל. אם pid= -1, אז המערכת מוכנה לטפל בכל process.
statloc - ה-status של ה-process שהסתיים.
options - בחרנו לשים WNOHANG. זה יגרום ל-waitpid לא להתקע אם יש עדיין processes חיים במערכת (אחרת היא היתה נתקעת ומחכה ש-process ימות).
מכאן, שכל עוד יש zombie process, ה-loop ימשיך. אחרת - יהיה return.
הפונקציה מחזירה את ה-process id במקרה שהכל תקין.
הפונקציה מחזירה 0 אם אין יותר zombies.
הפונקציה מחזירה 1- במקרה של שגיאה.
עד כאן על ה-server.
חבילת הקבצים שלנו מורכבת מחבילה של server וחבילה של client המקומפלות ורצות בנפרד.
הנה ה-client.
שם קובץ: tcpcli09.c:
הקובץ מכיל שתי פונקציות: main ו-str_cli. האחרונה מטפלת בשליחת וקבלת הודעות ל-server וממנו לאחר יצירת ה-connection.
נתחיל עם main:
שורה 30: בדיקה של מספר הארגומנטים. הפונקתיה מופעלת עם הכתובת של ה-server כארגומנט.
שורה 33: Socket - בדומה ל-server.
שורות 35-40: הכנה ל-conect וביצוע connect.
שימו לב ל-38: הפונקציה בונה את ה-address עפ"י [argv[1 המצביע על הכתובת של ה-server.
הפונקציה connect נתקעת עד להווצרות ה-connection.
שורה 41: קריאה לפונקציה str_cli שתטפל בהודעות ל-server.
str_cli - מודגשת בכתום למעלה.
str_cli בנויה כ-while loop.
3 הפונקציות העיקריות:
Fgets בשורה 8: קוראת characters מה-std io עם הפונקציה Fgets - זהו wrapper של fgets שנמצא ב-wrapstdio.c. מוסיף טיפול במקרה שגירה - הנה הוא:
Writen בשורה 14
סקרנו כבר את ה-wrapper של write למעלה. כותבים ל-socket את שני הארגומנטים שהתקבלו ע""י fgets והועתקו עם scanf. במקרה שלא נקלטו שני ארגומנטים, תודפס הודעת שגיאה.
Readn בשורה 16:
הפונקציה קוראת את ההודעה מה-socket. זהו ה-echo ששולח ה-server ומכיל את סכום 2 המספרים.
גם את Readn סקרנו והצגנו למעלה.
הרצת התוכנית.
נריץ את ה-client וה-server בשני חלונות שונים של conslole.
הנה העתק של ה-console בו רץ ה-client:
שם קובץ: tcpcli09.c:
#include "unp.h"
struct args {
long arg1;
long arg2;
};
struct result {
long sum;
};
- void
- str_cli(FILE *fp, int sockfd)
- {
- char sendline[MAXLINE];
- struct args args;
- struct result result;
- while (Fgets(sendline, MAXLINE, fp) != NULL) {
- if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) {
- printf("invalid input: %s", sendline);
- continue;
- }
- Writen(sockfd, &args, sizeof(args));
- if (Readn(sockfd, &result, sizeof(result)) == 0)
- err_quit("str_cli: server terminated prematurely");
- printf("%ld\n", result.sum);
- }
- }
- int
- main(int argc, char **argv)
- {
- int sockfd;
- struct sockaddr_in servaddr;
- if (argc != 2)
- err_quit("usage: tcpcli <IPaddress>");
- sockfd = Socket(AF_INET, SOCK_STREAM, 0);
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(SERV_PORT);
- Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
- Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
- str_cli(stdin, sockfd); /* do it all */
- exit(0);
- }
הקובץ מכיל שתי פונקציות: main ו-str_cli. האחרונה מטפלת בשליחת וקבלת הודעות ל-server וממנו לאחר יצירת ה-connection.
נתחיל עם main:
שורה 30: בדיקה של מספר הארגומנטים. הפונקתיה מופעלת עם הכתובת של ה-server כארגומנט.
שורה 33: Socket - בדומה ל-server.
שורות 35-40: הכנה ל-conect וביצוע connect.
שימו לב ל-38: הפונקציה בונה את ה-address עפ"י [argv[1 המצביע על הכתובת של ה-server.
הפונקציה connect נתקעת עד להווצרות ה-connection.
שורה 41: קריאה לפונקציה str_cli שתטפל בהודעות ל-server.
str_cli - מודגשת בכתום למעלה.
str_cli בנויה כ-while loop.
3 הפונקציות העיקריות:
Fgets בשורה 8: קוראת characters מה-std io עם הפונקציה Fgets - זהו wrapper של fgets שנמצא ב-wrapstdio.c. מוסיף טיפול במקרה שגירה - הנה הוא:
char *
Fgets(char *ptr, int n, FILE *stream)
{
char *rptr;
if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream))
err_sys("fgets error");
return (rptr);
}
Writen בשורה 14
סקרנו כבר את ה-wrapper של write למעלה. כותבים ל-socket את שני הארגומנטים שהתקבלו ע""י fgets והועתקו עם scanf. במקרה שלא נקלטו שני ארגומנטים, תודפס הודעת שגיאה.
Readn בשורה 16:
הפונקציה קוראת את ההודעה מה-socket. זהו ה-echo ששולח ה-server ומכיל את סכום 2 המספרים.
גם את Readn סקרנו והצגנו למעלה.
הרצת התוכנית.
נריץ את ה-client וה-server בשני חלונות שונים של conslole.
הנה העתק של ה-console בו רץ ה-client:
- ronen@tcpcli1 127.0.0.1
- 1 2
- 3
- a d
- invalid input: a d
- 44 232
- 276
- 4
- invalid input: 4
ה-client מופעל עם כתובת ה-loop back : 127.0.0.1.
שורה 5, שורה 10: אם הפרמטרים שמוקלדים אינם תקינים, מודפסת הודעת שגיאה.
שורות 2-3: ה-client מדפיס את הקלט ואת התשובה שקיבל מה-server.
אין תגובות:
הוסף רשומת תגובה