Κλήση Απομακρυσμένης Διαδικασίας (RPC)



Το κεφάλαιο αυτό είναι μια επισκόπιση της Κλήσης Απομακρυσμένης Διαδικασίας (Remote Procedure Call, RPC) και πιο συγκεκριμένα την μορφή Open Networl Computing (ONC) RPC της SUN.

Τί είναι το RPC

Το RPC είναι μια ισχυρή τεχνική για τη δημιουργία κατανεμημένων εφαρμογών πελάτη-διακομιστή. Βασίζεται στην ιδέα της συμβατικής, ή τοπικής, κλήσης διαδικασίας και την επεκτείνει ώστε η καλούμενη διαδικασία δεν χρειάζεται να βρίσκεται στον ίδιο χώρο διευθύνσεων με την καλούσα διαδικασία. Οι δύο διαδικασίες μπορεί να ανήκουν σε διεργασίες που βρίσκονται στον ίδιο υπολογιστή ή σε διαφορετικούς υπολογιστές συνδεδεμένους με δίκτυο. Με το RPC οι προγραμματιστές κατανεμημένων εφαρμογών μπορούν να αποφύγουν τις λεπτομέρειες της δικτυακής διεπαφής. Η ανεξαρτησία του RPC από το δίκτυο απομονώνει την εφαρμογή από τις φυσικές και λογικές λεπτομέρειες της επικοινωνίας δεδομένων.

Το RPC καθιστά το μοντέλο προγραμματισμού πελάτη/διακομιστή ισχυρότερο και ευκολότερο στην υλοποίηση. Σε makes the client/server model of computing more powerful and easier to program. Σε συνδυασμό με τον μεταγλωττιστή πρωτοκόλλων RPCGEN μπορούμε να δημιουργήσουμε πελάτες που καλούν απομακρυσμένες διαδικασίες μέσω της διεπαφής κλήσης τοπικών διαδικασιών.

Πώς Δουλεύει το RPC 

Το RPC είναι ανάλογο με τη κλήση μιας συνάτησης στη C/C++. Όπως και στη κλήση συνάστησης, έτσι και στο RPC, τα ορίσματα πρέπει να περάσουν στην απομακρυσμένη διαδικασία και η καλούσα συνάρτηση πρέπει να περιμένει την απόκριση της απομακρυσμένης διαδικασίας. Η Εικόνα που ακολουθεί δείχνει τη ροή της δραστηριότητας κατά την διάρκεια ενός RPC μεταξύ ενός πελάτη και ενός διακομιστή. Ο πελάτης καλεί τη διαδικασία και περιμένει (αναστολή εκτέλεσης) μέχρι να παραληφθεί η απάντηση ή να προκληθεί χρονοδιακοπή (timeout). Ο διακομιστής, όταν παραλάβει την αίτηση, καλεί μια ρουτίνα εξυπηρέτησης που εκτελεί τη διαδικασία και επιστρέφει το αποτέλεσμα στο διακομιστή. Στη συνέχεια, ο διακομιστής αποστέλει την απόκριση πίσω στο πελάτη. Ο πελάτης μπορεί να συνεχίσει μετά την ολοκλήρωση του  RPC. 

 

Ο Μηχανισμός Κλήσης Απομακρυσμένης Διαδικασίας (RPC).

Μια απομακρυσμένη διαδικασία ταυτοποιείται μοναδικά από μια τριπλέτα: (αριθμός προγράμματος, αριθμός έκδοσης, αριθμός διαδικασίας). Ο αριθμός προγράμματος ορίζει μια ομάδα απομακρυσμένων διαδικασιών, η κάθε μια με δικό της αριθμό. Το πρόγραμμα μπορεί να έχει πολλαπλές εκδόσεις. Κάθε έκδοση περιέχει το δικό της σύνολο διαδικασιών που μπορεί να κληθούν απομακρυσμένα. Με αυτό το τρόπο  μπορεί να έχουμε ταυτόχρονη ύπαρξη πολλών εκδόσεων της ίδιας RPC εφαρμογής.

