SunRPC

情報学類 分散システム					2008年01月08日

                                       筑波大学システム情報工学研究科
                                       コンピュータサイエンス専攻, 電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~yas/coins/dsys-2007/2008-01-08
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~yas/
http://www.cs.tsukuba.ac.jp/~yas/

■SunRPC

SunRPC は、Sun Microsystems社により開発され、仕様やソースコードが 公開された RPC の実装。ONC RPC (Open Network Computing) とも呼ばれる。 RFC にもなっている。

◆rpcgenコマンド

SunRPC のスタブ生成器は、 rpcgen というコマンド。

◆rpcgenコマンドとファイル

rpcgen コマンドを使うには、次のようなファイルを作成する。

rpcgenによるRPCプログラム開発で利用するファイル

図? rpcgenによるRPCプログラム開発で利用するファイル

name.x
インタフェースを記述。
name_client.c
クライアント側の main プログラム。
name_server.c
サーバ側で、RPC で呼び出されるプログラム。 (main()関数は、rpcgen により自動生成される。)

◆rpcgenコマンドの使い方

% rpcgen name.x [←]
次の4つのファイルが生成される。
name.h
そのRPCのプログラムで使う定数、データ構造、スタブ手続きのインタフェー ス。
name_clnt.c
クライアント側のスタブ。
name_xdr.c
name.x で定義したデータ構造について、 XDR のための手続き(整列化と非整列化を行なう手続き) 。 クライアント側とサーバ側の両方で使われる。
name_svc.c
サーバ側の main 関数とディスパッチ手続き。受け付けた RPC の要求を解析 して、開発者が定義した手続きを呼び出す。
これらのファイルの内容は、人間が十分読める。

◆手続きの識別

SunRPCでは、手続きの識別を、次のような情報で行う

◆portmap (portmapper)

クライアント、サーバ、portmapper

図? portmapper の働き

SunRPCでは, 最終的にはTCP/IPまたはUDP/IPでデータが送られる。プログラム 番号などTCP/IPまたはUDP/IPのポート番号を対応させる必要がある。

各ホストには portmap (portmapper) というサーバがいて、3つ組

プログラム番号,バージョン,プロトコル
を、TCP/IPまたはUDP/IPのポート番号へ変換する。

◆rpcinfo

portmap の情報は、 rpcinfo コマンドで表示できる。

% rpcinfo -p [←]
   program vers proto   port
    100000    2   tcp    111  portmapper
    100000    2   udp    111  portmapper
    100007    2   tcp   1024  ypbind
    100007    2   udp   1027  ypbind
    100007    1   tcp   1024  ypbind
    100007    1   udp   1027  ypbind
    100005    1   udp    831  mountd
    100005    2   udp    831  mountd
    100005    1   tcp    834  mountd
    100005    2   tcp    834  mountd
    100003    2   udp   2049  nfs
    100001    2   udp   4193  rstatd
    100001    3   udp   4193  rstatd
    100001    4   udp   4193  rstatd
% []
他のホストの情報も調べられる。
% rpcinfo -p hostname [←]
サーバは、起動時に、portmap に登録する。
bool_t
pmap_set(program, version, protocol, port)
		  u_long program;
		  u_long version;
		  int protocol;
		  u_short port;
クライアントは、呼び出す前に、ポート番号を調べる。
u_short
pmap_getport(address, program, version, protocol)
		      struct sockaddr_in *address;
		      u_long program;
		      u_long version;
		      u_int protocol;

スタブが自動的に呼び出すので、普段は気にすることはない。

portmap 自身も RPC で動いている。portmap の自身のポート番号は、111 と 決まっている。

■ディレクトリ一覧サービス

ディレクトリ一覧サービス(dirlist)。

◆インタフェース

   1:	struct delist {
   2:	        string del_name<> ;
   3:	        struct delist *del_next ;
   4:	};
   5:	
   6:	union dirlist_res
   7:	switch(int dlr_errno)
   8:	{
   9:	case 0:
  10:	        delist *dlr_head;
  11:	default:
  12:	        void;
  13:	};
  14:	
  15:	program DIRLIST_PROG {
  16:	    version DIRLIST_VERSION {
  17:	        dirlist_res DIRLIST(string) = 11 ;
  18:	    } = 1;
  19:	} = 0x20001003 ;
