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

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

実装する機能

では実際にFTPクライアントを作ってみましょう。 とはいえ、最初からあれだけのものを作るのは結構ホネです。 まずは機能を限定して「ログインしてファイルの一覧を表示する」だけの ものにします。

流れとしては

  • FTP サーバが動いているホストのポート21番に接続する
  • USER・PASS を送る
  • PORT で、FTP サーバに接続してほしいサーバとポート番号を知らせる
  • PORT で知らせたポート番号を見張る (データ用コネクション)
  • LIST でファイル一覧を要求する
  • データ用コネクションにファイル一覧が送られてくるので、それを表示する
  • QUIT で終了
となります。

複雑と言えば複雑なのですが、コマンド用コネクションの流れは HTTP クライアントや POP3 クライアントでやったことと同じですし、 データ用コネクションは echo サーバのところでやったように ポートを見張ればいいのです。

機能縮小版 FTP クライアント

スクリプトは以下のようになります。

ftp-client.pl

    1: #!/usr/local/bin/perl -w
    2: 
    3: # $Id: ftp-client.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:         or die "$hostnameは存在しないホストです。\n";
   19: 
   20:                 # ポート番号と IP アドレスをまとめて構造体に変換
   21: $sock_addr = pack_sockaddr_in($port, $iaddr);
   22: 
   23:                 # ソケット生成
   24: socket(COMMAND, PF_INET, SOCK_STREAM, 0)
   25:         or die "ソケットを生成できません。\n";
   26: 
   27:                 # 指定のホストの指定のポートに接続
   28: connect(COMMAND, $sock_addr)
   29:         or die "$hostname のポート $port に接続できません。\n";
   30: 
   31:                 # ファイルハンドル COMMAND をバッファリングしない
   32: select(COMMAND); $|=1; select(STDOUT);
   33: 
   34: 
   35: #---------- ユーザ認証 ---------------------------------
   36: 
   37: print COMMAND "USER $username\r\n";
   38: print COMMAND "PASS $password\r\n";
   39: 
   40: 
   41: #---------- データ用コネクションを作成 -----------------
   42: 
   43:                 # データコネクション用のソケット生成・アドレス割り付け
   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: $!";
   71: 
   72: 
   73: #---------- ローカルホストの IP アドレスを取得 ---------------
   74: 
   75: $local_sock_addr = getsockname(COMMAND);
   76: ($local_port, $local_addr) = unpack_sockaddr_in($local_sock_addr);
   77: $local_ip = inet_ntoa($local_addr);
   78:                 # IPアドレス aaa.bbb.ccc.ddd を aaa,bbb,ccc,dddという形式に
   79: $local_ip =~ s/\./,/g;
   80: 
   81: 
   82: #---------- PORT・LIST コマンドを送信 -------------------------
   83: 
   84:                 # FTP サーバに、データコネクションの IP アドレスとポートの情報を渡す
   85: printf COMMAND "PORT $local_ip,%d,%d\r\n"
   86:                ,$data_port/256,$data_port%256;
   87: 
   88:                 # ファイル一覧を送るよう要求
   89: print COMMAND "LIST\r\n";
   90: 
   91: 
   92: #---------- データコネクションを使って、データ受信 -----------
   93: 
   94:                 # FTP サーバ側からの接続を待つ
   95: accept(DATA, DATA_WAITING);
   96: 
   97:                 # 送られてくるデータの内容を表示
   98: while (<DATA>){
   99:     print $_;
  100: }
  101: 
  102: 
  103: #---------- 終了処理 ----------------------------------------
  104: 
  105:                 # データ用コネクションclose
  106: close(DATA);
  107: close(DATA_WAITING);
  108: 
  109:                 # QUITを送ってセッション終了
  110: print COMMAND "QUIT\r\n";
  111: close(COMMAND);
