Remote Procedure Calls (RPC) (2024)

Subsections
  • What Is RPC
  • How RPC Works
  • RPC Application Development
    • Defining the Protocol
    • Defining Client and Server Application Code
    • Compliling and running the application
  • Overview of Interface Routines
    • Simplified LevelRoutine Function
    • Top LevelRoutines
  • Intermediate Level Routines
    • Expert Level Routines
    • Bottom Level Routines
  • The Programmer's Interface to RPC
    • Simplified Interface
    • Passing Arbitrary Data Types
    • Developing High Level RPC Applications
      • Defining the protocol
    • Sharing the data
      • The Server Side
      • The Client Side
  • Exercise

This chapter provides an overview of Remote Procedure Calls (RPC) RPC.

RPC is a powerful technique for constructing distributed, client-serverbased applications. It is based on extending the notion of conventional,or local procedure calling, so that the called procedure need not existin the same address space as the calling procedure. The two processes maybe on the same system, or they may be on different systems with a networkconnecting them. By using RPC, programmers of distributed applicationsavoid the details of the interface with the network. The transportindependence of RPC isolates the application from the physical andlogical elements of the data communications mechanism and allows theapplication to use a variety of transports.

RPC makes the client/server model of computing more powerful and easier toprogram. When combined with the ONC RPCGEN protocol compiler(Chapter33) clients transparently make remote calls through alocal procedure interface.

An RPC is analogous to a function call. Like afunction call, when an RPC is made, the calling arguments are passed tothe remote procedure and the caller waits for a response to be returnedfrom the remote procedure. Figure32.1 shows the flow of activitythat takes place during an RPC call between two networked systems. Theclient makes a procedure call that sends a request to the server andwaits. The thread is blocked from processing until either a reply isreceived, or it times out. When the request arrives, the server calls adispatch routine that performs the requested service, and sends the replyto the client. After the RPC call is completed, the client programcontinues. RPC specifically supports network applications.

Remote Procedure Calls (RPC) (1)

Fig.32.1 Remote Procedure Calling MechanismA remote procedure is uniquely identified by the triple:(program number, version number, procedure number)The program number identifies a group of related remote procedures, each of which has a uniqueprocedure number.A program may consist of one or more versions. Each version consists of a collection of procedures whichare available to be called remotely. Version numbers enable multiple versions of an RPC protocol to beavailable simultaneously.Each version contains a a number of procedures that can be called remotely. Each procedure has aprocedure number.

Consider an example:

A client/server lookup in a personal database on a remote machine. Assuming thatwe cannot access the database from the local machine (via NFS).

We use UNIX to run a remote shell and execute the command this way. There aresome problems with this method:

  • the command may be slow to execute.
  • You require an login account on the remote machine.

The RPC alternative is to

  • establish an server on the remote machine that can repond to queries.
  • Retrieve information by calling a query which will be quicker thanprevious approach.

To develop an RPC application the following steps are needed:

  • Specify the protocol for client server communication
  • Develop the client program
  • Develop the server program

The programs will be compiled seperately. The communication protocol is achievedby generated stubs and these stubs and rpc (and other libraries) will need to belinked in.

Defining the Protocol

The easiest way to define and generate the protocol is to use a protocolcomplier such as rpcgen which we discuss is Chapter33.

For the protocol you must identify the name of the service procedures, and datatypes of parameters and return arguments.

The protocol compiler reads a definitio and automatically generates client andserver stubs.

rpcgen uses its own language (RPC language or RPCL) which looks verysimilar to preprocessor directives.

rpcgen exists as a standalone executable compiler that reads specialfiles denoted by a .x prefix.

So to compile a RPCL file you simply do

rpcgen rpcprog.x

This will generate possibly four files:

  • rpcprog_clnt.c -- the client stub
  • rpcprog_svc.c -- the server stub
  • rpcprog_xdr.c -- If necessary XDR (external data representation)filters
  • rpcprog.h -- the header file needed for any XDR filters.

The external data representation (XDR) is an data abstraction needed for machineindependent communication. The client and server need not be machines of thesame type.

Defining Client and Server Application Code

We must now write the the client and application code. They must communicate viaprocedures and data types specified in the Protocol.

The service side will have to register the procedures that may be called by theclient and receive and return any data required for processing.

The clientapplication call the remote procedure pass any required data and will receivethe retruned data.