17行目が手続きの定義。 DIRLIST が手続きの名前(手続き番号を示す定数の定義)。

この手続きの引数は、string 型、結果は、dirlist_res 型。SunRPC では、基本的には引数も結果も1つずつ。複数必要な時には、構造体を 定義する。

手続き DIRLIST の定義を program 番号と version 番号の括弧が取り囲んでいる。

dirlist_res は、共用体(可変長の構造体、Pascal 可変長レコード)。

dlr_errno の値が 0 の時
delist 型へのポインタ dlr_head
その他
何も送らない(dlr_errnoのみ)

struct delist は、自分自身へのポインタ(del_next)を含むリスト構造。

bin,lib,etc の3つの要素を含む。

図? struct delist の構造

一般的に RPC では、ポインタを含むデータ構造を送ることはできない。 SunRPC では、ポインタの先の1要素を再帰的に送る機能がある。 これにより、木構造や線形リストを送ることができる。

string del_name<> は、文字列型。可変長(<>)で最 大文字数には定められていない(<>の中が空)。

◆ヘッダファイル

インタフェース定義から、rpcgen コマンドによりヘッダファイル、クライア ント側スタブ、サーバ側スタブ、XDR によるマーシャリングを行うプログラム が生成される。

ヘッダファイルの主要部分は、以下の通りである。

[dirlist.h]

struct delist {
	char *del_name;
	struct delist *del_next;
};
typedef struct delist delist;

struct dirlist_res {
	int dlr_errno;
	union {
		delist *dlr_head;
	} dirlist_res_u;
};
typedef struct dirlist_res dirlist_res;

#define DIRLIST_PROG ((u_long)0x20001003)
#define DIRLIST_VERSION ((u_long)1)

#define DIRLIST ((u_long)11)
extern  dirlist_res * dirlist_1(char **, CLIENT *);
extern  dirlist_res * dirlist_1_svc(char **, struct svc_req *);

◆サーバ側のプログラム

[dirlist_server.c]

:	/*
   2:	        dirlist_server.c -- ディレクトリの内容を表示するRPCのプログラム(サーバ側)
   3:	        Created on: 2006/01/18 20:33:31
   4:	*/
   5:	
   6:	#include <sys/types.h>          /* opendir(2) */
   7:	#include <dirent.h>             /* opendir(2) */
   8:	#include <errno.h>              /* errno */
   9:	#include <stdlib.h>             /* malloc() */
  10:	#include <string.h>             /* strlen() */
  11:	#include <stdio.h>              /* snprintf() */
  12:	
  13:	#include "dirlist.h"
  14:	static struct delist *make_delist( DIR *dirp );
  15:	
  16:	dirlist_res *
  17:	dirlist_1_svc(char **argp, struct svc_req *rqstp)
  18:	{
  19:	    static dirlist_res  result;
  20:	    DIR *dirp ;
  21:	    char *dirname ;
  22:	
  23:	        xdr_free((xdrproc_t)xdr_dirlist_res, (char *)&result);
  24:	
  25:	        dirname = *argp ;
  26:	        dirp = opendir( dirname );
  27:	        if( dirp == 0 )
  28:	        {
  29:	            result.dlr_errno = errno ;
  30:	            return( &result );
  31:	        }
  32:	        result.dlr_errno = 0;
  33:	        result.dirlist_res_u.dlr_head = make_delist( dirp );
  34:	        closedir( dirp );
  35:	        return( &result );
  36:	}
  37:	
サーバ側では、手続き dirlist_1_svc() を記述する。 引数と結果は、rpcgen のソースで定義した構造体へのポインタ。

結果を返す時の構造体へのポインタを返す方法が問題。自動変数(auto 変数) にすると、呼出し側に戻った瞬間に無効になる。よく使われるのは、静的変数 (static 変数)に結果を代入して、返すことだが、マルチスレッドプログラミ ングでは大いに問題になる。

opendir(), readdir(), closedir() は、ディレクトリの内容を得るためのラ イブラリ関数。次のようなコードで、"/" の内容が表示される(ls -f /)。

	dirp = opendir( "/" );
	while( (dp = readdir(dirp)) != NULL )
	{
	    printf("%s\n", dp->d_name );
	}
	closedir( dirp );
