FTP クライアントを作ってみよう (3)

TOP >> ネットワークプログラミングの基礎知識 >> FTP クライアントを作ってみよう (3)

(追記) (追記ここまで)
(追記) (追記ここまで) (追記) (追記ここまで)

OS にポート番号を選ばせる

前項で書いた FTP クライアントのサンプルですが、細かいところを改善しましょう。 データコネクションを生成するときに、ポート番号 5000 から 65535 まで順番に bind しました。 これは、他のプログラムがどのポートを使っているかがわからないため、 総当りで調べたわけです。

前項のプログラムのその部分を再掲します。

ftp-client.pl

 44: for ( $data_port=5000 ; $data_port<65536 ; $data_port++ ){
 45: 
 46: # ソケット生成
 47: socket(DATA_WAITING, PF_INET, SOCK_STREAM, 0)
 48: or die "ソケットを生成できません。\n";
 49: 
 50: # ソケットオプション設定
 51: setsockopt(DATA_WAITING, SOL_SOCKET, SO_REUSEADDR, 1)
 52: or die "setsockoptでエラーが発生しました。\n";
 53: 
 54: # ソケットにアドレス(=名前)を割り付ける
 55: if ( bind(DATA_WAITING, pack_sockaddr_in($data_port, INADDR_ANY)) ){
 56: # 成功したら forループを抜ける
 57: last;
 58: } else {
 59: # 失敗したら次のポートのbindを試みる
 60: print "ポート$data_portのbindに失敗しました。\n";
 61: 
 62: # ポート65535まで試してもダメなら終了
 63: if ( $data_port == 65535 ){
 64: die "終了します。\n";
 65: }
 66: }
 67: }
 68: # OSに、クライアントからの接続を受け入れるよう指示
 69: listen(DATA_WAITING, SOMAXCONN)
 70: or die "listen: $!";

実はこんなことをしなくても、OS 側に勝手にポートを選ばせることができるのです。 前項のプログラムは、

bind(DATA_WAITING,sockaddr_in(ポート番号,INADDR_ANY))
としていましたが、
bind(DATA_WAITING,sockaddr_in(0,INADDR_ANY))
と、ポート番号を 0 に指定すると、 OS が勝手に空いているポートにソケットを割り当ててくれます。

すると、上記の部分は

ftp-client-2.pl

 43: # ソケット生成
 44: socket(DATA_WAITING, PF_INET, SOCK_STREAM, 0)
 45: || die "ソケットを生成できません。$!";
 46: 
 47: # ソケットオプション設定
 48: setsockopt(DATA_WAITING, SOL_SOCKET, SO_REUSEADDR, 1)
 49: || die "setsockoptでエラーが発生しました。$!";
 50: 
 51: # ソケットにアドレス(=名前)を割り付ける
 52: bind(DATA_WAITING, pack_sockaddr_in(0, INADDR_ANY));
 53: || die "bind に失敗しました。$!";
 54: 
 55: # OSに、クライアントからの接続を受け入れるよう指示
 56: listen(DATA_WAITING, SOMAXCONN)
 57: || die "listen できません。$!";
と短く書くことができます。 なお、PORT コマンドを送信するときにデータコネクションのポート番号を知らないといけないのですが、 OS にポート番号を選択させたので、何番のポートが割り当てられたのかわかりません。そのため、
 71: $local_sock_addr = getsockname(DATA_WAITING);
 72: ($data_port, $tmp) = unpack_sockaddr_in($local_sock_addr);
という処理が必要になります。 これは、データコネクション用ソケットに対して getsockbyname・unpack_sockaddr_in を使い、 ソケットに割り当てられたポート番号を取得しているのです。

この部分を書き換えたサンプルプログラムを以下に示します。

