システム・プログラム
電子・情報工学系
新城 靖
<yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~yas/coins/syspro-2004/2004-05-10
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~yas/
http://www.is.tsukuba.ac.jp/~yas/index-j.html
1:
2: /*
3: echo-server-nofork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork無し版)
4: ~yas/syspro/ipc/echo-server-nofork-fdopen.c
5: Created on 2004/05/09 19:08:47
6: */
7: #define _USE_BSD /* wait4() */
8: #include <stdio.h>
9: #include <sys/types.h> /* socket(), wait4() */
10: #include <sys/socket.h> /* socket() */
11: #include <netinet/in.h> /* struct sockaddr_in */
12: #include <sys/resource.h> /* wait4() */
13: #include <sys/wait.h> /* wait4() */
14: #include <netdb.h> /* getnameinfo() */
15:
16: extern void echo_server( int portno );
17: extern void echo_reply( int com );
18: extern void print_my_host_port( int portno );
19: extern void tcp_peeraddr_print( int com );
20: extern void sockaddr_print( struct sockaddr *addrp, int addr_len );
21: extern tcp_acc_port( int portno );
22: extern int fdopen_sock( int sock, FILE **inp, FILE **outp );
23:
24: main( int argc, char *argv[] )
25: {
26: int portno ;
27: if( argc >= 3 )
28: {
29: fprintf( stdout,"Usage: %s [portno] \n",argv[0] );
30: exit( -1 );
31: }
32: if( argc == 2 )
33: portno = strtol( argv[1],0,10 );
34: else
35: portno = getuid();
36: echo_server( portno );
37: }
38:
引数として、ポート番号を取る。ポート番号が与えられなければ、そのプロセ
スの UID (User ID) から生成する。UID は、個人個人を識別するための番号
(16ビット程度、システムによっては 32 ビット)である。この課題では、(1台
のコンピュータでは)個人ごとに別々の UID を使う必要がある。上のように
UID から生成する方法は、一般的ではない。(筑波大学情報学類のシステムで
はうまく働く。)
39: void
40: echo_server( int portno )
41: {
42: int acc,com ;
43: acc = tcp_acc_port( portno );
44: if( acc<0 )
45: exit( -1 );
46: print_my_host_port( portno );
47: while( 1 )
48: {
49: if( (com = accept( acc,0,0 )) < 0 )
50: {
51: perror("accept");
52: exit( -1 );
53: }
54: tcp_peeraddr_print( com );
55: echo_reply( com );
56: }
57: }
58:
tcp_acc_port() は、引数で与えられたポート番号を使って接続要求受付用ポー トを作成し、そのファイル記述子(ソケット)を返す。 このファイル記述子は、クライアント側とは異なり、そまままでは通信に 用いることはできない。 print_my_host_port() は、telnet で接続する時のヒントを表示する。
サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。
accept() は、接続要求を待つシステムコールである。クライアントから接続 が来るまで、システムコールを実行したまま止まっているように見える。接続 要求が届くと、TCP/IP通信路の開設され、通信用ポートのファイル記述子が返 される。このファイル記述子は、クライアント側と同様に 標準入出力(0,1,2)や open() システム・コー ルの結果と同じもので、 ファイルのコピー(システムコールの利用) で使った write() システムコールや read() システムコールの第一引数とし て使うことができる。つまり、write() システムコールを使うと、ネットワー クに対してデータを送り出すことができ、read() システムコールを使うとネッ トワークからデータを受け取ることができる。最後に不要になったら close() で解放する。
tcp_peeraddr_print() は、通信相手の IP アドレスとポート番号を表示する関数である。
59: #define BUFFERSIZE 1024
60:
61: void
62: echo_reply( int com )
63: {
64: char line[BUFFERSIZE] ;
65: int rcount ;
66: int wcount ;
67: FILE *in, *out ;
68:
69: if( fdopen_sock(com,&in,&out) < 0 )
70: {
71: fprintf(stderr,"fdooen()\n");
72: exit( 1 );
73: }
74: while( fgets(line,BUFFERSIZE,in) )
75: {
76: rcount = strlen( line );
77: printf("[%d] received (fd==%d) %d bytes, [%s]\n",getpid(),com,rcount,line );
78: fflush( stdout );
79: fprintf(out,"%s",line );
80: }
81: printf("[%d] connection (fd==%d) closed.\n",getpid(),com );
82: fclose( in );
83: fclose( out );
84: }
85:
この echo_reply() は、特定のクライアント専用のecho サービスを提供する。
クライアントからの要求が続く限り、動作する。
このプログラムでは、 クライアント側 と同様に fdopen_sock() を使って、通信可能なファイル記 述子 com から2つの FILE * を作成している。1つは、入力用、1つは出力 用である。その結果、 高水準入出力ライブラリ を使って通信が行えるようになっている。fprintf() で出力用の FILE * に書 き込むと、ネットワークに対してデータが送り出される。入力用の FILE * に fgets() を行うと、ネットワークからデータを受け取ることができる。
クライアントからの要求は、fgets() で読込んでいる。それを、サーバ側の端 末に printf() で表示している。fflush() は、printf() の内部のバッファ (stdoutのバッファ)に溜っているデータを書き出すものである。
クライアントには、fprintf() で結果を送り返している。
86: void
87: print_my_host_port( int portno )
88: {
89: char hostname[100] ;
90: gethostname( hostname,sizeof(hostname) );
91: hostname[99] = 0 ;
92: printf("run telnet %s %d \n",hostname, portno );
93: }
94:
print_my_host_port() は、telnet で接続する時のヒントを表示する。
gethostname() システムコールで自分自身のホスト名を取り出している。(正
式には、gethostname() の結果とインターネット的なホスト名(IPアドレスと
対応している))が一致して異ないことがある。
95: void
96: tcp_peeraddr_print( int com )
97: {
98: struct sockaddr_storage addr ;
99: int addr_len ;
100: addr_len = sizeof( addr );
101: if( getpeername( com, (struct sockaddr *)&addr, &addr_len )<0 )
102: {
103: perror("tcp_peeraddr_print");
104: return;
105: }
106: printf("[%d] connection (fd==%d) from ",getpid(),com );
107: sockaddr_print( (struct sockaddr *)&addr, addr_len );
108: printf("\n");
109: }
110:
111: void
112: sockaddr_print( struct sockaddr *addrp, int addr_len )
113: {
114: char host[BUFFERSIZE] ;
115: char port[BUFFERSIZE] ;
116: if( getnameinfo(addrp, addr_len, host, sizeof(host),
117: port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 )
118: return;
119: printf("%s:%s", host, port );
120: }
121:
tcp_peeraddr_print() は、通信相手(peer)のアドレス(TCP/IPの場合、IPアド
レスとポート番号)を表示する。通信相手のアドレスは、getpeername()
システムコールで得られる。
IP アドレスは、IPv4 では、32 ビット(int)であり、 ポート番号は、16 ビット(sort)である。 ここでは、getnameinfo() ライブラリ関数を用いてホスト名とポート番号の 文字列表現に変換している。この時、NUMERIC と指定しいるので、 IPv4 では、ドット「.」で区切られた10進数4つになる。
getnameinfo() が使われている部分では、以前は、gethostbyaddr() が使われ ていた。
122: tcp_acc_port( int portno )
123: {
124: struct sockaddr_in addr ;
125: int addr_len ;
126: int s ;
127:
128: if( (s = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
129: {
130: perror("socket");
131: return( -1 );
132: }
133:
134: memset( &addr, 0, sizeof(addr) );
135: addr.sin_family = AF_INET ;
136: addr.sin_addr.s_addr = INADDR_ANY ;
137: addr.sin_port = htons( portno );
138:
139: if( bind(s,(struct sockaddr *)&addr,sizeof(addr)) < 0 )
140: {
141: perror("bind");
142: fprintf(stderr,"port number %d is already used. wait a moment or kill another program.\n", portno );
143: return( -1 );
144: }
145: if( listen( s, 5 ) < 0 )
146: {
147: perror("listen");
148: close( s );
149: return( -1 );
150: }
151: return( s );
152: }
153:
tcp_acc_port() は、
通信路の開設
の仕事のうち、サーバ側で接続要求受付用ポートを作る関数である。
まず、クライアント側と同様に、ソケットを、socket() システムコールで作成している。
PF_INET と SOCK_STREAMの組み合わせ
なので、TCP を使うことを意味する。
socket() の引数で、PF_INET の変りに、AF_INET と書いてもよい。ここでは、 Protocol を選んでいるので、PF_ が正しいが、実際には、PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われいる。
ソケットが作成できたら、bind() システムコールで、サーバ側で利用するア ドレス(IPアドレスとポート番号)を設定する。IP アドレスは、IPv4 では普通、 INADDR_ANY を指定する。複数の IP アドレスがある時には、どれに要求が来 ても受け付ける。特定の IP アドレスを指定すると、そのアドレスに来た要求 だけを受け付けるようになる。
ポート番号は、引数で与えられたものを、htons() でネットワーク・バイトオー ダに変換して与える。
次に、listen() システムコールにより、要求受け付けを開始する。第2引数 は、最大何個のクライアントを接続要求待ちで待たせるか(待ち行列の長さ) を指定する。 重たいサーバを設計する時には、キューの長さを調節する。Apache (WWW サー バ) などでは、500 程度になっていることがある。
注意:このプログラムには 複数のクライアントに対してサービスを同時に提供できない という問題がある。
bind() する addr を、getaddrinfo() で調べる流儀(IPv6風)もある。この場 合、getaddrinfo()の第一引数には、NULL を入れ、hints.ai_flags には AI_PASSIVE を設定する。
実行例。
サーバ側。 サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。
注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。
% cp ~yas/syspro/ipc/echo-server-nofork-fdopen.c .クライアント側% make echo-server-nofork-fdopen
cc echo-server-nofork-fdopen.c -o echo-server-nofork-fdopen % ./echo-server-nofork-fdopen 1231
run telnet adonis9.coins.tsukuba.ac.jp 1231 [6419] connection (fd==4) from 130.158.86.28:47329 [6419] received (fd==4) 5 bytes, [123 ] [6419] received (fd==4) 5 bytes, [456 ] [6419] received (fd==4) 5 bytes, [790 ] [6419] connection (fd==4) closed. ^C %
![]()
% telnet adonis9.coins.tsukuba.ac.jp 1231Trying 130.158.86.29... Connected to adonis9.coins.tsukuba.ac.jp. Escape character is '^]'. 123
123 456
456 790
790 ^]
telnet> quit
Connection closed. %
![]()
複数のクライアントに対してサービスの同時に提供するには次のような方法が ある。
1: /*
2: EchoServerSingle.java -- 文字列を送受信するサーバ(TCP/IP, Java版, スレッドなし)
3: ~yas/syspro/ipc/EchoServer.java
4: Created on 2004/05/09 20:00:24
5: */
6:
7: import java.net.*;
8: import java.io.*;
9:
10: class EchoServerSingle
11: {
12: public static void main(String argv[]) throws IOException {
13: if( argv.length != 1 )
14: {
15: System.err.println("Usage: % java EchoServer port");
16: System.exit( -1 );
17: }
18: int portno = Integer.parseInt( argv[0] );
19: echo_server( portno );
20: }
引数として、ポート番号を取る。
22: public static void echo_server( int portno ) throws IOException
23: {
24: ServerSocket acc = new ServerSocket( portno );
25: print_my_host_port( portno );
26: while( true )
27: {
28: Socket com = acc.accept();
29: tcp_peeraddr_print( com );
30: EchoServerWorker esw = new EchoServerWorker(com);
31: esw.run();
32: }
33: }
ServerSocket() は、引数で与えられたポート番号を使って接続要求受付用ポー トを作成し、それに対応した ServerSocket のオブジェクトを返す。このオブ ジェクトは、Socket のオブジェクトとは異なり、そまままでは通信に用いる ことはできない。
print_my_host_port() は、telnet で接続する時のヒントを表示する。
サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。
accept() は、接続要求を待つメソッドである。クライアントから接続が来る まで、システムコールを実行したまま止まっているように見える。接続要求が 届くと、TCP/IP通信路の開設され、通信用ポートに対応したSocket クラスの オブジェクトが返される。このオブジェクトは、クライアント側と同様に getInputStream() や getOutputStream() により入出力可能なストリームを作 り出すことができる。
tcp_peeraddr_print() は、通信相手の IP アドレスとポート番号を表示する関数である。
以後の仕事は、EchoServerWorker というクラスのオブジェクトを作成して行 わせている。コンストラクタでは、Socket クラスのオブジェクトを渡してい だけであり、実際の処理は何も行われない。実際の処理は、run() メソッドで 行われる。
34: public static void print_my_host_port( int portno ) throws UnknownHostException
35: {
36: InetAddress ia = java.net.InetAddress.getLocalHost();
37: String hostname = ia.getHostName();
38: stdout.println("run telnet "+hostname+" "+portno );
39: }
40: public static void tcp_peeraddr_print( Socket com )
41: {
42: InetSocketAddress isa = (InetSocketAddress)com.getRemoteSocketAddress();
43: InetAddress ia = isa.getAddress();
44: String peerhostaddr = ia.getHostAddress();
45: int peerportno = isa.getPort();
46: stdout.println("connection (hash=="+com.hashCode()+") from "+peerhostaddr+":"+peerportno );
47: }
print_my_host_port() と tcp_peeraddr_print() は、それぞれ同じ同名のC の関数と同じ働きをする。
1: /*
2: EchoServerWorker.java -- 文字列を送受信するサーバ/ワーカ(TCP/IP, Java版)
3: ~yas/syspro/ipc/EchoServerWorker.java
4: */
5:
6: import java.net.*;
7: import java.io.*;
8:
9: public class EchoServerWorker implements Runnable
10: {
11: Socket com ;
12: EchoServerWorker( Socket com )
13: {
14: this.com = com ;
15: }
16: public void run()
17: {
18: try
19: {
20: BufferedReader in = new BufferedReader(
21: new InputStreamReader( com.getInputStream() ));
22: PrintStream out = new PrintStream( com.getOutputStream() );
23: String line;
24: while( (line = in.readLine())!= null )
25: {
26: stdout.println("received (hash=="+com.hashCode()+") "+
27: line.length()+" characters, ["+line+"]");
28: out.println( line );
29: }
30: stdout.println("connection (hash=="+com.hashCode()+") closed.");
31: in.close();
32: out.close();
33: com.close();
34: }
35: catch( IOException e )
36: {
37: stderr.println( e );
38: }
39: }
40: static java.io.BufferedReader stdin =
41: new java.io.BufferedReader( new java.io.InputStreamReader(System.in) );
42: static java.io.PrintStream stdout = System.out;
43: static java.io.PrintStream stderr = System.err;
44: }
echoサービスのクライアント
と同様に、Socket から BufferedReader と PrintStream を生成している。
そして、readLine() や println() により通信を行っている。
% diff -c EchoServerSingle.java EchoServer.java
EchoServerSingle.java Sun May 9 20:01:50 2004
--- EchoServer.java Sun May 9 19:59:33 2004
1,13 ****
/*
! EchoServerSingle.java -- 文字列を送受信するサーバ(TCP/IP, Java版, スレッドなし)
~yas/syspro/ipc/EchoServer.java
! Created on 2004/05/09 20:00:24
*/
import java.net.*;
import java.io.*;
! class EchoServerSingle
{
public static void main(String argv[]) throws IOException {
if( argv.length != 1 )
--- 1,13 ----
/*
! EchoServer.java -- 文字列を送受信するサーバ(TCP/IP, Java版)
~yas/syspro/ipc/EchoServer.java
! Created on 2004/02/14 16:22:13
*/
import java.net.*;
import java.io.*;
! class EchoServer
{
public static void main(String argv[]) throws IOException {
if( argv.length != 1 )
27,34 ****
{
Socket com = acc.accept();
tcp_peeraddr_print( com );
! EchoServerWorker esw = new EchoServerWorker(com);
! esw.run();
}
}
public static void print_my_host_port( int portno ) throws UnknownHostException
--- 27,34 ----
{
Socket com = acc.accept();
tcp_peeraddr_print( com );
! Thread th = new Thread( new EchoServerWorker(com) );
! th.start();
}
}
public static void print_my_host_port( int portno ) throws UnknownHostException
%
ヒント:子プロセスの終了を、ソフトウェア割り込み(signal, 後述) (SIGCHLD)で知る方法もある。複数の子プロセスが終了しても、割り込みは1 回しか起こらないことがあることに注意しなさい。
ソフトウェア割込みを使うと、accept() システムコールがエラーで戻って来 るシステムもある。その場合、エラー番号が EINTR なら単純に終了しないで、 再び accept() に向うべきである。
% ./echo-server-fork 1231このエラーは、実際に同じポート番号を別のプロセスが使っている時にも出る が、既に以前に使っていたプロセスが終了している時にも出ることがある。 後者の場合、次のオプションを付けると、この状態が緩和されることがある。bind: Address already in use port number 1231 is already used. wait a moment or kill another program. %
![]()
setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, 0, 0);この効果を確かめなさい。
また、同じポート番号の再利用を制限している理由を考えなさい。
pipe() の代りに、TCP/IP のストリームを使って、異なるホスト上のプロセス を接続して、フィルタの処理を行わせてみなさい。
ヒント:例題 pipe-rw-nodup.cを tcp_connect() やtcp_acc_port() を使って書き直すとよい。
プログラムを1つだけ作るのではなく、tcp_connect() をするものと tcp_acc_port() をするものの2つを作るとよい。tcp_acc_port() をする側で は、fork() をして、複数のクライアントに対応するのではなく、通 信要求受付用の socket() を close() して、1つの通信先に専念するものでよい。
この課題では、fork() は、しなくてもよい。パイプは、fork() でできた親子 関係のあるプロセス間で通信を行うものであるが、TCP/IPでは、親子関係がな いプロセス間で通信ができる。
TCP/IP なら本来双方向で使える。しかし、この練習問題だと単方向だけを使 う。こうすると、パイプと同じような使い方ができる。
練習問題45 で、3つ以上のプロセスを接続できるようにしなさい。
ヒント:練習問題30 3個のプロセスをパイプで結ぶに相当する。
プログラムを1つだけ作るのではなく、先頭用、中間用、末尾用の3つを作る とよい。中間用のものは、うまく作ると、何個でもはさみ込めるようになる。
ヒント:dup(), dup2(), close() などで、標準入出力を切り替えて、 execve() などで、プログラムを実行する。
たとえば、シェルに次のように打ち込むことを考える。
% command1 | command2 | command3この時、シェルは、fork() しながら2つのパイプで3つのプロセスを結び、 execve() など command1, command2, command3 を実行する。この問題では、 パイプではなくTCP/IP 結ぶ。そして、close(), dup(), close() して、 execve() でcommand1, command2, command3 を実行する。
この課題では、tcp_accept() 側で fork() を行う必要はない。1回のコマン ドを実行したら終了するようにする。
上の tcp_peeraddr_print() は、IP アドレスを数字で表示していた。 これを、ホスト名で表示するようにしなさい。
http サーバを作成しなさい。これは、受け取ったファイル名のファイルを fopen() して、内容を fread() し、それを接続先に fwrite() で返すように するとよい。
この課題では、特にセキュリティに気をつけなさい。必ず次の条件を満たすよ うなサーバを作りなさい。
~/public_html)以下のファ
イルしかアクセスできないようにすること。それには、ファイルを開く
(open(), fopen())時に、頭に環境変数 HOME の値と "/public_html" を付加
する方法がある。(他の方法もある。)なお、~ は、open() システム・
コールや fopen() ライブラリ関数では使えない。~ は、シェルや
emacs が独自にホーム・ディレクトリを調べて置き換えている。
.. が含まれていたら、アクセスを拒否すること。
.. は、1つ上のディレクトリを意味する。.. を許せば、
~/public_html 以外の場所、たとえば、
../Mail で、電子メールが盗まれる危険性がある。
---------------------- Content-Type: 拡張子 ---------------------- text/html .html text/plain .txt image/gif .gif image/jpeg .jpeg ----------------------
~yas/syspro/ipc/http-server.c に、常に同じ内容を返すhttp サーバがある。 これを出発点にしてもよい。その場合、次の手順で開発するとよい。
実際の WWW ブラウザ (Netscapeなど)で、動作を確認しなさい。telnet だけ では、きちんと HTTP のプロトコルに従っているか確認できないので、不十分 である。
finger サーバを作成しなさい。これは、受け取った文字列を引数にして、 finger コマンドを実行するとよい。そして、実行結果を、接続先に返すよう にするとよい。
finger コマンドのプロセスと finger サーバの間は、パイプで接続しなさい。 今までの例題で利用した方法を組み合わせることで実現することができるはず である(pipe(),fork(),dup(),close(),execve()など)。popen() ライブラリ 関数を利用してもよい。
finger コマンドのプロセスと finger サーバ間をパイプで結ぶ代わりに、 finger コマンドを exec する前に、配管工事をして、標準出力(1)をTCP/IPの ストリームに接続する方法もある。
受け取った文字列をそのまま popen() や system() などに渡すのは危険である。 というのも、これらの引数は、シェル(/bin/sh)が解釈するからである。 popen() や system() を使う場合には、 finger コ マンドが受付けるのに相応しいもの( isalpha() や isdigit() の並び)かどう かを検査しなさい。もし、シェルが解釈する特殊な文字列(|;<> など)が含まれていると、意図しないプログラムが実行させられることがある。
fork() や execve() を用いて直接 /usr/bin/finger を実行する方法はシェル を経由しないので、比較的安全である。
HTTP proxy 作りなさい。次の機能を、1つ以上付けなさい。