readdir() は、make_delist() の中で行われる。
  38:	static struct delist *
  39:	make_delist( DIR *dirp )
  40:	{
  41:	    struct dirent *dp ;
  42:	    struct delist *del ;
  43:	    int namelen;
  44:	        if( (dp = readdir(dirp)) == NULL )
  45:	        {
  46:	            return( 0 );
  47:	        }
  48:	        else
  49:	        {
  50:	            del = malloc(sizeof(struct delist));
  51:	            namelen = strlen( dp->d_name );
  52:	            del->del_name = malloc( namelen+1 );
  53:	            snprintf(del->del_name,namelen+1,"%s",dp->d_name );
  54:	            del->del_next = make_delist( dirp );
  55:	            return( del );
  56:	        }
  57:	}
make_delist() は、再帰呼び出しで、struct delist のリストを作る。

自動生成されるサーバのmainプログラムでは、 ポートマッパ(port mapper) へのプログラム番号とバージョン番号の登録される。

◆クライアント側のプログラム

[dirlist_client.c]
   1:	/*
   2:	        dirlist_client.c -- ディレクトリの内容を表示するRPCのプログラム(クライアント側)
   3:	        Created on: 2006/01/18 20:57:49
   4:	*/
   5:	
   6:	#include <stdlib.h>             /* exit() */
   7:	#include <stdio.h>              /* printf() */
   8:	
   9:	#include "dirlist.h"
  10:	
  11:	main( int argc, char *argv[] )
  12:	{
  13:	        if( argc != 3 )
  14:	        {
  15:	            fprintf(stderr,"usage: %% %s server_host dir\n", argv[0]);
  16:	            exit( 1 );
  17:	        }
  18:	        dirlist( argv[1], argv[2] );
  19:	}
  20:	
  21:	dirlist(char *host, char *dir)
  22:	{
  23:	        CLIENT *clnt;
  24:	        dirlist_res  *result;
  25:	        char *arg;
  26:	
  27:	        clnt = clnt_create (host, DIRLIST_PROG, DIRLIST_VERSION, "tcp");
  28:	        if( clnt == NULL )
  29:	        {
  30:	            clnt_pcreateerror( host );
  31:	            exit( 1 );
  32:	        }
  33:	
  34:	        arg = dir;
  35:	        result = dirlist_1( &arg, clnt );
  36:	        if( result == NULL )
  37:	        {
  38:	            clnt_perror( clnt, "call failed");
  39:	            exit( 1 );
  40:	        }
  41:	        print_dirlist_res( result );
  42:	        xdr_free( (xdrproc_t)xdr_dirlist_res, (char *)result );
  43:	        clnt_destroy( clnt );
  44:	}
  45:	

main() の引数は、サーバが動作しているホスト名とディレクトリ。

clnt_create() は、CLIENT 構造体を確保する。 引数は、サーバのホスト名、 プログラム番号バージョン番号、 通信に使うプロトコルである。

33行目で呼び出している dirlist_1() が、rpcgen によ り生成されたスタブ。引数は、インタフェースで定義された構造体 dirlistargへのポインタと、CLIENT 構造体へ のポインタ。結果は、インタフェースで定義された構造体 dirlistresへのポインタである。この関数は、rpcgen が生成する *_clnt.c というファイルに含まれてる。

スタブ dirlist_1()は、成功すると内部で malloc() を呼 び出しメモリを確保して、そこにサーバから受け取った応答メッセージを 非整列化(unmarshalling) して保存する。このメモリは、利用し終わると xdr_free()で 解放する。

	free( result );
だと、トップレベルの構造体のメモリしか解放されない。内部に含まれている 構造体へのポインタや文字列のメモリを解放するためには、 xdr_free()を呼び出す。

  46:	print_dirlist_res( dirlist_res *result )
  47:	{
  48:	        printf("errno: %d (%s)\n",
  49:	               result->dlr_errno, strerror(result->dlr_errno));
  50:	        switch( result->dlr_errno )
  51:	        {
  52:	        case 0:
  53:	            print_delist( result->dirlist_res_u.dlr_head );
  54:	            break;
  55:	        default:
  56:	            break;
  57:	        }
  58:	}
  59:	
  60:	print_delist( struct delist *del )
  61:	{
  62:	        if( del == NULL )
  63:	        {
  64:	        }
  65:	        else
  66:	        {
  67:	            printf("%s\n",del->del_name );
  68:	            print_delist( del->del_next );
  69:	        }
  70:	}
