公開日:6/29/2021 更新日:11/26/2023
本記事ではFTPサーバの準備方法、ソケット通信でFTPサーバにファイル送信を行うサンプルコードを載せています。FTPサーバへの理解の一助となれたら幸いです。
サーバーとクライアント間で、ファイルを送受信する通信の決まりごとです。この決まり事をプロトコルと良います。サーバー側はデーモン(常駐プログラム)として待機していて、クライアント側からの依頼を待ちます。クライアントとサーバ間の依頼はポート番号21でやりとりして、ファイル転送などのデータのやり取りはポート番号20で行います。前者のポート番号21の接続をコントロールコネクションと呼び 、後者のポート番号20の接続をデータコネクションと呼びます。
以下のサイトから、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 に設定されてるので必要があれば適宜変更してください。
■注意
デフォルトでは、下記設定がそれぞれ 4800 bytes/s となっており速度制限がかかっている。制限を外す場合は 0 の値を設定すると良い。
ftpserver.user.{username}.uploadrate
:The maximum number of bytes per second the user is allowed to upload files. 0 disables the check.
ftpserver.user.{username}.downloadrate
:The maximum number of bytes per second the user is allowed to download files. 0 disables the check.
PropertiesUserManager (Apache FtpServer Core 1.2.0 API)
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サーバを起動した後に、先ほど作成したユーザー名:ftptest、パスワード:ftptest でログインできることを確認します。
先ほど設定した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サーバー側に接続する |
アクティブモードでデータコネクションを確立しようとした場合、接続要求がサーバー側からクライアントに出されることが特徴です。クライアント側がファイアウォールのポートを開放していない場合、データコネクションが確立できず接続失敗となります。なのでセキュリティ面から、ファイアウォールに穴開けをしないで済むパッシブモードが採用されるケースが多いです。アクティブモードのファイル転送の手順としては、まず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.
ファイル送信成功
パッシブモードでは、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