שקעים Sockets
שיטת העברת מידע ע"י sockets הפכה לשיטה
סטנדרטית למדי למימוש העברת מידע באפליקציות רב תהליכיות מסוימות, אולי מפני
שבניגוד לאמצעים הקודמים שראינו, sockets מאפשר גם להעביר מידע בין תהליכים על מחשבים נפרדים ,
כלומר מאפשר לממש חישוב מבוזר.
האמצעי socket מבוסס על פרוטוקולי
תקשורת. בכדי להשתמש בהם צריך קצת להתמצא בזה.
האמצעי הזה
מבדיל בין תהליכים שהם שרתים (servers)
לבין תהליכים שהם לקוחות (clients). תהליכי שרת מקבלים מידע מתהליכים שהם אינם
מכירים, אבל תהליכי לקוחות חייבים לדעת בדיוק מיהו תהליך השרת שהם פונים אליו.
מודלים
של תקשורת
ישנם שני מודלים בסיסיים של העברת מידע על פני
רשת תקשורת, שתקשורת באמצעות sockets הוא
חלק ממנו. מודל אחד המודל המבוסס על
התחברות connection oriented model
ומודל המותר על התחברות connectionless
oriented model.
המודל המבוסס על התחברות הוא משהו כמו שיחת
טלפון: שני הגורמים המתקשרים ביניהם מנהלים למעשה תקשורת מתואמת, דו כיוונית גם
כאשר המידע מועבר רק בכיוון אחד. התהליך השולח אינו מעביר מידע מבלי שקודם יווצר
חיבור עם התהליך המקבל והוא יודע על המקום שהמידע שהוא שולח מגיע. תקשורת במודל הזה משתמשים בפרוטוקול הנקרא TCP – Transmission Control Protocol. רק במודל הזה אפשר להעביר מידע כזרם לא מפורמט
של מידע.
המודל המוותר על התחברות הוא משהו כמו דואר
אלקטרוני או שליחת הודעות: התהליך המעביר מידע
שולח חבילות מידע מבלי לתאם את זה מראש עם התהליך המקבל ולמען האמת אינו יודע
בוודאות שהתהליך המקבל יקרא את המידע. תקשורת במודל הזה משתמשים בפרוטוקול הנקרא UDP – User Datagram Protocol.
השימוש ב-socket עובר מספר שלבים.
השלב
הראשון הוא להתביית על
כתובת IP. כתובות IP מצינות את כתובת של המחשב המחובר לאינטרנט, כיום הוא מהפורמט של 4
מספרים עשרוניים בני עד שלוש ספרות עשרוניות לכל היותר עם נקודות ביניהם למשל
195.65.43.154 . ב-Unix
תוכנית הרצה רק על המחשב הנוכחי יכולה להשתמש ב- IP 127.0.0.1. בכדי שקוד המערכת תוכל להתביית על IP מסוים, היא צריכה להתמיר את ה-IP לפורמט מסוים. בדרך כלל התוכניתן
לא צריך להיות מודע לפרטים הטכניים של המימוש.
על מנת להגדיר משתנה המכיל IP
בפורמט הנכון על התוכניתן להכיל את קובץ
ה-header
#include <arpa/inet.h>
להגדיר משתנה מסוג
in_addr_t ולהציב לתוכו ערך
מותמר ע"י קריאה לפונקציה inet_addr שההכרזה עליה הינה
in_addr_t
inet_addr(const char *ip_address);
שימוש אופייני
לאמצעי הזה הינו
in_addr_t server;
…….
server =
inet_addr("195.65.43.154");
כאשר תוכנית
מעוניינת להתייחס אל המחשב שהיא רצה עליו ב-header file בשם <netinet/in.h>
מוגדר קבוע סמלי INADDR_ANY החוסך מהמתכנת להתיחס לכתובת המקומית ולכתוב תוכני שתרוץ על כל מחשב.
ערוצים ports
כאשר רוצים להעביר
מידע בין תוכניות באמצעי הזה צריך לבודד את התוכנית בתוך המחשב. כאן הדבר נעשה ע"י מספרי ערוץ port number. תוכנית הנחשבת לשרת להקשיב למספר port במחשב שעליו הוא רץ ואילו תוכנית
לקוח חייב לפנות ליעד שהוא שילוב של כתובת מחשב וגם מספר port.
מספרי ה-port עד 1024 הם בחזקת מספרים שמורים
למערכת. חלק מהם מוקצים למטרות סטנדרטיות ידועות, למשל 22 מוקצה ל-ssh (מסך עבודה מוצפן), 25 ל-smtp (העברת דואר אלקטרוני), 21 ל-ftp (העברת קבצים בין מחשבים) וכו'.
ממשק ה-Socket
על מנת לשמר
מידע על כתובת מחשב ומספר port,
קיימת הגדרה של רשומה סטנדרטית בתוך קובץ ה-header
<sys/socket.h>:
struct sockaddr {
sa_family_t sa_family; /* saddr
family */
char sa_data[]; /*socket address */
};
הרשומה הזו
נקראת רשומה גנרית משום שסוגים שונים של sockets נמצאים בשימוש אם מדובר בין תקשורת בין תהליכים באותו מחשב לבין sockets המתקשרים בין תהליכים על
מחשבים שונים.
הגדרה של
רשומה של socket המשמשת ספציפית להעברת מידע
במסגרת רשת תקשורת נמצאת ב-<netinet/in.h>:
struct sockaddr_in{
sa_family_t sin_family; /* internet
address family */
in_port_t sin_port; /* port number */
struct in_addr sin_addr; /* holds the IP address */
unsigned char sin_zero[8]; /* filling */
};
נקודות
קצה להעברה transport
end point
לכל סוגי
העברת המידע הן תהליך השרת וכן תהליך הלקוח חייבים לקבוע לעצמם נקודות קצה. אלה הם מזהים המממשים את החיבור בין שני
התהליכים. יצירה של נקודת קצה נעשה
ע"י קריאת המערכת socket:
#include
<sys/socket.h>
int socket(int domain, int type, int
protocol);
הפרמטר domain מציין היכן ישמש ה-socket. לדוגמא, אם מציבים לתוכו את הקבוע הסמלי AF_NET ה-socket ישמש על פני הרשת ואם מציבים את
הקבוע הסמלי AF_UNIX ה-socket ישמש לתקשורת פנימית
במחשב.
הפרמטר ה-type קובע איזה משני
מודלי התקשורת (TCP עם התחברות או UDB בלי) ה-socket ישתמש. הצבה של הקבוע
הסמלי SOCK_STREAM
לפרמטר הזה יקבע את ה-socket לפי
המודל הכולל התחברות (TCP ) והצבה של הקבוע הסמלי SOCK_DGRAM יקבע את המודל ללא התחברות (UDB).
הפרמטר protocol מציין איזה פרוטוקול
משתמשים. על פי רוב הערך המוצב שם יהיה 0,
ואז המערכת תשתמש בפרוטוקול ברירת המחדל – TCP או UDB
בהתאם לערך של הפרמטר type.
מימוש
קישור bind
#include
<sys/socket.h>
#include <sys/types.h>
int bind(int sockfd, const struct sockaddr
*address, size_t add_len);
קריאת המערכת bind מקשרת בין מספר ה-file descriptor של ה-socket לבין כתובת הרשת האמיתי.
הפרמטר sockfd הוא
המספר השלם המזוהה עם ה-socket, הפרמטר
address הוא מצביע לרשומה המכילה
את המידע על כתובת הרשת. המצביע הוא מסוג הרשומה הגנרית, אבל המידע בפועל עשוי
להיות רשומה מסוג אחר. לשם כך קיים הפרמטר
add_len המכיל את האורך של הרשומה
האמיתית, שעשויה להיות ארוכה יותר מהגנרי. כאשר מעבירים את הפוינטר בפועל צריך לעשות casting אם רוצים להימנע מלפחות
אזהרות של הקומפילר.
סגירת socket
הדבר בדרך כלל
נעשה ע"י הרוטינה close ל-file descriptor.
ניסיון לקרוא מ-socket
סגור יחזיר הודאת שגיאה של סוף הערוץ כמו בניסיון לקרוא מעבר לסוף קובץ. ניסיון לכתוב ל-socket סגור ע"י send או write יגרום לסיגנל SIGPIPE בדומה לכתיבה ל-pipe סגור (ועשוי להעיף את התהליך
הכותב). ביצוע close על socket במודל המחובר מבטיח הגעת
כל המידע שנכתב ליעד וחזרה מ-close
עשוי להתעקב עד שהמידע יקלט ביעד. במוגל
ללא התחברות ה-socket
נסגר מיד.
קריאות
מערכת למודל מבוסס התחברות (connected)
אתחול
האזנה listen
#include
<sys/socket.h>
int listen(int sockfd, int queue_size);
קריאה ל-listen יוצר מצב שבו התהליך יכול
להגיב למידע המגיע ל-socket
(ע"י קריאה לקריאות מערכת נוספות accept ו-recv).
הפרמטר sockfd הוא
כמו קודם. הפרמטר queue_size מאפשר לקורא להנחות את ה-socket לצבור מספר בקשות התחברות. הסטנדרד אינו מחויב לצבירה של יותר מחמש בקשות.
ביצוע
קבלה accept
#include
<sys/socket.h>
#include <sys/types.h>
int accept(int sockfd, struct sockaddr
*address, size_t *add_len);
קריאת
המערכת accept
גורמת לתהליך המאזין ל-socket
להמתין עד שבקשת התחברות תגיע. כאשר הבקשה
מגיעה, accept
מחזיר כתוצאת פונקציה שלם שהוא ה-file
descriptor המשמש לשליחת מידע מ ואל המבקש.
השלם sockfd כמו קודם, ואילו שני הפרמטרים הבאים מאפשרים לתהליך המבצע accept את רשומ הפרטים (כתובת
אינטרנט, מספר port
וכו') של התהליך המבקש התחברות. מאחר
ותהליך השרת בדרך כלל לא צריך את המידע הזה, ניתן לותר על המידע ע"י העברת
ערך פרמטר NULL
בשני הפרמטרים. אם מעבירים כתובות של
רשומה ושלם, אזי *add_len צריך להכיל את גודל
הרשומה שאליה המערכת יכולה לכתוב מידע, ואחרי החזרה מספר הבתים שנכתבו בפועל.
ביצוע
פעולת connect
במודל TCP בעוד שתהליך השרת מתחבר ללקוח דרך ה-socket ע"י פעולת accept הלקוח מתחבר לשרת
ע"י פעולת connect.
#include
<sys/socket.h>
#include <sys/types.h>
int connect(int csockfd, struct sockaddr
*address, size_t *add_len);
הפוינטר address מצביע לרשומה המכילה את
פרטי ה-socket של
השרת, ו-add_len הוא
הגודל האמיתי של הרשומה הזו בבתים. הפרמטר
csockfd הוא המזהה של ה-socket של הלקוח.
תהליך הלקוח
נעצר עד שתהליך השרת מתחבר לפנייתו.
קבלת
מידע recv
#include <sys/socket.h>
#include <sys/types.h>
int recv(int sockfd, void *buffer, size_t length, int flags);
recv
קולט את המידע בפועל, מנחה את המערכת לכתוב את המידע החל מכתובת buffer הערוך לקלוט עד length בתים. כתוצאת פונקציה הוא
מחזיר את מספר הבתים שהגיעו בפועל.
הפרמטר
האחרון flags מאפשר (אם לא
מותרים עליו ע"י ערך אפס) להנחות את צורת קבלת הנתונים. ערכים אפשריים ל-flags הם:
MSG_PEEK – קבל את המידע מבלי באופן "רשמי" ל"קרוא" אותו.
MSG_OOB-
התעלם ממידע רגיל וקבל רק מידע מיוחד מסוגים מסוימים כמו מידע על פסיקה. לא ניכנס
לזה כאן
MSG_WAITALL – חכה עד שכל המידע מגיע.
שליחת
מידע send
#include <sys/socket.h>
#include <sys/types.h>
int send(int sockfd, void *buffer, size_t length, int flags);
קריאת המערכת send מנחה את המערכת להעתיק length בתים החל מכתובת buffer ולשלוח אותם ל-sockfd. תוצאת הפונקציה הוא 1 או 1- בהתאם להצלחה או
כשלון. גם כאן הפרמטר flags מאפשר להנחות את המערכת
(ויתור ע"י אפס). ערכים אפשריים הם:
MSG_OOB – שלח רק מידע מיוחד.
MSG_DONTROUTE – התעלם מפרוטוקולי ניתוב ושלח את המידע ישירות ליעד (או הנתיב הישיר
ביותר). נושא הקשור לרשתות תקשורת שלא
ניכנס אליו כאן.
קריאות
מערכת למודל מבוסס ללא התחברות connectionless))
המודל ללא
התחברות אינו משתמש בקריאות המערכת accept,listen ו- connect.
קבלה ושליחת מידע נעשה ע"י קריאות מערכת sendto ו-ecvfrom כאשר recv הוא למעשה מקרה פרטי של recvfrom.
קבלת
מידע recvfrom
#include <sys/socket.h>
#include <sys/types.h>
int recvfrom(int sockfd, void *buffer, size_t length, int flags, const
struct sockaddr *address, size_t *add_len);
recvfrom הוא כמו recv רק
אם תוספת של פרמטרים address ו-add_len שיש להם את התפקיד של
להעביר למשתנים של הץוכנית הקוראת רשומה עם פרטי השולח וגודל הרשומה כמו ב-accept. אם ערך הפרמטר address הוא NULL אזי recvfrom מתנהג בדיוק כמו recv.
יש צורך
בפרמטרים הנוספים כי במודל הזה לא עושים accept.
שליחת
מידע sendto
#include <sys/socket.h>
#include <sys/types.h>
int sendto(int sockfd, void *buffer, size_t length, int flags, const
struct sockaddr *address, size_t add_len);
קריאת המערכת send היא כמו send בתוספת פרמטרים address ו-add_len בדומה ל-connect.
יש צורך בהם משום שבמודל הזה לא עושים connect.
תכנות
המודל מבוסס התחברות (connected)
מהלך הריצה של התוכנית של השרת הוא בדרך כלל
לפי האב טיפוס הבא:
קרא ל-socket בכדי ליצור נקודת קצה לשרת
קרא ל- bind בכדי לקשר את תוכנית השרת לנקודת
הקצה של התוכנית
החל listen למידע נכנס
הכנס ללולאה:
בצע accept למידע נכנס
צור תהליך חדש (fork) שיקבל, יטפל במידע חדש וישלח
אותו חזרה ללקוח: יקרא ל-recv בשביל לקבל את הקלט, יתמיר אותו וישלח אותו
חזרה ע"י קריאה ל-send
מהלך הריצה של
הלקוח הוא בעיקרו:
קרא
ל- connect בכדי להתחבר לשרת
קרא ל-send לשלוח מידע לשרת
קרא ל-recv בשביל לקבל את התשובה מהשרת.
כאן נראה
תוכנית שבו תפקידו של השרת הוא לקבל טקסט באנגלית, להתמיר את תוכנו לאותיות
גדולות, ולשלוח חזרה ללקוח. את תוכנית
השרת פשוט מרצים ברקע. להלן תסריט אפשרי:
% cc sock_server.c -o server
% ./server &
[1]
16359
%
cc sock_client.c -o client
%
./client
Original message = Hello World!
Returned message = HELLO WORLD!
%
תוכנית
השרת:
/*
sock_server.c */
#include
<sys/types.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#include
<arpa/inet.h>
#include
<stdio.h>
#include
<stdlib.h>
#include
<strings.h>
#include
<signal.h>
int32_t sockfd;
void catcher(int sig)
{
close(sockfd);
exit(0);
}
/* catcher */
int main()
{
char c;
struct sockaddr_in server ={AF_INET, 7000,
INADDR_ANY};
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if( sockfd == -1)
{
printf("can not create
socket");
exit(-1);
} /* if */
signal(SIGPIPE, catcher);
if( bind(sockfd,(struct sockaddr*)
&server,
sizeof(server)) == -1)
{
perror("bind");
exit(1);
} /* if */
if(listen(sockfd, 10) == -1)
{
perror("listen");
exit(1);
}/* if */
for(; ;)
{
int32_t newsockfd = accept(sockfd, NULL,
NULL);
if (newsockfd < 0)
{
perror("accept");
exit(1);
} /* if */
// perform read write operations ...
if ( fork() == 0)
{
while( recv(newsockfd, &c, 1, 0)
> 0)
{
c = toupper(c);
send(newsockfd, &c, 1, 0);
} /* while */
/* if the client is no longer sending
information,
the socket can be closed and the
child process
terminated */
close(newsockfd);
exit(0);
}/* if */
}
return 0;
}
תוכנית
הלקוח:
/*
sock_client.c */
#include
<sys/types.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#include
<arpa/inet.h>
#include
<stdio.h>
#include
<stdlib.h>
#include
<strings.h>
int main()
{
int32_t sockfd;
char str[64] = "Hello World!";
char str1[64];
struct sockaddr_in server = {AF_INET,
7000};
int i;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
perror("socket");
exit(1);
} /* if */
server.sin_addr.s_addr =
inet_addr("127.0.0.1");
if(connect(sockfd,(struct sockaddr*)
&server,
sizeof(server)) == -1)
{
perror("connect");
exit(1);
}
// perform read write operations ...
i = 0;
do {
send(sockfd, &str[i], 1, 0);
if ( recv(sockfd, &str1[i], 1, 0)
<= 0)
{
fprintf(stderr, "\nServer
has died\n");
exit(1);
} /* if */
i++;
}
while (str[i-1] != '\0');
close(sockfd);
printf("Original message = %s\n",
str);
printf("Returned message = %s\n",
str1);
return 0;
} /* main */
תכנות
המודל ללא התחברות (connectionless)
מהלך הריצה של התוכנית של השרת הוא בדרך כלל
לפי האב טיפוס הבא:
מהלך השרת
בעיקרו:
בקש socket
בצע לו bind
כנס ללולאה
איסופית:
קרא
ל-recvfrom. אם יצאת ממנו באופן תקין
סימן שנשלח לך מידע,
אותו קיבלת יחד עם פרטי ה-socket של השולח.
בצע
את המוטל עליך עם מידע הקלט.
קרא ל-sendto על מנת לשלוח את התשובה שלך למי
ששלח לך מידע.
מהלך הלקוח בעיקרו:
בקש לעצמך socket
בצע לו bind
קרא ל-sendto לשלוח מידע ל-socket של השרת תוך שאתה מעביר
לו
את פרטי ה-socket שלך.
קרא ל-recv או recvfrom על מנת לקבל את התשובה.
ההבדל הבולט
ביותר בין שני המודלים של העברת אינפורמציה הינו שבמודל מבוסס התחברות המידע הוא
דו-כיווני על socket
יחיד (זה של השרת בעצם) בעוד שבמודל ללא התחברות לשרת וללקוח יש כל אחד socket משלו. המידע לשרת מגיע דרך ה-socket של השרת והמידע ללקוח
מגיע דרך ה-socket של
הלקוח.
להלן תוכנית
דוגמא שעושה אותו דבר כמו תוכנית הדוגמא הקודמת רק במודל חסר התחברות.
תסריט אפשרי:
%
cc sock_server2.c -o server2
% cc sock_client2.c -o client2
%
./server2 &
[1]
16432
%
./client2
Original message = Hello World!
Returned message = HELLO WORLD!
%
תוכנית
השרת:
/*
sock_server2.c */
#include
<sys/types.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#include
<arpa/inet.h>
#include
<stdio.h>
#include
<stdlib.h>
#include
<strings.h>
#define
SIZE sizeof(struct sockaddr_in)
int main()
{
struct sockaddr_in server = {AF_INET,
7000,INADDR_ANY};
int32_t sockfd;
char c;
struct sockaddr_in client;
int client_ln = SIZE;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(
sockfd == -1)
{
printf("can not create
socket");
exit(-1);
} /* if */
if( bind(sockfd,(struct sockaddr*)
&server,
SIZE) == -1)
{
perror("bind");
exit(1);
} /* if */
for(; ;)
{
// perform read write operations ...
if( recvfrom(sockfd, &c, 1, 0,
(struct sockaddr *)&client,
&client_ln) == -1)
{
perror("recvfrom");
continue;
} /* if */
c = toupper(c);
if (sendto(sockfd, &c, 1, 0,
(struct sockaddr *) &client,
client_ln) ==
-1)
{
perror("sendto");
continue;
} /* if */
} /* for */
return 0;
} /* main */
תוכנית
הלקוח:
/*
sock_client2.c */
#include
<sys/types.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#include
<arpa/inet.h>
#include
<stdio.h>
#include
<stdlib.h>
#include
<strings.h>
int main()
{
int32_t sockfd;
char str[64] = "Hello World!";
char str1[64];
struct sockaddr_in server = { AF_INET,
7000};
struct sockaddr_in client = { AF_INET,
INADDR_ANY, INADDR_ANY};
int i;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
perror("socket");
exit(-1);
} /* if */
server.sin_addr.s_addr =
inet_addr("127.0.0.1");
if( bind(sockfd,(struct sockaddr*)
&client,
sizeof(client)) == -1)
{
perror("bind");
exit(1);
}
// perform read write operations ...
i = 0;
do {
if (sendto(sockfd, &str[i], 1,
0,(struct sockaddr*) &server,
sizeof(server))== -1)
{
perror("sendto");
continue;
} /* if */
if ( recv(sockfd, &str1[i], 1, 0)
<= 0)
{
fprintf(stderr, "\nServer
has died\n");
exit(1);
} /* if */
i++;
}
while (str[i-1] != '\0');
close(sockfd);
printf("Original message = %s\n",
str);
printf("Returned message = %s\n",
str1);
return 0;
}