Επομένως, όταν ο πελάτης θέλει να καλέσει μια απομακρυσμένη διαδικασία πρέπει να γνωρίζει την διεύθυνση του διακομιστή και αυτή τη τριπλέτα. Η πληροφορία αυτή μπορεί να βρεθεί με διάφορους τρόπους. Για παράδειγμα το αρχείο /etc/rpc δίνει τέτοιες γενικές πληροφορίες (όχι μόνο για το συγκεκριμένο σύστημα, κατ' αντιστοιχία με το αρχείο /etc/rpcservices):

$ cat /etc/rpc 
# This file contains user readable names that can be used in place of rpc
# program numbers.

portmapper 100000 portmap sunrpc
rstatd 100001 rstat rstat_svc rup perfmeter
rusersd 100002 rusers
nfs 100003 nfsprog
ypserv 100004 ypprog
mountd 100005 mount showmount
ypbind 100007
walld 100008 rwall shutdown
yppasswdd 100009 yppasswd
etherstatd 100010 etherstat
rquotad 100011 rquotaprog quota rquota
sprayd 100012 spray
...
sgi_fam 391002
ugidd 545580417
fypxfrd 600100069 freebsd-ypxfrd
bwnfsd 788585389

Οι υπηρεσίες (daemons) RPC που φαίνονται σε αυτή τη λίστα είναι οι επίσημα καταχωρημένες (γνωστές) υπηρεσίες RPC, πολλές από τις οποίες μπορείτε να εγκαταστήσετε στον υπολογιστή σας, με τη μορφή πελάτη ή διακομιστή (αν έχετε τα κατάλληλα δικαιώματα) μεταφορτώνοντας και εγκαθιστώντας τα αντίστοιχα διαθέσιμα πακέτα. Έτσι μπορείτε να δείτε τη λειτουργία μιας έτοιμης εφαρμογής RPC. Για παράδειγμα, η εφαρμογή RPC rusers αποτελείται από τo πέλατη (rusers) και το διακομιστή (rusersd) και επιτρέπει να εκτελούμε μια απομακρυσμένη εντολή users.

Η εντολή rpcinfo εμφανίζει τις εφαρμογές RPC που είναι καταχωρημένες στο συγκεκριμένο σύστημα, πχ:

$rpcinfo -p localhost
    program vers proto   port
    100000    2   tcp    111  portmapper
    100000    2   udp    111  portmapper
    100002    2   tcp  35938  rusersd
    100002    2   udp  35938  rusersd

Εδώ φαίνεται οτι στο συγκεκριμένο σύστημα έχει καταχωρηθεί (δηλαδή έχει εκτελεστεί) o δαίμονας ruserd, άρα το σύστημα μπορεί να απαντήσει σε σχετικά RPC αιτήματα.

H εντολή  rpcinfo -d <progmam> <vers> διαγράφει μια εφαρμογή που έχει καταχωρηθεί στο σύστημα (απαιτούνται δικαιώματα διαχειριστή).

Η δικτυακή υπηρεσία portmap πρέπει να έχει εγκατασταθεί στο διακομιστή πριν τη χρήση RPC. H portmap απεικονίζει τις εφαρμογές RPC του συστήματος σε θύρες TCP/IP. Ο πελάτης RPC απευθύνει ερώτημα στο portmap για να συνδέσει την εφαρμογή διακομιστή RPC με συγκεκριμένη θύρα. Η υπηρεσία portmap συνδέεται πάντα με συγκεκριμένη θύρα, ενώ η απεικόνιση των υπόλοιπων RPC υπηρεσιών μπορεί να μεταβάλλεται.

$ cat /etc/services | grep portmap
sunrpc 111/tcp portmapper # RPC 4.0 portmapper
sunrpc 111/udp portmapper
rpc2portmap 369/tcp
rpc2portmap 369/udp # Coda portmapper
To RPC είναι ανεξάρτητο από το TCP/IP, δηλαδή δεν υποθέτει κάποια συγκεκριμένη αξιόπιστη δικτυακή υποδομή. Γι' αυτό το λόγο, για παράδειγμα προβλέπει λειτουργίες χρονοδιακοπής στις υπηρεσίες του.  Παρ' όλα αυτά, με δεδομένη τη γενικευμένη χρήση του TCP/IP, οι περισσότερες υλοποιήσεις RPC αναφέρονται στο TCP/IP.

Το RPC προβλέπει επίσης την αναπαράσταση εξωτερικών δεδομένων (external data representation, XDR) που επιτρέπει την μεταβίβαση παραμέτρων και επιστροφή αποτελεσμάτων σε/από κλήσεις διαδικασιών που εκτελούνται σε διαφορετικά συστήματα. Αυτό επιτυγχάνεται με τη σειριοποίηση (serializing) και την αποσειριοποίηση (deserializing): τα δεδομένα μετατρέπονται ουσιαστικά σε byte strings τα οποία απο/ανα-συντίθενται από το πελάτη και το διακομιστή.

Ανάπτυξη Εφαρμογών RPC

Θεωρείστε το παρακάτω παράδειγμα:

Προσπέλαση μιας προσωπικής βάσης δεδομένων (ή αρχείου) σε ένα απομακρυσμένο υπολογιστή με το μοντέλο πελάτη-διακομιστή. Έστω οτι δεν μπορούμε να προσπελάσουμε το αρχείο μέσω κατανεμημένου συστήματος αρχείων (πχ NFS).

Η συνήθης λύση είναι η σύνδεση μέσω ενός απομακρυσμένου κελύφους και η εκτέλεση μιας εντολής, ενός σεναρίου ή προγράμματος.

Η εναλλακτική λύση με RPC έχει ως εξής:

Τα προγράμματα θα μεταγλωττιστούν ξεχωριστά. Το πρωτόκολλο επικοινωνίας επιτυγχάνεται μέσω ειδικών προγραμμάτων-στελεχών (stubs)  τα οποία πρέπει να συνδεθούν με τις εφραρμογές, όπως και οι απαραίτητες βιβλιοθήκες .

Kαθορισμός του Πρωτοκόλλου

Ο ευκολότερος τρόπος καθορισμού και υλοποίησης ενός πρωτοκόλλου είναι μέσω ενός μεταγλωττιστή πρωτοκόλλων, όπως το rpcgen. Το πρωτόκολλο πρέπει να 'γνωρίζει' τα ονόματα των διαδικασιών, οι τύποι δεδομένων των παραμέτρων και των ορισμάτων επιστροφής. Ο μεταγλωττιστής πρωτοκόλλων διαβάζει ένα αρχείο ορισμού και παράγει αυτόματα τα στελέχη / σκελετούς (stubs / skeletons) πελάτη και διακομιστή.

Το rpcgen χρησιμοποιεί δική του γλώσσα ορισμού (RPC language ή RPCL) που μοιάζει πολύ με οδηγίες προεπεξεργαστή. Το rpcgen εκτελείται ως αυτόνομος μεταγλωττιστής που διαβάζει αρχεία με κατάληξη .x.

Επομενως η μεταγλώττιση έχει ως εξής:

rpcgen rpcprog.x

Τυπικά παράγονται τέσσερα αρχεία:

Η αναπαράσταση εξωτερικών δεδομένων (external data representation, XDR) είναι μια λογική αφαίρεση που επιτρέπει την μεταφορά σύνθετων τύπων δεδομένων μέσω δικτύου και μεταξύ διαφορετικών συστημάτων.

Το rpcgen cμπορεί προαιρετικά να παράγει επιπλέον (δείτε τις σελίδε εγχειριδίου):

To rpcgeni μειώνει σημαντικά το χρόνο ανάπτυξης εφαρμογών RPC. Στη συνέχεια, ο προγραμματιστής απλά γράφει και συνδέει το κώδικα εφαρμογής πελάτη και διακομιστή. Επίσης, μπορεί αν θέλει να επέμβει για παραπέρα βελτιστοποίηση του κώδικα που έχει παραχθεί.

Κώδικας Εφαρμογής Πελάτη και Διακομιστή

Τώρα πρέπει να γράψουμε τον κώδικα της εφαρμογής του πελάτη και του διακομιστή, οι οποίοι πρέπει να επικοινωνούν με διαδικασίες και δεδομένα που καθορίζονται από το Πρωτόκολλο.

Η πλευρά του διακομιστή πρέπει να έχει 'φορτώσει' τις διαδικασίες που μπορεί να κληθούν από τον πελάτη και, στη συνέχεια να παραλάβει τα δεδομένα, να εκτελέσει τη κατάλληλη διαδικασία και να επιστρέψει τα αποτελέσματα.

Η πλευρά του πελάτη πρέπει να καλέσει την απομακρυσμένη διαδικασία, να αποστείλει τις παραμέτρους και να παραλάβει τα αποτελέσματα.

Μεταγλώττιση και εκτέλεση εφαρμογής

Έστω οτι η εφαρμογή του πελάτη λέγεται rpcprog.c, η εφαρμογή του διακομιστή rpcsvc.c και το πρωτόκολλο έχει οριστεί στο αρχείο rpcprog.x και το rpcgen παρήγαγε τα αρχεία που εξηγήθηκαν παραπάνω : rpcprog_clnt.c, rpcprog_svc.c, rpcprog_xdr.c, rpcprog.h.

Τα προγράμματα πελάτη και διακομιστή πρέπει να περιλαμβάνουν την οδηγία #include "rpcprog.h"

Τότε:

        rpcgen rpcprog.x -- παράγονται τα αρχεία pcprog_clnt.c, rpcprog_svc.c, rpcprog.h και, αν απαιτείται rpcprog_xdr.c

Τώρα απλά εκτελούμε τα προγράμματα rpcprog και rpcsvc στο πελάτη και διακομιστή αντίστοιχα. Πρώτα πρέπει οι διαδικασίες να καταχωρηθούν (registered) στον διακομιστή, δηλαδή να εκτελείται η υπηρεσία portmap. Η παραπάνω μεταγλώττιση μπορεί να υλοποιηθεί με makefiles.

Επισκόπιση των Ρουτινών Διεπαφής

To RPC έχει πολλαπλά επίπεδα διεπαφών εφαρμογής. Τα επίπεδα αυτά παρέχουν διαφοροποιημένο βαθμό ελέγχου της εφαρμογής και αντίστοιχα περίπλοκο κώδικα εφαρμογής. Οι ρουτίνες RPC χωρίζονται σε Απλοποιημένο Επίπεδο, Ανώτερο Επίπεδο, Ενδιάμεσο Επίπεδο, Έμπειρο Επίπεδο και Χαμηλό Επίπεδο. Η ενότητα αυτή παρέχει μια επισκόπηση των ρουτινών διεπαφής για κάθε επίπεδο.  Οι απαραίτητες δηλώσεις βιβλιοθήκης είναι βρίσκονται στο κατάλογο /usr/include/rpc.h :

#include <rpc/types.h>		/* some typedefs */
#include <netinet/in.h>

/* external data representation interfaces */
#include <rpc/xdr.h> /* generic (de)serializer */

/* Client side only authentication */
#include <rpc/auth.h> /* generic authenticator (client side) */

/* Client side (mostly) remote procedure call */
#include <rpc/clnt.h> /* generic rpc stuff */

/* semi-private protocol headers */
#include <rpc/rpc_msg.h> /* protocol for rpc messages */
#include <rpc/auth_unix.h> /* protocol for unix style cred */
#include <rpc/auth_des.h> /* protocol for des style cred */

/* Server side only remote procedure callee */
#include <rpc/svc.h> /* service manager and multiplexer */
#include <rpc/svc_auth.h> /* service side authenticator */

Στο κατάλογο usr/include/rpcsvc.h βρίσκονται οι δηλώσεις βιβλιοθήκης .x και .h για τις γνωστές υπηρεσίες RPC που μπορείτε να εγκαταστήσετε ως πελάτης ή διακομιστής (πχ rusers ή rusersd).

bootparam.h       mount.x         nis_tags.h  rstat.h     ypclnt.h
bootparam_prot.h  nfs_prot.h      nis.x       rstat.x     yp.h
bootparam_prot.x  nfs_prot.x      nlm_prot.h  rusers.h    yppasswd.h
key_prot.h        nis_callback.h  nlm_prot.x  rusers.x    yppasswd.x
key_prot.x        nis_callback.x  rex.h       sm_inter.h  yp_prot.h
klm_prot.h        nis.h           rex.x       sm_inter.x  ypupd.h
klm_prot.x        nislib.h        rquota.h    spray.h     yp.x
mount.h           nis_object.x    rquota.x    spray.x

Ρουτίνες Απλοποιημένου Επιπέδου 

Είναι οι ρουτίνες που χρησιμοποιούνται στις περισσότερες εφαρμογές.

rpc_reg() -- Εκτελείται από την εφαρμογή διακομιστή. Καταχωρεί μια διαδικασία στο διακομιστή για να μπορεί να χρησιμοποιηθεί σε RPC. Καθορίζει τη τριπλέτα των αριθμών της απομακρυσμένης διαδικασίας.

rpc_call() -- Εκτελείται από την εφαρμογή πελάτη. Καλεί την απομακρυσμένη διαδικασία σε δεδομένο διακομιστή.

rpc_broadcast() -- Εκπέμπει (broadcast) μήνυμα κλήσης σε ένα δεδομένο δίκτυο.

Ρουτίνες Ανώτερου Επιπέδου

Στο ανώτερο επίπεδο, η διεπαφή είναι επίσης απλή, αλλά ο προγραμματιστής πρέπει να δημιουργήσει ένα client handle και ένα server handle. Το επίπεδο αυτό είναι χρήσιμο όταν θέλουμε να αναπτύξουμε εφαρμογές RPC που μπορούν να εκτελούνται σε διαφορετικές δικτυακές υποδομές.

clnt_create() -- Γενικευμένη δημιουργία πελάτη. Το πρόγραμμα λέει στη συνάρτηση clnt_create() που βρίσκεται ο διακομιστής και το τύπο της δικτυακής υποδομής προς χρήση.

clnt_create_timed()-- Όμοια με τη clnt_create() αλλά επιτρέπει στο προγραμματιστή να καθορίσει το μέγιστο επιτρεπόπμενο χρόνο πριν από τη πρόκληση χρονοδιακοπής.

svc_create() -- Γενικευμένη δημιουργία διακομιστή για διάφορους τύπους δικτυακής υποδομής. Το πρόγραμμα λέει στη συνάρτηση svc_create() ποιά υποδομή θα χρησιμοποιηθεί.

clnt_call() -- Συνάρτηση εκτέλεσης RPC από το πελάτη προς το διακομιστή.

Ρουτίνες Ενδιαμέσου Επιπέδου

Οι ρουτίνες ενδιαμέσου επιπέδου επιτρέπουν τον έλεγχο επιπλέον λεπτομερειών. Τα προγράμματα είναι πιο περίπλοκα και εκτελούνται πιο αποδοτικά.

clnt_tp_create() -- Δημιουργία client handle για συγκεκριμένη δικτυακή υποδομή.

clnt_tp_create_timed() -- Όμοια με τη clnt_tp_create() αλλά με επιπλέον διαχείριση χρονοδιακοπής.

svc_tp_create() -- Δημιουργία server handle για συγκεκριμένη δικτυακή υποδομή.

clnt_call() -- Συνάρτηση εκτέλεσης RPC από το πελάτη προς το διακομιστή.

Ρουτίνες Εμπείρου Επιπέδου

Το έμπειρο επίπεδο περιέχει μεγαλύτερο σύνολο συναρτήσεων για τη διαχείριση παραμέτρων της δικτυακής υποδομής.

clnt_tli_create() -- Δημιουργία client handle για συγκεκριμένη δικτυακή υποδομή.

svc_tli_create() -- Δημιουργία server handle για συγκεκριμένη δικτυακή υποδομή.

rpcb_set() -- Απεικονίζει (συνδέει) ένα RPC service σε μια δικτυακή διεύθυνση.

rpcb_unset() -- Διαγράφει μια απεικόνιση που δημιουργήθηκε με τη rpcb_set().

rpcb_getaddr() -- Ανακαλεί τη δικτυακή διεύθυνση ενός RPC service.

svc_reg() -- Συσχετίζει ένα ζεύγος (πρόγραμμα, έκδοση)  με συγκεκριμένη συνάρτηση.

svc_unreg() -- Διαγράφει μια συσχέτιση που δημιουργήθηκε με τη svc_reg().

clnt_call() -- Συνάρτηση εκτέλεσης RPC από το πελάτη προς το διακομιστή.

Ρουτίνες Χαμηλού Επιπέδου

The bottom level contains routines used for full control of transport options.

clnt_dg_create() -- Δημιουργία client handle για connectionless δικτυακή υποδομή (datagram).

svc_dg_create() -- Δημιουργία server handle για connectionless δικτυακή υποδομή (datagram).

clnt_vc_create() -- Δημιουργία client handle για connection-oriented δικτυακή υποδομή (virtual circuit).

svc_vc_create() -- Δημιουργία client handle για connection-oriented δικτυακή υποδομή (virtual circuit).

clnt_call() -- Συνάρτηση εκτέλεσης RPC από το πελάτη προς το διακομιστή.


Ανάπτυξη εφαρμογών RPC με το rpcgen 

Η εφαρμογή rpcgen παρέχει στο προγραμματιστή ένα σχετικά απλό πλαίσιο για τη συγγραφή κατανεμημένων εφαρμογών. Οι εφαρμογές πελάτη και διακομιστή μπορούν να γραφούν ανεξάρτητα και να συνδεθούν με την υποδομή (στελέχη πελάτη και διακομιστή). Στη παρακάτω ενότητα αναλύονται ορισμένα παραδείγματα εφαρμογών που έχουν αναπτυχθεί με το rpcgen. Παραπέρα πληροφορίες μπορούν να βρεθούν στο εγχειρίδιο man rpcgen.

Μετατροπή Τοπικών Διαδικασιών σε Απομακρυσμένες

Έστω οτι μια εφαρμογή εκτελείται τοπικά σε ένα υπολογιστή και θέλουμε να την μετατρέψουμε σε κατανεμημένη, δηλαδή να εκτελείται σε απομακρυσμένο υπολογιστή. Το παράδειγμα που ακολουθεί δείχνει τη βαθμιαία μετατροπή ενός προγράμματος που γράφει ένα μήνυμα σε μια οθόνη τερματικού. 

Τοπική Εφαρμογή printmesg.c:

/* printmsg.c: print a message on the user onsole */
#include <stdio.h>
main(int argc, char *argv[])

{
char *message;
if (argc != 3) {
fprintf(stderr, "usage: %s <message> \n",argv[0]);
exit(1);
}
message = argv[1];
if (!printmessage(message)) {
fprintf(stderr,"%s: couldn¹t print your message\n",argv[0]);
exit(1);
}
printf("Message Delivered!\n");
exit(0);
}

/* Print a message to the console device: Return a boolean indicating whether
the message was actually printed. Note: before execution check the pathname
of your current console e.g. by executing who command */

printmessage(char *msg)

{
FILE *f;
f = fopen("/dev/pts/0", "w");
if (f == (FILE *)NULL) {
return (0);
}
fprintf(f, "%s\n", msg);
fclose(f);
return(1);
}

Για τοπική χρήση η μεταγλώττιση και εκτέλεση είναι ως εξής:

$ gcc printmsg.c -o printmsg
$ ./printmsg "Hello, there."
Hello, there.
Message delivered!
$

Αν η συνάρτηση printmessage() μετατραπεί σε απομακρυσμένη διαδικασία, μπορεί να κληθεί από οπουδήποτε στο δίκτυο. Τα βήματα μετατροπής είναι τα ακόλουθα:

Πρώτα, καθορίζουμε τους τύπους δεδομένων των παραμέτρων της διαδικασίας καθώς και του αποτελέσματος (επιστροφής). Οι παράμετροι της συνάρτησης printmessage() είναι ένα string, και το αποτέλεσμα ένας ακέραιος.

Τώρα μπορούμε να καθορίσουμε το πρωτόκολλο σε γλώσα RPC (RPCL) που περιγράφει την απομακρυσμένη έκδοση της διαδικασίας:

/* msg.x: Remote msg printing protocol */
program MESSAGEPROG {
version PRINTMESSAGEVERS {
int PRINTMESSAGE(string) = 1;
} = 1;
} = 0x20000001;

Οι απομακρυσμένες διαδικασίες πάντα δηλώνονται σαν τμήματα ενός απομακρυσμένου προγράμματος (program). Ο παραπάνω κώδικας δηλώνει ένα ολόκληρο πρόγραμμα που περιέχει μια μόνο διαδικασία, τη PRINTMESSAGE.

Στο παράδειγμα,

Μπορεί να υπάρχουν περισσότερες από μια εκδόσεις του προγράμματος, και ένα πρόγραμμα μπορεί να περιέχει περισσότερες διαδικασίες. Οι αριθμοί εκδόσεων αυξάνονται όταν αλλάζει η λειτουργικότητα του απομακρυσμένου προγράμματος. Οι διαδικασίες μπορεί να μεταβληθούν ή να προστεθούν νέες διαδικασίες.  

Σημείωση: τα ονόματα του προγράμματος και της διαδικασίας συνήθως γράφονται με κεφαλαία γράμματα (χωρίς αυτό να είναι απαραίτητο).  Ο τύπος της παραμέτρου δεν είναι char * αλλά string. Αυτό γιατί η δηλωση char * της C είναι ασαφής: δείκτης σε χαρακτήρα ή συμβολοσειρά; Η δήλωση string αναφέρεται καθαρά σε πίνακα χαρακτήρων που τερματίζεται με '\0'.

Τώρα πρέπει να γράψουμε τα δύο προγράμματα εφαρμογής:

Η διαδικασία μεταγλώττισης και εκτέλεσης έχει ως εξής:

απαιτείται η σύνδεση με τη βιβλιοθήκη libnsl, που περιλαμβάνει τις συναστήσεις δικτύωσης, όπως και αυτές για RPC και XDR.

Η εκτέλεση μπορεί να γίνει είτε σε ένα υπολογιστή (σε δύο παράθυρα τερματικού) ή σε δύο διαφορετικούς υπολογιστές, πελάτη και διακομιστή. Αν τα συστήματα είναι ίδια η μεταγλώττιση μπορεί να γίνει σε ένα σύστημα και απλά να μεταφερθεί το εκτελέσιμο.

Σε αυτό το παράδειγμα δεν παράχθηκε ρουτίνα XDR επειδή η εφαρμογή χρησιμοποιεί απλούς τύπους δεδομένων που ήδη διατίθενται στη βιβλιοθήκη libnsl.

Ας δούμε προσεκτικότερα τη λειτουργία του rpcgen με βάση το msg.x:

/*
* Please do not edit this file.
* It was generated using rpcgen.
*/

#ifndef _MSG_H_RPCGEN
#define _MSG_H_RPCGEN

#include <rpc/rpc.h>

#ifdef __cplusplus
extern "C" {
#endif

#define MESSAGEPROG 0x20000001
#define PRINTMESSAGEVERS 1

#if defined(__STDC__) || defined(__cplusplus)
#define PRINTMESSAGE 1
extern int * printmessage_1(char **, CLIENT *);
extern int * printmessage_1_svc(char **, struct svc_req *);
extern int messageprog_1_freeresult (SVCXPRT *, xdrproc_t, caddr_t);

#else /* K&R C */
#define PRINTMESSAGE 1
extern int * printmessage_1();
extern int * printmessage_1_svc();
extern int messageprog_1_freeresult ();
#endif /* K&R C */

#ifdef __cplusplus
}
#endif

#endif /* !_MSG_H_RPCGEN */
/*
 * Please do not edit this file.
 * It was generated using rpcgen.
 */

#include <memory.h> /* for memset */
#include "msg.h"

/* Default timeout can be changed using clnt_control() */
static struct timeval TIMEOUT = { 25, 0 };

int *
printmessage_1(char **argp, CLIENT *clnt)
{
    static int clnt_res;

    memset((char *)&clnt_res, 0, sizeof(clnt_res));
    if (clnt_call (clnt, PRINTMESSAGE,
        (xdrproc_t) xdr_wrapstring, (caddr_t) argp,
        (xdrproc_t) xdr_int, (caddr_t) &clnt_res,
        TIMEOUT) != RPC_SUCCESS) {
        return (NULL);
    }
    return (&clnt_res);
}
/*
 * Please do not edit this file.
 * It was generated using rpcgen.
 */

#include "msg.h"
#include <stdio.h>
#include <stdlib.h>
#include <rpc/pmap_clnt.h>
#include <string.h>
#include <memory.h>
#include <sys/socket.h>
#include <netinet/in.h>

#ifndef SIG_PF
#define SIG_PF void(*)(int)
#endif

static void
messageprog_1(struct svc_req *rqstp, register SVCXPRT *transp)
{
    union {
        char *printmessage_1_arg;
    } argument;
    char *result;
    xdrproc_t _xdr_argument, _xdr_result;
    char *(*local)(char *, struct svc_req *);

    switch (rqstp->rq_proc) {
    case NULLPROC:
        (void) svc_sendreply (transp, (xdrproc_t) xdr_void, (char *)NULL);
        return;

    case PRINTMESSAGE:
        _xdr_argument = (xdrproc_t) xdr_wrapstring;
        _xdr_result = (xdrproc_t) xdr_int;
        local = (char *(*)(char *, struct svc_req *)) printmessage_1_svc;
        break;

    default:
        svcerr_noproc (transp);
        return;
    }
    memset ((char *)&argument, 0, sizeof (argument));
    if (!svc_getargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) {
        svcerr_decode (transp);
        return;
    }
    result = (*local)((char *)&argument, rqstp);
    if (result != NULL && !svc_sendreply(transp, (xdrproc_t) _xdr_result, result)) {
        svcerr_systemerr (transp);
    }
    if (!svc_freeargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) {
        fprintf (stderr, "%s", "unable to free arguments");
        exit (1);
    }
    return;
}

int
main (int argc, char **argv)
{
    register SVCXPRT *transp;

    pmap_unset (MESSAGEPROG, PRINTMESSAGEVERS);

    transp = svcudp_create(RPC_ANYSOCK);
    if (transp == NULL) {
        fprintf (stderr, "%s", "cannot create udp service.");
        exit(1);
    }
    if (!svc_register(transp, MESSAGEPROG, PRINTMESSAGEVERS, messageprog_1, IPPROTO_UDP)) {
        fprintf (stderr, "%s", "unable to register (MESSAGEPROG, PRINTMESSAGEVERS, udp).");
        exit(1);
    }

    transp = svctcp_create(RPC_ANYSOCK, 0, 0);
    if (transp == NULL) {
        fprintf (stderr, "%s", "cannot create tcp service.");
        exit(1);
    }
    if (!svc_register(transp, MESSAGEPROG, PRINTMESSAGEVERS, messageprog_1, IPPROTO_TCP)) {
        fprintf (stderr, "%s", "unable to register (MESSAGEPROG, PRINTMESSAGEVERS, tcp).");
        exit(1);
    }
       
        svc_run ();
    fprintf (stderr, "%s", "svc_run returned");
    exit (1);
    /* NOTREACHED */
}

Εξωτερική Αναπαράσταση Δεδομένων (XDR)

 Το rpcgen μπορεί επίσης να παράγει ρουτίνες XDR, οι οποίες επιτρέπουν τη μετατροπή σύνθετων δομών δεδομένων από/προς μορφή XDR .

 Το παράδειγμα εξετάζει μια υπρεσία εμφάνισης των περιεχομένων απομακρυσμένου καταλόγου.

  To αρχείο περιγραφής του  RPC Πρωτοκόλλου σε RPCL είναι το dir.x:

/*
* dir.x: Remote directory listing protocol
* This example demonstrates the functions of rpcgen.
*/

const MAXNAMELEN = 255; /* max length of directory entry */
typedef string nametype<MAXNAMELEN>; /* director entry */
typedef struct namenode *namelist; /* link in the listing */

/* A node in the directory listing */

struct namenode {
nametype name; /* name of directory entry */
namelist next; /* next entry */
};

/*
* The result of a READDIR operation
* Α truly portable application would use an agreed upon list of error codes
* rather than (as this sample program does) rely upon passing UNIX errno's
* back (stored in errorcode). In this example: The union is used here to
* discriminate between successful and unsuccessful remote calls.
*/

union readdir_res switch (int errorcode) {
case 0:
namelist list; /* no error: return directory listing */
default:
void; /* error occurred: nothing else to return */
};

/* The directory program definition */

program DIRPROG {
version DIRVERS {
readdir_res
READDIR(nametype) = 1;
} = 1;
} = 0x20000076;

Το πρωτόκολλο ορίζει οτι ο πελάτης καλεί τη διαδικασία READDIR με παράμετρο το όνομα του καταλόγου (τύπου nametype, ουσιαστικά string με MAXNAMELEN). Ο διακομιστής επιστρέφει τo union του τύπου readdir_res , το οποίο δεν περιέχει τίποτε (αν επιστραφεί errorcode!=0)  ή το δείκτη στη κεφαλή μιας λίστας του τύπου namelist, η οποία θα αναγνωστεί με διάσχιση από την εφαρμογή του πελάτη. Τύποι δεδομένων όπως ο readdir_res μπορούν να καθοριστούν από τον προγραμματιστή με χρήση των struct, union, και enum που αναγνωρίζει η RPCL. Οι λέξεις κλειδιά δεν επαναλαμβάνονται στη δήλωση μεταβλητών.  Προς αποφυγή προβλημάτων μεταγλώττισης καλό είναι να αποφεύγονται υπερβολικά σύνθετες δομές, πχ unions of unions. Κατά τα άλλα, η RPCL και η XDR ακολουθούν τη λογική της C στους ορισμούς των τύπων δεδομένων.

Η εκτέλεση του rpcgen στο dir.x παράγει τέσσερα αρχεία:

Το τελευταίο αρχείο περιέχει τις ρουτίνες XDR για τη μετατροπή των σύνθετων δομών δεδομένων. Για κάθε τύπο δεδομένων RPCL που χρησιμοποιείται στο .x, το rpcgen θεωρεί η βιβλιοθήκη libnsl περιέχει μια αντίστοιχη συνάρτηση με όνομα το όνομα τπου τύπου με το πρόθεμα xdr_ (for example, xdr_int). Για κάθε σύνθετο τύπο δεδομένων που ορίζει ο χρήστης στο αρχείο .x, το rpcgen παράγει την απαιτούμενη συνάρτηση xdr_ και ενσωματώνονται στο αρχείο _xdr.c. Αν δεν υπάρχουν τέτοιοι σύνθετοι τύποι στο αρχείο .x  (όπως πχ στο αρχείο msg.x), τότε δεν παράγεται αρχείο _xdr.c. Φυσικά θα μπορούσε κάποιος να γράψει ή να τροποποιήσει μια xdr_ ρουτίνα αλλά γενικά δεν συνιστάται.

Παρακάτω εμφανίζονται τα αρχεία dir.h και dir_xdr.c hόπως παράγονται από το rpcgendr_ . Φαίνεται καθαρά η συσχέτιση των σύνθετων δομών δεδομένων με τις δομές και ρουτίνες XDR. Τα αρχεία των στελεχών πελάτη και διακομιστή είναι ουσιαστικά ίδια με αυτά που ήδη παρουσιάστηκαν παραπάνω.

Το αρχείο dir.h :

/*
 * Please do not edit this file. It was generated using rpcgen.
 */

#ifndef _DIR_H_RPCGEN
#define _DIR_H_RPCGEN

#include <rpc/rpc.h>

#ifdef __cplusplus
extern "C" {
#endif

#define MAXNAMELEN 255

typedef char *nametype;
typedef struct namenode *namelist;

struct namenode {
    nametype name;
    namelist next;
};
typedef struct namenode namenode;

struct readdir_res {
    int errorcode;
    union {
        namelist list;
    } readdir_res_u;
};
typedef struct readdir_res readdir_res;

#define DIRPROG 0x20000076
#define DIRVERS 1

#if defined(__STDC__) || defined(__cplusplus)
#define READDIR 1
extern  readdir_res * readdir_1(nametype *, CLIENT *);
extern  readdir_res * readdir_1_svc(nametype *, struct svc_req *);
extern int dirprog_1_freeresult (SVCXPRT *, xdrproc_t, caddr_t);

#else /* K&R C */
#define READDIR 1
extern  readdir_res * readdir_1();
extern  readdir_res * readdir_1_svc();
extern int dirprog_1_freeresult ();
#endif /* K&R C */

/* the xdr functions */

#if defined(__STDC__) || defined(__cplusplus)
extern  bool_t xdr_nametype (XDR *, nametype*);
extern  bool_t xdr_namelist (XDR *, namelist*);
extern  bool_t xdr_namenode (XDR *, namenode*);
extern  bool_t xdr_readdir_res (XDR *, readdir_res*);

#else /* K&R C */
extern bool_t xdr_nametype ();
extern bool_t xdr_namelist ();
extern bool_t xdr_namenode ();
extern bool_t xdr_readdir_res ();

#endif /* K&R C */

#ifdef __cplusplus
}
#endif

#endif /* !_DIR_H_RPCGEN */

To αρχείο dir_xdr.c

/*
 * Please do not edit this file. It was generated using rpcgen.
 */

#include "dir.h"

bool_t
xdr_nametype (XDR *xdrs, nametype *objp)
{
    register int32_t *buf;

     if (!xdr_string (xdrs, objp, MAXNAMELEN))
         return FALSE;
    return TRUE;
}

bool_t
xdr_namelist (XDR *xdrs, namelist *objp)
{
    register int32_t *buf;

     if (!xdr_pointer (xdrs, (char **)objp, sizeof (struct namenode), (xdrproc_t) xdr_namenode))
         return FALSE;
    return TRUE;
}

bool_t
xdr_namenode (XDR *xdrs, namenode *objp)
{
    register int32_t *buf;

     if (!xdr_nametype (xdrs, &objp->name))
         return FALSE;
     if (!xdr_namelist (xdrs, &objp->next))
         return FALSE;
    return TRUE;
}

bool_t
xdr_readdir_res (XDR *xdrs, readdir_res *objp)
{
    register int32_t *buf;

     if (!xdr_int (xdrs, &objp->errorcode))
         return FALSE;
    switch (objp->errorcode) {
    case 0:
         if (!xdr_namelist (xdrs, &objp->readdir_res_u.list))
             return FALSE;
        break;
    default:
        break;
    }
    return TRUE;
}

Τώρα προχωρούμε στην παρουσίαση των δύο προγραμμάτων εφαρμογής. Το πρόγραμμα του διακομιστή, dir_proc.c είναι ως εξής:

/*
* dir_proc.c: remote readdir implementation
*/

#include <dirent.h>
#include <errno.h>
#include "dir.h" /* Created by rpcgen */

extern char *malloc(); /* or include stdlib.h */
extern char *strdup(); /* or include string.h */

readdir_res *
readdir_1_svc(nametype *dirname, struct svc_req *req)

{
DIR *dirp;
struct dirent *d;
namelist nl;
namelist *nlp;

static readdir_res res; /* must be static! */

/* Open directory */
dirp = opendir(*dirname);

if (dirp == (DIR *)NULL) {
res.errorcode = errno;
return (&res);
}

/* Free previous result */
xdr_free(xdr_readdir_res, &res);

/*
* Collect directory entries. Memory allocated here is freed by
* xdr_free the next time readdir_1 is called
*/

nlp = &res.readdir_res_u.list;
while (d = readdir(dirp)) {
nl = *nlp = (namenode *) malloc(sizeof(namenode));
if (nl == (namenode *) NULL) {
res.errorcode= EAGAIN;
closedir(dirp);
return(&res);
}
nl->name = strdup(d->d_name);
nlp = &nl->next;
}

*nlp = (namelist)NULL;

/* Return the result */
res.errorcode = 0;
closedir(dirp);
return (&res);
}

Σημειώστε τη χρήση της βιβλιοθήκης errno.h. Μετά από την εκτέλεση κλήσεων συστήματος, οι οποίες συνήθως επιστρέφουν error codes, η μεταβλητή errno (που δηλώνεται μέσω της βιβλιοθήκης) λαμβάνει την κατάλληλη τιμή: 0 για επιτυχή εκτέλεση ή άλλη τιμή για ανεπιτυχή εκτέλεση. Η τιμή αυτή καθορίζεται από τη βιβλιοθήκη και συνήθως έχει και κάποιο λεκτικό αντίστοιχο, πχ EAGAIN.(βλ. εγχειρίδιο της βιβλιοθήκης). Η τιμή του errno αποθηκεύεται στο errorcode και αποστέλεται στην εφαρμογή του πελάτη.

Η εφαρμογή πελάτη λέγεται rls.c και φαίνεται παρακάτω:

/*
* rls.c: Remote directory listing client
*/

#include <stdio.h>
#include <errno.h>
#include "dir.h" /* generated by rpcgen */

main(int argc, char *argv[])


{
CLIENT *clnt;
char *server;
char *dir;
readdir_res *result;
namelist nl;

if (argc != 3) {
fprintf(stderr, "usage: %s host directory\n",argv[0]);
exit(1);
}

server = argv[1];
dir = argv[2];

/*
* Create client "handle" used for calling MESSAGEPROG
* on the server designated on the command line.
*/

clnt = clnt_create(server, DIRPROG, DIRVERS, "tcp");

if (clnt == (CLIENT *)NULL) {
clnt_pcreateerror(server);
exit(1);
}

result = readdir_1(&dir, clnt);

if (result == (readdir_res *)NULL) {
clnt_perror(clnt, server);
exit(1);
}

/* Okay, we successfully called
* the remote procedure.
*/

if (result->errorcode != 0) {
/* Remote system error. Print error message and die.
*/

errno = result->errorcode;
perror(dir);
exit(1);
}

/* Successfully got a directory listing.
* Print it.
*/

for (nl = result->readdir_res_u.list;
nl != NULL;
nl = nl->next) {
printf("%s\n", nl->name);
}

xdr_free(xdr_readdir_res, result);
clnt_destroy(clnt);
exit(0);
}

Και πάλι σημειώστε τη χρήση της συνάρτησης perror() σε συνδυασμό τη βιβλιοθήκη errno.h. Πρώτα η μεταβλητή errno λαμβάνει τιμή από τη μεταβλητή errorcode, που έχει σταλεί από την εφαρμογή διακομιστή. Στη συνέχεια η perror() Οι εντολές μεταγλώττισης και εκτέλεσης δίνονται συνολικά παρακάτω:

$ rpcgen dir.x
$ gcc rls.c dir_clnt.c dir_xdr.c -o rls -lnsl
$ gcc dir_svc.c dir_proc.c dir_xdr.c -o dir_svc -lnsl
remote$ ./dir_svc
local$./rls remote /usr/share/lib
....
ascii
eqnchar
greek
kbd
marg8
tabclr
tabs
tabs4
local$

Σημείωση: Προσέξτε τη χρήση της συνάρτησης xdr_free() για την απελευθέρωση μνήμης που δεσμεύει η συνάρτηση malloc(). Η συνάρτηση xdr_free() εκτελεί παρόμοια λειτουργία με τη γνωστή συνάρτηση free() που ελευθερώνει δυναμικά δεσμευμένη μνήμη όταν πια δεν χρησιμοποιείται. Η διαφορά είναι οτι στην xdr_free() rδίνουμε ως όρισμα και τη ρουτίνα XDR που διαχειρίζεται τη δομή, για παράδειγμα xdr_free(xdr_readdir_res, &res) ή  xdr_free(xdr_readdir_res, result);. Στη μεταγλώττιση μπορεί να λάβετε κάποια warnings εξ' αιτίας του τρόπου δήλωσης των ρουτινών XDR: μην ανησυχήσετε.

Άλλα χαρακτηριστικά του rpcgen

Το rpcgen υποστηρίζει όλες τις συνήθεις οδηγίες προεπεξεργασίας της C. Η προεπεξεργασία C του αρχείου .x εκτελείται πριν από τη μεταγλώττιση μέσω του rpcgen. Αν ο προεπεξεργαστής cpp δεν βρίσκεται στη προκαθορισμένη διαδρομή τότε χρησιμοποιούμε την επιλογή '-' Y. Για παράδειγμα:

rpcgen -Y /usr/local/bin test.x

Οι γραμμές του αρχείου .x που ξεκινούν με '%' περνούν απ' ευθείας στα αρχεία που παράγονται. Επιπλέον μπορεί να επιλεγεί η αποστολή τους σε συγκεκριμένο αρχείο μόνο:

RPC_HDR -- προς το αρχείο .h
RPC_XDR -- προς το αρχείο XDR.c
RPC_SVC -- αρχείο στελέχους διακομιστή
RPC_CLNT -- αρχείο στελέχους πελάτη
Μερικές ενδιαφέρουσες επιλογές του rpcgen δίνονται παρακάτω: 

Επιλογή Flag Σχόλιο
C-style '-' N Λέγεται και Newstyle
ANSI C '-' C Συνήθως μαζί με -N
MT-Safe code '-' M ΜultiΤhreading-safe κώδικας

'-' Sc Client-side template

'-' Ss Server-side template

'-' Sm Makefile template

'-' a All templates

Τα templates προσφέρουν προσχέδια για τα προγράμματα εφαρμογής πελάτη και διακομιστή, καθώς επίσης και ένα τυπικό Makefile. 

Για παράδειγμα η εντολή:

rpcgen -a dir.x
παράγει πέρα από τα αρχεία που έχουμε ήδη μελετήσει τα αρχεία dir_client.c, dir_server.c και Makefile.dir, τα οποία φαίνονται παρακάτω.
/*
* dir_client.c
* This is sample code generated by rpcgen.
* These are only templates and you can use them
* as a guideline for developing your own functions.
*/

#include "dir.h"

void
dirprog_1(char *host)
{
CLIENT *clnt;
readdir_res *result_1;
nametype readdir_1_arg1;

#ifndef DEBUG
clnt = clnt_create (host, DIRPROG, DIRVERS, "udp");
if (clnt == NULL) {
clnt_pcreateerror (host);
exit (1);
}
#endif /* DEBUG */

result_1 = readdir_1(readdir_1_arg1, clnt);
if (result_1 == (readdir_res *) NULL) {
clnt_perror (clnt, "call failed");
}
#ifndef DEBUG
clnt_destroy (clnt);
#endif /* DEBUG */
}

int
main (int argc, char *argv[])
{
char *host;

if (argc < 2) {
printf ("usage: %s server_host\n", argv[0]);
exit (1);
}
host = argv[1];
dirprog_1 (host);
exit (0);
}


/*
 *
dir_server.c
 * This is sample code generated by rpcgen.
 * These are only templates and you can use them
 * as a guideline for developing your own functions.
 */

#include "dir.h"

readdir_res *
readdir_1_svc(nametype arg1,  struct svc_req *rqstp)
{
    static readdir_res  result;

    /*
     * insert server code here
     */

    return &result;
}

# Makefile.dir
# This is a template Makefile generated by rpcgen

# Parameters
CLIENT = dir_client
SERVER = dir_server

SOURCES_CLNT.c =
SOURCES_CLNT.h =
SOURCES_SVC.c =
SOURCES_SVC.h =
SOURCES.x = dir.x

TARGETS_SVC.c = dir_svc.c dir_server.c dir_xdr.c
TARGETS_CLNT.c = dir_clnt.c dir_client.c dir_xdr.c
TARGETS = dir.h dir_xdr.c dir_clnt.c dir_svc.c dir_client.c dir_server.c

OBJECTS_CLNT = $(SOURCES_CLNT.c:%.c=%.o) $(TARGETS_CLNT.c:%.c=%.o)
OBJECTS_SVC = $(SOURCES_SVC.c:%.c=%.o) $(TARGETS_SVC.c:%.c=%.o)

# Compiler flags
CFLAGS += -g
LDLIBS += -lnsl
RPCGENFLAGS =

# Targets
all : $(CLIENT) $(SERVER)

$(TARGETS) : $(SOURCES.x)
rpcgen $(RPCGENFLAGS) $(SOURCES.x)

$(OBJECTS_CLNT) : $(SOURCES_CLNT.c) $(SOURCES_CLNT.h) $(TARGETS_CLNT.c)

$(OBJECTS_SVC) : $(SOURCES_SVC.c) $(SOURCES_SVC.h) $(TARGETS_SVC.c)

$(CLIENT) : $(OBJECTS_CLNT)
$(LINK.c) -o $(CLIENT) $(OBJECTS_CLNT) $(LDLIBS)

$(SERVER) : $(OBJECTS_SVC)
$(LINK.c) -o $(SERVER) $(OBJECTS_SVC) $(LDLIBS)

clean:
$(RM) core $(TARGETS) $(OBJECTS_CLNT) $(OBJECTS_SVC) $(CLIENT) $(SERVER)

Ασκήσεις

Άσκηση 12834

Αναπτύξτε ένα σύνολο προγραμμάτων RPC που να επιτρέπει σε ένα χρήστη να αναζητά αρχεία με αρχεία με το όνομά τους σε απομακρυσμένο κατάλογο, πχ  file.c ή *.c ή και *.x.

Άσκηση 12837

Αναπτύξτε ένα σύνολο προγραμμάτων RPC που να επιτρέπει σε ένα χρήστη να εκτελεί remote grep. Μπορείτε να χρησιμοποιήσετε την τοπική υλοποίηση του grep.



Dave Marshall
1/5/1999
Μετάφραση και προσαρμογή στα Ελληνικά
Κ.Γ. Μαργαρίτης
16/2/2010