科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网网络频道用Java构建稳定的Ftp服务器 (3)

用Java构建稳定的Ftp服务器 (3)

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

下面我们要处理用户连接,也就是FtpConnection类。Ftp连接本质上是一个状态机,当FtpConnection接收到用户命令后,根据当前状态决定响应及下一个状态。

作者:asklxf 来源:J2ME开发网 2008年6月23日

关键字: 网吧 网吧组网 网吧服务器架设

  • 评论
  • 分享微博
  • 分享邮件

  下面我们要处理用户连接,也就是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 accessdenied.");

  }

  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再改进!:

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章