ftp-client-2.pl

 1: #!/usr/local/bin/perl -w
 2: 
 3: # $Id: ftp-client-2.pl,v 1.2 2002年02月05日 17:53:09 68user Exp $
 4: 
 5: use Socket; # Socketモジュールを使う
 6: 
 7: $hostname = 'localhost';
 8: $username = 'zxr400';
 9: $password = '';
 10: 
 11: #---------- コマンドコネクションを作成 -----------------
 12: 
 13: # FTP プロトコルを使う
 14: $port = getservbyname('ftp', 'tcp');
 15: 
 16: # ホスト名を、IPアドレスの構造体に変換
 17: $iaddr = inet_aton($hostname)
 18: || die "$hostname は存在しないホストです。$!";
 19: 
 20: # ポート番号と IP アドレスをまとめて構造体に変換
 21: $sock_addr = pack_sockaddr_in($port, $iaddr);
 22: 
 23: # ソケット生成
 24: socket(COMMAND, PF_INET, SOCK_STREAM, 0)
 25: || die "ソケットを生成できません。$!";
 26: 
 27: # 指定のホストの指定のポートに接続
 28: connect(COMMAND, $sock_addr)
 29: || die "$hostname のポート $port に接続できません。$!";
 30: 
 31: # ファイルハンドル COMMAND をバッファリングしない
 32: select(COMMAND); $|=1; select(STDOUT);
 33: 
 34: 
 35: #---------- ユーザ認証 ---------------------------------
 36: 
 37: print COMMAND "USER $username\n";
 38: print COMMAND "PASS $password\n";
 39: 
 40: 
 41: #---------- データ用コネクションを作成 -----------------
 42: 
 43: # ソケット生成
 44: socket(DATA_WAITING, PF_INET, SOCK_STREAM, 0)
 45: || die "ソケットを生成できません。$!";
 46: 
 47: # ソケットオプション設定
 48: setsockopt(DATA_WAITING, SOL_SOCKET, SO_REUSEADDR, 1)
 49: || die "setsockoptでエラーが発生しました。$!";
 50: 
 51: # ソケットにアドレス(=名前)を割り付ける
 52: bind(DATA_WAITING, pack_sockaddr_in(0, INADDR_ANY));
 53: || die "bind に失敗しました。$!";
 54: 
 55: # OSに、クライアントからの接続を受け入れるよう指示
 56: listen(DATA_WAITING, SOMAXCONN)
 57: || die "listen できません。$!";
 58: 
 59: 
 60: #---------- ローカルホストの IP アドレスを取得 ---------------
 61: 
 62: $local_sock_addr = getsockname(COMMAND);
 63: ($tmp, $local_addr) = unpack_sockaddr_in($local_sock_addr);
 64: $local_ip = inet_ntoa($local_addr);
 65: # IPアドレス aaa.bbb.ccc.ddd を aaa,bbb,ccc,dddという形式に
 66: $local_ip =~ s/\./,/g;
 67: 
 68: 
 69: #---------- データコネクションのポート番号を取得 -------------
 70: 
 71: $local_sock_addr = getsockname(DATA_WAITING);
 72: ($data_port, $tmp) = unpack_sockaddr_in($local_sock_addr);
 73: 
 74: 
 75: #---------- PORT・LIST コマンドを送信 -------------------------
 76: 
 77: # FTP サーバに、データコネクションの IP アドレスとポートの情報を渡す
 78: printf COMMAND "PORT $local_ip,%d,%d\n"
 79: ,$data_port/256,$data_port%256;
 80: 
 81: # ファイル一覧を送るよう要求
 82: print COMMAND "LIST\n";
 83: 
 84: 
 85: #---------- データコネクションを使って、データ受信 -----------
 86: 
 87: # FTP サーバ側からの接続を待つ
 88: accept(DATA, DATA_WAITING);
 89: 
 90: # 送られてくるデータの内容を表示
 91: while (<DATA>){
 92: print $_;
 93: }
 94: 
 95: 
 96: #---------- 終了処理 ----------------------------------------
 97: 
 98: # データ用コネクションclose
 99: close(DATA);
 100: close(DATA_WAITING);
 101: 
 102: # QUITを送ってセッション終了
 103: print COMMAND "QUIT\n";
 104: close(COMMAND);