There are several levels of application interfaces that may be used to developRPC applications. We will briefly disuss these below before exapnading thhe mostcommon of these in later chapters.

Compliling and running the application

Let us consider the full compilation model required to run a RPC application.Makefiles are useful for easing the burden of compiling RPC applications but itis necessary to understand the complete model before one can assemble aconvenient makefile.

Assume the the client program is called rpcprog.c, the service programis rpcsvc.c and that the protocol has been defined in rpcprog.x and thatrpcgen has been used to produce the stub and filter files: rpcprog_clnt.c, rpcprog_svc.c, rpcprog_xdr.c, rpcprog.h.

The client and server program must include (#include "rpcprog.h"

You must then:

  • compile the client code:
    cc -c rpcprog.c
  • compile the client stub:
    cc -c rpcprog_clnt.c
  • compile the XDR filter:
    cc -c rpcprog_xdr.c
  • build the client executable:
    cc -o rpcprog rpcprog.o rpcprog_clnt.o rpcprog_xdr.c
  • compile the service procedures:
    cc -c rpcsvc.c
  • compile the server stub:
    cc -c rpcprog_svc.c
  • build the server executable:
    cc -o rpcsvc rpcsvc.o rpcprog_svc.o rpcprog_xdr.c

Now simply run the programs rpcprog and rpcsvc on the client andserver respectively. The server procedures must be registered before the clientcan call them.

RPC has multiple levels of application interface to its services. These levels provide different degrees ofcontrol balanced with different amounts of interface code to implement. In order of increasing control andcomplexity. This section gives a summary of the routines available at each level.Simplified Interface Routines

The simplified interfaces are used to make remote procedure calls to routines on other machines, andspecify only the type of transport to use. The routines at this level are used for most applications.Descriptions and code samples can be found in the section, Simplified Interface @ 3-2.

Simplified LevelRoutine Function

rpc_reg() -- Registers a procedure as an RPC program on alltransports of the specified type.

rpc_call() -- Remote calls the specifiedprocedure on the specified remote host.

rpc_broadcast() -- Broadcasts a callmessage across all transports of the specified type. Standard InterfaceRoutines The standard interfaces are divided into top level, intermediatelevel, expert level, and bottom level. These interfaces give a developermuch greater control over communication parameters such as the transportbeing used, how long to wait beforeresponding to errors andretransmitting requests, and so on.

Top LevelRoutines

At the top level, the interface is still simple, but the programhas to create a client handle before making a call or create a serverhandle before receiving calls. If you want the application to run on alltransports, use this interface. Use of these routines and code samplescan be found in Top Level Interface

clnt_create() -- Generic client creation. The programtells clnt_create() where the server is located and the type oftransport to use.

clnt_create_timed() Similar to clnt_create() butlets the programmer specify the maximum time allowed for each type oftransport tried during the creation attempt.

svc_create() -- Creates serverhandles for all transports of the specified type. The program tellssvc_create() which dispatch function to use.

clnt_call() -- Client calls aprocedure to send a request to the server.

The intermediate level interface of RPC lets you control details.Programs written at these lower levels are more complicated but run moreefficiently. The intermediate level enables you to specify the transportto use.

clnt_tp_create() -- Creates a client handle for thespecified transport.

clnt_tp_create_timed() -- Similar to clnt_tp_create()but lets the programmer specify the maximum time allowed.svc_tp_create() Creates a server handle for the specified transport.

clnt_call() -- Client calls a procedure to send a request to theserver.

Expert Level Routines

The expert level contains a larger set of routines with which to specify transport-related parameters. Useof these routines

clnt_tli_create() -- Creates a client handle for the specifiedtransport.

svc_tli_create() -- Creates a server handle for the specifiedtransport.

rpcb_set() -- Calls rpcbind to set a map between an RPC serviceand a network address.

rpcb_unset() -- Deletes a mapping set by rpcb_set().

rpcb_getaddr() -- Calls rpcbind to get the transport addresses ofspecified RPC services.

svc_reg() -- Associates the specified program andversion number pair with the specified dispatch routine.

svc_unreg() -- Deletes an association set by svc_reg().

clnt_call() -- Client calls a procedure to send a request to theserver.

Bottom Level Routines

The bottom level contains routines used for full control of transportoptions.

clnt_dg_create() -- Creates an RPC client handle for thespecified remote program, using a connectionless transport.

svc_dg_create() -- Creates an RPC server handle, using aconnectionless transport.

clnt_vc_create() -- Creates an RPC client handle forthe specified remote program, using a connection-oriented transport.

svc_vc_create() -- Creates an RPC server handle, using aconnection-oriented transport.

clnt_call() -- Client calls a procedure tosend a request to the server.

This section addresses the C interface to RPC and describes how to writenetwork applications using RPC. For a complete specification of theroutines in the RPC library, see the rpc and related manpages.

Simplified Interface

The simplified interface is the easiest level to use because it does notrequire the use of any other RPC routines. It also limits control of theunderlying communications mechanisms. Program development at this levelcan be rapid, and is directly supported by the rpcgen compiler. Formost applications, rpcgen and its facilities are sufficient. Some RPCservices are not available as C functions, but they are available as RPCprograms. The simplified interface library routines provide direct accessto the RPC facilities for programs that do not require fine levels ofcontrol.

Routines such as rusers are in the RPC services librarylibrpcsvc. rusers.c, below, is a program that displays thenumber of users on a remote host. It calls the RPC library routine,rusers.

The program.c program listing:

#include <rpc/rpc.h> #include <rpcsvc/rusers.h>#include <stdio.h>/** a program that calls the* rusers() service*/main(int argc,char **argv){int num;if (argc != 2) { fprintf(stderr, "usage: %s hostname\n", argv[0]); exit(1); }if ((num = rnusers(argv[1])) < 0) { fprintf(stderr, "error: rusers\n"); exit(1); }fprintf(stderr, "%d users on %s\n", num, argv[1] );exit(0);}

Compile the program with:

cc program.c -lrpcsvc -lnsl

The Client Side

There is just one function on the client side of the simplified interface rpc_call().

It has nine parameters:

int rpc_call (char *host /* Name of server host */, u_long prognum /* Server program number */, u_long versnum /* Server version number */, xdrproc_t inproc /* XDR filter to encode arg */, char *in /* Pointer to argument */, xdr_proc_t outproc /* Filter to decode result */, char *out /* Address to store result */, char *nettype /* For transport selection */);

This function calls the procedure specified by prognum, versum, andprocnum on the host. The argument to be passedto the remote procedure is pointed to by the in parameter, and inproc is the XDR filter to encode this argument. The out parameter is anaddress where the result from the remote procedure is to be placed. outprocis an XDR filter which will decode the result and place it at this address.

Theclient blocks on rpc_call() until it receives a reply from the server. Ifthe server accepts, it returns RPC_SUCCESS with the value of zero. It willreturn a non-zero value if the call was unsuccessful. This value can be cast tothe type clnt_stat, an enumerated type defined in the RPC include files(<rpc/rpc.h>) and interpreted by the clnt_sperrno() function. Thisfunction returns a pointer to a standard RPC error message corresponding to theerror code. In the example, all "visible" transports listed in /etc/netconfig are tried. Adjusting the number of retries requires use of thelower levels of the RPC library. Multiple arguments and results are handled bycollecting them in structures.

The example changed touse the simplified interface, looks like

#include <stdio.h>#include <utmp.h> #include <rpc/rpc.h>#include <rpcsvc/rusers.h>/* a program that calls the RUSERSPROG* RPC program*/main(int argc, char **argv){ unsigned long nusers; enum clnt_stat cs; if (argc != 2) { fprintf(stderr, "usage: rusers hostname\n"); exit(1); } if( cs = rpc_call(argv[1], RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, xdr_void, (char *)0, xdr_u_long, (char *)&nusers, "visible") != RPC_SUCCESS ) { clnt_perrno(cs); exit(1); } fprintf(stderr, "%d users on %s\n", nusers, argv[1] ); exit(0);}

Since data types may be represented differently on different machines,rpc_call() needs both the type of, and a pointer to, the RPC argument(similarly for the result). For RUSERSPROC_NUM, the return value is anunsigned long, so the first return parameter of rpc_call() isxdr_u_long (which is for an unsigned long) and the second is&nusers (which points to unsigned long storage). BecauseRUSERSPROC_NUM has no argument, the XDR encoding function of rpc_call() is xdr_void() and its argument is NULL.

The Server Side

The server program using thesimplified interface is very straightforward. It simply calls rpc_reg() toregister the procedure to be called, and then it calls svc_run(), the RPClibrary's remote procedure dispatcher, to wait for requests to come in.

rpc_reg()has the following prototype:

rpc_reg(u_long prognum /* Server program number */, u_long versnum /* Server version number */, u_long procnum /* server procedure number */, char *procname /* Name of remote function */, xdrproc_t inproc /* Filter to encode arg */, xdrproc_t outproc /* Filter to decode result */, char *nettype /* For transport selection */);

svc_run() invokes service procedures in response to RPC call messages. Thedispatcher in rpc_reg() takes care of decoding remote procedure argumentsand encoding results, using the XDR filters specified when the remote procedurewas registered. Some notes about the server program:

  • Most RPC applications followthe naming convention of appending a _1 to the function name. The sequence_n is added to the procedure names to indicate the version number nof the service.
  • The argument and result are passed as addresses. This is true for all functionsthat are called remotely. If you pass NULL as a result of a function, thenno reply is sent to the client. It is assumed that there is no reply to send.
  • The result must exist in static data space because its value is accessed afterthe actual procedure has exited. The RPC library function that builds the RPCreply message accesses the result and sends the value back to the client.
  • Onlya single argument is allowed. If there are multiple elements of data, they shouldbe wrapped inside a structure which can then be passed as a single entity.
  • Theprocedure is registered for each transport of the specified type. If the typeparameter is (char *)NULL, the procedure is registered for all transportsspecified in NETPATH.

You can sometimesimplement faster or more compact code than can rpcgen. rpcgen handles thegeneric code-generation cases. The following program is an example of ahand-coded registration routine.It registers asingle procedure and enters svc_run() to service requests.

#include <stdio.h> #include <rpc/rpc.h>#include <rpcsvc/rusers.h>void *rusers();main(){ if(rpc_reg(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, rusers, xdr_void, xdr_u_long, "visible") == -1) { fprintf(stderr, "Couldn't Register\n"); exit(1); } svc_run(); /* Never returns */ fprintf(stderr, "Error: svc_run returned!\n"); exit(1);}

rpc_reg() can be called as many times as is needed to register differentprograms, versions, and procedures.

Passing Arbitrary Data Types

Data types passed to and received from remote procedures can be any of a set of predefinedtypes, or can be programmer-defined types. RPC handles arbitrary data structures,regardless of different machines' byte orders or structure layout conventions, by alwaysconverting them to a standard transfer format called external data representation (XDR)before sending them over the transport. The conversion from a machine representation to XDRis called serializing, and the reverse process is called deserializing. The translatorarguments of rpc_call() and rpc_reg() can specify an XDR primitive procedure,like xdr_u_long(), or a programmer-supplied routine that processes a completeargument structure. Argument processing routines must take only two arguments: a pointer tothe result and a pointer to the XDR handle.

The following XDR Primitive Routines are available:

xdr_int() xdr_netobj() xdr_u_long() xdr_enum()xdr_long() xdr_float() xdr_u_int() xdr_bool()xdr_short() xdr_double() xdr_u_short() xdr_wrapstring()xdr_char() xdr_quadruple() xdr_u_char() xdr_void()

The nonprimitive xdr_string(), which takes more than two parameters, is called fromxdr_wrapstring().

For an example of a programmer-supplied routine, the structure:

struct simple { int a; short b; } simple;

contains the calling arguments of a procedure. The XDR routine xdr_simple()translates the argument structure as shown below:

#include <rpc/rpc.h>#include "simple.h"bool_t xdr_simple(XDR *xdrsp, struct simple *simplep){ if (!xdr_int(xdrsp, &simplep->a)) return (FALSE); if (!xdr_short(xdrsp, &simplep->b)) return (FALSE); return (TRUE);}

An equivalent routine can be generated automatically by rpcgen (SeeChapter33).

An XDR routine returns nonzero (a C TRUE) if it completessuccessfully, and zero otherwise.

For more complex data structures use the XDR prefabricated routines:

xdr_array() xdr_bytes() xdr_reference()xdr_vector() xdr_union() xdr_pointer()xdr_string() xdr_opaque()

For example, to send a variable-sized array of integers, it is packaged in a structure containing the arrayand its length:

struct varintarr {int *data;int arrlnth;} arr;

Translate the array with xdr_array(), as shown below:

bool_t xdr_varintarr(XDR *xdrsp, struct varintarr *arrp){ return(xdr_array(xdrsp, (caddr_t)&arrp->data, (u_int *)&arrp->arrlnth, MAXLEN, sizeof(int), xdr_int));}
The arguments of xdr_array() are the XDR handle, a pointer to the array,a pointer tothe size of the array, the maximum array size, the size of each array element, and apointer to the XDR routine to translate each array element. If the size of the array isknown in advance, use xdr_vector() instread as is more efficient:
int intarr[SIZE];bool_t xdr_intarr(XDR *xdrsp, int intarr[]){ return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int), xdr_int));}