print_dirlist_res() は、struct dirlist_res を表示する。 print_delist() は、struct delist を表示する。 構造体が再帰している所で、関数も再帰している。

◆コンパイル

% wget http://www.coins.tsukuba.ac.jp/~yas/coins/dsys-2007/2008-01-08/ex/dirlist.x [←]
% wget http://www.coins.tsukuba.ac.jp/~yas/coins/dsys-2007/2008-01-08/ex/dirlist_server.c [←]
% wget http://www.coins.tsukuba.ac.jp/~yas/coins/dsys-2007/2008-01-08/ex/dirlist_client.c [←]
% wget http://www.coins.tsukuba.ac.jp/~yas/coins/dsys-2007/2008-01-08/ex/Makefile [←]
% emacs Makefile [←]
% make [←]
rpcgen  dirlist.x
gcc -g -DDEBUG    -c -o dirlist_clnt.o dirlist_clnt.c
gcc -g -DDEBUG    -c -o dirlist_client.o dirlist_client.c
rpcgen  dirlist.x
gcc -g -DDEBUG    -c -o dirlist_xdr.o dirlist_xdr.c
gcc -g -DDEBUG     -o dirlist_client  dirlist_clnt.o dirlist_client.o dirlist_xdr.o  
gcc -g -DDEBUG    -c -o dirlist_svc.o dirlist_svc.c
gcc -g -DDEBUG    -c -o dirlist_server.o dirlist_server.c
gcc -g -DDEBUG     -o dirlist_server  dirlist_svc.o dirlist_server.o dirlist_xdr.o 
% []
Solaris, Linux では、-lnsl が必要。Solaris, MacOSX では、-DDEBUG をつ けてコンパイルとfork() しないでフォアグランドで動くサーバができる。

◆サーバ側の実行

実行する時には、ウインドウを2つ開いて、それぞれでサーバとクライアント を走らせる。まずサーバ側から先に実行する。
% ./dirlist_server [←]
サーバ側は自動的には止まらないので、止めたい時には、^C を押す。

サーバを動かしているホストの別の端末で rpcinfo -p を実行すると、プログ ラム番号536875011 (0x20001003) のプログラムが表示される。

% rpcinfo -p [←]
   program vers proto   port
    100000    2   tcp    111  portmapper
    100000    2   udp    111  portmapper
    100024    1   udp   1013  status
    100024    1   tcp    974  status
    100021    0   udp   1000  nlockmgr
    100021    1   udp   1000  nlockmgr
    100021    3   udp   1000  nlockmgr
    100021    4   udp   1000  nlockmgr
    100021    0   tcp    973  nlockmgr
    100021    1   tcp    973  nlockmgr
    100021    3   tcp    973  nlockmgr
    100021    4   tcp    973  nlockmgr
 536875011    1   udp  53972
 536875011    1   tcp  60723
% []

MacOSX 10.4 では、portmapp が動作していないことがある。rpcinfo -p で何 も表示されない時には、root で 次のようにして実行する。

launchctl start com.apple.portmap

◆クライアント側の実行

サーバと同じホストで動かす時には、ホスト名には、localhost を使うと便利。
% ./dirlist_client localhost /usr [←]
errno: 0 (Unknown error: 0)
.
..
.DS_Store
bin
epkg
include
info
lib
libexec
local
local3
sbin
share
standalone
X11R6
% []

■SunRPC で使えるデータ型

RPCのインタフェースで使えるデータ構造は、C言語とほぼ同じであるが、可変 長のデータが扱えるよう拡張されてる部分や、制限されている部分がある。

◆基本型

◆列挙型

ほぼC言語と同等に書ける。自動的に typedef される。

◆構造体

ほぼC言語と同等に書れる。自動的に typedef される。

◆定数

RPCのインタフェースの定義では、定数を記述することができる。

const LSIZE = 80 ;
これは、rpcgen により、C言語のプリプロセッサのマクロ #defineに置き換えられる。

◆配列

