自己動手建立FTP伺服器

前面瞭解了FTP的各項指令及狀態碼,就能夠依照FTP指令的內容來建立FTP Server程式。在標準的FTP Server軟體規格中,明顯有完整的方法解決檔案系統中的各種需求。為了能快速取得學習成果,此範例以被動模式並去除部份指令來得到入門實作的體驗。




Server

啟動FTP Server
package ftp;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

//FTP Server
public class Server {
	private int controlPort = 8888; //接收指令埠
	private ServerSocket welcomeSocket;
	boolean serverRunning = true;
	
	//啟動FTP Server
	public static void main(String[] args) {
		new Server();
	}

	public Server() {
		try {
			welcomeSocket = new ServerSocket(controlPort);
		} catch (IOException e) {
			System.out.println("無法建立網路服務");
			System.exit(-1);
		}
		System.out.println("FTP服務建立在 " + controlPort);
		int noOfThreads = 0;
		while (serverRunning) {
			try {
				Socket client = welcomeSocket.accept();
				//循序遞增連接埠給新連線傳輸檔案
				int dataPort = controlPort + noOfThreads + 1;
				Worker w = new Worker(client, dataPort);
				System.out.println("收到請求建立連線");
				noOfThreads++;
				w.start();
			} catch (IOException e) {
				System.out.println("收到請求但連線失敗");
				e.printStackTrace();
			}
		}
		try {
			welcomeSocket.close();
			System.out.println("已停止服務");
		} catch (IOException e) {
			System.out.println("無法停止服務");
			System.exit(-1);
		}
	}
}

Worker

FTP傳輸工作
package ftp;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

//FTP傳輸工作
public class Worker extends Thread {	
	
	//TODO 使用者資料應採用認証服務機制
	private userStatus currentUserStatus = userStatus.NOTLOGGEDIN;
	private String validUser = "john";//單使用者帳號
	private String validPassword = "12345";//單使用者密碼
	// 主要路徑
	private String root;
	private String currDirectory;
	private String fileSeparator = "\\";
	// 傳輸元件
	private Socket controlSocket;
	private PrintWriter controlOutWriter;
	private BufferedReader controlIn;
	private ServerSocket dataSocket;
	private Socket dataConnection;
	private PrintWriter dataOutWriter;
	private int dataPort;
	private transferType transferMode = transferType.ASCII;
	
	private boolean quitCommandLoop = false;
	
	public Worker(Socket client, int dataPort) {
		super();
		this.controlSocket = client;
		this.dataPort = dataPort;
		this.root = System.getProperty("user.home");// 取得server上的JVM使用者目錄
		this.currDirectory = root + fileSeparator + "Desktop";// 嘗試存取桌面
	}
	
