Javaで安定したFtpサーバを構築【回転】


Ftpサービスは最もよく使われるネットワークサービスの一つであり、wwwが流行している今日、Ftpは以前ほど広く使われていないが、多くの大学などの科学研究機関では、Ftpは依然として最もよく使われるファイル交換方式である.
Ftpサーバの構築は、サーバが複雑なグラフィックインタフェースを必要としないため、Ftpクライアントの構築よりも簡単です.従来のC/C++よりも,Javaを用いたマルチスレッドとネットワークプログラミングにより,安定で信頼性の高いFtpサーバを容易に開発できる.
Ftpプロトコルの概要
File Transfer Protocol、ファイル転送プロトコル、その名の通り、Ftpはファイルの転送に用いられ、FtpプロトコルはTCPプロトコルに基づいているので、1つのFtpセッションが始まる前に、クライアントとサーバはまずTCP接続を確立しなければならない.このTCP接続は通常制御接続と呼ばれ、クライアントはこの接続を通じてサーバにFTPコマンドを送信し、サーバはコマンドを処理した後、応答コードが返されます.
各コマンドには、少なくとも1つの応答が必要です.複数の場合は、区別しやすいようにします.FTP応答は3つの数字で構成され,後にいくつかのテキストが続く.数字には十分な情報があり、クライアントプログラムは後のテキストを知らなくても何が起こったのかを知ることができます.テキスト情報はサーバに関連しており、異なるユーザー、異なるサーバには異なるテキスト情報がある可能性があります.テキストと数字はスペースで、テキストの後は改行()で終わります.テキストが1行より多い場合は、最初の行に複数行のテキストであることを示す情報が必要であり、最後の行にも終了行としてマークされます.例えば、クライアントが現在のディレクトリを取得するコマンド「PWD」を送信すると、サーバの応答は次のようになります.
200/pub/incoming
応答コードの3桁の数字には明確な意味があります.
•1 xxは、コマンドが受け入れられたことを示すための予備応答を決定するが、要求された動作は初期化され、次のコマンドに進む前に別の応答を待つ.
•2 xxは応答完了を確定し、要求された操作が完了し、新しいコマンドを実行することができる.
•3 xxは中間応答を確定し、命令は受け入れられたが、要求された操作は停止された.
•4 xxは応答の完了を一時的に拒否し、コマンドを受け入れなかったが、エラーは一時的であり、サーバが忙しいなどのメッセージを後で再送信することができる.
•5 yzは、ログインを拒否するなどのエラーを表す応答の完了を永遠に拒否します.
2番目の数字は、次の意味を表します.

x 0 xフォーマットエラー;

x 1 xこのような応答は、情報を要求するためである.

x 2 xこのような応答は制御とデータ接続に関するものである.

x 3 x認証とアカウントログインプロセスについて;

x 4 xは使用されていません.

x 5 xこのような応答はファイルシステムに関するものである.
一般的な対応は次のとおりです.

200コマンドの実行に成功しました.

202コマンドが実装されていない.

230ユーザログイン;

331ユーザー名が正しいので、パスワードが必要です.

450要求されたファイル操作が実行されていない.

500コマンドは認識できません

502コマンドが実装されていません
1つのFtpセッション中、常に1つの制御接続があり、クライアントがファイルを要求すると、1つのデータ接続があるが、FTPプロトコルは、制御接続をオフにすれば、データ接続(ある場合)もオフにしなければならないことを規定している.
異なるFTPサーバによるFTPコマンドのサポートの程度は異なるかもしれませんが、TCP規格はすべてのFTPサーバが実現しなければならないコマンドを定義しており、この最小コマンドセットを実現するFTPサーバを構築することを目標としています.
基本的なFTPプロトコルとセッションについては前述したが,Javaを用いて簡単なFtpサーバを開発する.
簡単にするために、2つのクラスのみを設計します.1つのFtpServerクラスはリスニングに使用され、1つのFtpConnectionクラスは1つのユーザー接続を表し、各接続は1つのスレッドを使用します.
FtpServerは、サーバソケットを初期化し、ユーザー接続をリスニングします.Ftpサーバのルートディレクトリを初期化するためのパラメータを受け入れます.

package jftp;

import java.net.*;

public class FtpServer extends Thread {

    public static final int FTP_PORT = 21; // default port

    ServerSocket ftpsocket = null;

    public static void main(String[] args) {

        if(args.length!=1) {

            System.out.println("Usage:");

            System.out.println("java FtpServer [root dir]");

            System.out.println("nExample:");

            System.out.println("java FtpServer C:\\ftp\\");

            return;

        }

        FtpConnection.root = args[0];

        System.out.println("[info] ftp server root: " + FtpConnection.root);

        new FtpServer().start();

    }