とりあえず大前提として、FTP サーバにログインするためには、 相手先のホスト名、ユーザ名、パスワードが必要になります。 誰かのアカウント情報を例とすることはできませんので、 ここは anonymous FTP サーバにログインすることにします。
    7: $hostname = 'localhost';
    8: $username = 'zxr400';
    9: $password = '';
ユーザ名を anonymous (あるいは ftp)、パスワードにメールアドレスを 指定することで、自動的に anonymous FTP にログインできることは ご存知だと思います。

FTP サーバへの接続まで

まずは FTP クライアントの中の、コマンド処理を行う部分からみていきましょう。
   13:                 # FTP プロトコルを使う
   14: $port = getservbyname('ftp', 'tcp');
   15: 
   16:                 # ホスト名を、IPアドレスの構造体に変換
   17: $iaddr = inet_aton($hostname)
   18:         or die "$hostnameは存在しないホストです。\n";
   19: 
   20:                 # ポート番号と IP アドレスをまとめて構造体に変換
   21: $sock_addr = pack_sockaddr_in($port, $iaddr);
   22: 
   23:                 # ソケット生成
   24: socket(COMMAND, PF_INET, SOCK_STREAM, 0)
   25:         or die "ソケットを生成できません。\n";
   26: 
   27:                 # 指定のホストの指定のポートに接続
   28: connect(COMMAND, $sock_addr)
   29:         or die "$hostname のポート $port に接続できません。\n";
   30: 
   31:                 # ファイルハンドル COMMAND をバッファリングしない
   32: select(COMMAND); $|=1; select(STDOUT);
HTTP クライアントとほどんど一緒です。唯一違うのは
   14: $port = getservbyname('ftp', 'tcp');
で、http が ftp に変わっただけですね。

念のため、もう一度軽く説明しておくと、

  • getservbyname で $port に FTP のポート番号 (21) を代入します。 プログラムの中にマジックナンバを書くと読みにくくなるので、わざとこういう関数を利用しているのです。
  • ソケットを生成します。
  • inet_aton でホスト名を IP アドレスを表す構造体に変換します。
  • pack_sockaddr_in で、「"IP アドレスを表す構造体" と "ポート番号" をひとまとめにしたもの」を作ります。
  • socket でソケットを生成します。ソケットは相手側とのデータ入出力の出入口となります。
  • connect で相手先に接続します。 このとき pack_sockaddr_in で作られた構造体を使います。 以後はソケット COMMAND に対して読み書きすることで、 相手先とのデータのやりとりができます。
  • ソケット COMMAND に対してバッファリングしないようにします。
となります。
ここまででコマンド用コネクションの接続は成功しているので、 ユーザ認証を行います。
   37: print COMMAND "USER $username\r\n";
   38: print COMMAND "PASS $password\r\n";
USER・PASS を送っているだけですね。サンプルということで、 ユーザ認証が成功したかどうかはチェックしていません。 パスワードを間違わないように。

データコネクションの準備

データ用のコネクションを作成します。 データコネクションは FTP サーバ側から接続してきますので、 echo サーバのように特定のポートを見張らなくてはいけません。
   43:                 # データコネクション用のソケット生成・アドレス割り付け
   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: }
まずポート番号 5000 を bind して、もし失敗したらポート 5001 番を試すことに します。それでもダメなら 5002、5003 …と順に試していき、ポート 65535 番まで 全て失敗したら残念ながら終了します。

  • socket でソケット DATA を作成する。
  • setsockopt でソケット DATA に対してソケットオプションを設定。
  • bind でアドレスを割り当てる
という処理を ポート番号 5000・5001・5002… と順に試していきます。
   69: listen(DATA_WAITING, SOMAXCONN)
   70:     or die "listen: $!";
データコネクションを listen します。 ここに処理が来たということは、データコネクションの bind に成功したということです。 listen を実行した時点で、クライアントからの接続は受け入れることができますが、 実際に FTP サーバ側からの接続が来るのは、PORT・LIST コマンドを送った後です。
→ 関数説明: listen

