TCP/IP 與 Internet 網路:第九章 RPC 高階程式介面 上一頁 下一頁
9-5 RPC 程式範例
首先我們建立一個簡單遠端程序範例,來探討它的開發過程,也許會讓讀者比較容易進入情況。我們構想是伺服端提供三個數學運算的遠端程序讓客戶端呼叫,伺服端收到客戶端所傳的參數,經過計算後再傳回結果給客戶端。三個遠端程序功能如下:
● add():客戶端呼叫該程序時,會攜帶兩個整數(Integer)參數給遠端程序,遠端程序將兩個參數相加後,傳回給客戶端。
● multiply():客戶端呼叫該遠端程式時,會攜帶兩個整數參數,執行後傳回兩數值的乘績。
● cube():客戶端傳遞一個整數參數,遠端程序傳回該參數三次方的值。
我們在伺服端製作上述的運算程式,當客戶端以 RPC 呼叫執行,伺服端運算後傳回結果給客戶端。我們依照上一節所敘述的步驟來建構如下:
9-5-1 建立規格檔 - math.x
/* * math.x */ /* data structure used by procedure */ struct intpair { int a; int b; }; /* procedure definition */ program MATHPROG { version MATHVERS { int ADD(intpair) =1; int MULTIPLY(intpair) =2; int CUBE(int) =3; } = 1; } = 0x20000001; |
規格檔的副檔名必須是『.x』,它包含程序呼叫之間的參數宣告和遠端程序名稱。範例中包含如下:
★ 傳遞參數宣告:結構參數 intpair 中包含兩個整數 a 和 b,此參數作為客戶端和伺服端傳遞資料使用。在此宣告後經由 rpcgen 編譯後,會產生 XDR 的資料轉換檔。
★ 程式名稱:以『program』關鍵字宣告程序名稱。如上例為 MATHPROG { ….. } = 0x20000001,也表示此程式號碼為 0x20000001,大掛號內({ … })為程式內容。
★ 版本名稱:以『version』關鍵字宣告版本名稱。如上例為 MATHVERS { … } = 1,也表示此版本號碼是 1,大掛號內({ … })包含著遠端程序的函數名稱。
★ 程序名稱:宣告程序名稱。如上例中 int ADD(intpair) = 1,int 表示該程序傳回一個整數,所接收參數是 intpair,而該程序編號為 1。
由上例可以瞭解,規格檔只要制定程序號碼、版本、以及程序名稱,並宣告所傳遞的參數,另外,這些規格也是給伺服器和客戶端編寫程式的依據。
9-5-2 編譯規格檔 - rpcgen
利用 rpcgen 編譯規則檔(math.x)如下:
$ rpcgen math.x
編譯後會產生下列檔案:
● math.h:程式標頭檔,作為編寫遠端程序和客戶端呼叫程序的依據。
● math_clnt.c:客戶基礎檔(Client Stub),一般都希望使用者不要去修改它。
● math_svc.c:伺服基礎檔(Server Stub),一般也希望使用者不要修改它。
● math_xdr.c:XDR 資料格式轉換檔,不要修改它。
以下分別介紹這些檔案:
(A) 標頭檔 – math.h
經過 rpcgen 編譯後所產生的 math.h 內容如下:
/* * Please do not edit this file. * It was generated using rpcgen. */
#ifndef _MATH_H_RPCGEN #define _MATH_H_RPCGEN
#include <rpc/rpc.h>
#ifdef __cplusplus extern "C" { #endif
struct intpair { int a; int b; }; typedef struct intpair intpair;
#define MATHPROG 0x20000001 #define MATHVERS 1
#if defined(__STDC__) || defined(__cplusplus) #define ADD 1 extern int * add_1(intpair *, CLIENT *); extern int * add_1_svc(intpair *, struct svc_req *); #define MULTIPLY 2 extern int * multiply_1(intpair *, CLIENT *); extern int * multiply_1_svc(intpair *, struct svc_req *); #define CUBE 3 extern int * cube_1(int *, CLIENT *); extern int * cube_1_svc(int *, struct svc_req *); extern int mathprog_1_freeresult (SVCXPRT *, xdrproc_t, caddr_t);
#else /* K&R C */ #define ADD 1 extern int * add_1(); extern int * add_1_svc(); #define MULTIPLY 2 extern int * multiply_1(); extern int * multiply_1_svc(); #define CUBE 3 extern int * cube_1(); extern int * cube_1_svc(); extern int mathprog_1_freeresult (); #endif /* K&R C */
/* the xdr functions */
#if defined(__STDC__) || defined(__cplusplus) extern bool_t xdr_intpair (XDR *, intpair*);
#else /* K&R C */ extern bool_t xdr_intpair ();
#endif /* K&R C */
#ifdef __cplusplus } |
基本上,標頭檔不希望被使用者修改,只要當編寫遠端程式和客戶端程式時,將它加入包含檔(Include File)即可,但我們必須了解其內容,才可依照標頭檔規範來編寫程式。
首先在標頭檔裡宣告傳遞參數 intpair,以及程式號碼(MATHPROG)和版本號碼(MATHVERS)。並且宣告三對程序結構,其中 add_1() 為客戶端呼叫 add() 程序,而 add_1_svc 為伺服端的遠端程序,兩者成為一對程序;相同的,multply_1() 與 multiply_1_svc()、cube_1() 與 cub_1_svc() 也各自成一對呼叫與被呼叫的程序。每一程序加入 _1,這表示版本 1 的意思。當我們在編寫遠端程序或客戶端程式都必須依照這些宣告內容來製作,雙方程式才會一致性。
(B) 客戶基礎檔 - math_clnt.c
math_cln.c 是 math.x 經過 rpcgen 編譯後所產生的,基本上也是不希望使用者去編改它。客戶基礎檔(Client Stub)主要是作為呼叫程式和遠端被呼叫程式之間的交換檔,因此,針對程序之間的交換格式必需給予標準規範,如 ADD() 函數以 add_1(intpair *argp, CLIENT *clnt) 作為呼叫格式。在每一個呼叫函數中都會包含較低層式的 clnt_call() 函數和有關 XDR 轉換函數。math_cln.c 程式範例如下:
# cat math_cln.c /* * Please do not edit this file. * It was generated using rpcgen. */
#include <memory.h> /* for memset */ #include "math.h"
/* Default timeout can be changed using clnt_control() */ static struct timeval TIMEOUT = { 25, 0 };
int * add_1(intpair *argp, CLIENT *clnt) { static int clnt_res;
memset((char *)&clnt_res, 0, sizeof(clnt_res)); if (clnt_call (clnt, ADD, (xdrproc_t) xdr_intpair, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr_t) &clnt_res, TIMEOUT) != RPC_SUCCESS) { return (NULL); } return (&clnt_res); } int * multiply_1(intpair *argp, CLIENT *clnt) { static int clnt_res;
memset((char *)&clnt_res, 0, sizeof(clnt_res)); if (clnt_call (clnt, MULTIPLY, (xdrproc_t) xdr_intpair, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr_t) &clnt_res, TIMEOUT) != RPC_SUCCESS) { return (NULL); } return (&clnt_res); } int * cube_1(int *argp, CLIENT *clnt) { static int clnt_res;
memset((char *)&clnt_res, 0, sizeof(clnt_res)); if (clnt_call (clnt, CUBE, (xdrproc_t) xdr_int, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr_t) &clnt_res, TIMEOUT) != RPC_SUCCESS) { return (NULL); } return (&clnt_res); } int * cube_1(int *argp, CLIENT *clnt) { static int clnt_res;
memset((char *)&clnt_res, 0, sizeof(clnt_res)); if (clnt_call (clnt, CUBE, (xdrproc_t) xdr_int, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr_t) &clnt_res, TIMEOUT) != RPC_SUCCESS) { return (NULL); } return (&clnt_res); } |
(C) 伺服基礎檔 - math_svc.c
伺服基礎檔(Server Stub)也是規格檔(math.x)經由 rpcgen 編譯所產生,基本上也是不希望使用者去編改它。以下是 math_svc.c 的檔案範例,我們可以發現伺服程式的主程式(main())是在基礎檔內,因此爾後編寫遠端程式時,就不需要再編寫主程式,而只要針對遠端程序編寫即可。另外,伺服基礎檔也是 Client/Server程序之間呼叫的交換程序,因此也必須制定遠端程序的標準規範,以及有關 XDR 資料轉換的程序。math_svc.c 的程式範例如下:
# cat math_svc.c /* * Please do not edit this file. * It was generated using rpcgen. */
#include "math.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 mathprog_1(struct svc_req *rqstp, register SVCXPRT *transp) { union { intpair add_1_arg; intpair multiply_1_arg; int cube_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 ADD: _xdr_argument = (xdrproc_t) xdr_intpair; _xdr_result = (xdrproc_t) xdr_int; local = (char *(*)(char *, struct svc_req *)) add_1_svc; break; case MULTIPLY: _xdr_argument = (xdrproc_t) xdr_intpair; _xdr_result = (xdrproc_t) xdr_int; local = (char *(*)(char *, struct svc_req *)) multiply_1_svc; break; case CUBE: _xdr_argument = (xdrproc_t) xdr_int; _xdr_result = (xdrproc_t) xdr_int; local = (char *(*)(char *, struct svc_req *)) cube_1_svc; break; default: svcerr_noproc (transp); return; } memset ((char *)&argument, 0, sizeof (argument)); if (!svc_getargs (transp, _xdr_argument, (caddr_t) &argument)) { svcerr_decode (transp); return; } result = (*local)((char *)&argument, rqstp); if (result != NULL && !svc_sendreply(transp, _xdr_result, result)) { svcerr_systemerr (transp); } if (!svc_freeargs (transp, _xdr_argument, (caddr_t) &argument)) { fprintf (stderr, "unable to free arguments"); exit (1); } return; } int main (int argc, char **argv) { register SVCXPRT *transp; pmap_unset (MATHPROG, MATHVERS); transp = svcudp_create(RPC_ANYSOCK); if (transp == NULL) { fprintf (stderr, "cannot create udp service."); exit(1); } if (!svc_register(transp, MATHPROG, MATHVERS, mathprog_1, IPPROTO_UDP)) { fprintf (stderr, "unable to register (MATHPROG, MATHVERS, udp)."); exit(1); } transp = svctcp_create(RPC_ANYSOCK, 0, 0); if (transp == NULL) { fprintf (stderr, "cannot create tcp service."); exit(1); } if (!svc_register(transp, MATHPROG, MATHVERS, mathprog_1, IPPROTO_TCP)) { fprintf (stderr, "unable to register (MATHPROG, MATHVERS, tcp)."); exit(1); } svc_run (); fprintf (stderr, "svc_run returned"); exit (1); /* NOTREACHED */ } |
(D) 資料格式轉換檔 – math_xdr.c
同樣的,資料格式轉換檔也不希望使用者去修改它,它主要是做傳遞參數資料結構的轉換,對我們編寫程式較沒有影響。但如果使用者希望作較低層次的程式編寫,就必須參考有關 XDR 庫存函數的使用方法,這不在本書的介紹範圍內。
# cat math_xdr.c /* * Please do not edit this file. * It was generated using rpcgen. */ #include "math.h" bool_t xdr_intpair (XDR *xdrs, intpair *objp) { register int32_t *buf;
if (!xdr_int (xdrs, &objp->a)) return FALSE; if (!xdr_int (xdrs, &objp->b)) return FALSE; return TRUE; } |
9-5-3 編寫遠端程序 – math_proc.c
遠端程序放置於伺服器端,可在伺服端製作,也可以在客戶端完成後再檔案傳輸到伺服器端。由 math_svc.c 檔案中,可以發現主程式已在該檔案內規劃完成,我們只要編寫欲被呼叫的程序即可,當然編寫時必須參考標頭檔(math.h)和遠端基礎檔(math_svc.c)內針對資料和函數的宣告。math_proc.c 程式範例如下:
#include <rpc/rpc.h> /* math.h is generated by rpcgen */ #include "math.h"
int * add_1_svc(pair, sv) intpair *pair; struct svc_req *sv; { static int result; result = pair->a + pair->b; return(&result); }
int * multiply_1_svc(pair, sv) intpair *pair; struct svc_req *sv; { static int result; result = pair->a * pair->b; return(&result); }
int * cube_1_svc(base, sv) int *base; struct svc_req *sv; { static int result; int baseval = *base; result = baseval * baseval * baseval; return(&result); } |
其中 add_1_svc()、multiply_1_svc() 與 cube_1_svc在標頭檔(math.h)已宣告完成,也表示版本為 1(_1),另外結構資料 svc_req 也是在標頭檔宣告。其它有關函數功能的實現,也必須在遠端程序內製作完成,譬如,本範例的乘法、加法和三次方的運算。
9-5-4 編寫客戶端程式 – math_req.c
其實編寫客戶端程式比伺服端程序較為複雜,其中會牽涉到一些 RPC 庫存函數的功能呼叫,當然伺服端也會有功能呼叫,但大多實現在基礎檔(Server Stub)內,使用者也大多不需要去修改它。但客戶端也許會針對不同伺服端要求程序呼叫,因此有關功能呼叫部份必需由使用者自行規劃。同樣的,編寫時也必須參考標頭檔(math.h)和基礎檔(math_cln.c)內有關資料和程序的宣告。首先我們來看 math_req.c 的客戶端程式範例,在下一節再來探討 RPC 程式庫的呼叫方法,math_req.c 範例如下:
# cat math_req.c /* * Client Program, math_req.c */ #include <stdio.h> #include <rpc/rpc.h> #include "math.h"
main (argc, argv) int argc; char *argv[]; { CLIENT *cl; intpair numbers; int *result;
if (argc != 4) { fprintf(stderr, "%s: usage: %s server num1 num2\n", argv[0], argv[0]); exit(1); } cl = clnt_create(argv[1], MATHPROG, MATHVERS, "tcp"); if (cl == NULL) { clnt_pcreateerror(argv[1]); exit(1); } numbers.a = atoi(argv[2]); numbers.b = atoi(argv[3]);
result = add_1(&numbers, cl); if (result == NULL) { clnt_perror(cl, "add_1"); exit(1); } printf("The add (%d, %d) procedure returned %d\n", numbers.a, numbers.b, *result);
result = multiply_1(&numbers, cl); if (result == NULL) { clnt_perror(cl, "multiply_1"); exit(1); } printf("The multiply(%d, %d) procedure returned %d\n", numbers.a, numbers.b, *result);
result = cube_1(&numbers.a, cl); if (result == NULL) { clnt_perror(cl, "cube_1"); exit(1); } printf("The cube (%d) procedure returned %d\n", numbers.a, *result); exit(0); } |
在上例中使用一個特殊函數 clnt_creat(),此為客戶端連接遠端程序的功能呼叫,我們將在下一節介紹。
9-5-5 編譯程式與執行 - cc
● 伺服器端編譯如下:(lnsl 中的 l 是 L 的小寫)
$ cc –o math_proc math_proc.c math_svc.c math_xdr.c –lnsl
● 客戶端編譯如下:
$ cc –o math_req math_req.c math_clnt.c math_xdr.c –lnsl
● 伺服器端執行如下:(主機位址 163.15.2.62)
$ math_proc &
如欲刪除伺服程式,則利用 ps –ef 查詢出該程式的 PID 號碼,再利用 kill 命令將其刪除。
● 客戶端執行如下:
$ math_req 163.15.2.62 45 21
執行結果如下:
$ math_req 163.15.2.62 45 21 The add (45, 21) procedure returned 66 The multiply(45, 21) procedure returned 945 The cube (45) procedure returned 91125 |
9-5-6 RPC 訊息報告 – rpcinfo
我們可以利用 rpcinfo 命令來查詢 RPC 的使用情況,命令格式如下:(root 系統管理者才有權限使用)
# rpcinfo –p [host]
# rpcinfo [-n portnum] –u host program [version]
# rpcinfo [-n portnum] –t host program [version]
# rpcinfo –b program version
# rpcinfo –d program version
如查詢本機電腦上的 RPC 訊息如下;
# rpcinfo –p program vers proto port 100000 2 tcp 111 portmapper 100000 2 udp 111 portmapper 100021 1 udp 1024 nlockmgr 100021 3 udp 1024 nlockmgr 100021 1 tcp 1024 nlockmgr 100021 3 tcp 1024 nlockmgr 100024 1 udp 1013 status 100024 1 tcp 1015 status 536870920 1 udp 1027 536870920 1 tcp 1026 536870913 1 udp 1028 536870913 1 tcp 1027 |