XDR converts quantities to 4-byte multiples when serializing. For arrays of characters,each character occupies 32 bits. xdr_bytes() packs characters. It has four parameterssimilar to the first four parameters of xdr_array().

Null-terminated strings are translated by xdr_string(). It is like xdr_bytes()with no length parameter. On serializing it gets the string length from strlen(), andon deserializing it creates a null-terminated string.

xdr_reference() callsthe built-in functions xdr_string() and xdr_reference(), which translatespointers to pass a string, and struct simple from the previous examples. Anexample use of xdr_reference() is as follows:

struct finalexample { char *string; struct simple *simplep; } finalexample;bool_t xdr_finalexample(XDR *xdrsp, struct finalexample *finalp){ if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN)) return (FALSE); if (!xdr_reference( xdrsp, &finalp->simplep, sizeof(struct simple), xdr_simple)) return (FALSE); return (TRUE);}

Note thatxdr_simple() could have been called here instead of xdr_reference().

Developing High Level RPC Applications

Let us now introduce some further functions and see how we develop an application using high level RPCroutines. We will do this by studying an example.

We will develop a remote directory reading utility.

Let us first consider how we would write a local directory reader. We have seem how to do this alreadyin Chapter19.

Consider the program to consist of two files:

  • lls.c -- the main program which calls a routine in a local module read_dir.c
    /* * ls.c: local directory listing main - before RPC */#include <stdio.h>#include <strings.h>#include "rls.h"main (int argc, char **argv){ char dir[DIR_SIZE]; /* call the local procedure */ strcpy(dir, argv[1]);/* char dir[DIR_SIZE] is coming and going... */ read_dir(dir); /* spew-out the results and bail out of here! */ printf("%s\n", dir); exit(0);}
  • read_dir.c -- the file containing the local routine read_dir().
    /* note - RPC compliant procedure calls take one input and return one output. Everything is passed by pointer. Return values should point to static data, as it might have to survive some while. */#include <stdio.h>#include <sys/types.h>#include <sys/dir.h> /* use <xpg2include/sys/dirent.h> (SunOS4.1) or <sys/dirent.h> for X/Open Portability Guide, issue 2 conformance */#include "rls.h"read_dir(char *dir) /* char dir[DIR_SIZE] */{ DIR * dirp; struct direct *d; printf("beginning "); /* open directory */ dirp = opendir(dir); if (dirp == NULL) return(NULL); /* stuff filenames into dir buffer */ dir[0] = NULL; while (d = readdir(dirp)) sprintf(dir, "%s%s\n", dir, d->d_name); /* return the result */ printf("returning "); closedir(dirp); return((int)dir); /* this is the only new line from Example 4-3 */}
  • the header file rls.h contains only the following (for now at least)
    #define DIR_SIZE 8192

    Clearly we need to share the size between the files. Later when we develop RPC versions moreinformation will need to be added to this file.