    public void run() {

        Socket client = null;

        try {

            ftpsocket = new ServerSocket(FTP_PORT);

            System.out.println("[info] listening port: " + FTP_PORT);

            for(;;) {

                client = ftpsocket.accept();

                new FtpConnection(client).start();

            }

        }

        catch(Exception e) { e.printStackTrace(); }

    }

}

お客様が接続するたびに、新しいFtpConnectionスレッドを作成してユーザーにサービスを提供します.最大接続数を簡単に制限して、Ftpサーバの負担が重すぎないようにすることができます.
次に、FtpConnectionクラスというユーザー接続を処理します.Ftp接続は本質的にステータスマシンであり、FtpConnectionがユーザコマンドを受信すると、現在のステータスに基づいて応答および次のステータスが決定されます.しかし、複雑なステートマシンの実装を考慮する必要はありません.リスニング/受信/処理/応答だけでいいです.

package jftp;

import java.net.*;

import java.io.*;

import java.util.*;

import java.text.*;

public class FtpConnection extends Thread {

    /**     */

    static public String root = null;

    private String currentDir = "/"; //     

    private Socket socket;

    private BufferedReader reader = null;

    private BufferedWriter writer = null;

    private String clientIP = null;

    private Socket tempSocket = null; // tempSocket      

    private ServerSocket pasvSocket = null; //       

    private String host = null;

    private int port = (-1);

    public FtpConnection(Socket socket) {

        this.socket = socket;

        this.clientIP = socket.getInetAddress().getHostAddress();

    }

