学习了计算机网络之后,利用java写了一个ftp服务器。
一、实现的ftp命令
实现了基本的user,pass,list,port,quit,retr,cwd,stor等命令
二、以上命令所对应的功能
对应的功能是:下载,上传,获取服务器目录,切换目录等
三、用于测试的ftp客户端:windows自带的ftp客户端
四、实现的思想
1、使用ServerSocket进行监听,每个控制连接的请求到来之后,开启一个线程进行处理(这里使用的java bio,效率较差,对于控制连接最好使用NIO处理,之后会再写个
nio的实现)
2、 对于命令使用工厂方法模式进行设计,当需要添加新的命令的时候,只需要添加一个新的命令类,实现相应接口,修改工厂产生逻辑,而不用修改其他的程序代码。可
扩展性较好,同时符合开闭原则。
五、实现过程中碰到的问题
1、对于tcp与socket的关系理解错误,以为所有的数据的输入都是要经过serverSocket().accept()方法。其实,ServerSocket.accept()所对应的是tcp里面的三次握手建
立连接的阶段,之后的tcp的连接由客户端和服务器端的一对socket来维护,是属于establish阶段,在这个阶段,通信是全双工的,任何一方都能够发送数据。
socket.close()对应的阶段是断开连接(四次挥手)的阶段。
2、刚开始对于ftp协议不是很理解,不知道他的工作方式是怎样的,后来在看了tcp协议卷里面的ftp的内容之后,才知道ftp命令和应答码是关键。eg:刚开始测试时,在
输入用户名之后,不会提示输入密码的。原因:没有返回对应的应答码:331. 另外要注意的是:返回的数据要以换行回车作为结束--\r\n.
六、代码列表
简单说明:
ftpServer:是服务器的主程序,入口,同时负责监听本地的21号端口。
ControllerThread.java:用于处理控制连接的线程(每一个控制连接请求对应一个线程)ps:实在很浪费(流量小,连接多)。
Share:一些全局性数据的维护。
Command:是命令接口,定义了一个所有命令都要实现的方法。
CommandFactory:命令工厂,通过传人的参数,决定生成的命令对象。
UserCommand,PortCommand等:是具体ftp命令的实现
七、详细代码
1、FtpServer:
package ftpServer;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class FtpServer {
private int port;
ServerSocket serverSocket;
public FtpServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
//初始化系统信息
Share.init();
}
public void listen() throws IOException {
Socket socket = null;
while(true) {
//这个是建立连接,三次握手的过程,当连接建立了之后,两个socket之间的通讯是直接通过流进行的,不用再通过这一步
socket = serverSocket.accept();
ControllerThread thread = new ControllerThread(socket);
thread.start();
}
}
public static void main(String args[]) throws IOException {
FtpServer ftpServer = new FtpServer(21);
ftpServer.listen();
}
}
2、ControllerThread
package ftpServer;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
/**
* @author onroadrui
* 用于处理控制连接数据请求的线程
* 控制连接:在创建之后,直到socket.close()(四次挥手的过程),
* 都是tcp里面的establish的阶段。可以自由地传输数据(全双工的)
* */
public class ControllerThread extends Thread{
private int count = 0;
//客户端socket与服务器端socket组成一个tcp连接
private Socket socket;
//当前的线程所对应的用户
public static final ThreadLocal<String> USER = new ThreadLocal<String>();
//数据连接的ip
private String dataIp;
//数据连接的port
private String dataPort;
//用于标记用户是否已经登录
private boolean isLogin = false;
//当前目录
private String nowDir = Share.rootDir;
public String getNowDir() {
return nowDir;
}
public void setNowDir(String nowDir) {
this.nowDir = nowDir;
}
public void setIsLogin(boolean t) {
isLogin = t;
}
public boolean getIsLogin() {
return isLogin;
}
public Socket getSocket() {
return socket;
}
public String getDataIp() {
return dataIp;
}
public void setDataIp(String dataIp) {
this.dataIp = dataIp;
}
public String getDataPort() {
return dataPort;
}
public void setDataPort(String dataPort) {
this.dataPort = dataPort;
}
public ControllerThread(Socket socket) {
this.socket = socket;
}
public void run() {
System.out.println("hello");
BufferedReader reader;
try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Writer writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while(true) {
//第一次访问,输入流里面是没有东西的,所以会阻塞住
if(count == 0)
{
writer.write("220");
writer.write("\r\n");
writer.flush();
count++;
}
else {
//两种情况会关闭连接:(1)quit命令 (2)密码错误
if(!socket.isClosed()) {
//进行命令的选择,然后进行处理,当客户端没有发送数据的时候,将会阻塞
String command = reader.readLine();
if(command !=null) {
String[] datas = command.split(" ");
Command commandSolver = CommandFactory.createCommand(datas[0]);
//登录验证,在没有登录的情况下,无法使用除了user,pass之外的命令
if(loginValiate(commandSolver)) {
if(commandSolver == null)
{
writer.write("502 该命令不存在,请重新输入");
}
else
{
String data = "";
if(datas.length >=2) {
data = datas[1];
}
commandSolver.getResult(data, writer,this);
}
}
else
{
writer.write("532 执行该命令需要登录,请登录后再执行相应的操作\r\n");
writer.flush();
}
}
}
else {
//连接已经关闭,这个线程不再有存在的必要
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
System.out.println("结束tcp连接");
}
}
public boolean loginValiate(Command command) {
if(command instanceof UserCommand || command instanceof PassCommand) {
return true;
}
else
{
return isLogin;
}
}
}
3、Share
package ftpServer;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
/**
* 所有线程共享的变量
* */
public class Share {
/*根目录的路径*/
public static String rootDir = "C:"+File.separator;
/*允许登录的用户*/
public static Map<String,String> users = new HashMap<String,String>();
/*已经登录的用户*/
public static HashSet<String> loginedUser = new HashSet<String>();
/*拥有权限的用户*/
public static HashSet<String> adminUsers = new HashSet<String>();
//初始化根目录,权限用户,能够登录的用户信息
public static void init(){
String path = System.getProperty("user.dir") + "/bin/server.xml";
File file = new File(path);
SAXBuilder builder = new SAXBuilder();
try {
Document parse = builder.build(file);
Element root = parse.getRootElement();
//配置服务器的默认目录
rootDir = root.getChildText("rootDir");
System.out.print("rootDir is:");
System.out.println(rootDir);
//允许登录的用户
Element usersE = root.getChild("users");
List<Element> usersEC = usersE.getChildren();
String username = null;
String password = null;
System.out.println("\n所有用户的信息:");
for(Element user : usersEC) {
username = user.getChildText("username");
password = user.getChildText("password");
System.out.println("用户名:"+username);
System.out.println("密码:"+password);
users.put(username,password);
}
/*
//拥有put权限和delete权限的用户
System.out.println("\n管理员用户:");
Element adminUsersE = root.getChild("adminUsers");
for(Element adminUserTemp: (List<Element>)adminUsersE.getChildren()) {
username = adminUserTemp.getText();
//System.out.println("用户名:"+username);
adminUsers.add(username);
} */
} catch (JDOMException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、Command
package ftpServer;
import java.io.Writer;
interface Command {
/**
* @param data 从ftp客户端接收的除ftp命令之外的数据
* @param writer 网络输出流
* @param t 控制连接所对应的处理线程
* */
public void getResult(String data,Writer writer,ControllerThread t);
}
package ftpServer;
public class CommandFactory {
public static Command createCommand(String type) {
type = type.toUpperCase();
switch(type)
{
case "USER":return new UserCommand();
case "PASS":return new PassCommand();
case "LIST":return new DirCommand();
case "PORT":return new PortCommand();
case "QUIT":return new QuitCommand();
case "RETR":return new RetrCommand();
case "CWD":return new CwdCommand();
case "STOR":return new StoreCommand();
default :return null;
}
}
}
6、UserCommand
package ftpServer;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
public class UserCommand implements Command{
/**
* 检验是否有这个用户名存在
* */
@Override
public void getResult(String data,Writer writer,ControllerThread t) {
String response = "";
if(Share.users.containsKey(data)) {
ControllerThread.USER.set(data);
response = "331";
}
else {
response = "501";
}
try {
writer.write(response);
writer.write("\r\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
7、PassComand
package ftpServer;
import java.io.IOException;
import java.io.Writer;
public class PassCommand implements Command{
@Override
public void getResult(String data, Writer writer,ControllerThread t) {
System.out.println("execute the pass command");
System.out.println("the data is "+data);
//获得用户名
String key = ControllerThread.USER.get();
String pass = Share.users.get(key);
String response = null;
if(pass.equals(data)) {
System.out.println("登录成功");
Share.loginedUser.add(key);
t.setIsLogin(true);
response = "230 User "+key+" logged in";
}
else {
System.out.println("登录失败,密码错误");
response = "530 密码错误";
}
try {
writer.write(response);
writer.write("\r\n");
writer.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
9、PortCommand
package ftpServer;
import java.io.IOException;
import java.io.Writer;
public class PortCommand implements Command{
@Override
public void getResult(String data, Writer writer,ControllerThread t) {
String response = "200 the port an ip have been transfered";
try {
String[] iAp = data.split(",");
String ip = iAp[0]+"."+iAp[1]+"."+iAp[2]+"."+iAp[3];
String port = Integer.toString(256*Integer.parseInt(iAp[4])+Integer.parseInt(iAp[5]));
System.out.println("ip is "+ip);
System.out.println("port is "+port);
t.setDataIp(ip);
t.setDataPort(port);
writer.write(response);
writer.write("\r\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
10、DirComand:这个对应的是List命令。。。没改回来
package ftpServer;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.net.UnknownHostException;
public class DirCommand implements Command{
/**
* 获取ftp目录里面的文件列表
* */
@Override
public void getResult(String data, Writer writer,ControllerThread t) {
String desDir = t.getNowDir()+data;
System.out.println(desDir);
File dir = new File(desDir);
if(!dir.exists()) {
try {
writer.write("210 文件目录不存在\r\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
else
{
StringBuilder dirs = new StringBuilder();
System.out.println("文件目录如下:");
dirs.append("文件目录如下:\n");
String[] lists= dir.list();
String flag = null;
for(String name : lists) {
System.out.println(name);
File temp = new File(desDir+File.separator+name);
if(temp.isDirectory()) {
flag = "d";
}
else {
flag = "f";
}
dirs.append("\t");
dirs.append(flag);
dirs.append(" ");
dirs.append(name);
dirs.append("\n");
}
//开启数据连接,将数据发送给客户端,这里需要有端口号和ip地址
Socket s;
try {
writer.write("150 open ascii mode...\r\n");
writer.flush();
s = new Socket(t.getDataIp(), Integer.parseInt(t.getDataPort()));
BufferedWriter dataWriter = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
dataWriter.write(dirs.toString());
dataWriter.flush();
s.close();
writer.write("220 transfer complete...\r\n");
writer.flush();
} catch (NumberFormatException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
11、CwdCommand
package ftpServer;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
/**
* 改变工作目录
* */
public class CwdCommand implements Command{
@Override
public void getResult(String data, Writer writer, ControllerThread t) {
String dir = t.getNowDir() +File.separator+data;
File file = new File(dir);
try {
if((file.exists())&&(file.isDirectory())) {
String nowDir =t.getNowDir() +File.separator+data;
t.setNowDir(nowDir);
writer.write("250 CWD command succesful");
}
else
{
writer.write("550 目录不存在");
}
writer.write("\r\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
12、RetrCommand
package ftpServer;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
/**
* 处理文件的发送
* */
public class RetrCommand implements Command{
@Override
public void getResult(String data, Writer writer, ControllerThread t) {
Socket s;
String desDir = t.getNowDir()+File.separator+data;
File file = new File(desDir);
System.out.println(desDir);
if(file.exists())
{
try {
writer.write("150 open ascii mode...\r\n");
writer.flush();
s = new Socket(t.getDataIp(), Integer.parseInt(t.getDataPort()));
BufferedOutputStream dataOut = new BufferedOutputStream(s.getOutputStream());
byte[] buf = new byte[1024];
InputStream is = new FileInputStream(file);
while(-1 != is.read(buf)) {
dataOut.write(buf);
}
dataOut.flush();
s.close();
writer.write("220 transfer complete...\r\n");
writer.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
else {
try {
writer.write("220 该文件不存在\r\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
13、StoreCommand
package ftpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.net.Socket;
public class StoreCommand implements Command{
@Override
public void getResult(String data, Writer writer, ControllerThread t) {
try{
writer.write("150 Binary data connection\r\n");
writer.flush();
RandomAccessFile inFile = new
RandomAccessFile(t.getNowDir()+"/"+data,"rw");
//数据连接
Socket tempSocket = new Socket(t.getDataIp(),Integer.parseInt(t.getDataPort()));
InputStream inSocket
= tempSocket.getInputStream();
byte byteBuffer[] = new byte[1024];
int amount;
//这里又会阻塞掉,无法从客户端输出流里面获取数据?是因为客户端没有发送数据么
while((amount =inSocket.read(byteBuffer) )!= -1){
inFile.write(byteBuffer, 0, amount);
}
System.out.println("传输完成,关闭连接。。。");
inFile.close();
inSocket.close();
tempSocket.close();
//断开数据连接
writer.write("226 transfer complete\r\n");
writer.flush();
}
catch(IOException e){
e.printStackTrace();
}
}
}
14、QuitStore
package ftpServer;
import java.io.IOException;
import java.io.Writer;
public class QuitCommand implements Command{
@Override
public void getResult(String data, Writer writer, ControllerThread t) {
try {
writer.write("221 goodbye.\r\n");
writer.flush();
writer.close();
t.getSocket().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}