This local program would be compiled as follows:

cc lls.c read_dir.c -o lls

Now we want to modify this program to work over a network: Allowing us to inspect directories of aremote server accross a network.

The following steps will be required:

  • We will have to convert the read_dir.c, to run on the server.
    • We will have to register the server and the routine read_dir() on the server/.
  • The client lls.c will have to call the routine as a remote procedure.
  • We will have to define the protocol for communication between the client and the server programs.

Defining the protocol

We can can use simple NULL-terminated strings for passing and receivong the directory name anddirectory contents. Furthermore, we can embed the passing of these parameters directly in the clientand server code.

We therefore need to specify the program, procedure and version numbers for client and servers. Thiscan be done automatically using rpcgen or relying on prdefined macros in the simlifiedinterface. Here we will specify them manually.

The server and client must agree ahead of time what logical adresses thney will use (Thephysical addresses do not matter they are hidden from the application developer)

Program numbers are defined in a standard way:

  • 0x00000000 - 0x1FFFFFFF: Defined by Sun
  • 0x20000000 - 0x3FFFFFFF: User Defined
  • 0x40000000 - 0x5FFFFFFF: Transient
  • 0x60000000 - 0xFFFFFFFF: Reserved

We will simply choose a user deifnined value for our program number. The version and procedurenumbers are set according to standard practice.

