公開日:6/29/2021  更新日:3/26/2022

  • twitter
  • facebook
  • line

【Java】ソケット通信でFTPサーバにファイル送信するプログラムを作成してみた#アクティブモード#パッシブモード

はじめに

本記事ではFTPサーバの準備方法、ソケット通信でFTPサーバにファイル送信を行うサンプルコードを載せています。FTPサーバへの理解の一助となれたら幸いです。

目次

  1. FTP (File Transfer Protocol) とは?
  2. FTPサーバの準備
    1. 設定ファイルの編集
    2. FTPサーバの起動
    3. FTPサーバにログイン
  3. ログイン処理まで実装
    1. リファクタリング
  4. ファイルアップロードを実装
    1. アクティブモード (Active Mode)
    2. パッシブモード (Passive Mode)
  5. 参考までに

FTP (File Transfer Protocol) とは?

サーバーとクライアント間で、ファイルを送受信する通信の決まりごとです。この決まり事をプロトコルと良います。サーバー側はデーモン(常駐プログラム)として待機していて、クライアント側からの依頼を待ちます。クライアントとサーバ間の依頼はポート番号21でやりとりして、ファイル転送などのデータのやり取りはポート番号20で行います。前者のポート番号21の接続をコントロールコネクションと呼び 、後者のポート番号20の接続をデータコネクションと呼びます。
イメージ図

FTPサーバの準備

以下のサイトから、Apache MINA のFTPサーバをダウンロードします。
Apache MINA Project
本記事では執筆時点 (2021/06/29) での最新版 Apache FtpServer 1.1.1 Release を使用してます。

設定ファイルの編集

apache-ftpserver-1.1.1\res\conf\users.properties を編集します。デフォルトで admin と anonymous ユーザーが既に存在するので、以下のようにテスト用ユーザーの設定をファイルに追記します。例では、ユーザー名とパスワードが ftptest となるユーザーを追加しています。

ftpserver.user.ftptest.userpassword=ftptest
ftpserver.user.ftptest.homedirectory=./res/home
ftpserver.user.ftptest.enableflag=true
ftpserver.user.ftptest.writepermission=true
ftpserver.user.ftptest.maxloginnumber=20
ftpserver.user.ftptest.maxloginperip=2
ftpserver.user.ftptest.idletime=300
ftpserver.user.ftptest.uploadrate=4800
ftpserver.user.ftptest.downloadrate=4800

次に、apache-ftpserver-1.1.1\res\conf\ftpd-typical.xml を編集します。
パスワード認証が暗号化されないように、file-user-manager のタグに encrypt-passwords の設定を下記のように追加してください。

<file-user-manager file="./res/conf/users.properties" encrypt-passwords="clear"/>

また、デフォルトでポートが 2121 に設定されてるので必要があれば適宜変更してください。

FTPサーバの起動

apache-ftpserver-1.1.1\bin\ftpd.bat を実行するとFTPサーバを起動することはできるのですが、そのままだとftptest でログイン出来ないので以下のようなbatファイルを作ります。xxxxx の部分は各自のフォルダパスを設定してください。要はftpd.batにftpd-typical.xmlの設定ファイルを引数に設定して実行すれば良いわけです。

xxxxx\apache-ftpserver-1.1.1\bin\ftpd.bat xxxxx\apache-ftpserver-1.1.1\res\conf\ftpd-typical.xml

FTPサーバにログイン

FTPサーバを起動した後に、先ほど作成したユーザー名:ftptest、パスワード:ftptest でログインできることを確認します。
FTPログイン

ログイン処理まで実装

