一、概述
| 在傳統的編程概念中,過程是由程式員在本地編譯完成,並只能局限在本地啟動並執行一段代碼,也即其主程式和過程之間的運行關係是本地調用關係。因此這種結構在網路日益發展的今天已無法適應實際需求。總而言之,傳統程序呼叫模式無法充分利用網路上其他主機的資源(如CPU、 Memory等),也無法提高代碼在實體間的共用程度,使得主機資源大量浪費。 |
| 而本文要介紹的RPC編程,正是很好地解決了傳統過程所存在的一系列弊端。通過RPC我們可以充分利用非共用記憶體的多處理器環境(例如通過區域網路串連的多台工作站),這樣可以簡便地將你的應用分布在多台工作站上,應用程式就像運行在一個多處理器的電腦上一樣。你可以方便的實現過程代碼共用,提高系統資源的利用率,也可以將以大量數值處理的操作放在處理能力較強的系統上運行,從而減輕前端機的負擔。 |
如前所述RPC其實也是一種C/S的編程模式,有點類似C/S Socket 編程模式,但要比它更高一層。當我們在建立RPC服務以後,用戶端的調用參數通過底層的RPC傳輸通道,可以是UDP,也可以是TCP(也即TI-RPC —無關性傳輸),並根據傳輸前所提供的目的地址及RPC上層應用程式號轉至相應的RPC Application Porgramme Server ,且此時的用戶端處於等待狀態,直至收到應答或Time Out逾時訊號。具體的流程圖如圖1。當伺服器端獲得了請求訊息,則會根據註冊RPC時告訴RPC系統的常式入口地址,執行相應的操作,並將結果返回至用戶端。
當一次RPC調用結束後,相應線程發送相應的訊號,用戶端程式才會繼續運行。
| 當然,一台服務主機上可以有多個遠程過程提供服務,那麼如何來表示一個唯一存在的遠程過程呢。一個遠程過程是有三個要素來唯一確定的:程式號、版本號碼和過程號。程式號是用來區別一組相關的並且具有唯一過程號的遠程過程。一個程式可以有一個或幾個不同的版本,而每個版本的程式都包含一系列能被遠程調用的過程,通過版本的引入,使得不同版本下的RPC能同時提供服務。每個版本都包含有許多可供遠程調用的過程,每個過程則有其唯一標示的過程號。 |
| 通過以上對RPC原理的簡介後,我們再來繼續討論如何來開發基於RPC的應用系統。一般而言在開發RPC時,我們通常分為三個步驟: |
| 這裡所說的通訊協定是指定義服務過程的名稱、調用參數的資料類型和返回參數的資料類型,還包括底層傳輸類型(可以是 UDP或TCP),當然也可以由RPC底層函數自動選擇連線類型建立TI-RPC。最簡單的協議產生的方法是採用協議編譯工具,常用的有Rpcgen,我會在後面執行個體中詳細描述其使用方法。 |
| 開發用戶端和伺服器端的程式時,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:
/* time.x: Remote time printing protocol */
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(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 message ", 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");
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 time ",argv[0]);
exit(1);
}
printf("From the Time Server ...%s ",result);
clnt_destroy( clnt );
exit(0);
}
有了以上的三段代碼後,就可用rpcgen 編譯工具進行RPC協議編譯,命令如下:
$rpcgen 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 //