Οι υποδοχές ορίζονται στα πλαίσια ενός πεδίου (domain). Το πεδίο καθορίζει, πρώτον, το χώρο διεθύνσεων -άρα τo μονοσήμαντο όνομα μιας υποδοχής (στα όρια του πεδίου)- και, δεύτερον, το πρωτόκολλο (protocol) που χρησιμοποιεί η υποδοχή για την επικοινωνία. Επομένως μπορεί να επιτευχθεί επικοινωνία μόνο μεταξύ υποδοχών που ανήκουν στο ίδιο πεδίο. Οι τύποι πεδίων ορίζονται στη πρότυπη βιβλιοθήκη sys/socket.h, όπου βρίσκονται και τα πρωτότυπα των συναρτήσεων. Οι συχνότερα χρησιμοποιούμεννοι τύποι πεδίων είναι οι UNIX και Internet.
Το πεδίο UNIX παρέχει ένα χώρο διευθύνσεων σε ένα μεμονωμένο σύστημα UNIX/Linux. Οι υποδοχές UNIX ονομάζονται με βάση το όνομα διαδρομής (path) μέσα στο σύστημα. Το πεδίο UNIX δηλώνεται ως PF_UNIX (ή PF_LOCAL ή PF_FILE).
Το πεδίο Internet παρέχει ένα χώρο διεθύνσεων σε συστήματα δικτυωμένα μέσω του πρωτοκόλλου TCP/IP. Η ονομασία ακολουθεί τις γνωστές συβάσεις ονομασίας του Internet. Το πεδίο Internet δηλώνεται ως PF_INET.
Ο τύπος ορίζει τις ιδιότητες επικοινωνίας που είναι ορατές στην εφαρμογή που χρησιμοποιεί την υποδοχή. Επιτρέπεται επικοινωνία μεταξύ υποδοχών του αυτού τύπου. Ορισμένοι βασικοί τύποι υποδοχών:
Η συνάρτηση int socket(int domain, int type, int protocol)
καλείται από μια διεργασία για τη δημιουργία μιας υποδοχής τύπου type, στο πεδίο domain και με πρωτόκολλο protocol. Αν το πρωτόκολλο δεν οριστεί (τιμή 0), το σύστημα χρησιμοποιεί ένα προεπιλεγμένο πρωτόκολλο για το συγκεκριμένο τύπο. Η συνάρτηση επιστρέφει τον περιγραφέα της υποδοχής.
Μια άλλη διεργασία που θέλει να επικοινωνήσει με τη προηγούμενη δεν έχει τρόπο να ονομάσει την υποδοχή, μέχρις ότου η υποδοχή να λάβει διεύθυνση. Στο πεδίο UNIX η διεύθυνση συνήθως αποτελείται από ένα ή δύο ονόματα διαδρομής. Στο πεδίο Internet η διεύθυνση αποτελείται από ένα συνδυασμό IP διεύθυνσης και αριθμού θύρας (port).
Η συνάρτηση int bind(int s, const struct sockaddr *name, int namelen)
καλείται για να συνδέσει μια υποδοχή (με περιγραφέα s) με ένα όνομα διαδρομής ή μια IP διεύθυνση, δηλαδή το όνομα (που ορίζεται στη δομή sockadrr). Το μήκος του ονόματος δηλώνεται στο namelen.Υπάρχουν αρκετοί διαφορετικοί τρόποι κλήσης της συνάρτησης bind(), ανάλογα με τo πεδίο της υποδοχής. Αυτό που αλλάζει σε κάθε κλήση είναι ο ορισμός της δομής sockaddr.
#include <sys/socket.h>
...
bind (sd, (struct sockaddr *) &addr, length);
char
sa_data[14]
-- Το όνομα διαδρομής.
#include <sys/un.h>
...
bind (sd, (struct sockaddr_un *) &addr, length);
char sun_path[108]
-- Το όνομα
διαδρομής.
struct in_addr sin_addr
-- H IP
διεύθυνση. Ορίζεται με αριθμητική μορφή ή ως όνομα, με πολλές
παραλλαγές.
unsigned short int sin_port
-- Ο αριθμός θύρας (0 έως 65,535). Μπορεί να οριστεί και με όνομα.
Ακολουθούν ορισμένα παραδείγματα δημιουργίας και ονομασίας υποδοχών που εξηγούν τη χρήση των παραπάνω συναρτήσεων.
Δημιουργία υποδοχής στο πεδίο UNIX. Όπως βλέπουμε η δημιουργία υποδοχής ισοδυναμεί με δημιουργία αρχείου (ή σωλήνωσης). Η διαγραφή της υποδοχής γίνεται με unlink() ή rm (). Σημειώστε τη διαδικασία υπολογισμού του μήκους του ονόματος. Στο παράδειγμα γίνεται αρκετά αναλυτικά, θα μπορούσε να γίνει απλούστερα με τη μακροεντολή SUN_LEN() που ορίζεται στη βιβλιοθήκη sys/un.h.
#include <stddef.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
int make_named_socket (const char *filename)
{
struct sockaddr_un name;
int sock;
int size;
/* Create the socket. */
sock = socket (PF_UNIX, SOCK_DGRAM, 0);
if (sock < 0)
{
perror ("socket");
exit (EXIT_FAILURE);
}
/* Bind a name to the socket. */
name.sun_family = AF_LOCAL;
strncpy (name.sun_path, filename, sizeof (name.sun_path));
name.sun_path[sizeof (name.sun_path) - 1] = '\0';
/* The size of the address is the offset of the start of the filename,plus its length,
plus one for the terminating null byte. Alternatively you can just do: size = SUN_LEN (&name); */
size = (offsetof (struct sockaddr_un, sun_path) + strlen (name.sun_path) + 1);
if (bind (sock, (struct sockaddr *) &name, size) < 0)
{
perror ("bind");
exit (EXIT_FAILURE);
}
return sock;
}
Δημιουργία υποδοχής με αριθμητική IP διεύθυνση. Παρατηρούμε οτι σε αυτή τη περίπτωση η IP διεύθυνση sin_addr ορίζεται ως δομή με ένα μόνο πεδίο, το unsigned int s_addr (0 έως 2^32 - 1).
Υπάρχουν αρκετές μακροεντολές που περιλαμβάνουν προκαθορισμένες διευθύνσεις, όπως INADDR_LOOPBACK, INADDR_BROADCAST κλπ. Η μακροεντολή INADDR_ANY επιπτρέπει στο σύστημα να αποδώσει αυτό οποιαδήποτε εισερχόμενη IP διεύθυνση ώστε να επιτευχθεί σύνδεση με απομακρυσμένο υπολογιστή που αναμένει αίτημα σύνδεσης στη συγκεκριμένη θύρα.
Οι συναρτήσεις htons(), htonl() χρησιμποιούνται για να 'μεταφράσουν', αν είναι απαραίτητο, τη σειρά αποθήκευσης των bytes (byte order) ακεραίων (short και long) μεταξύ του οικείου υπολογιστή και του δικτύου (host to network). Αυτό γίνεται για να εξασφαλιστεί επικοινωνία μεταξύ 'big-endian' και 'long-endian' συστημάτων. Αντίστοιχα υπάρχουν και οι συναρτήσεις ntohs(), ntohl() (network to host).
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int make_socket (unsigned short int port)
{
int sock;
struct sockaddr_in name;
/* Create the socket. */
sock = socket (PF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror ("socket");
exit (EXIT_FAILURE);
}
/* Give the socket a name. */
name.sin_family = AF_INET;
name.sin_port = htons (port);
name.sin_addr.s_addr = htonl (INADDR_ANY);
if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
{
perror ("bind");
exit (EXIT_FAILURE);
}
return sock;
}
Δημιουργία υποδοχής με χρήση συναρτήσεων και δηλώσεων που συλλέγουν πληροφορίες για τον υπολογιστή μέ βάση το όνομα hostname. Το όνομα hostname χρησιμοποιείται ως κλειδή αναζήτησης πληροφοριών σχετικά με τον υπολογιστή σε μια 'βάση δεδομένων υπολογιστών' (host database). Η απλούστερη host database είναι το αρχείο /etc/hosts που διατηρεί κάθε σύστημα UNIX/Linux.
Η συνάρτηση gethostbyname() αναζητά πληροφορίες για τον υπολογιστή με το όνομα hostname στη βάση δεδομένων και τις αποθηκεύει στη δομή hostinfo. Η δομή είναι αρκετά σύνθετη και ορίζεται στη βιβλιοθήκη netinet.h/in.h. Από τη δομή hostinfo αντιγράφουμε στη δομή sockaddr την IP διεύθυνση του υπολογιστή.
Αντίστοιχη διαδικασία μπορεί να υλοποιηθεί και για τη τιμή port, η οποία θα μπορούσε να εξαχθεί από μια βάση δεδομένων υπηρεσιών (service database) έτσι ώστε η θύρα λαμβάνει τη τιμή που αντιστοιχεί στο όνομα της αντιστοιχης υπηρεσίας. Η συνηθισμένη βάση δεδομένων είναι το αρχείο /etc/services. Υπάρχουν πολλές και ενδιαφέρουσες συναρτήσεις και επιλογές σχετικές με το Internet, δείτε το εγχειρίδιο της βιβλιοθήκης GCC.
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
void init_sockaddr (struct sockaddr_in *name,
const char *hostname,
unsigned short int port)
{
struct hostent *hostinfo;
name->sin_family = AF_INET;
name->sin_port = htons (port);
hostinfo = gethostbyname (hostname);
if (hostinfo == NULL)
{
fprintf (stderr, "Unknown host %s.\n", hostname);
exit (EXIT_FAILURE);
}
name->sin_addr = *(struct in_addr *) hostinfo->h_addr;
}
Συνήθως η σύνδεση μέσω υποδοχών δεν είναι συμμετρική. Η μια διεργασία λειτουργεί ως διακομιστής και η άλλη ως πελάτης. Ο διακομιστής συνδέει την υποδοχή του σε προσυμφωνημένο όνομα διαδρομής ή θύρα. Στη συνέχεια αναμένει αιτήσεις επικοινωνίας άλλων διεργασιών μέσω της υποδοχής.
Στη περίπτωση υποδοχών ροής (SOCK_STREAM), ο διακομιστής καλεί τη συνάρτηση
int listen(int s, int backlog)
που καθορίζει τον περιγραφέα της υποδοχής s, και τον αριθμό συνδέσεων σε ουρά backlog.
Ο πελάτης συνδέεται στην υποδοχή του διακομιστή με τη συνάρτηση connect() που έχει σύνταξη και λειτουργία αντίστοιχη με τη συνάρτηση bind().
#include <sys/socket.h>
...
int connect(int s, (struct sockaddr *) &server, int namelen)
#include <sys/un.h>
...
connect (sd, (struct sockaddr_un *) &server, length);
#include <netinet/in.h>
...
connect (sd, (struct sockaddr_in *) &server, length);
Αν η υποδοχή του πελάτη δεν έχει ονομαστεί με προηγούμενη χρήση της συνάρτησης bind(),το σύστημα αυτόματα επιλέγει όνομα και ονομάζει την υποδοχή.
Στη περίπτωση που ο τύπος της υποδοχής είναι SOCK_STREAM ο διακομιστής απαντά στo αίτημα connect() με τη συνάρτηση accept() ώστε να ολοκληρωθεί η επικοινωνία.
Η συνάρτηση int accept(int s, struct sockaddr *addr, int *addrlen)
επιστρέφει ένα νέο περιγραφέα υποδοχής, που αναφέρεται μόνο στη συγκεκριμένη σύνδεση. Ένας διακομιστής μπορεί να έχει ταυτόχρονα πολλαπλές συνδέσεις SOCK_STREAM σε μια υποδοχή.
Υπάρχουν αρκετές συναρτήσεις για την μεταφορά δεδομένων σε υποδοχή SOCK_STREAM. Αυτές είναι οι write(), read(), int send(int s, const char *msg, int len, int flags), και int recv(int s, char *buf, int len, int flags). Οι send() και recv() είναι όμοιες με τις read() και write(), αλλά με περισσότερες επιλογές ελέγχου (παράμετρος flags)
Η παράμετρος flags διαμορφώνεται από το bitwise OR κάποιων από τα παρακάτω:
A SOCK_STREAM socket is discarded by calling close().
Μια υποδοχή datagram δεν απαιτεί τη πρότερη εγκαθίδρυση σύνδεσης. Κάθε μήνυμα περιέχει τη διεύθυνση προορισμού. Αν απαιτείται κάποια συγκεκριμένη τοπική διεύθυνση, πριν από κάθε μεταφορά δεδομένων πρέπει να υπάρχει μια κλήση bind(). Τα δεδομένα αποστέλονται με τις κλήσεις sendto() ή sendmsg(). Η συνάρτηση sendto() είναι ίδια με τη συνάρτηση send() με επιπλέον καθορισμό δίεύθυνσης. Η παραλαβή γίνεται με τις κλήσεις recvfrom() ή recvmsg(). Ενώ η συνάρτηση recv() απαιτεί ένα buffer για τα δεδομένα, η συνάρτηση recvfrom() απαιτεί δύο buffers, ένα για τα δεδομένα και ένα για τη διεύθυνση.
Οι υποδοχές datagram μπορούν να χρησιμοποιήσουν τη συνάρτηση connect() για σύνδεση με υποδοχή, σε συνδυασμό με τις συναρτήσεις. send() και recv() για τη μεταφορά.
Οι συναρτήσεις accept() και listen() δεν χρησιμοποιούνται με υποδοχές datagram.
Ακολουθούν αρκετά ζεύγη προγραμμάτων πελάτη - διακομιστή με χρήση υποδοχών.
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#define NSTRS 3 /* no. of strings */
#define ADDRESS "mysocket" /* addr to connect */
/*
* Strings we send to the client.
*/
char *strs[NSTRS] = {
"This is the first string from the server.\n",
"This is the second string from the server.\n",
"This is the third string from the server.\n"
};
main()
{
char c;
FILE *fp;
int fromlen;
register int i, s, ns, len;
struct sockaddr_un saun, fsaun;
/*
* Get a socket to work with. This socket will
* be in the UNIX domain, and will be a
* stream socket.
*/
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
perror("server: socket");
exit(1);
}
/*
* Create the address we will be binding to.
*/
saun.sun_family = AF_UNIX;
strcpy(saun.sun_path, ADDRESS);
/*
* Try to bind the address to the socket. We
* unlink the name first so that the bind won't
* fail.
*
* The third argument indicates the "length" of
* the structure, not just the length of the
* socket name.
*/
unlink(ADDRESS);
len = sizeof(saun.sun_family) + strlen(saun.sun_path);
if (bind(s, &saun, len) < 0) {
perror("server: bind");
exit(1);
}
/*
* Listen on the socket.
*/
if (listen(s, 5) < 0) {
perror("server: listen");
exit(1);
}
/*
* Accept connections. When we accept one, ns
* will be connected to the client. fsaun will
* contain the address of the client.
*/
if ((ns = accept(s, &fsaun, &fromlen)) < 0) {
perror("server: accept");
exit(1);
}
/*
* We'll use stdio for reading the socket.
*/
fp = fdopen(ns, "r");
/*
* First we send some strings to the client.
*/
for (i = 0; i < NSTRS; i++)
send(ns, strs[i], strlen(strs[i]), 0);
/*
* Then we read some strings from the client and
* print them out.
*/
for (i = 0; i < NSTRS; i++) {
while ((c = fgetc(fp)) != EOF) {
putchar(c);
if (c == '\n')
break;
}
}
/*
* We can simply use close() to terminate the
* connection, since we're done with both sides.
*/
close(s);
exit(0);
}
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#define NSTRS 3 /* no. of strings */
#define ADDRESS "mysocket" /* addr to connect */
/*
* Strings we send to the server.
*/
char *strs[NSTRS] = {
"This is the first string from the client.\n",
"This is the second string from the client.\n",
"This is the third string from the client.\n"
};
main()
{
char c;
FILE *fp;
register int i, s, len;
struct sockaddr_un saun;
/*
* Get a socket to work with. This socket will
* be in the UNIX domain, and will be a
* stream socket.
*/
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
perror("client: socket");
exit(1);
}
/*
* Create the address we will be connecting to.
*/
saun.sun_family = AF_UNIX;
strcpy(saun.sun_path, ADDRESS);
/*
* Try to connect to the address. For this to
* succeed, the server must already have bound
* this address, and must have issued a listen()
* request.
*
* The third argument indicates the "length" of
* the structure, not just the length of the
* socket name.
*/
len = sizeof(saun.sun_family) + strlen(saun.sun_path);
if (connect(s, &saun, len) < 0) {
perror("client: connect");
exit(1);
}
/*
* We'll use stdio for reading
* the socket.
*/
fp = fdopen(s, "r");
/*
* First we read some strings from the server
* and print them out.
*/
for (i = 0; i < NSTRS; i++) {
while ((c = fgetc(fp)) != EOF) {
putchar(c);
if (c == '\n')
break;
}
}
/*
* Now we send some strings to the server.
*/
for (i = 0; i < NSTRS; i++)
send(s, strs[i], strlen(strs[i]), 0);
/*
* We can simply use close() to terminate the
* connection, since we're done with both sides.
*/
close(s);
exit(0);
}
Exercise 12776
Configure the above socket_server.c and socket_client.c programs for you system and compile and run them. You will need to set up socket ADDRESS definition.