簡介
RPC(Remote Procedure Call,遠程過程調用)是建立在Socket之上的,出於一種類比的願望,在一臺機器上運行的主程序,可以調用另一臺機器上準備好的子程序,就像LPC(本地過程調用).越底層,代碼越複雜、靈活性越高、效率越高;越上層,抽象封裝的越好、代碼越簡單、效率越差。Socket和RPC的區別再次說明了這點。在傳統的編程概念中,過程是由程序員在本地編譯完成,並只能侷限在本地運行的一段代碼,也即其主程序和過程之間的運行關係是本地調用關係。因此這種結構在網絡日益發展的今天已無法適應實際需求。衆所周知,傳統過程調用模式無法充分利用網絡上其他主機的資源(如CPU、Memory等),也無法提高代碼在實體間的共享程度,使得主機資源大量浪費。
通過RPC我們可以充分利用非共享內存的多處理器環境(例如通過局域網連接得多臺工作站),這樣可以簡便地將你的應用分佈在多臺工作站上,應用程序就像運行在一個多處理器的計算機上一樣。你可以方便的實現過程代碼共享,提高系統資源的利用率,也可以將以大量數值處理的操作放在處理能力較強的系統上運行,從而減輕前端機的負擔。
RPC作爲普遍的C/S開發方法,開發效率高效,可靠.但RPC方法的基本原則是--以模塊調用的簡單性忽略通訊的具體細節,以便程序員不用關心C/S之間的通訊協議,集中精力對付實現過程.這就決定了 RPC生成的通訊包不可能對每種應用都有最恰當的處理辦法,與Socket方法相比,傳輸相同的有效數據,RPC佔用更多的網絡帶寬.
RPC是在Socket的基礎上實現的,它比socket需要更多的網絡和系統資源.另外,在對程序優化時,程序員雖然可以直接修改由rpcgen產生的令人費解的源程序,但對於追求程序設計高效率的RPC而言,獲得的簡單性則被大大削弱.
RPC的結構原理及其調用機制
如前所述RPC其實也是種C/S的編程模式,有點類似C/S Socket 編程模式,但要比它更高一層。當我們在建立RPC服務以後,客戶端的調用參數通過底層的RPC傳輸通道,可以是UDP,也可以是TCP(也即TI-RPC—無關性傳輸),並根據傳輸前所提供的目的地址及RPC上層應用程序號轉至相應的RPC應用程序服務端,且此時的客戶端處於等待狀態,直至收到應答或Time Out超時信號。當服務器端獲得請求消息,則會根據註冊RPC時告訴RPC系統的例程入口地址,執行相應的操作,並將結果返回至客戶端。
當一次RPC調用結束後,相應線程發送相應的信號,客戶端程序纔會繼續運行。
在這個過程中,一個遠程過程是有三個要素來唯一確定的:程序號、版本號和過程號。
程序號是用來區別一組相關的並且具有唯一過程好的遠程過程。一個程序可以有一個或幾個不同的版本,而每個版本的程序都包含一系列能被遠程調用的過程,通過版本的引入,使得不同版本下的RPC能同時提供服務。每個版本都包含有許多可供遠程調用的過程,每個過程則有其唯一標示的過程號。
基於RPC的應用系統開發
通過以上對RPC原理的簡介後,我們再來繼續討論如何來開發基於RPC的應用系統。
一般而言在開發RPC時,我們通常分爲三個步驟:
a、 定義說明客戶/服務器的通信協議:這裏所說的通信協議是指定義服務過程的名稱、調用參數的數據類型和返回參數的數據類型,還包括底層傳輸類型(可以是UDP或TCP),當然也可以由RPC底層函數自動選擇連接類型建立TI-RPC。最簡單的協議生成的方法是採用協議編譯工具,常用的有Rpcgen,我會在後面實例中詳細描述其使用方法。
b、 開發客戶端程序。
c、 開發服務器端程序。
開發客戶端和服務器端的程序時,RPC提供了我們不同層次的開發例程調用接口。不同層次的接口提供了對RPC不同程度控制。一般可分爲5個等級的編程接口,接下來我們分別討論一下各層所提供的功能函數。
簡單層例程
簡單層是面向普通RPC應用,爲了快速開發RPC應用服務而設計的,他提供瞭如下功能函數。
函數名 |
功能描述 |
Rpc_reg( ) |
在一特定類型的傳輸層上註冊某個過程,來作爲提供服務的RPC程序 |
Rpc_call( ) |
遠程調用在指定主機上指定的過程 |
Rpc_Broadcast( ) |
向指定類型的所有傳輸端口上廣播一個遠程過程調用請求 |
高層例程
在這一層,程序需要在發出調用請求前先創建一個客戶端句柄,或是在偵聽請求前先建立一個服務器端句柄。程序在該層可以自由的將自己的應用綁在所有的傳輸端口上,它提供瞭如下功能函數。
函數名 |
功能描述 |
Clnt_create( ) |
程序通過這個功能調用,告訴底層RPC服務器的位置及其傳輸類型 |
Clnt_create_timed( ) |
定義每次嘗試連接的超時最大時間 |
Svc_create( ) |
在指定類型的傳輸端口上建立服務器句柄,告訴底層RPC事件過程的相應入口地址 |
Clnt_call() |
向服務器端發出一個RPC調用請求 |
中間層例程
中間層向程序提供更爲詳細的RPC控制接口,而這一層的代碼變得更爲複雜,但運行也更爲有效,它提供瞭如下功能函數。
函數名 |
功能描述 |
Clnt_tp_create( ) |
在指定的傳輸端口上建立客戶端句柄 |
Clnt_tp_create_timed( ) |
定義最大傳輸時延 |
Svc_tp_creaet( ) |
在指定的傳輸端口上建立服務句柄 |
Clnt_call( ) |
向服務器端發出RPC調用請求 |
專家層例程
這層提供了更多的一系列與傳輸相關的功能調用,它提供瞭如下功能函數。
函數名 |
功能描述 |
Clnt_tli_create( ) |
在指定的傳輸端口上建立客戶端句柄 |
Svc_tli_create( ) |
在指定的傳輸端口上建立服務句柄 |
Rpcb_set( ) |
通過調用rpcbind將RPC服務和網絡地址做映射 |
Rpcb_unset( ) |
刪除rpcb_set( ) 所建的映射關係 |
Rpcb_getaddr( ) |
調用rpcbind來犯會指定RPC服務所對應的傳輸地址 |
Svc_reg( ) |
將指定的程序和版本號與相應的時間例程建起關聯 |
Svc_ureg( ) |
刪除有svc_reg( ) 所建的關聯 |
Clnt_call( ) |
客戶端向指定的服務器端發起RPC請求 |
底層例程
該層提供了所有對傳輸選項進行控制的調用接口,它提供瞭如下功能函數。
函數名 |
功能描述 |
Clnt_dg_create( ) |
採用無連接方式向遠程過程在客戶端建立客戶句柄 |
Svc_dg_create( ) |
採用無連接方式建立服務句柄 |
Clnt_vc_create( ) |
採用面向連接的方式建立客戶句柄 |
Svc_vc_create( ) |
採用面向連接的方式建立RPC服務句柄 |
Clnt_call( ) |
客戶端向服務器端發送調用請求 |
實例介紹
以下我將通過實例向讀者介紹通過簡單層RPC的實現方法。通常在此過程中我們將使用RPC協議編譯工具—Rpcgen。Rpcgen 工具用來生成遠程程序接口模塊,它將以RPC語言書寫的源代碼進行編譯,Rpc 語言在結構和語法上同C語言相似。由Rpcgen 編譯生成的C源程序可以直接用C編譯器進行編譯,因此整個編譯工作將分爲兩個部分。Rpcgen的源程序以.x結尾,通過其編譯將生成如下文件:
a) 一個頭文件(.h)包括服務器和客戶端程序變量、常量、類型等說明。
b) 一系列的XDR例程,它可以對頭文件中定義的數據類型進行處理。
c) 一個Server 端的標準程序框架。
d) 一個Client 端的標準程序框架。
當然,這些輸出可以是選擇性的,Rpcgen 的編譯選項說明如下:
選項 |
功能 |
‘-’ a |
生成所有的模板文件 |
‘-’ Sc |
生成客戶端的模板文件 |
‘-’ Ss |
生成服務器端的模板文件 |
‘-’ Sm |
生成Makefile 文件 |
(詳見Solaris Rpcgen Manaul)
Rpcgen 源程序 time.x:
- program TIMEPROG {
- version PRINTIMEVERS {
- string PRINTIME(string) = 1; /* 過程號 */
- } = 1; /* 版本號 */
- } = 0x20000001; /* 程序號 */
time_proc.c源程序:
- /* time_proc.c: implementation of the remote procedure "printime" */
- #include <stdio.h>
- #include <rpc/rpc.h> /* always needed */
- #include "time.h" /* time.h will be generated by rpcgen */
- #include <time.h>
- /* Remote version of "printime" */
- char ** printime_1_svc(char **msg,struct svc_req *req)
- {
- static char * result; /* must be static! */
- static char tmp_char[100];
- time_t rawtime;
- FILE *f;
- f = fopen("/tmp/rpc_result", "a+");
- if (f == (FILE *)NULL) {
- strcpy(tmp_char,"Error");
- result = tmp_char;;
- return (&result);
- }
- fprintf(f, "%s", *msg); //used for debugging
- fclose(f);
- time(&rawtime);
- sprintf(tmp_char,"Current time is :%s",ctime(&rawtime));
- result =tmp_char;
- return (&result);
- }
rtime.c源代碼
- /*
- * rtime.c: remote version
- * of "printime.c"
- */
- #include <stdio.h>
- #include "time.h" /* time.h generated by rpcgen */
- main(int argc, char **argv)
- {
- CLIENT *clnt;
- char *result;
- char *server;
- char *message;
- if (argc != 3) {
- fprintf(stderr, "usage: %s host messagen", argv[0]);
- exit(1);
- }
- server = argv[1];
- message = argv[2];
- /*
- * Create client "handle" used for
- * calling TIMEPROG on the server
- * designated on the command line.
- */
- //clnt = clnt_create(server, TIMEPROG, PRINTIMEVERS, "visible");
- clnt = clnt_create(server, TIMEPROG, PRINTIMEVERS, "TCP");
- if (clnt == (CLIENT *)NULL) {
- /*
- * Couldn't establish connection
- * with server.
- * Print error message and die.
- */
- clnt_pcreateerror(server);
- exit(1);
- }
- /*
- * Call the remote procedure
- * "printime" on the server
- */
- result =*printime_1(&message,clnt);
- if (result== (char *)NULL) {
- /*
- * An error occurred while calling
- * the server.
- * Print error message and die.
- */
- clnt_perror(clnt, server);
- exit(1);
- }
- /* Okay, we successfully called
- * the remote procedure.
- */
- if (strcmp(result,"Error")==0){
- /*
- * Server was unable to print
- * the time.
- * Print error message and die.
- */
- fprintf(stderr, "%s: could not get the timen",argv[0]);
- exit(1);
- }
- printf("From the Time Server ...%s",result);
- clnt_destroy( clnt );
- exit(0);
- }
編譯方法
有了以上的三段代碼後,就可用rpcgen 編譯工具進行RPC協議編譯,命令如下:
$rpcgen time.x(rpcgen -C -a -M -N time.x(多個參數時))
rpcgen 會自動生成time.h、time_svc.c、time_clnt.c
再用系統提供的gcc進行C的編譯,命令如下:
$gcc rtime.c time_clnt.c -o rtime –lnsl //客戶端編譯
$gcc time_proc.c time_svc.c -o time_server –lnsl //服務器端編譯
編譯成功後即可在Server端運行time_server,立即將該服務綁定在rpc服務端口上提供服務。在客戶端運行./rdate hostname msg (msg 是一字符串,筆者用來測試時建立的),立即會返回hostname 端的時間。
出錯問題
Cannot register service
RPC: Authentication error; why = Client credential too weak unable to register (TIMEPROG, PRINTIMEVERS, udp)
解決方法:使用root賬戶即可(同時檢查portmap是否安裝了)
time_proc.c:7: error: conflicting types for printime_1?
源程序time_proc.c中報錯
解決方法,改爲char ** printime_1_svc(char **msg,struct svc_req *req)即可。
RPC: Unknown protocol
這個原因可能是rpc依賴包沒有安裝,或者使用函數clnt_create的時候,最後一個參數應該不使用visible,而是tcp。
參考文檔:http://docs.freebsd.org/44doc/psd/22.rpcgen/paper.pdf
Linux.C 高級程序員指南