We still have the DIR_SIZE definition required from the local version as the size of thedirectory buffer is rewquired by bith client and server programs.

Our new rls.h file contains:

#define DIR_SIZE 8192#define DIRPROG ((u_long) 0x20000001) /* server program (suite) number */#define DIRVERS ((u_long) 1) /* program version number */#define READDIR ((u_long) 1) /* procedure number for look-up */

Sharing the data

We have mentioned previously that we can pass the data a simple strings. We need to define an XDRfilter routine xdr_dir() that shares the data. Recall that only one encoding and decodingargument can be handled. This is easy and defined via the standard xdr_string() routine.

The XDR file, rls_xrd.c, is as follows:

#include <rpc/rpc.h>#include "rls.h"bool_t xdr_dir(XDR *xdrs, char *objp){ return ( xdr_string(xdrs, &objp, DIR_SIZE) ); }

The Server Side

We can use the original read_dir.c file. All we need to do is register the procedure and startthe server.

The procedure is registered with registerrpc() function. This is prototypes by:

registerrpc(u_long prognum /* Server program number */, u_long versnum /* Server version number */, u_long procnum /* server procedure number */, char *procname /* Name of remote function */, xdrproc_t inproc /* Filter to encode arg */, xdrproc_t outproc /* Filter to decode result */);