再びコマンドコネクション

データ用コネクションの準備はできましたので、コマンド用コネクションを通じて、 「データコネクションの IP アドレスとポート番号」を FTP サーバ側に知らせます。 このとき
PORT 10,0,0,1,19,136
というような PORT コマンドを送るわけですが、 IP アドレス(この場合は10.0.0.1)と データコネクションの PORT 番号 (この場合は19×256+136=5000) の情報が必要になります。 既に bind のループで $data_port にポート番号が入っていますので、 あとは IP アドレスを取得しなければいけません。


ローカルホスト (FTP クライアントが動いているホスト) の IP アドレスを取得すればよいのですが、これには getsockname というソケット自身の 情報を取得する関数を使います。
   75: $local_sock_addr = getsockname(COMMAND);
   76: ($local_port, $local_addr) = unpack_sockaddr_in($local_sock_addr);
   77: $local_ip = inet_ntoa($local_addr);
順に見ていくと、
  • getsockname で「 "IPアドレスを表す構造体" と "ポート番号" をひとまとめにしたもの」を取得します。
  • それを unpack_sockaddr_in で、"IPアドレスを表す構造体" と "ポート番号" に分割します。
  • そして "IPアドレスを表す構造体" を inet_ntoa で IP アドレスを表す文字列に変換します。
という流れになります。これは FTP サーバへの接続部分で、 inet_atonpack_sockaddr_in を使って行った変換の逆をやっているわけです。
→ 関数説明: getsockname
→ 関数説明: unpack_sockaddr_in

   79: $local_ip =~ s/\./,/g;
$local_ip は 10.0.0.1 などのような IP アドレス表記になっていますが、 PORT コマンドに渡す際は「.」(ドット)ではなく「,」(カンマ)で区切らなくてはいけないので、 s/\./,/gで変換します。最終的には、$local_ip は 10,0,0,1 という文字列になるわけです。
   85: printf COMMAND "PORT $local_ip,%d,%d\r\n"
   86:                ,$data_port/256,$data_port%256;
   89: print COMMAND "LIST\r\n";
PORT コマンド、LIST コマンドを FTP サーバに送信します。 先ほど作った $local_ip (IPアドレス) と、 $data_port (ポート番号÷256,ポート番号÷256の余り) を 組み合わせて、ポート番号の情報を伝えます。例えば FTP クライアントが 192.168.1.1 で 動いており、データ用コネクションを張るためにポート 12345 番を listen しているなら、
PORT 192,168,1,1,48,57
という文字列を送ります (12345=48×256+57)。
   95: accept(DATA, DATA_WAITING);
accept します。既に PORT・LIST コマンドは送ったので、既に FTP サーバ側から接続要求が来ているかもしれません (まだ来ていないかもしれませんが、そのうち接続しにくるはずです)。

新しくソケット DATA が作られ、今後データコネクションはソケット DATA を 通してデータのやりとりをします。ソケット DATA_WAITING は、 さらに次の接続が来た場合のためのものですが、FTP プロトコルの 仕様としては新たな接続があることはあり得ません。

→ 関数説明: accept

   98: while (<DATA>){
   99:     print $_;
  100: }
データコネクション用のソケットからデータを読み込み、内容をそのまま表示します。 先ほどコマンドコネクションで FTP サーバに LIST コマンドを送りましたので、 送られてくるのはファイルの一覧です。
  106: close(DATA);
  107: close(DATA_WAITING);
  108: 
  109:                 # QUITを送ってセッション終了
  110: print COMMAND "QUIT\r\n";
  111: close(COMMAND);
while ループが終了したら、データコネクションは FTP サーバ側から切断されますが、 念のためこちらでも close(DATA) しておきましょう。さらにソケット DATA_WAITING も クローズします。

最後に残ったコマンド用コネクションは、FTP サーバに QUIT コマンドを送り、 ソケットをクローズします。

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

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