    public void run() {

        String command;

        try {

            System.out.println(clientIP + " connected.");

            socket.setSoTimeout(60000); // ftp    

            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

            response("220-    ......");

            response("220-    ......");

            response("220             “-”");

            for(;;) {

                command = reader.readLine();

                if(command == null)

                    break;

                System.out.println("command from " + clientIP + " : " + command);

                parseCommand(command);

                if(command.equals("QUIT")) //   QUIT  

                    break;

            }

        }

        catch(Exception e) { e.printStackTrace(); }

        finally {

            try {

                if(reader!=null) reader.close();

            }catch(Exception e) {}

            try {

                if(writer!=null) writer.close();

            }catch(Exception e) {}

            try {

                if(this.pasvSocket!=null) pasvSocket.close();

            }catch(Exception e) {}

            try {

                if(this.tempSocket!=null) tempSocket.close();

            }catch(Exception e) {}

            try {

                if(this.socket!=null) socket.close();

            }catch(Exception e) {}

        }

        System.out.println(clientIP + " disconnected.");

    }

//FtpConnection run()            /    ,   QUIT ,    ,  Ftp  。

//         :

private void response(String s) throws Exception {

    // System.out.println("  [RESPONSE] "+s);

    writer.write(s);

    writer.newLine();

    writer.flush(); //    flush         

}

//        

private static String pad(int length) {

    StringBuffer buf = new StringBuffer();

    for (int i = 0; i < length; i++)

        buf.append((char)' ');

    return buf.toString();

}

//     

private String getParam(String cmd, String start) {

    String s = cmd.substring(start.length(), cmd.length());

    return s.trim();

}

//     

private String translatePath(String path) {

    if(path==null) return root;

    if(path.equals("")) return root;

    path = path.replace('/', '\\');

    return root + path;

}

//       ,        

private String getFileLength(long length) {

    String s = Long.toString(length);

    int spaces = 12 - s.length();

    for (int i = 0; i < spaces; i++)
                 s = " " + s;
          return s;

}

//           ,       ,      ,    LIST        :

private void parseCommand(String s) throws Exception {

    if(s==null || s.equals(""))

        return;

    if(s.startsWith("USER ")) {

        response("331 need password");

    }

    else if(s.startsWith("PASS ")) {

        response("230 welcome to my ftp!");

    }

    else if(s.equals("QUIT")) {

        response("221     !");

    }

    else if(s.equals("TYPE A")) {

        response("200 TYPE set to A.");

    }

    else if(s.equals("TYPE I")) {

        response("200 TYPE set to I.");

    }

    else if(s.equals("NOOP")) {

        response("200 NOOP OK.");

    }

    else if(s.startsWith("CWD")) { //       ,            

        this.currentDir = getParam(s, "CWD ");

        response("250 CWD command successful.");

    }

    else if(s.equals("PWD")) { //       

        response("257 \"" + this.currentDir + "\" is current directory.");

    }

    else if(s.startsWith("PORT ")) {

        //     

        String[] params = getParam(s, "PORT ").split(",");

        if(params.length<=4 || params.length>=7)

            response("500 command param error.");

        else {

            this.host = params[0] + "." + params[1] + "." + params[2] + "." + params[3];

            String port1 = null;

            String port2 = null;

            if(params.length == 6) {

                port1 = params[4];

                port2 = params[5];

            }

            else {

                port1 = "0";

                port2 = params[4];

            }

            this.port = Integer.parseInt(port1) * 256 + Integer.parseInt(port2);

            response("200 command successful.");

        }

    }

    else if(s.equals("PASV")) { //       

        if(pasvSocket!=null)

            pasvSocket.close();

        try {

            pasvSocket = new ServerSocket(0);

            int pPort = pasvSocket.getLocalPort();

            String s_port;

            if(pPort<=255)

                s_port = "255";

            else {

                int p1 = pPort / 256;

                int p2 = pPort - p1*256;

                s_port = p1 + "," + p2;

            }

            pasvSocket.setSoTimeout(60000);

            response("227 Entering Passive Mode ("

                + InetAddress.getLocalHost().getHostAddress().replace('.', ',')

                + "," + s_port + ")");

        }

        catch(Exception e) {

            if(pasvSocket!=null) {

                pasvSocket.close();

                pasvSocket = null;

            }

        }

    }

    else if(s.startsWith("RETR")) { //    

        String file = currentDir + (currentDir.endsWith("/") ? "" : "/") + getParam(s, "RETR");

        System.out.println("download file: " + file);

        Socket dataSocket;

        //       PASV PORT        socket

        if(pasvSocket!=null)

            dataSocket = pasvSocket.accept();

        else

            dataSocket = new Socket(this.host, this.port);

        OutputStream dos = null;

        InputStream fis = null;

        response("150 Opening ASCII mode data connection.");

        try {

            fis = new BufferedInputStream(new FileInputStream(translatePath(file)));

            dos = new DataOutputStream(new BufferedOutputStream(dataSocket.getOutputStream()));

            //         :

            byte[] buffer = new byte[20480]; //      20k

            int num = 0; //           

            do {

                num = fis.read(buffer);

                if(num!=(-1)) {

                    //   :

                    dos.write(buffer, 0, num);

                    dos.flush();

                }

            } while(num!=(-1));

            fis.close();

            fis = null;

            dos.close();

            dos = null;

            dataSocket.close();

            dataSocket = null;

            response("226 transfer complete."); //         

        }

        catch(Exception e) {

            response("550 ERROR: File not found or access denied.");

        }

        finally {

            try {

                if(fis!=null) fis.close();

                if(dos!=null) dos.close();

                if(dataSocket!=null) dataSocket.close();

            }

            catch(Exception e) {}

        }

    }

    else if(s.equals("LIST")) { //        

        Socket dataSocket;

        //       PASV PORT        socket

        if(pasvSocket!=null)

            dataSocket = pasvSocket.accept();

        else

            dataSocket = new Socket(this.host, this.port);

        PrintWriter writer = new PrintWriter(new BufferedOutputStream(dataSocket.getOutputStream()));

        response("150 Opening ASCII mode data connection.");

        try {

            responseList(writer, this.currentDir);

            writer.close();

            dataSocket.close();

            response("226 transfer complete.");

        }

        catch(IOException e) {

            writer.close();

            dataSocket.close();

            response(e.getMessage());

        }

        dataSocket = null;

    }

    else {

        response("500 invalid command"); //        ,      

    }

}

//   LIST  

private void responseList(PrintWriter writer, String path) throws IOException {

    File dir = new File(translatePath(path));

    if(!dir.isDirectory())

        throw new IOException("550 No such file or directory");

    File[] files = dir.listFiles();

    String dateStr;

    for(int i=0; i        dateStr = new SimpleDateFormat("MMM dd hh:mm").format(new Date(files[i].lastModified()));

        if(files[i].isDirectory()) {

            writer.println("drwxrwxrwx  1 ftp      System            0 "

            + dateStr + " " + files[i].getName());

        }

        else {

            writer.println("-rwxrwxrwx  1 ftp      System "

            + getFileLength(files[i].length()) + " " + dateStr + " " + files[i].getName());

        }

    }

    String file_header = "-rwxrwxrwx  1 ftp      System            0 Aug  5 19:59 ";

    String dir_header =  "drwxrwxrwx  1 ftp      System            0 Aug 15 19:59 ";

    writer.println("total " + files.length);

    writer.flush();

}

}

基本的に私たちのFtpはすでに実行することができて、私たちがFtpConnectionの中でUSERとPASSコマンドを処理することに気づいて、直接200 OKを返して、もしユーザー名とパスワードを検証する必要があるならば、また相応のコードを追加する必要があります.
Ftpサーバのデバッグ方法
最も簡単な方法は、既存のFtpクライアントを使用して、CuteFtpを推奨することです.クライアントから送信されたコマンドとサーバ応答を常に印刷しているので、サーバの出力結果を簡単に見ることができます.
もう一つの小さなBugは、ファイルリストがCuteFtpで正常に表示され、他のFtpクライアントで正常に表示されるとは限らない.これは、出力応答の「互換性」がまだ十分ではないことを示している.暇があれば、FtpのRFCを見てから改善してください.:)