ישנן מספר שיטות לתקשורת בין 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:
- /*
- * stream_server.c
- *
- * Created on: May 3, 2012
- */
- /*
- ** 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 <netdb.h>
- #include <arpa/inet.h>
- #include <sys/wait.h>
- #include <signal.h>
- #define PORT "3490" // the port users will be connecting to
- #define BACKLOG 10 // how many pending connections queue will hold
- void sigchld_handler(int s)
- {
- while(waitpid(-1, NULL, WNOHANG) > 0);
- }
- // get sockaddr, IPv4 or IPv6:
- void *get_in_addr(struct sockaddr *sa)
- {
- if (sa->sa_family == AF_INET) {
- return &(((struct sockaddr_in*)sa)->sin_addr);
- }
- return &(((struct sockaddr_in6*)sa)->sin6_addr);
- }
- int main(void)
- {
- int sockfd, new_fd; // listen on sock_fd, new connection on new_fd
- struct addrinfo hints, *servinfo, *p;
- struct sockaddr_storage their_addr; // connector's address information
- socklen_t sin_size;
- struct sigaction sa;
- int yes=1;
- char s[INET6_ADDRSTRLEN];
- int rv;
- memset(&hints, 0, sizeof hints);
- hints.ai_family = AF_UNSPEC;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_flags = AI_PASSIVE; // use my IP
- if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
- fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
- return 1;
- }
- // loop through all the results and bind to the first we can
- for(p = servinfo; p != NULL; p = p->ai_next) {
- if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
- 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);
- perror("server: socket");
- continue;
- }
- if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
- sizeof(int)) == -1) {
- perror("setsockopt");
- exit(1);
- }
- if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
- close(sockfd);
- perror("server: bind");
- continue;
- }
- 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);
- break;
- }
- if (p == NULL) {
- fprintf(stderr, "server: failed to bind\n");
- return 2;
- }
- freeaddrinfo(servinfo); // all done with this structure
- 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);
- }
- printf("server: waiting for connections...\n");
- while(1) { // main accept() loop
- sin_size = sizeof their_addr;
- new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
- if (new_fd == -1) {
- perror("accept");
- continue;
- }
- inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr),s, sizeof s);
- printf("server: got connection from %s\n", s);
- if (!fork()) { // this is the child process
- close(sockfd); // child doesn't need the listener
- if (send(new_fd, "Hello, world!", 13, 0) == -1)
- perror("send");
- close(new_fd);
- exit(0);
- }
- close(new_fd); // parent doesn't need this
- }
- return 0;
- }
בהדגש צהוב - 4 הפונקציות העיקריות בתהליך יצירת התקשורת ע"י ה-sever- ראו סכימת בלוקים למעלה:
- socket
- bind
- listen
- accept/
נסקור את ארבעתן, תוך כדי הסברים על שאר הפונקציות המודגשות בצבע.
socket
- 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 הנה הן כאן:
- hints.ai_family = AF_UNSPEC;
- hints.ai_socktype = SOCK_STREAM;
- 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
משם לקחתי את הדוגמא.
- /*
- * client.c
- *
- * Created on: May 3, 2012
- */
- #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>
- #include <arpa/inet.h>
- #define PORT "3490" // the port client will be connecting to
- #define MAXDATASIZE 100 // max number of bytes we can get at once
- // get sockaddr, IPv4 or IPv6:
- void *get_in_addr(struct sockaddr *sa)
- {
- if (sa->sa_family == AF_INET) {
- return &(((struct sockaddr_in*)sa)->sin_addr);
- }
- return &(((struct sockaddr_in6*)sa)->sin6_addr);
- }
- int main(int argc, char *argv[])
- {
- int sockfd, numbytes;
- char buf[MAXDATASIZE];
- struct addrinfo hints, *servinfo, *p;
- int rv;
- char s[INET6_ADDRSTRLEN];
- if (argc != 2) {
- fprintf(stderr,"usage: client hostname\n");
- exit(1);
- }
- memset(&hints, 0, sizeof hints);
- ints.ai_family = AF_UNSPEC;
- hints.ai_socktype = SOCK_STREAM;
- if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) {
- fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
- return 1;
- }
- // loop through all the results and connect to the first we can
- for(p = servinfo; p != NULL; p = p->ai_next) {
- if ((sockfd = socket(p->ai_family, p->ai_socktype,p->ai_protocol)) == -1) {
- perror("client: socket");
- continue;
- }
- if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
- close(sockfd);
- perror("client: connect");
- continue;
- }
- break;
- }
- if (p == NULL) {
- fprintf(stderr, "client: failed to connect\n");
- return 2;
- }
- inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
- s, sizeof s);
- printf("client: connecting to %s\n", s);
- freeaddrinfo(servinfo); // all done with this structure
- if ((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
- perror("recv");
- exit(1);
- }
- buf[numbytes] = '\0';
- printf("client: received '%s'\n",buf);
- close(sockfd);
- return 0;
- }
כאן נוכל לקצר בהסברים, היות שהפונקציות דומות לאלה שבהם השתמשנו עבור ה-server.
ההבדל - כפי שהשתקף בסכמת הבלוקים למעלה - כאן תהליך ההתחברות מופעל ע"י שתי פונקציות:
socket ו-connect. שתיהן מודגשות בצהוב.
את socket כבר ראינו ב-server.
הנה ה-prototype של connect:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
את ה-sockaddr וה-addrlen אנו מקבלים, כמו ב-server מפונקצית העזר getaddrinfo:
- 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.
אין תגובות:
הוסף רשומת תגובה