שקעים 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;

  }