服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

用java构建稳定的ftp服务器 (3)

下面我们要处理用户连接,也就是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<files.length; 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再改进!:

over

扫描关注微信公众号