休息食客

随心而动

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  16 随笔 :: 7 文章 :: 2 评论 :: 0 Trackbacks

[转:资源来自互联网,仅供分享]

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服务器。

 

我们用Java来开发一个简单的Ftp服务器。

 

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

 

FtpServer负责初始化ServerSocket并监听用户连接,它接受一个参数来初始化Ftp服务器的根目录:

package com.loubing.ftp;

import java.net.*;


public class FtpServer extends Thread {
 public static final int ftpPort = 21;//定义ftp服务器端口为21
 ServerSocket ftpServer = null;

 /**
  * @param args
  */
 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 socket = null ;
  try {
   ftpServer = new ServerSocket(ftpPort);
   System.out.println("[info] listening port: " + ftpPort);
   while(true){
    socket = ftpServer.accept();
    new FtpConnection(socket).start();
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

}




package com.loubing.ftp;

import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.Date;

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 注意最后一行消息没有”-“");
   while(true){
    command = reader.readLine();
    if (command == null) {
     break;
    }
    System.out.println("command from " + clientIP + ":" + command);
    parseCommand(command);
    if(command.equals("QUIT"))break;//收到QUIT命令
   }
  } 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) {}
  }
 }
 
 //发送信息
 private void response(String s) throws Exception {
  writer.write(s);
  writer.newLine();
  writer.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;
 }
 
 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中处理USERPASS命令,直接返回200 OK,如果需要验证用户名和口令,还需要添加相应的代码。

 

如何调试Ftp服务器?

 

有个最简单的方法,便是使用现成的Ftp客户端,推荐CuteFtp,因为它总是把客户端发送的命令和服务器响应打印出来,我们可以非常方便的看到服务器的输出结果。

 

另外一个小Bug,文件列表在CuteFtp中可以正常显示,在其他Ftp客户端不一定能正常显示,这说明输出响应的“兼容性”还不够好,有空了看看FtpRFC再改进!:

 

资源来自互联网,仅供分享


posted on 2014-08-07 14:27 休息食客 阅读(277) 评论(0)  编辑  收藏

只有注册用户登录后才能发表评论。


网站导航: