יום חמישי

IPC - Sockets: Client-Server Example

ישנן מספר שיטות לתקשורת בין Processes. כאן נדגים תקשורת בעזרת Socket. נראה תקשורת בין Processes על אותו מעבד. בפוסט אחר נדגים Socket על לתקשורת על הרשת - השימוש זם יעשה ממש באותו API.
Socket הוא אמצעי לתקשורת בין Processes.

נתאר כאן תהליך התקשרות בין server  לבין client.

ה-server שבדוגמא מחכה בהאזנה לבקשת connect מ-client. הדיאגרמה הבאה מתארת את התהליכים המתבצעים ב-Server. 



התבליכים הנ"ל יודגמו בדוגמא שתוצג מיד. 4 המלבנים העליונים הם התהליכים הסטנדרטיים שמתבצעים ב-server. במימוש הזה ה-server "נתקע" על accept ומחכה ל-client שיתחבר. שתי הצורות התחתונות הם מימוש אופייני ל-server שמגיב לקריאות של הרבה clients: עבור כל client יוצרים process חדש עם fork, וחוזרים להאזנה ב-parent process.
התהליכים ב-client פשוטים יותר, והם מוצגים בדיאגרמה הבאה:


ה-client שולח בקשת connect. אם לא יצליח להתחבר ל-server - למשל אם ה-server עדיין לא רץ, הפעולה תכשל.

הדוגמא נלקחה מתוך:
 Beej's Guide to Network Programming Using Internet Sockets

ה-server  מאזין על פורט מספר 3490. כשהוא מזהה client, הוא מיצר עבורו process חדש בעזרת fork ושולח ל-client משם הודעת "Hello World"
הנה הקוד של ה-server ואחריו הקוד של ה-client:





  1. /*
  2.  * stream_server.c
  3.  *
  4.  *  Created on: May 3, 2012
  5.  */
  6. /*
  7. ** server.c -- a stream socket server demo
  8. */
  9. #include <stdio.h>
  10. #include <stdlib.h>
  11. #include <unistd.h>
  12. #include <errno.h>
  13. #include <string.h>
  14. #include <sys/types.h>
  15. #include <sys/socket.h>
  16. #include <netinet/in.h>
  17. #include <netdb.h>
  18. #include <arpa/inet.h>
  19. #include <sys/wait.h>
  20. #include <signal.h>
  21. #define PORT "3490" // the port users will be connecting to
  22. #define BACKLOG 10 // how many pending connections queue will hold

  23. void sigchld_handler(int s)
  24. {
  25. while(waitpid(-1, NULL, WNOHANG) > 0);
  26. }
  27. // get sockaddr, IPv4 or IPv6:
  28. void *get_in_addr(struct sockaddr *sa)
  29. {
  30. if (sa->sa_family == AF_INET) {
  31. return &(((struct sockaddr_in*)sa)->sin_addr);
  32. }
  33. return &(((struct sockaddr_in6*)sa)->sin6_addr);
  34. }
  35. int main(void)
  36. {
  37. int sockfd, new_fd; // listen on sock_fd, new connection on new_fd
  38. struct addrinfo hints, *servinfo, *p;
  39. struct sockaddr_storage their_addr; // connector's address information
  40. socklen_t sin_size;
  41. struct sigaction sa;
  42. int yes=1;
  43. char s[INET6_ADDRSTRLEN];
  44. int rv;
  45. memset(&hints, 0, sizeof hints);
  46. hints.ai_family = AF_UNSPEC;
  47. hints.ai_socktype = SOCK_STREAM;
  48. hints.ai_flags = AI_PASSIVE; // use my IP
  49. if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
  50. fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
  51. return 1;
  52. }
  53. // loop through all the results and bind to the first we can
  54. for(p = servinfo; p != NULL; p = p->ai_next) {
  55. if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
  56. printf("\n in loop! p->ai_addr=0x%x, p->ai_addrle=0x%x  p->ai_protocol=0x%x", p->ai_addr, p->ai_addrlen,  p->ai_protocol);
  57. perror("server: socket");
  58. continue;
  59. }
  60. if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
  61. sizeof(int)) == -1) {
  62. perror("setsockopt");
  63. exit(1);
  64. }
  65. if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
  66. close(sockfd);
  67. perror("server: bind");
  68. continue;
  69. }
  70. printf("\n p->ai_addr=0x%x, p->ai_addrle=0x%x  p->ai_protocol=0x%x", p->ai_addr, p->ai_addrlen,  p->ai_protocol);
  71. break;
  72. }
  73. if (p == NULL) {
  74. fprintf(stderr, "server: failed to bind\n");
  75. return 2;

  76. }
  77. freeaddrinfo(servinfo); // all done with this structure
  78. perror("listen");
  79. exit(1);
  80. }
  81. sa.sa_handler = sigchld_handler; // reap all dead processes
  82. sigemptyset(&sa.sa_mask);
  83. sa.sa_flags = SA_RESTART;
  84. if (sigaction(SIGCHLD, &sa, NULL) == -1) {
  85. perror("sigaction");
  86. exit(1);
  87. }
  88. printf("server: waiting for connections...\n");
  89. while(1) { // main accept() loop
  90. sin_size = sizeof their_addr;
  91. new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
  92. if (new_fd == -1) {
  93. perror("accept");
  94. continue;
  95. }
  96. inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr),s, sizeof s);
  97. printf("server: got connection from %s\n", s);
  98. if (!fork()) { // this is the child process
  99. close(sockfd); // child doesn't need the listener
  100. if (send(new_fd, "Hello, world!", 13, 0) == -1)
  101. perror("send");
  102. close(new_fd);
  103. exit(0);
  104. }
  105. close(new_fd); // parent doesn't need this
  106. }
  107. return 0;
  108. }

בהדגש צהוב - 4 הפונקציות העיקריות בתהליך יצירת התקשורת ע"י ה-sever- ראו סכימת בלוקים למעלה:
- socket
- bind
- listen
- accept/

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

 socket
שורה 56  - יצירת socket. הנה ה=prototype של socket:

int socket(int domain, int type, int protocol);

נסקור את הערכים האפשריים עבור כל אחד מ-3 הפרמטרים של socket:
domain (או בשם אחר - address family) יכול לקבל את הערכים הבאים - ציינתי כאן רק את הערכים הרלונטים לנו:
AF_INET - מערכת עם IPv4
AF_INET6 - מערכת עם IPv6
AF_UNSPEC - יכול להתאים לכל מערכת.
 type
SOCK_DGRAM -  שימוש ב-datagram. זה בד"כ אומר -UDP.
SOCK_STREAM - שימוש במערכת full duplex בד"כ - TCP.
SOC_SEQPACKET - מערכת full duplex sequntial, למשל SCTP.

protocol

0 או IPPROTO_IP - המערכת תבחר את הפרוטוקול הדיפולטיבי בהתאם ל-type, כך שעבור SOCK_STREAM הפרוטוקול שיבחר הוא IP_PROTO_TCP.
IPPROTO_TCP
IPPROTO_UDP
IPPROTO_RAW
IPPROTO_ICMP
IPPROTO_IPV6

זה היה ההסבר על שלושת הפרמטרים של הפונקציה socket, אבל במימוש הנ"ל בוצע קודם תהליך להכנת הפרמטרים הנ"ל בעזרת הפונקציה getaddrinfo


הסבר על getaddrinfo:
מכניסים לתוך הפונקציה פרמטרים, והיא כבר תכין את הפרמטרים הדרושים עבור socket.

getaddrinfo - שימו לב לשימוש ב-structure בשם servinfo לצורך הכנסת הפרמטרים ל-לפונקציה socket.
הנה שורה 50 בה הפונקציה מופעלת:
(rv = getaddrinfo(NULL, PORT, &hints, &servinfo)
הנה ה-prototype  שלה:


int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);


נסביר את הפרמטרים שלה:
node - לשים NULL כשמכינים פרמטרים ל-socket.
service - מספר הפורט או service name. אנחנו נשים תמיד את מספר הפורט.
struct addrinfp *hints - נתייחס כאן רק לחברים ב-addrinfo המעניינים אותנו. זה נמצא בשורות 47-49 הנה הן כאן:

  1. hints.ai_family = AF_UNSPEC;
  2. hints.ai_socktype = SOCK_STREAM;
  3. hints.ai_flags = AI_PASSIVE; // use my IP


.ai_family - כמו שדה ה-domain בפונקציה socket. ראה פרוט למעלה. יכול לקבל AF_INET, AF_INET6 ו-AF_UNSPEC, כמו ב- domain למעלה.
ai_socktype - כמו שדה ה-type של הפונקציה socket - ראו למעלה. עבור tcp נכניס את הערך SOCK_STREAM.
ai_flags - הערך AI_PASSIVE יגרום למערכת לשלוף את ה-IP המקומי.



 struct addrinfo **res  - כאן מתקבלת התוצאה. נראה שהתוצאה מסודרת כרשימה מקושרת של structures מסוג  addrinfo. כל strucutre כזה מייצג כתובת network שמתאימה לפרמטרים node ו-service. הלופ בשורה 56 רץ על פני ה-res  ומנסה להפעיל את socket.


bind
שורה 66: 
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {


זו הפעולה של חיבור בין ה-socket לבין ה-port במחשב הלוקאלי.

הנה ה-prototype של bind:
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

הפרמטר הראשון הוא ה-descriptor שמתקבל מהפונקציה socket.
שני הפרמטרים האחרים, מתקבלים מתוך ה-addrinfo struct שיצרנו עם הפונקציה getaddrinfo.


listen
שורה 80:
if (listen(sockfd, BACKLOG) == -1) {
ה-prototype של listen:

int listen(int sockfd, int backlog);


כאשר sockfd הוא ה-descriptor שמתקבל מתוך הפונקציה socket,
ו-backlog הוא  מספר ה-connections המקסימלי שיאופשר. הגדרנו כאן 10 - שורה 22.

הפונקציה listen מתחילה תהליך האזנה ל-clients שמבקשים להתחבר. אחריה מופעלת accept.

accept

שורה 95: 
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);


הנה ה-prototype שלה:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);


sockfd - כרגיל, ה-descriptor שמחזירה socket.
addr - זהו ה-struct אליו יכנסו הנתונים של ה-host שהתחבר. נראה מיד את השימוש בהם.
addrlen - הגודל של struct addr.


accept "נתקעת" עד שנוצר connection עם client כלשהו.
לגבי ה-struct sockaddrr *addr:
לכאן כאמור יכנסו נתוני ההתחברות של ה-client ושולפים אותם עם inet_ntop.

שורה 99:
inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr),s, sizeof s);

זוהי  פונקציה שממירה את ה-structure ל-string.
ה-prototype שלה:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);


הסבר הפרמטרים: 
af - זהו ה-address family , מקבל את הערכים:
AF_INET - מערכת עם IPv4
AF_INET6 - מערכת עם IPv6
נמצא ב- their_addr.ss_family (ראה שורה 99...)
*src - הכתובת מתוך ה-structure במקרה שלנו, שליפת הכתובת מתוך- their_address.
*dst - הכתובת של ה-destination string. נכניס את ה-string ל-buffer בשם s.
size - הגודל של ה-destination string.



-> סיימנו עם 4 הפונקציות של ה-sever, בעזרתן יצרנו את התקשורת. 
כעת, ברגע שנוצר חיבור עם client נצור עבורו process עצמאי ע"י fork. ומתוך ה-child process נשלח הודעה ל-client ע"י send:
if (send(new_fd, "Hello, world!", 13, 0) == -1)


