[轉:資源來自互聯網,僅供分享]
用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中處理USER和PASS命令,直接返回200 OK,如果需要驗證用戶名和口令,還需要添加相應的代碼。
如何調試Ftp服務器?
有個最簡單的方法,便是使用現成的Ftp客戶端,推薦CuteFtp,因為它總是把客戶端發送的命令和服務器響應打印出來,我們可以非常方便的看到服務器的輸出結果。
另外一個小Bug,文件列表在CuteFtp中可以正常顯示,在其他Ftp客戶端不一定能正常顯示,這說明輸出響應的“兼容性”還不夠好,有空了看看Ftp的RFC再改進!:)
資源來自互聯網,僅供分享