	public void run() {
		try {
			controlIn = new BufferedReader(new InputStreamReader(controlSocket.getInputStream()));
			controlOutWriter = new PrintWriter(controlSocket.getOutputStream(), true);
			sendMsgToClient("220 歡迎訊息....");
			while (!quitCommandLoop) {
				executeCommand(controlIn.readLine());
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				controlIn.close();
				controlOutWriter.close();
				controlSocket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	//執行指令
	private void executeCommand(String c) {
		int index = c.indexOf(' ');
		String command = ((index == -1) ? c.toUpperCase() : (c.substring(0, index)).toUpperCase());
		String args = ((index == -1) ? null : c.substring(index + 1, c.length()));
		switch (command) {
			case "USER":handleUser(args);break;
			case "PASS":handlePass(args);break;
			case "CWD":handleCwd(args);break;
			case "LIST":handleNlst(args);break;
			case "NLST":handleNlst(args);break;
			case "PWD":handlePwd();break;
			case "QUIT":handleQuit();break;
			case "PASV":handlePasv();break;
			case "EPSV":handleEpsv();break;
			case "SYST":handleSyst();break;
			case "FEAT":handleFeat();break;
			case "PORT":handlePort(args);break;
			case "EPRT":handlePort(parseExtendedArguments(args));break;
			case "RETR":handleRetr(args);break;
			case "MKD":handleMkd(args);break;
			case "RMD":handleRmd(args);break;
			case "TYPE":handleType(args);break;
			case "STOR":handleStor(args);break;
			default:sendMsgToClient("501 Unknown command");break;
		}
	}
	
	//一般訊息
	private void sendMsgToClient(String msg) {
		controlOutWriter.println(msg);
	}
	
	//狀態訊息
	private void sendDataMsgToClient(String msg) {
		if (dataConnection == null || dataConnection.isClosed()) {
			sendMsgToClient("425 No data connection was established");
		} else {
			dataOutWriter.print(msg + '\r' + '\n');
		}
	}
	
	private void openDataConnectionPassive(int port) {
		try {
			dataSocket = new ServerSocket(port);
			dataConnection = dataSocket.accept();
			dataOutWriter = new PrintWriter(dataConnection.getOutputStream(), true);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	private void openDataConnectionActive(String ipAddress, int port) {
		try {
			dataConnection = new Socket(ipAddress, port);
			dataOutWriter = new PrintWriter(dataConnection.getOutputStream(), true);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	private void closeDataConnection() {
		try {
			dataOutWriter.close();
			dataConnection.close();
			if (dataSocket != null) {
				dataSocket.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		dataOutWriter = null;
		dataConnection = null;
		dataSocket = null;
	}
	
	private void handleUser(String username) {
		if (username.toLowerCase().equals(validUser)) {
			sendMsgToClient("331 User name okay, need password");
			currentUserStatus = userStatus.ENTEREDUSERNAME;
		} else if (currentUserStatus == userStatus.LOGGEDIN) {
			sendMsgToClient("530 User already logged in");
		} else {
			sendMsgToClient("530 Not logged in");
		}
	}
	
	private void handlePass(String password) {
		if (currentUserStatus == userStatus.ENTEREDUSERNAME && password.equals(validPassword)) {
			currentUserStatus = userStatus.LOGGEDIN;
			sendMsgToClient("230-Welcome");
			sendMsgToClient("230 User logged in successfully");
		}else if (currentUserStatus == userStatus.LOGGEDIN) {
			sendMsgToClient("530 User already logged in");
		}else {
			sendMsgToClient("530 Not logged in");
		}
	}
	
	private void handleCwd(String args) {
		String filename = currDirectory;
		if (args.equals("..")) {
			int ind = filename.lastIndexOf(fileSeparator);
			if (ind > 0) {
				filename = filename.substring(0, ind);
			}
		}else if ((args != null) && (!args.equals("."))) {
			filename = filename + fileSeparator + args;
		}
		File f = new File(filename);
		if (f.exists() && f.isDirectory() && (filename.length() >= root.length())) {
			currDirectory = filename;
			sendMsgToClient("250 The current directory has been changed to " + currDirectory);
		} else {
			sendMsgToClient("550 Requested action not taken. File unavailable.");
		}
	}
	
	private void handleNlst(String args) {
		if (dataConnection == null || dataConnection.isClosed()) {
			sendMsgToClient("425 No data connection was established");
		} else {
			String[] dirContent = nlstHelper(args);
			if (dirContent == null) {
				sendMsgToClient("550 File does not exist.");
			} else {
				sendMsgToClient("125 Opening ASCII mode data connection for file list.");
				for (int i = 0; i < dirContent.length; i++) {
					sendDataMsgToClient(dirContent[i]);
				}
				sendMsgToClient("226 Transfer complete.");
				closeDataConnection();
			}
		}
	}
	
	private String[] nlstHelper(String args) {
		String filename = currDirectory;
		if (args != null) {
			filename = filename + fileSeparator + args;
		}
		File f = new File(filename);
		if (f.exists() && f.isDirectory()) {
			return f.list();
		} else if (f.exists() && f.isFile()) {
			String[] allFiles = new String[1];
			allFiles[0] = f.getName();
			return allFiles;
		} else {
			return null;
		}
	}
	
	private void handlePort(String args) {
		String[] stringSplit = args.split(",");
		String hostName = stringSplit[0] + "." + stringSplit[1] + "." + stringSplit[2] + "." + stringSplit[3];
		int p = Integer.parseInt(stringSplit[4]) * 256 + Integer.parseInt(stringSplit[5]);
		openDataConnectionActive(hostName, p);
		sendMsgToClient("200 Command OK");
	}
	
	private void handlePwd() {
		sendMsgToClient("257 \"" + currDirectory + "\"");
	}
	
	private void handlePasv() {		
		String myIp = "127.0.0.1";
		String myIpSplit[] = myIp.split("\\.");
		int p1 = dataPort / 256;
		int p2 = dataPort % 256;
		sendMsgToClient("227 Entering Passive Mode (" + myIpSplit[0] + "," + myIpSplit[1] + "," + myIpSplit[2] + ","+ myIpSplit[3] + "," + p1 + "," + p2 + ")");
		openDataConnectionPassive(dataPort);
	}
	
	private void handleEpsv() {
		sendMsgToClient("229 Entering Extended Passive Mode (|||" + dataPort + "|)");
		openDataConnectionPassive(dataPort);
	}
	
	private void handleQuit() {
		sendMsgToClient("221 Closing connection");
		quitCommandLoop = true;
	}

	private void handleSyst() {
		sendMsgToClient("215 COMP4621 FTP Server Homebrew");
	}
	
	private void handleFeat() {
		sendMsgToClient("211 END");
	}
	
	private void handleMkd(String args) {
		if (args != null && args.matches("^[a-zA-Z0-9]+$")) {
			File dir = new File(currDirectory + fileSeparator + args);

			if (!dir.mkdir()) {
				sendMsgToClient("550 Failed to create new directory");
			} else {
				sendMsgToClient("250 Directory successfully created");
			}
		} else {
			sendMsgToClient("550 Invalid name");
		}
	}
	
	private void handleRmd(String dir) {
		String filename = currDirectory;
		if (dir != null && dir.matches("^[a-zA-Z0-9]+$")) {
			filename = filename + fileSeparator + dir;
			File d = new File(filename);
			if (d.exists() && d.isDirectory()) {
				d.delete();
				sendMsgToClient("250 Directory was successfully removed");
			} else {
				sendMsgToClient("550 Requested action not taken. File unavailable.");
			}
		} else {
			sendMsgToClient("550 Invalid file name.");
		}
	}
	
	private void handleType(String mode) {
		if (mode.toUpperCase().equals("A")) {
			transferMode = transferType.ASCII;
			sendMsgToClient("200 OK");
		} else if (mode.toUpperCase().equals("I")) {
			transferMode = transferType.BINARY;
			sendMsgToClient("200 OK");
		} else
			sendMsgToClient("504 Not OK");
	}
	
	private void handleRetr(String file) {
		File f = new File(currDirectory + fileSeparator + file);
		if (!f.exists()) {
			sendMsgToClient("550 File does not exist");
		}else {
			if (transferMode == transferType.BINARY) {
				BufferedOutputStream fout = null;
				BufferedInputStream fin = null;
				sendMsgToClient("150 Opening binary mode data connection for requested file " + f.getName());
				try {
					fout = new BufferedOutputStream(dataConnection.getOutputStream());
					fin = new BufferedInputStream(new FileInputStream(f));
				} catch (Exception e) {
					e.printStackTrace();
				}
				byte[] buf = new byte[1024];
				int l = 0;
				try {
					while ((l = fin.read(buf, 0, 1024)) != -1) {
						fout.write(buf, 0, l);
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
				try {
					fin.close();
					fout.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
				sendMsgToClient("226 File transfer successful. Closing data connection.");
			}else {
				sendMsgToClient("150 Opening ASCII mode data connection for requested file " + f.getName());
				BufferedReader rin = null;
				PrintWriter rout = null;
				try {
					rin = new BufferedReader(new FileReader(f));
					rout = new PrintWriter(dataConnection.getOutputStream(), true);
				} catch (IOException e) {
					e.printStackTrace();
				}

				String s;
				try {
					while ((s = rin.readLine()) != null) {
						rout.println(s);
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
				try {
					rout.close();
					rin.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
				sendMsgToClient("226 File transfer successful. Closing data connection.");
			}
		}
		closeDataConnection();
	}
	
	private void handleStor(String file) {
		if (file == null) {
			sendMsgToClient("501 No filename given");
		} else {
			File f = new File(currDirectory + fileSeparator + file);

			if (f.exists()) {
				sendMsgToClient("550 File already exists");
			}else {
				if (transferMode == transferType.BINARY) {
					BufferedOutputStream fout = null;
					BufferedInputStream fin = null;
					sendMsgToClient("150 Opening binary mode data connection for requested file " + f.getName());
					try {
						fout = new BufferedOutputStream(new FileOutputStream(f));
						fin = new BufferedInputStream(dataConnection.getInputStream());
					} catch (Exception e) {
						e.printStackTrace();
					}					
					byte[] buf = new byte[1024];
					int l = 0;
					try {
						while ((l = fin.read(buf, 0, 1024)) != -1) {
							fout.write(buf, 0, l);
						}
					} catch (IOException e) {
						e.printStackTrace();
					}
					try {
						fin.close();
						fout.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
					sendMsgToClient("226 File transfer successful. Closing data connection.");
				}else {
					sendMsgToClient("150 Opening ASCII mode data connection for requested file " + f.getName());

					BufferedReader rin = null;
					PrintWriter rout = null;
					try {
						rin = new BufferedReader(new InputStreamReader(dataConnection.getInputStream()));
						rout = new PrintWriter(new FileOutputStream(f), true);

					} catch (IOException e) {
						e.printStackTrace();
					}

					String s;

					try {
						while ((s = rin.readLine()) != null) {
							rout.println(s);
						}
					} catch (IOException e) {
						e.printStackTrace();
					}
					try {
						rout.close();
						rin.close();
					} catch (IOException e) {						
						e.printStackTrace();
					}
					sendMsgToClient("226 File transfer successful. Closing data connection.");
				}
			}
			closeDataConnection();
		}
	}
	
	private String parseExtendedArguments(String extArg) {
		String[] splitArgs = extArg.split("\\|");
		String ipAddress = splitArgs[2].replace('.', ',');
		int port = Integer.parseInt(splitArgs[3]);
		int p1 = port / 256;
		int p2 = port % 256;
		return ipAddress + "," + p1 + "," + p2;
	}
	
	private enum transferType {
		ASCII, BINARY
	}
	private enum userStatus {
		NOTLOGGEDIN, ENTEREDUSERNAME, LOGGEDIN
	}
}

建立好的FTP Server可以用Windows內建的檔案總管檢視,但是下載客戶端軟體測試結果將較為準確。
大部份的FTP軟體對於UTF8的相容性極差,無論是客戶端或是伺服器端都有機會發生問題。本範例的客戶端以winscp為教具發生中文訊息的悲劇,因此所有訊息以原文輸出。

狀態碼

有興趣研究的人可以試著將狀態碼加入,並且嘗試更多的指令對應。
private String getCodeMsg(String code) {		
		switch (code) {
		case "100": return "The requested action is being initiated, expect another reply before proceeding with a new command.";
		case "110": return "Restart marker replay . In this case, the text is exact and not left to the particular implementation; it must read: MARK yyyy = mmmm where yyyy is User-process data stream marker, and mmmm server's equivalent marker (note the spaces between markers and '=').";
		case "120": return "Service ready in nnn minutes.";
		case "125": return "Data connection already open; transfer starting.";
		case "150": return "File status okay; about to open data connection.";
		case "200": return "The requested action has been successfully completed.";
		case "202": return "Command not implemented, superfluous at this site.";
		case "211": return "System status, or system help reply.";
		case "212": return "Directory status.";
		case "213": return "File status.";
		case "214": return "Help message. Explains how to use the server or the meaning of a particular non-standard command. This reply is useful only to the human user.";
		case "215": return "NAME system type. Where NAME is an official system name from the registry kept by IANA.";
		case "220": return "Service ready for new user.";
		case "221": return "Service closing control connection.";
		case "225": return "Data connection open; no transfer in progress.";
		case "226": return "Closing data connection. Requested file action successful (for example, file transfer or file abort).";
		case "227": return "Entering Passive Mode (h1,h2,h3,h4,p1,p2).";
		case "228": return "Entering Long Passive Mode (long address, port).";
		case "229": return "Entering Extended Passive Mode (|||port|).";
		case "230": return "User logged in, proceed. Logged out if appropriate.";
		case "231": return "User logged out; service terminated.";
		case "232": return "Logout command noted, will complete when transfer done.";
		case "234": return "Specifies that the server accepts the authentication mechanism specified by the client, and the exchange of security data is complete. A higher level nonstandard code created by Microsoft.";
		case "250": return "Requested file action okay, completed.";
		case "257": return "'PATHNAME' created.";
		case "300": return "The command has been accepted, but the requested action is on hold, pending receipt of further information.";
		case "331": return "User name okay, need password.";
		case "332": return "Need account for login.";
		case "350": return "Requested file action pending further information";
		case "400": return "The command was not accepted and the requested action did not take place, but the error condition is temporary and the action may be requested again.";
		case "421": return "Service not available, closing control connection. This may be a reply to any command if the service knows it must shut down.";
		case "425": return "Can't open data connection.";
		case "426": return "Connection closed; transfer aborted.";
		case "430": return "Invalid username or password";
		case "434": return "Requested host unavailable.";
		case "450": return "Requested file action not taken.";
		case "451": return "Requested action aborted. Local error in processing.";
		case "452": return "Requested action not taken. Insufficient storage space in system.File unavailable (e.g., file busy).";
		case "500": return "Syntax error, command unrecognized and the requested action did not take place. This may include errors such as command line too long.";
		case "501": return "Syntax error in parameters or arguments.";
		case "502": return "Command not implemented.";
		case "503": return "Bad sequence of commands.";
		case "504": return "Command not implemented for that parameter.";
		case "530": return "Not logged in.";
		case "532": return "Need account for storing files.";
		case "534": return "Could Not Connect to Server - Policy Requires SSL";
		case "550": return "Requested action not taken. File unavailable (e.g., file not found, no access).";
		case "551": return "Requested action aborted. Page type unknown.";
		case "552": return "Requested file action aborted. Exceeded storage allocation (for current directory or dataset).";
		case "553": return "Requested action not taken. File name not allowed.";
		case "600": return "Replies regarding confidentiality and integrity";
		case "631": return "Integrity protected reply.";
		case "632": return "Confidentiality and integrity protected reply.";
		case "633": return "Confidentiality protected reply.";
		case "10000": return "Common Winsock Error Codes[2] (These are not FTP return codes)";
		case "10054": return "Connection reset by peer. The connection was forcibly closed by the remote host.";
		case "10060": return "Cannot connect to remote server.";
		case "10061": return "Cannot connect to remote server. The connection is actively refused by the server.";
		case "10066": return "Directory not empty.";
		case "10068": return "Too many users, server is full.";			
		default:
			return "501 Unknown command";			
		}
	}

private String getCodeMsg(String code) {  
  switch (code) {
  case "110": return "重新啟動標記回覆。";
  case "120": return "服務就緒,在 nnn 分鐘。";
  case "125": return "資料連線已經開啟;傳輸開始。";
  case "150": return "檔案狀態無誤;將開啟資料連線。";
  case "200": return "指令已經沒有問題了。";
  case "202": return "在這個站台的指令不實作、 多餘。";
  case "211": return "系統狀態或系統說明回覆。";
  case "212": return "目錄狀態。";
  case "213": return "檔案狀態。";
  case "214": return "說明訊息。";
  case "215": return "名稱系統型別,其中名稱是一個正式的系統名稱從指派的數字的文件中的清單。";
  case "220": return "供新使用者的服務。";
  case "221": return "服務正在關閉控制連接。如果可以請登出。";
  case "225": return "資料連線已開啟;沒有正在傳輸中。";
  case "226": return "關閉資料連線。要求的檔案動作成功 (例如,檔案傳輸或檔案中止)。";
  case "227": return "進入被動模式 (h1、 h2、 h3、 h4、 p1,p2)。";
  case "229": return "延伸被動模式輸入。";
  case "230": return "使用者已登入,繼續執行。";
  case "232": return "已登入,使用者的權限的安全性資料交換。";
  case "234": return "的-安全性資料交換完成。";
  case "235": return "已順利完成安全性資料交換。";
  case "250": return "要求的檔案動作無誤,完成。";
  case "257": return "路徑名稱 」 建立。";
  case "331": return "的使用者名稱無誤,需要密碼。";
  case "332": return "需要登入帳戶。";
  case "334": return "要求安全性機制 [確定]。";
  case "335": return "安全性資料是可接受的。更多資料才能完成安全性資料交換。";
  case "336": return "使用者名稱無誤,需要密碼。";
  case "350": return "要求的檔案動作擱置中的其他相關資訊。";
  case "421": return "服務無法使用,正在關閉控制連接。如果服務知道其必須關閉,這可能是對任何命令的回覆。";
  case "425": return "無法開啟資料連接。";
  case "426": return "連接已關閉;傳輸中止。";
  case "431": return "需要某些無法使用的資源,處理安全性。";
  case "450": return "未採取要求的檔案動作。檔案無法使用 (例如,檔案忙碌)。";
  case "451": return "要求的動作已中止。正在處理本機錯誤。";
  case "452": return "要求未採取的動作。在系統中沒有足夠的儲存空間。";
  case "500": return "語法錯誤,無法辨認的命令。這可能包括命令列過長的錯誤。";
  case "501": return "參數或引數中的語法錯誤。";
  case "502": return "未執行命令。";
  case "503": return "錯誤的命令順序。";
  case "504": return "並未實作該參數的命令。";
  case "521": return "無法開啟 資料連線,使用這個連接埠正常設定。";
  case "522": return "伺服器不支援要求的網路通訊協定。";
  case "530": return "未登入。";
  case "532": return "需要帳戶以儲存檔案。";
  case "533": return "命令保護層級拒絕原則的原因。";
  case "534": return "原則原因拒絕要求。";
  case "535": return "(雜湊、 序列等等) 的失敗的安全性檢查。";
  case "536": return "要求的連接埠正常層級不支援的機制。";
  case "537": return "命令保護層級不支援的安全性機制。";
  case "550": return "要求未採取的動作。檔案無法使用 (例如,找不到檔案或沒有存取權)。";
  case "551": return "要求的動作已中止: 頁面類型不明。";
  case "552": return "要求的檔案動作已中止。超過儲存配置 (針對目前的目錄或資料集)。";
  case "553": return "要求未採取的動作。不允許的檔案名稱。";
  case "631": return "完整性保護回覆。";
  case "632": return "機密性和完整性保護回覆。";
  case "633": return "機密性保護回覆。";
  /*case "150": return "FTP 使用兩個連接埠: 21 傳送命令,而 20 用於傳送資料。狀態碼 150 表示伺服器將在連接埠 20開啟新的連線,以便傳送某些資料。";
  case "226": return "命令開啟資料連接在連接埠 20,要執行的動作,例如傳送檔案。已順利完成此動作,以及資料連線已關閉。";
  case "230": return "用戶端傳送正確的密碼之後,就會顯示此狀態碼。它會指出使用者已成功地登入。";
  case "331": return "用戶端傳送使用者名稱後,您會看到這個狀態碼。不論所提供的使用者名稱是否為系統上有效的帳戶,皆會出現相同的狀態碼。";
  case "426": return "命令開啟資料連線來執行動作,該動作已取消,但資料連線已關閉。";
  case "530": return "此狀態碼表示使用者無法登入,因為使用者名稱和密碼組合不正確。如果您利用使用者帳戶登入,則您可能輸入錯誤的使用者名稱或密碼,或您已選擇僅允許匿名存取。如果您使用「匿名」帳戶登入,您可能已設定 IIS 來拒絕匿名存取。";
  case "550": return "命令不會執行,因為指定的檔案無法使用。例如,當您嘗試取得的檔案不存在,或當您嘗試將檔案放置在沒有寫入權限的目錄中時,就會發生這個狀態碼。";*/
 
  default:
   return "501 Unknown command";   
  }
 }

中文的狀態輸出僅供參考...實作結果並不符合期待。

沒有留言:

張貼留言