RPCのインタフェースの定義では、配列を定義することができる。配列には、 固定長と可変長の2種類あり、固定長は、C言語と同じ。可変長は、次のよう に[]の代わりに<>とする。

int varray<>;

<> の中には、最大長を書くこともできる。これは、 rpcgen により、次のような構造体に置き換えられる。
struct {
	u_int varray_len;
	int *varray_val;
} varray;
typedef struct varray varray ;
C言語でプログラムを作成する時には、この varray_len に要素数を、 varray_val に、配列の先頭のポインタをセットする。

int a1[10] ;

varray v1 ;
	v1.varray_len = 10 ;
	v1.varray_val = &a1[0] ;

◆文字列

文字列を送るには、特殊な string 型を使う。
string s<> ;
これは、C言語では、char * になるが、文字列のつもりで次のよう に書いても、文字列は送られない。
char *s ;
これも、rpcgen により、やはり char * にコンパイルされる が、この場合、ポインタの先の1文字しか送られない。C言語の文字列を送り たい時には、必ず string を使うこと。

◆ポインタ

SunRPC ではポインタ型も送ること ができるが、ポインタの先の1要素しか送られない。 複数要素を送りたい時には、配列を使う。

argv

C言語の main の引数と同様の構造を送りたい時には、次のようにする。

typedef string argstr_t<>; 
typedef argstr_t argvt<>;

◆opaque(不定形)

大量のデータをそのまま送るには、 opaque 型 ( 不定形型 ) を使う。これには固定長と可変長がある。
opaque fileblock[512] ;
opaque filedata<> ;
opaque の代わりにchar の配列にすると、文字と見なして1 文字1文字変換が行なわれることになり、非常に遅くなる。場合によっては文 字コードの変換が行なわれる。opaque では、そのような変換は一切 行なわれず、そのままの形で送られる。

◆union共用体

RPCのインタフェースの定義では、共用体(可変長の構造体)が書ける。
union int_result_t
switch( int status )
{
case OK:
	int	data ;
default:
	void;
};
これは、次のようにコンパイルされる。
struct int_result_t {
        int status;
        union {
                int data;
        } int_result_t_u;
};
status という値がOK の時だけdata が有効になる。 すなわち、その時だけ実際にネットワークにたいしてdata の部分が 送られる。

◆bool_t

RPCのインタフェースの定義では、bool_t という型が使える。値は、 TRUEFALSE

XDRによる構造体のファイルへの保存

Sunの技術で、構造体をファイルに保存することができる。

構造体を整列化し、他のプロセスに通信メッセージとして送る代わりにファイ ルに保存する。

SunRPCでは、xdrstdio_create() という関数が用意されている。 ストリーム入出力(FILE *) に対して構造体を読み書きすることができるようになる。

RPCと同様に、構造体にポインタが含まれていた場合、再帰的にファイルに保 存される。異なる機種で読み出すことができる。

■練習問題

RPCのプログラムの開発は、次のような手順で行うことが多い。 ヒント: インタフェース記述やクライアント側のプログラムは、例題をプロト タイプとする方法がある。つまり、まずは、例題をコピーし、プログラム番号 を書き換え、不要な部分を削除する。その後、必要な部分を書き足す。

練習問題(1) カウンタ

次のような手続きを持つカウンタを RPC サーバとして実現しなさい。

練習問題(2) 整数のスタック

次のようなインタフェースを持つハッシュ表を RPC で実現しなさい。

[intstack.x]

enum intstack_error {
	INTSTACK_OK = 0,
	INTSTACK_OVERFLOW = 1,
	INTSTACK_UNDERFLOW = 2
};

struct pop_result {
    int data;
    int error;
};

program INTSTACK_PROG { 
   version INTSTACK_VERSION {
       intstack_error	PUSH(int)  = 11 ;
       pop_result	POP(void)  = 12 ;
   } = 1 ;
} = 0x20051002 ;

練習問題(3) 整数のスタック(演算付き)

練習問題(2) で、次のような手続きを追加しなさい。
add()
スタックから要素を2つ取り出し、加算を行い、結果をスタックに保存する。
sub()
スタックから要素を2つ取り出し、減算を行い、結果をスタックに保存する。
negate()
スタックから要素を1つ取り出し、符号を反転し、結果をスタックに保存する。
スタック・アンダーフローが起きたときには、エラーを返しなさい。