先ほど設定したFTPサーバにログインする所までをコーディングしてみます。
何をしているのか分かりやすくするため、関数を用意せずべた書きして流れを見ていきます。
Socketを通してFTPサーバにメッセージを送ったり、応答メッセージを受け取ったりしています。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class FTPClientTest {

	public static void main(String[] args) {
		//接続設定
		String hostname = "localhost";
		int port = 2121;
		String username = "ftptest";
		String password = "ftptest";

		try {
			//FTPサーバに接続
			Socket socket = new Socket(hostname,port);
			BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			BufferedWriter bw =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
			//応答メッセージ出力
			String buffer = null;
			buffer = br.readLine();
			System.out.println(buffer);
			//ログイン処理:ユーザ名入力
			bw.write("USER"+" "+username);
			bw.newLine();
			bw.flush();
			//応答メッセージ出力
			buffer = br.readLine();
			System.out.println(buffer);
			//ログイン処理:パスワード入力
			bw.write("PASS"+" "+password);
			bw.newLine();
			bw.flush();
			//応答メッセージ出力
			buffer = br.readLine();
			System.out.println(buffer);
                        //コネクションを切断
                        socket.close();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

実行結果

220 Service ready for new user.
331 User name okay, need password for ftptest.
230 User logged in, proceed.

リファクタリング

FTPクライアントプログラムではソケット通信を介してメッセージの送受信を繰り返します。なので、①ソケット通信のコネクションを張る関数、②メッセージを受信する関数、③メッセージを送信する関数 を用意してソースの可読性を改善していきます。

public class FTPClientTest {

	public static void main(String[] args) {
		//接続設定
		String hostname = "localhost";
		int port = 2121;
		String username = "ftptest";
		String password = "ftptest";
		Object[] controlHandler = new Object[3];

		try {
			//FTPサーバに接続
			controlHandler = connect(hostname,port);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//ログイン処理:ユーザ名入力
			sendCommand(controlHandler,"USER"+" "+username);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//ログイン処理:パスワード入力
			sendCommand(controlHandler,"PASS"+" "+password);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
        //コントロールコネクションを切断
			((Socket)controlHandler[0]).close();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	//FTPサーバに接続
	private static Object[] connect(String hostname,int port) throws Exception{
		Object[] obj = new Object[3];
		obj[0] = new Socket(hostname,port);
		obj[1] = new BufferedReader(new InputStreamReader(((Socket)obj[0]).getInputStream()));
		obj[2] = new BufferedWriter(new OutputStreamWriter(((Socket)obj[0]).getOutputStream()));
		return obj;
	}
	//FTPサーバからメッセージ取得
	private static String getMessage (Object[] obj) throws Exception{
		BufferedReader br = (BufferedReader) obj[1];
		return br.readLine();
	}
	//FTPコマンドを送信
	private static void sendCommand (Object[] obj,String command) throws Exception{
		BufferedWriter bw = (BufferedWriter) obj[2];
		bw.write(command);
		bw.newLine();
		bw.flush();
	}
}

ファイルアップロードを実装

ログインする所まで出来たので、次はデータファイルをローカルからFTPサーバにアップロードする処理をコーディングしていきます。FTPの転送モードにはアクティブモードとパッシブモードが存在します。

種類 特徴
アクティブモード FTPサーバー側からFTPクライアントに接続
パッシブモード FTPクライアントからFTPサーバー側に接続する

アクティブモード (Active Mode)

アクティブモードでデータコネクションを確立しようとした場合、接続要求がサーバー側からクライアントに出されることが特徴です。クライアント側がファイアウォールのポートを開放していない場合、データコネクションが確立できず接続失敗となります。なのでセキュリティ面から、ファイアウォールに穴開けをしないで済むパッシブモードが採用されるケースが多いです。アクティブモードのファイル転送の手順としては、まずPORTコマンドにてデータコネクションで使用するIPアドレスとポート番号をサーバに通知します。次にサーバ側が通知されたクライアント側の宛先ポートに接続した後に、接続されたデータコネクションを使用してファイルの転送が行われます。
今回は、ポート番号 55020を指定してデータコネクションを確立しています。PORTコマンドですが、214×256+236 が55020となるので、サーバ側は55020のポートでクライアント (IPアドレス:127,0,0,1) に接続してきます。

PORT 127,0,0,1,214,236

アクティブモードでファイルをアップロードするサンプルコードは以下です。

public class FTPClientActiveTest {
	public static void main(String[] args) {
		//接続設定
		String hostname = "localhost";
		int port = 2121;
		int dataPort = 55020;
		String username = "ftptest";
		String password = "ftptest";
		Object[] controlHandler = new Object[3];
		Object[] dataHandler = new Object[3];
		String uploadFileName = "test2.txt";
		String localFilePath = "C:\\Users\\atsus\\Desktop\\test1.txt";

		try {
			//コントロールコネクションに接続
			controlHandler = connect(hostname,port);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//ログイン処理:ユーザ名入力
			sendCommand(controlHandler,"USER"+" "+username);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//ログイン処理:パスワード入力
			sendCommand(controlHandler,"PASS"+" "+password);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//Activeモードでファイル送信  
			dataHandler = connectActive(controlHandler,dataPort,uploadFileName);
			if (sendFile(dataHandler,localFilePath)) {
				System.out.println("ファイル送信成功");
			}else {
				System.out.println("ファイル送信失敗");
			}
			//コネクションを切断
			((Socket)controlHandler[0]).close();
			((Socket)dataHandler[0]).close();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	//コントロールコネクションに接続
	private static Object[] connect(String hostname,int port) throws Exception{
		Object[] obj = new Object[3];
		obj[0] = new Socket(hostname,port);
		obj[1] = new BufferedReader(new InputStreamReader(((Socket)obj[0]).getInputStream()));
		obj[2] = new BufferedWriter(new OutputStreamWriter(((Socket)obj[0]).getOutputStream()));
		return obj;
	}
	//アクティブモードでのデータコネクション接続
	private static Object[] connectActive(Object[] controlHandler,int port,String uploadFileName) throws Exception{
		Object[] obj = new Object[3];
		ServerSocket serverSocket = new ServerSocket(port);
		sendCommand(controlHandler,"PORT"+" "+"127,0,0,1,"+String.valueOf(port>>8)+","+String.valueOf(port & 0xff));
		System.out.println("PORT"+" "+"127,0,0,1,"+String.valueOf(port>>8)+","+String.valueOf(port & 0xff));
		System.out.println(getMessage(controlHandler));
		sendCommand(controlHandler,"STOR" +" " + uploadFileName);
		System.out.println(getMessage(controlHandler));
		Socket socket = serverSocket.accept();
		serverSocket.close();
		obj[0] = socket;
		obj[1] = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		obj[2] = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
		return obj;
	}
	//FTPサーバからメッセージ取得
	private static String getMessage (Object[] obj) throws Exception{
		BufferedReader br = (BufferedReader) obj[1];
		return br.readLine();
	}
	//FTPコマンドを送信
	private static void sendCommand (Object[] obj,String command) throws Exception{
		BufferedWriter bw = (BufferedWriter) obj[2];
		bw.write(command);
		bw.newLine();
		bw.flush();
	}
	//ファイルを送信
	public static boolean sendFile(Object[] dataHandle,String LocalFilePath){
		Socket socket= (Socket)dataHandle[0];
		BufferedInputStream is = null;
		BufferedOutputStream os = null;
		try{
			is = new BufferedInputStream(new FileInputStream(new File(LocalFilePath)));
			os = new BufferedOutputStream(socket.getOutputStream());
			byte[] buffer = new byte[1800];
			int size = 0;
			while((size=is.read(buffer))!= -1){
				os.write(buffer,0,size);
			}
			os.flush();
			os.close();
			is.close();
			return true;
		}catch(IOException e){
			System.out.println(e.getMessage());
			return false;
		}
	}
}

実行結果

220 Service ready for new user.
331 User name okay, need password for ftptest.
230 User logged in, proceed.
PORT 127,0,0,1,214,236
200 Command PORT okay.
150 File status okay; about to open data connection.
ファイル送信成功

パッシブモード (Passive Mode)

パッシブモードでは、FTPクライアント側からPASV(EPSV)コマンドを用いてFTPサーバ側で新たにサーバソケットを作成し、接続先のポート番号を通知します。今回の実行例では、PASVコマンドにより以下の応答が返ってきました。応答は毎回ランダムとなります。

227 Entering Passive Mode (127,0,0,1,243,97)

接続先のポート番号は、243×256+97で計算されるので

62305

となります。
パッシブモードでファイルをアップロードするサンプルコードは以下です。

public class FTPClientPassiveTest {
	public static void main(String[] args) {
		//接続設定
		String hostname = "localhost";
		int port = 2121;
		String username = "ftptest";
		String password = "ftptest";
		Object[] controlHandler = new Object[3];
		Object[] dataHandler = new Object[3];
		String uploadFileName = "test2.txt";
		String localFilePath = "C:\\Users\\atsus\\Desktop\\test1.txt";

		try {
			//コントロールコネクションに接続
			controlHandler = connect(hostname,port);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//ログイン処理:ユーザ名入力
			sendCommand(controlHandler,"USER"+" "+username);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//ログイン処理:パスワード入力
			sendCommand(controlHandler,"PASS"+" "+password);
			//応答メッセージ出力
			System.out.println(getMessage(controlHandler));
			//パッシブモードに変更
			sendCommand(controlHandler,"PASV");
			String passiveMessage = getMessage(controlHandler);
			System.out.println(passiveMessage);
			//パッシブポートを計算
			int passiveDataPortNo = createPassivePort(passiveMessage);
			System.out.println(passiveDataPortNo);
			//データコネクションに接続してファイル送信
			dataHandler = connect(hostname,passiveDataPortNo);
			sendCommand(controlHandler,"STOR" +" " + uploadFileName);
			System.out.println(getMessage(controlHandler));
			if (sendFile(dataHandler,localFilePath)) {
				System.out.println("ファイル送信成功");
			}else {
				System.out.println("ファイル送信失敗");
			}
			//コネクションを切断
			((Socket)controlHandler[0]).close();
			((Socket)dataHandler[0]).close();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	//コントロールコネクションに接続
	private static Object[] connect(String hostname,int port) throws Exception{
		Object[] obj = new Object[3];
		obj[0] = new Socket(hostname,port);
		obj[1] = new BufferedReader(new InputStreamReader(((Socket)obj[0]).getInputStream()));
		obj[2] = new BufferedWriter(new OutputStreamWriter(((Socket)obj[0]).getOutputStream()));
		return obj;
	}
	//FTPサーバからメッセージ取得
	private static String getMessage (Object[] obj) throws Exception{
		BufferedReader br = (BufferedReader) obj[1];
		return br.readLine();
	}
	//FTPコマンドを送信
	private static void sendCommand (Object[] obj,String command) throws Exception{
		BufferedWriter bw = (BufferedWriter) obj[2];
		bw.write(command);
		bw.newLine();
		bw.flush();
	}
	//ファイルを送信
	public static boolean sendFile(Object[] obj,String LocalFilePath){
		Socket socket= (Socket)obj[0];
		BufferedInputStream is = null;
		BufferedOutputStream os = null;
		try{
			is = new BufferedInputStream(new FileInputStream(new File(LocalFilePath)));
			os = new BufferedOutputStream(socket.getOutputStream());
			byte[] buffer = new byte[1800];
			int size = 0;
			while((size=is.read(buffer))!= -1){
				os.write(buffer,0,size);
			}
			os.flush();
			os.close();
			is.close();
			return true;
		}catch(IOException e){
			System.out.println(e.getMessage());
			return false;
		}
	}
	//パッシブポートの組み立て
	public static int createPassivePort(String msg){
		int startPosition = msg.indexOf("(")+1;
		int endPosition = msg.indexOf(")");
		String trimeMessage = msg.substring(startPosition,endPosition);
		String[] messageList = trimeMessage.split(",");
		return Integer.parseInt(messageList[4])*256+Integer.parseInt(messageList[5]);
	}
}

実行結果

220 Service ready for new user.
331 User name okay, need password for ftptest.
230 User logged in, proceed.
227 Entering Passive Mode (127,0,0,1,243,97)
62305
150 File status okay; about to open data connection.
ファイル送信成功

参考までに

ファイルをアップロードした際に550 権限エラーが発生した場合。

550 /ファイル名: Permission denied.

FTPホームディレクトリへのファイル書き込み権限がないためエラーが発生しています。
apache-ftpserver-1.1.1\res\conf\users.properties に設定された追加ユーザの書き込み権限がtrueになっていることを確認して下さい。

ftpserver.user.ftptest.writepermission=true

戻る