java-recipes

ホーム ネットワーク › N-08: FTP ファイル転送

N-08: FTP ファイル転送(FTPClient ソケット実装)

FTP(File Transfer Protocol)は、ファイルをサーバーにアップロード・ダウンロードするためのプロトコルです。 このページでは FTP プロトコルの仕組みを TCP ソケットで実装しながら理解します。 コマンドチャネル(制御用)とデータチャネル(転送用)の 2 本の接続を使う点が特徴です。

⚠️ このページは「仕組みを理解する」ための実装です

実務で FTP を使う場合は Apache Commons Net FTPClient クラスを使用してください。 ソケットで手作りすると、タイムアウト処理・SSL/TLS(FTPS)対応・ マルチライン応答の処理など多くの考慮事項があります。

FTP プロトコルの仕組み

FTP は 2 本のコネクションを使います。 「コマンドチャネル(ポート 21)」でコマンドを送受信し、 「データチャネル(PASV で動的に決まるポート)」でファイルを転送します。 この 2 チャネル構成が FTP の最大の特徴です。

チャネルポート用途
コマンドチャネル21(固定)コマンド送信・応答受信(接続中ずっと維持)
データチャネルPASV で動的決定ファイルの実際の転送(転送ごとに開いて閉じる)

PASV ポート計算式

// サーバーから受信する応答例

227 Entering Passive Mode (192,168,1,1,196,10)

// カッコ内の最後の2数字を使う

データチャネルポート = 196 × 256 + 10 = 50186

サンプルコード

Java 8 版では FTP プロトコルのフローと主要コマンドを説明し、PASV ポート計算を実装しています。 Java 17 版では record で FTP コマンドを型安全に表現し、switch 式で応答コードを分類します。 Java 21 版では sealed interface と パターンマッチング switch で FTP 応答カテゴリを型安全に処理します。

Sample.java
import java.io.*;
import java.net.Socket;

public class FtpClientSample {

    // FTP プロトコルの主要コマンドと応答コード
    static void showFtpProtocol() {
        System.out.println("=== FTP プロトコルの基本 ===");
        System.out.println();
        System.out.println("【接続フロー】");
        System.out.println("1. コマンドチャネル接続: Socket(ftpserver, 21)");
        System.out.println("   S: 220 FTP Service Ready");
        System.out.println("2. 認証:");
        System.out.println("   C: USER anonymous");
        System.out.println("   S: 331 Password required");
        System.out.println("   C: PASS guest@example.com");
        System.out.println("   S: 230 Login successful");
        System.out.println("3. パッシブモード(ファイアウォール対応):");
        System.out.println("   C: PASV");
        System.out.println("   S: 227 Entering Passive Mode (127,0,0,1,196,10)");
        System.out.println("   → データチャネルのポートを計算: 196*256+10 = 50186");
        System.out.println("4. バイナリ転送モード:");
        System.out.println("   C: TYPE I");
        System.out.println("   S: 200 Type set to I");
        System.out.println("5. ファイルアップロード:");
        System.out.println("   C: STOR filename.txt");
        System.out.println("   S: 150 Opening data channel");
        System.out.println("   (データソケットでファイル送信)");
        System.out.println("   S: 226 Transfer complete");
        System.out.println("6. ファイルダウンロード:");
        System.out.println("   C: RETR filename.txt");
        System.out.println("   S: 150 Opening data channel");
        System.out.println("   (データソケットでファイル受信)");
        System.out.println("   S: 226 Transfer complete");
        System.out.println("7. 切断:");
        System.out.println("   C: QUIT");
        System.out.println("   S: 221 Goodbye");
    }

    // PASV モードのポート番号を計算
    static int parsePasvPort(String pasvResponse) {
        // 例: "227 Entering Passive Mode (192,168,1,1,196,10)"
        int start = pasvResponse.indexOf('(');
        int end = pasvResponse.indexOf(')');
        if (start < 0 || end < 0) {
            throw new IllegalArgumentException("PASV レスポンスの形式が不正: " + pasvResponse);
        }
        String[] parts = pasvResponse.substring(start + 1, end).split(",");
        int p1 = Integer.parseInt(parts[4].trim());
        int p2 = Integer.parseInt(parts[5].trim());
        return p1 * 256 + p2;
    }