練習問題(4) 整数の待ち行列

練習問題(2) を参考にし て、待ち行列(queue)を作成しなさい。

練習問題(5) dirlistlongサービス

ディレクトリ一覧サービス(dirlist)を拡張 して、ls -l の用に、型、モード、サイズ、最終更新時刻などを表示できるよ うにしなさい。

ヒント:stat() システムコールや lstat() システムコールでファイルの属性 を得る。RPC では、単に string を返すのではなく、属性も返す。 返す属性は、自分で .x ファイルに定義して、struct stat からコピーする。 サーバ側で文字列に変換する方法も考えられるが、クライアント側で printf() 等で文字列にすることが望ましい。

struct stat にある属性のうち、数個を送ればよい。全てを送らなくてもよい。

練習問題(6) ハッシュ表

次のようなインタフェースを持つハッシュ表を RPC で実現しなさい。

[hashtable.x]

typedef string key_t<256>;
struct keyvalue_t { 
   key_t key; 
   int   value ;
};
typedef key_t  keyarray_t<>;

program HASHTABLE_PROG { 
   version HASHTABLE_VERSION {
       int        INIT(void)       = 10 ; 
       int        PUT(keyvalue_t)  = 11 ; 
       int        GETVALUE(key_t)  = 12 ; 
   } = 1 ;
} = 0x20051001 ;
サーバ側では、hcreate(), hsearch() 等のライブラリ関数を用いてもよい。

練習問題(7) getenv/putenv

getenv() とputenv()/setenv() を使って、RPCサーバの環境変数を読み書きす るようなプログラムを作りなさい。

注意:putenv() の引数は、strdup() すること。(同じ名前の環境変数がある と、ゴミが出てきてしまうが、この課題では問題ないとする。)

練習問題(8) SunRPC自由課題

その他、上記の練習問題と同程度以上の複雑さを持つSunRPC のサーバ、および、 クライアントを作成しなさい。自分自身が今までに作成したプログラムを、 RPC で実行可能にしてもよい。プログラムの中には、RPC にするものに適して いないものがある。この課題を選択する時には、事前に教官に相談しなさい。

■課題

上の練習問題のうち、1つを選択して提出しなさい。 締切りは 2008年1月20日 日曜日 23:59:59 とする。 (注意:再提出になることがある。)

◆レポートの提出方法

まず、次のような「テキストファイル」を作成する。漢字コードとしては、 JIS、Shift-JIS、EUC を受け付ける。(PDF, RTF, ワープロの文書ファイルで は受付ない。テキストでも Unicode (UTF) は、受け付けない。ZIP や tar, gzip 等で固めたり圧縮しないように。)

----------------------------------------------------------------------
学籍番号: 200504321
名前: 漢字の名前
課題番号:M
練習問題番号:N
題名:subject

<内容>
----------------------------------------------------------------------
本文の先頭に学籍番号と名前(漢字の名前がある人は、漢字で)を書きなさい。 課題番号としては、「1」 と記述しなさい。 練習問題番号とは「★練習問題」に続いて表示されている番号である。 題名には、電子メールの Subject: と同様に、内容に即したものをつける。

<内容>は、日本語(または英語)で書きなさい。文章には、述語を付ける。 体言止めは、使ってはならない。単にプログラムを含めるのではなく、「以下 に○○のプログラムを示す」と書くこと。<内容>には、プログラムだけでな く、実行結果(入力と出力)と説明をつける。

問題を難しい方に変えてた場合、または、最初から難しい問題を解いた場合に は、<内容>の部分で主張しなさい。

作成したファイルを、次のページから投稿する。

分散システム/ レポート提出ページ

上で書いた課題番号、題名を繰り返し指定する。さらに、WWW ブラウザの機能 を使って作成したレポートのファイルを選択する。 最後に、「提出」ボタンを押す。

提出されたレポートは、次のボタンで表示できます。

もし、提出に失敗したり、提出には成功しても確認画面に現れない場合には、 新城(yas@is.tsukuba.ac.jp)かTAに、連絡しなさい。

レポートを再提出する時には、どの部分を修正したのかが簡単にわかるように 説明しなさい。

提出したレポートは、講義が終るまで保存しなさい。


Last updated: 2008/01/15 14:26:27
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>