休息食客

          隨心而動

            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 休息食客 閱讀(288) 評論(0)  編輯  收藏

          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 昌江| 涿鹿县| 云林县| 微山县| 通许县| 盐山县| 平江县| 海林市| 新密市| 左贡县| 张家川| 永州市| 竹溪县| 综艺| 故城县| 临猗县| 定日县| 浦县| 岑巩县| 彭山县| 凤冈县| 马鞍山市| 新乡县| 塔河县| 富川| 焦作市| 特克斯县| 汝南县| 杂多县| 郎溪县| 油尖旺区| 宜黄县| 广南县| 贺兰县| 上饶县| 枣阳市| 澄江县| 乃东县| 昔阳县| 铁岭县| 灌南县|