`
Arthur_Wen
  • 浏览: 29964 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

用Java构建稳定的Ftp服务器【转】

    博客分类:
  • Java
阅读更多

Ftp服务是最常用的网络服务之一,虽然在www风行的今天,Ftp已经远不如以前使用得广泛,但是在许多大学等科研单位,Ftp仍然是最常用的文件交换方式。

构建一个Ftp服务器要比构建一个Ftp客户端来得简单,因为服务器不需要复杂的图形界面。相比传统的C/C++,使用Java的多线程和网络编程能令我们更轻易地开发出稳定可靠的Ftp服务器。

Ftp协议简介

File Transfer Protocol,文件传输协议,顾名思义,Ftp就是用于文件的传输,Ftp协议是基于TCP协议的,因此,在一个Ftp会话开始前,客户端和服务器必须首先建立一个TCP连接,这个TCP连接通常被称作控制连接,客户端通过此连接向服务器发送FTP命令,服务器处理命令后,将返回一个响应码。

每个命令必须有最少一个响应,如果是多个,要易于区别。FTP响应由三个数字构成,后面是一些文本。数字带有足够的信息,客户端程序不用知道后面的文本就知道发生了什么。文本信息与服务器相关,不同的用户,不同的服务器可能有不同的文本信息。文本和数字以空格间隔,文本后以换行符(\n)结束。如果文本多于一行,第一行内要有信息表示这是多行文本,最后一行也要标记为结束行。比如客户端发送获取当前目录的命令“PWD”,服务器的响应可能是:


200 /pub/incoming


响应码的三位数字都有明确的含义:


•1xx 确定预备应答,这类响应用于说明命令被接受,但请求的操作正在被初始化,在进入下一个命令前等待另外的应答。
•2xx 确定完成应答,要求的操作已经完成,可以执行新命令。
•3xx 确定中间应答,命令已接受,但要求的操作被停止。
•4xx 暂时拒绝完成应答,未接受命令,但错误是临时的,过一会儿可以再次发送消息,比如服务器忙。
•5yz 永远拒绝完成应答,此类响应码一般表示错误,如拒绝登陆。

第二位数字代表的意义:


x0x 格式错误;



x1x 此类应答是为了请求信息的;



x2x 此类应答是关于控制和数据连接的;



x3x 关于认证和帐户登录过程;



x4x 未使用;



x5x 此类应答是关于文件系统的;



常见的相应有:


200 命令执行成功;



202 命令未实现;



230 用户登录;



331 用户名正确,需要口令;



450 请求的文件操作未执行;



500 命令不可识别



502 命令未实现



一个Ftp会话过程中,始终有一个控制连接,如果客户端请求文件,则会有一个数据连接,但FTP协议规定:只要关闭了控制连接,数据连接(如果有)也必须关闭。

不同的FTP服务器对FTP命令的支持程度可能不同,但是TCP标准定义了所有FTP服务器都必须实现的命令,我们的目标就是构建一个实现这个最小命令集的FTP服务器。

前面讨论了基本的FTP协议和会话,下面我们用Java来开发一个简单的Ftp服务器。

为了简单起见,我们只设计两个类:一个FtpServer类用于监听,一个FtpConnection类代表一个用户连接,每个连接都使用一个线程。

FtpServer负责初始化ServerSocket并监听用户连接,它接受一个参数来初始化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再改进!:)

分享到:
评论

相关推荐

    java实现FTP服务器

    接下来,我们将深入分析如何使用Java来构建一个基本的FTP服务器。 #### FTP协议的特点与操作模式 FTP协议具有以下特点: 1. **数据传输模式**:FTP支持ASCII和Binary两种数据传输模式。ASCII模式适用于文本文件的...

    一个用Java写的FTP服务器程序

    你可以通过阅读和理解这些源码来深入学习如何使用Java构建FTP服务器。源码通常会包含服务器启动、客户端连接处理、命令解析、文件操作等核心逻辑,是学习FTP协议和Java网络编程的宝贵资源。 总的来说,Java实现的...

    java定时从ftp服务器更新相关文件

    Java定时从FTP服务器更新相关文件是一项常见的任务,特别是在自动化数据同步和备份的场景中。这里主要涉及的技术点包括FTP协议、Java编程以及文件系统操作。本文将深入探讨这些知识点,并提供一个基于`ftp4j`库的...

    用Java实现FTP服务器 .rar_FTP服务器_ftp_java ftp_java ftp 服务器_java ftp服务器

    标题中的“用Java实现FTP服务器”表明我们要讨论的是如何使用Java编程语言来构建一个FTP(File Transfer Protocol)服务器。FTP是一种用于在互联网上传输文件的标准协议。Java由于其跨平台性和丰富的类库,成为了...

    java语言实现ftp服务器.rar_ftp_ftp 服务器_ftp 服务器实现_java ftp 服务器_服务器

    Java语言实现FTP服务器涉及到多个关键知识点,包括FTP协议的理解、Socket编程、多线程处理以及文件I/O操作等。以下是对这些知识点的详细说明: ...通过学习和实践,你可以构建出稳定且安全的FTP服务器。

    FTP服务器 java

    在本项目中,我们将利用Java的Socket网络编程机制来构建一个简单的FTP服务器。 Socket编程是基于TCP/IP协议的,它为两台计算机之间提供了一种可靠的通信连接。在FTP服务器中,Socket用于建立客户端和服务器之间的...

    java实现ftp服务器端配套程序

    Java实现FTP服务器端配套程序是一种将Java编程语言用于构建文件传输协议(FTP)服务端的应用。FTP是一种在互联网上广泛使用的标准协议,用于在客户端和服务器之间上传、下载文件。在Java中,我们可以利用标准库中的`...

    java ftp服务器实例

    通过Java来实现FTP服务器,我们可以利用其强大的类库和面向对象特性,构建稳定且可扩展的文件管理系统。 首先,我们要了解Java中的`java.net`和`java.io`这两个核心包,它们提供了基础的网络通信和文件操作功能。在...

    ftp.rar_FTP 系统_FTP服务器_ftp_ftp java_java 实现 FTP 服务器 源码

    FTP(File Transfer Protocol)是一种广泛使用的互联网协议,用于在不同计算机之间传输文件。...通过学习和分析这些源码,我们可以更深入地了解FTP协议的实现机制,以及如何在Java环境中构建一个自定义的FTP服务器。

    java ftp客户端,服务端

    要实现FTP客户端,首先需要创建一个Socket连接到FTP服务器,然后通过输入输出流(InputStream和OutputStream)进行数据交互。客户端通常需要执行的操作包括登录、改变工作目录、列出目录内容、上传和下载文件等。 ...

    java 实现ftp 服务器

    Java 实现FTP服务器是一项常见的编程任务,这通常涉及到网络编程和文件传输协议的理解。FTP(File Transfer Protocol)是一种用于在互联网上传输文件的标准协议。在Java中实现FTP服务器,我们可以利用Java的内置类库...

    FTP_sever-java.rar_doc_ftp readme_java ftp_java ftp服务器

    在这个名为"FTP_sever-java.rar_doc_ftp readme_java ftp_java ftp服务器"的压缩包中,我们可以找到与使用Java语言实现FTP服务器相关的资源。这个压缩包包含一个"readme.doc"文档和一个名为"第2章 FTP客户端"的文件...

    java搭建ftp服务器

    6. 构建 FTP 服务器:一个简单的 Java FTP 服务器通常包括以下组件: - `FtpServer` 类:负责初始化 `ServerSocket` 并监听客户端连接。它可以设置 FTP 服务器的根目录,监听的端口号通常是 21。 - `FtpConnection...

    Ftp.zip_ftp java_java ftp_java ftp server_java 实现 FTP 服务器 源码

    在这个名为“Ftp.zip”的压缩包中,包含的是一个基于Java实现的简单FTP服务器和客户端的源码,这对于学习和理解FTP协议的工作原理以及如何用Java编程语言来实现FTP服务和客户端非常有帮助。 首先,我们来看看`...

    ftp_collision_java.rar_ftp java_java ftp_java ftp 服务器

    压缩包中的“www.pudn.com.txt”可能是文档或链接,可能包含了关于FTP服务器与Java实现的更详细介绍,例如可能包括代码示例、教程链接或其他资源。而“ftp_java”可能是一个Java源代码文件或者项目,包含了解决上述...

    Java FTP 实现跨服务器文件上传下载

    首先,需要创建一个FTPClient实例并连接到FTP服务器: ```java FTPClient ftpClient = new FTPClient(); try { ftpClient.connect("ftp.server.com", 21); ftpClient.login("username", "password"); } catch...

    JAVA实现FTP协议中的服务器和客户端\\

    因此,我们需要使用第三方库或Java内置的`javax.net.ssl.SSLSocket`(用于加密的FTP,即FTPS)来构建FTP客户端和服务器。 对于Java实现FTP服务器,我们可以使用Apache的Commons Net库。这个库提供了一个`FTPServer`...

    java FTP server

    开发者可以通过查看这些内容来学习和理解如何实现一个功能完备的Java FTP服务器,也可以直接部署和使用这个服务器。如果需要进一步了解或定制功能,可以深入研究源代码,学习其中的类和方法是如何实现FTP协议规范的...

    Java 基于Swing的FTP上传下载程序

    在这个项目中,Swing被用来构建用户界面,提供按钮、文本框等元素,让用户可以交互地输入FTP服务器的连接信息以及选择本地文件进行上传或下载。 FTP协议是一种用于网络上文件传输的标准协议,它允许用户在两台...

Global site tag (gtag.js) - Google Analytics