The parameters a similarly defined as in the rpc_reg simplified interface function. We havealready discussed the setting of the parametere with the protocol rls.h header files and therls_xrd.c XDR filter file.

The svc_run() routine has also been discussed previously.

The full rls_svc.c code is as follows:

#include <rpc/rpc.h>#include "rls.h"main(){ extern bool_t xdr_dir(); extern char * read_dir(); registerrpc(DIRPROG, DIRVERS, READDIR, read_dir, xdr_dir, xdr_dir); svc_run();}

The Client Side

At the client side we simply need to call the remote procedure. The function callrpc() does this. It is prototyped as follows:

callrpc(char *host /* Name of server host */, u_long prognum /* Server program number */, u_long versnum /* Server version number */, char *in /* Pointer to argument */, xdrproc_t inproc /* XDR filter to encode arg */, char *out /* Address to store result */ xdr_proc_t outproc /* Filter to decode result */);

We call a local function read_dir() which uses callrpc() to call the remote procedure thathas been registered READDIR at the server.

The full rls.c program is as follows:

/* * rls.c: remote directory listing client */#include <stdio.h>#include <strings.h>#include <rpc/rpc.h>#include "rls.h"main (argc, argv)int argc; char *argv[];{char dir[DIR_SIZE]; /* call the remote procedure if registered */ strcpy(dir, argv[2]); read_dir(argv[1], dir); /* read_dir(host, directory) */ /* spew-out the results and bail out of here! */ printf("%s\n", dir); exit(0);}read_dir(host, dir)char *dir, *host;{ extern bool_t xdr_dir(); enum clnt_stat clnt_stat; clnt_stat = callrpc ( host, DIRPROG, DIRVERS, READDIR, xdr_dir, dir, xdr_dir, dir); if (clnt_stat != 0) clnt_perrno (clnt_stat);}

Exercise 12833

Compile and run the remote directory example rls.c etc. Run both the client ande sreverlocally and if possible over a network.

Dave Marshall
1/5/1999
Remote Procedure Calls (RPC) (2024)

References

Top Articles
Latest Posts
Article information

Author: Mrs. Angelic Larkin

Last Updated:

Views: 6289

Rating: 4.7 / 5 (67 voted)

Reviews: 82% of readers found this page helpful

Author information

Name: Mrs. Angelic Larkin

Birthday: 1992-06-28

Address: Apt. 413 8275 Mueller Overpass, South Magnolia, IA 99527-6023

Phone: +6824704719725

Job: District Real-Estate Facilitator

Hobby: Letterboxing, Vacation, Poi, Homebrewing, Mountain biking, Slacklining, Cabaret

Introduction: My name is Mrs. Angelic Larkin, I am a cute, charming, funny, determined, inexpensive, joyous, cheerful person who loves writing and wants to share my knowledge and understanding with you.