    // FTP コマンド一覧(主要)
    static void showFtpCommands() {
        System.out.println("\n=== 主要 FTP コマンド ===");
        System.out.println("USER <name>  : ユーザー名送信");
        System.out.println("PASS <pwd>   : パスワード送信");
        System.out.println("PWD          : 現在のディレクトリ表示");
        System.out.println("CWD <dir>    : ディレクトリ変更");
        System.out.println("LIST         : ファイル一覧取得");
        System.out.println("MKD <dir>    : ディレクトリ作成");
        System.out.println("RMD <dir>    : ディレクトリ削除");
        System.out.println("STOR <file>  : ファイルアップロード");
        System.out.println("RETR <file>  : ファイルダウンロード");
        System.out.println("DELE <file>  : ファイル削除");
        System.out.println("PASV         : パッシブモード(データチャネルポート取得)");
        System.out.println("TYPE I       : バイナリ転送モード");
        System.out.println("TYPE A       : テキスト転送モード");
        System.out.println("QUIT         : 切断");
    }

    // 応答コードの意味
    static void showResponseCodes() {
        System.out.println("\n=== FTP 応答コード ===");
        System.out.println("1xx: 処理継続中");
        System.out.println("2xx: 成功");
        System.out.println("  220: サービス準備完了");
        System.out.println("  226: 転送完了");
        System.out.println("  230: ログイン成功");
        System.out.println("  250: 操作成功");
        System.out.println("3xx: 追加情報要求");
        System.out.println("  331: パスワード要求");
        System.out.println("4xx: 一時エラー");
        System.out.println("5xx: 永続エラー");
        System.out.println("  530: ログイン失敗");
        System.out.println("  550: ファイルが見つからない");
    }

    public static void main(String[] args) {
        showFtpProtocol();
        showFtpCommands();
        showResponseCodes();

        System.out.println("\n=== PASV ポート計算のデモ ===");
        String pasvResponse = "227 Entering Passive Mode (192,168,1,1,196,10)";
        int port = parsePasvPort(pasvResponse);
        System.out.println("PASV レスポンス: " + pasvResponse);
        System.out.println("データチャネルポート: 196*256+10 = " + port);

        System.out.println("\n実務では Apache Commons Net(FTPClient クラス)を使用してください");
        System.out.println("このサンプルは FTP プロトコルの仕組み理解が目的です");
    }
}

よくあるミス・注意点

⚠️ アクティブモードとパッシブモードの混同

FTP にはアクティブモード(PORT コマンド)とパッシブモード(PASV コマンド)があります。 アクティブモードはサーバーがクライアントに接続するため、 クライアント側のファイアウォールで接続が遮断されることがあります。 ファイアウォール環境では必ずパッシブモード(PASV)を使用してください。

// アクティブモード(NG: ファイアウォールで遮断されやすい)
// C: PORT 192,168,1,100,200,50  ← クライアントが待ち受けポートを指定
// S: サーバーからクライアントに接続してくる

// パッシブモード(OK: ファイアウォールに優しい)
// C: PASV
// S: 227 Entering Passive Mode (192,168,1,1,196,10)
// C: クライアントがサーバーのデータポートに接続する

バイナリモードとテキストモードの違い

FTP のデフォルト転送モードはテキストモード(TYPE A)です。 テキストモードでは改行コードが OS に合わせて変換されるため、 バイナリファイル(画像・PDF・ZIP など)が破損します。 ファイル転送前に必ず TYPE I でバイナリモードに切り替えてください。

// バイナリファイル転送前に必ず設定
C: TYPE I
S: 200 Type set to I (Binary mode)

// テキストファイルの場合(改行変換が必要な場合のみ)
C: TYPE A
S: 200 Type set to A (ASCII mode)

Java バージョンごとの違い

Java 8 ではすべてクラスと通常の変数宣言で実装します。 Java 17 では record で コマンドを不変なデータとして表現でき、switch 式で応答コードを簡潔に分類できます。 Java 21 では sealed interface と パターンマッチング switch を組み合わせることで、応答カテゴリの網羅的な処理が コンパイル時に保証されます。

テストする観点

  • PASV レスポンスのポート計算が正しいか (例: 196,10196×256+10=50186
  • PASV レスポンスの形式が不正な場合にIllegalArgumentException が発生するか
  • 応答コードの分類(1xx/2xx/3xx/4xx/5xx)が正しいか (境界値: 100・199・200・299・300・399・400・499・500・599)
  • FTP コマンド文字列が CRLF(\\r\\n)で終端されているか
  • 引数なしコマンド(PASV・QUIT など)と引数ありコマンド(USER・STOR など)でコマンド行の形式が正しいか

GitHub でソースコードを見る →