Passive モード

先程作成した FTP クライアントは、ファイル一覧を取得する だけのものでした。その流れは
FTP クライアント FTP サーバ
 USER ------ コマンド用コネクション ------>
 PASS ------ コマンド用コネクション ------>
 PORT ------ コマンド用コネクション ------>
 LIST ------ コマンド用コネクション ------>
 <----- データ用コネクション --------- ファイル一覧送信 QUIT ------ コマンド用コネクション ------>
となります。これは「Active モード」というもので、 データコネクションの確立の際、 「FTP サーバ側が能動的に FTP クライアント側に接続する」 という動作になります。

一方、「Passive モード」というものがあります。このモードでは、 「FTP サーバ側が受け手となり、FTP クライアントからの接続を待つ」 という動作になります。つまり、FTP クライアントは、 bind・listen・accept などのサーバ的な動作をする必要がなくなるのです。

FTP クライアントがファイアーウォールの内側にいて、 外部から接続できない場合、Active モードではうまくいかない場合があります。 そういうときには Passive モードを使います。ftp コマンドでは、

ftp> passive
Passive mode on.
とすると、それ以降 Passive モードになります。もう一度
ftp> passive
Passive mode off.
とすると、Active モードに戻ります。

Passive モードでの FTP プロトコル

Passive モードでは、PORT コマンドは使いません。 その代わりに、PASV コマンドを使用します。 流れとしては
FTP クライアント FTP サーバ
 USER ------ コマンド用コネクション ------>
 PASS ------ コマンド用コネクション ------>
 PASV ------ コマンド用コネクション ------>
 LIST ------ コマンド用コネクション ------>
 connect ------ データ用コネクション -------->
 <----- データ用コネクション --------- ファイル一覧送信 QUIT ------ コマンド用コネクション ------>
となります。Active モードとの違いは、データコネクションの確立方式だけです。 再度、ftp に -d オプションを付けて、プロトコルの流れを見てみましょう。
ftp> passive
Passive mode on.
passive で Passive モードに移行しても、FTP プロトコルとしては 何も送受信しません。では、ls でファイル一覧を表示しましょう。
ftp> ls
---> PASV
227 Entering Passive Mode (10,0,0,1,156,67)
---> LIST
コマンドコネクションで FTP クライアントが PASV を送信します。 すると、サーバからは
Entering Passive Mode (10,0,0,1,156,67)
というレスポンスが返ってきます。これは、FTP サーバが
10.0.0.1 というホストのポート 40003 (×ばつ256+67) で
データ用コネクションの接続を受け入れ中である
ということです。 それを受けて、FTP クライアントは 10.0.0.1 のポート 40003 に接続します。 すると、ファイル一覧が FTP サーバ側から送信されてきます。

では、続けて ファイルを get しましょう。

ftp> get README.TXT
---> PASV
227 Entering Passive Mode (10,0,0,1,156,75)
---> RETR README.TXT
get をタイプすると、再度 PASV を送信し、ポート番号を取得します。 つまり ls・get・put などをタイプするたびに、 FTP クライアントは毎回 PASV を送信し、そのレスポンスとして、 IP アドレスとポート番号を取得します。

指定された IP アドレス + ポート番号に接続すると、データの送受信が始まります。 LIST (ls) ならファイル一覧が FTP サーバから送られてきます。 RETR (get) ならファイルの内容です。 STOR (put) なら、こちらからファイルの内容を送信しないといけません。

本来は、ここで Passive モードを利用した FTP クライアントを 例として上げたいところですが、いろいろと事情があり、 それはできません。次節 で FTP プロトコルを 解説した後、Passive モードに対応した高機能版 FTP プロトコルを作成します。

(追記) (追記ここまで) (追記) (追記ここまで)

TOP >> ネットワークプログラミングの基礎知識 >> FTP クライアントを作ってみよう (3)

ご意見・ご指摘は Twitter: @68user までお願いします。



AltStyle によって変換されたページ (->オリジナル) /