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

前へ << FTP クライアントを作ってみよう (2) FTP クライアントを作ってみよう (4) >> 次へ

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 (156×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 プロトコルを作成します。

前へ << FTP クライアントを作ってみよう (2) FTP クライアントを作ってみよう (4) >> 次へ

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