עד כאן מימוש ה-server. נעבור ל-client.

הנה הקוד של ה-client, אבל שוב אתן קרדיט ל 
 Beej's Guide to Network Programming Using Internet Sockets
משם לקחתי את הדוגמא.





  1. /*
  2.  * client.c
  3.  *
  4.  *  Created on: May 3, 2012

  5.  */

  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <unistd.h>
  9. #include <errno.h>
  10. #include <string.h>
  11. #include <netdb.h>
  12. #include <sys/types.h>
  13. #include <netinet/in.h>
  14. #include <sys/socket.h>
  15. #include <arpa/inet.h>
  16. #define PORT "3490" // the port client will be connecting to
  17. #define MAXDATASIZE 100 // max number of bytes we can get at once
  18. // get sockaddr, IPv4 or IPv6:
  19. void *get_in_addr(struct sockaddr *sa)
  20. {
  21. if (sa->sa_family == AF_INET) {
  22. return &(((struct sockaddr_in*)sa)->sin_addr);
  23. }
  24. return &(((struct sockaddr_in6*)sa)->sin6_addr);
  25. }
  26. int main(int argc, char *argv[])
  27. {
  28. int sockfd, numbytes;
  29. char buf[MAXDATASIZE];
  30. struct addrinfo hints, *servinfo, *p;
  31. int rv;
  32. char s[INET6_ADDRSTRLEN];
  33. if (argc != 2) {
  34. fprintf(stderr,"usage: client hostname\n");
  35. exit(1);
  36. }
  37. memset(&hints, 0, sizeof hints);
  38.         ints.ai_family = AF_UNSPEC;
  39. hints.ai_socktype = SOCK_STREAM;
  40. if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) {
  41. fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
  42. return 1;
  43. }
  44. // loop through all the results and connect to the first we can
  45. for(p = servinfo; p != NULL; p = p->ai_next) {
  46. if ((sockfd = socket(p->ai_family, p->ai_socktype,p->ai_protocol)) == -1) {
  47. perror("client: socket");
  48. continue;
  49. }
  50. if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
  51. close(sockfd);
  52. perror("client: connect");
  53. continue;
  54. }
  55. break;
  56. }
  57. if (p == NULL) {
  58. fprintf(stderr, "client: failed to connect\n");
  59. return 2;
  60. }
  61. inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
  62. s, sizeof s);
  63. printf("client: connecting to %s\n", s);
  64. freeaddrinfo(servinfo); // all done with this structure
  65. if ((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
  66. perror("recv");
  67. exit(1);
  68. }
  69. buf[numbytes] = '\0';
  70. printf("client: received '%s'\n",buf);
  71. close(sockfd);
  72. return 0;
  73. }


כאן נוכל לקצר בהסברים, היות שהפונקציות דומות לאלה שבהם השתמשנו עבור ה-server.
ההבדל - כפי שהשתקף בסכמת הבלוקים למעלה - כאן תהליך ההתחברות מופעל ע"י שתי פונקציות:
socket ו-connect. שתיהן מודגשות בצהוב.
את socket כבר ראינו ב-server.
הנה ה-prototype של connect:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
את ה-sockaddr וה-addrlen אנו מקבלים, כמו ב-server מפונקצית העזר getaddrinfo:
  1. if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) {


נזכר שוב ב-prototype של getaddrinfo:

int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);

הפעם ה- *node הוא הכתובת של ה-server. הפונקציה שלנו מקבלת אותו כארגומנט (argv).
בניית ה-hints דומה לזו שראינו כבר.
עם ההתחברות, ה-clinet מוכן לפעולה!
שורה 67 - קליטת נתונים מה-socket. נקלו את ההודעה "Hello World" ששולח ה-server, ונדפיס אותה - שורות 72-73.

זאת היתה הדוגמא לתקשורת client/server עם TCP protocol.

אין תגובות:

הוסף רשומת תגובה