Java Socket 实现简易的网盘

Java上机作业要求使用Java Socket设计一个简易的网盘,前段时间一直没有机会写,现在在这里整理一下。

上机作业要求如下:

(1) 网盘是一个常见的功能,客户端可以将自己的文件,通过客户端界面,上传到服务器端的网盘,也可以下载该文件。
(2)该软件支持1个客户端,1个服务器端。服务器提供网盘空间。
(3)首先运行服务器。服务器运行之后,客户端运行网盘客户端。
(4)运行客户端。用户能够输入昵称。确定,则连接到服务器。连接成功,即可出现客户端面。
(5)客户端网盘界面如下:可以在网盘中新建文件夹,删除空文件夹,重命名文件夹;可以将自己电脑上某个文件上传到网盘中的某个文件夹下(支持单文件),可以删除单个文件、重命名文件、下载单个文件。
————————————————————————————————————
使用方法如下:
(1)客户端初次启动后,服务器上,建立客户端昵称为名称的网盘文件夹;以后该客户端的文件全部存放在服务器该文件夹下。
(2)客户端初次启动,网盘界面上没有任何内容。
(3)客户端新建文件夹、删除空文件夹、重命名文件夹,实际上相当于在服务器网盘文件夹内进行相应操作;客户端上传文件,文件通过Socket上传到服务器端网盘上相应位置;删除、重命名文件的原理相同;下载文件,相当于通过Socket将文件从服务器传给客户端。
(4)客户端应该能够显示自己网盘中的文件夹结构,当进行操作后,应该能进行更新。

服务器端

界面设计

界面使用javax.swing开发
服务器端用Server包装,只包含一个JTextArea,用于显示客户端发送的指令,同时包含服务器端的根目录root、当前访问目录temproot和一些常用的引用。

public class Server extends JFrame implements Runnable {
	JTextArea jTextArea = new JTextArea();
	File root = new File("ServerDocument/");
	File temproot = null;
	InputStream iStream = null;
	OutputStream oStream = null;
	PrintStream pStream = null;
	BufferedReader bReader = null;
	String msg = null;
	String[] msgs = null;
	ServerSocket serverSocket = null;
	Socket socket = null;

通信

服务器端和客户端的通信用Java Socket,将Socket的OutputStream和InputStream包装成PrintStream和BufferedReader,用字符串指令进行通信。同时用服务器类实现Runnable接口,成为一个线程,用死循环接收客户端指令,根据消息调用不同功能。

服务器端和客户端之间的指令的格式为“标记+‘#’+信息”,标记如下:

static String ExistMsg = "EXIST";
static String NotExistMsg = "NEXIST";
static String LoginMsg = "LOGIN";
static String DownloadMsg = "DOWNLOAD";
static String UploadMsg = "UPLOAD";
static String VisitMsg = "VISIT";
static String ReturnMsg = "RETURN";
static String NewDirMsg = "NEWDIR";
static String RenaDirMsg = "REDIR";
static String DelDirMsg = "DELDIR";
static String NewFileMsg = "NEWFILE";
static String RenaFileMsg = "REFILE";
static String DelFileMsg = "DELFILE";
static String CheckTypeMsg = "CHECKTYPE";
static String IsDirMsg = "ISDIR";
static String IsFileMsg = "ISFILE";
static String UpdateMsg = "UPDATE";

Server的构造函数如下:

public Server() throws Exception {
	this.add(jTextArea);
	this.setTitle("服务器");
	this.setSize(600, 400);
	this.setVisible(true);
	this.setLocation(500, 500);
	this.setDefaultCloseOperation(EXIT_ON_CLOSE);
	new Thread(this).start();
}

run()函数在窗体开启后一直运行,首先new一个Server Socket和一个Socket,等待客户端接入:

@Override
public void run() {
	try {
		serverSocket = new ServerSocket(9900);
		socket = serverSocket.accept(); // 接入客户端

客户端接入后在JTextArea上显示相关信息,并且获取输入输出流:

		jTextArea.append("客户端接入" + "\n");
		pStream = new PrintStream(socket.getOutputStream());
		bReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

首先读入用户昵称,并在根目录下以该昵称新建文件夹:

		String newClient = bReader.readLine(); // 读入用户昵称
		newClient = newClient.split("#")[1];
		temproot = new File(root.getPath() + "/" + newClient + "/"); // 根据名称修改根目录
		root = new File(temproot.getPath());
		jTextArea.append(temproot.getPath() + "\n");
		if (!temproot.exists()) {
			temproot.mkdirs(); // 新建目录
		}

使用死循环不断读入指令,并根据指令调用相关功能:

		while (true) {
			msg = bReader.readLine(); // 从客户端读入命令信息
			msgs = msg.split("#"); // 根据命令前缀判断命令信息
			jTextArea.append(msg + "\n");

			if (msgs[0].equals(DownloadMsg)) { // 当客户端要求下载文件
				DownloadFileAction();
			} else if (msgs[0].equals(UploadMsg)) {
				UploadFileAction();
			} else if (msgs[0].equals(VisitMsg)) {
				VisitDirAction();
			} else if (msgs[0].equals(UpdateMsg)) {
				UpdateList();
			} else if (msgs[0].equals(CheckTypeMsg)) {
				CheckType();
			} else if (msgs[0].equals(NewDirMsg)) {
				NewDirAction();
			} else if (msgs[0].equals(RenaDirMsg)) {
				RenameDirAction();
			} else if (msgs[0].equals(DelDirMsg)) {
				DeleteDirAction();
			} else if (msgs[0].equals(RenaFileMsg)) {
				RenameFileAction();
			} else if (msgs[0].equals(DelFileMsg)) {
				DeleteFileAction();
			}
		}
	} catch (Exception e) {
		// TODO: handle exception
	}
}

下载文件

客户端发送的下载指令为“DownloadMsg#相对路径”,相对路径即文件在该用户所属文件夹下的相对路径,服务器端使用FileInputStream从文件读取数据,用socket传输数据。在文件传输完成后,需要断开socket,并进行重连。

	void DownloadFileAction() throws Exception { // 客户端新建下载
		msg = msgs[1]; // 获取相对路径
		File file = new File(temproot, msg);

		if (!file.exists()) {
			jTextArea.append(msg + NotExistMsg + "\n");
			pStream.println(NotExistMsg);
			return;
		}

		jTextArea.append(msg + ExistMsg + "\n");
		pStream.println(ExistMsg);
		iStream = new FileInputStream(file);
		oStream = socket.getOutputStream();
		// pStream = new PrintStream(oStream);
		// pStream.println(msg);

		byte[] data = new byte[2048]; // 输出数据
		int len = 0;
		while ((len = iStream.read(data)) != -1) {
			oStream.write(data, 0, len);
			oStream.flush();
		}
		jTextArea.append("文件下载完毕" + "\n");
		iStream.close();
		oStream.close(); // 关闭输入输出流
		socket.close();
		socket = serverSocket.accept();
		pStream = new PrintStream(socket.getOutputStream());
		bReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		jTextArea.append("客户端重新接入" + "\n");
	}

上传文件

客户端发送的上传指令为“UploadMsg#相对路径”,相对路径为文件在用户所属文件夹下存储的位置,服务器端从socket读入数据,使用FileOutputStream向文件写入数据。在文件传输完成后,需要断开socket,并进行重连。

	void UploadFileAction() throws Exception { // 客户端新建上传
		msg = msgs[1];

		File file = new File(temproot, msg);
		if (!file.exists()) {
			file.createNewFile();
		}
		iStream = socket.getInputStream();
		oStream = new FileOutputStream(file);
		byte[] data = new byte[2048];
		int len = 0;
		while ((len = iStream.read(data)) != -1) {
			oStream.write(data, 0, len);
			oStream.flush();
		}
		jTextArea.append("文件上传完成\n");
		iStream.close();
		oStream.close(); // 关闭输入输出流
		socket.close();
		socket = serverSocket.accept();
		pStream = new PrintStream(socket.getOutputStream());
		bReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		jTextArea.append("客户端重新连接\n");
	}

更新列表

客户端向服务器端发送访问或返回指令,以修改temproot的访问目录,并更新显示的文件列表。服务器端向客户端发送当前访问目录下的文件名称列表,指令格式为“ …@Dir1@Dir2@so on ”或“ 返回上一级@Dir1@Dir2@so on ”,其中前缀为‘…’表示当前已到达网盘的根目录。

// 访问子目录
	void VisitDirAction() throws Exception {
		msg = msgs[1];

		if (msg.equals(ReturnMsg)) {
			if (temproot.getPath().equals(root.getPath())) {
				// 若已到达根目录,则直接返回
				return;
			} else {
				// 否则向上一级
				temproot = temproot.getParentFile();
			}
		} else {
			temproot = new File(temproot, msg);
			if (!temproot.isDirectory()) {
				// 若选中项为文件,则仍访问其双亲目录
				temproot = temproot.getParentFile();
				return;
			}
		}
	}

	// 更新列表
	void UpdateList() throws Exception {
		String[] lists = temproot.list();
		String string = null;
		if (temproot.getPath().equals(root.getPath())) {
			string = "...";
		} else {
			string = "返回上一级";
		}
		for (String list : lists) {
			string = string + "@" + list;
		}
		pStream.println(string);
	}

新建(删除)文件(夹)

指令的格式为“新建(删除)#文件(夹)名”,使用Java的文件操作实现,这里以新建文件夹为例:

	void NewDirAction() throws Exception {
		String newDirName = msgs[1];
		File newDir = new File(temproot, newDirName);
		if (newDir.exists()) {
			pStream.println(ExistMsg);
		} else {
			pStream.println(NotExistMsg);
			newDir.mkdirs();
		}
	}

重命名文件(夹)

指令格式为“重命名指令#当前文件(夹)名#新文件(夹)名”,用Java的文件操作实现,这里以文件重命名为例:

	void RenameFileAction() throws Exception {
		String preFileName = msgs[1];
		String newFileName = msgs[2];
		String[] name = preFileName.split("\\."); // 由于split函数通过正则表达式分隔,因此这里使用"\\."分隔
		if (name[0].equals(newFileName)) {
			pStream.println(NotExistMsg);
			return;
		}
		String postfix = name[1];
		File newfile = new File(temproot, newFileName + "." + postfix);
		File prefile = new File(temproot, preFileName);
		if (newfile.exists() || !prefile.exists() || !prefile.isFile() || !prefile.renameTo(newfile)) {
			pStream.println(ExistMsg);
		} else {
			pStream.println(NotExistMsg);
		}
	}

客户端

界面设计

客户端的界面我想设计成类似于window操作系统这样子,但是没有菜单栏,只通过列表显示文件夹和文件,通过双击访问子目录或右键选中后弹出菜单。
客户端界面也用JFrame实现,包含一个JPanel,两个包装成弹出菜单的JPopupMenu,用于存储文件列表的Vector和显示文件列表的JList,以及滚动条JScrollPane。
同时客户端中也会包含根目录等内容,以及和服务器端相同的指令。

public class Client extends JFrame {
	private String NickName = null;
	private File root = new File("ClientDocument");
	File temproot = new File(root.getPath());
	Socket socket = null;
	InputStream iStream = null;
	OutputStream oStream = null;
	PrintStream pStream = null;
	BufferedReader bReader = null;
	String msg = null;
	String[] msgs = null;

	private JPanel jPanel = new JPanel();
	private DirMenu dirMenu = new DirMenu();
	private FileMenu fileMenu = new FileMenu();
	Vector<String> dirsvect = new Vector<>();
	private JList dirlist = new JList<>(dirsvect);
	private JScrollPane jScrollPane = new JScrollPane(dirlist);

构造函数

打开客户端时,会首先要求输入用户的昵称,若点击取消则会使用默认昵称default。

public Client() throws Exception {
		NickName = JOptionPane.showInputDialog("请输入昵称");
		if(NickName == null) {
			NickName = "default";
		}

初始化客户端界面

		this.setTitle(NickName);
		this.setSize(600, 400);
		this.setVisible(true);
		this.setLocation(400, 200);
		this.setDefaultCloseOperation(EXIT_ON_CLOSE);

客户端接入服务器,向服务器发送登录指令和昵称。

		socket = new Socket("127.0.0.1", 9900);
		pStream = new PrintStream(socket.getOutputStream());
		bReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		pStream.println(LoginMsg + "#" + NickName);

客户端更新文件列表,并设置字体等。

		UpdateList();
		dirlist.setFont(new Font("宋体", Font.BOLD, 15));
		this.add(jScrollPane);
		jPanel.setSize(getMaximumSize());

为JList绑定鼠标监听器,当鼠标左击或右击文件或目录时会选中该文件或目录,点击空白区域会取消选中,双击目录会调用访问函数,右击文件或目录时会调用相关函数,弹出不同类型的菜单。

		dirlist.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				JList theList = (JList) e.getSource();
				if (e.getButton() == e.BUTTON1 && e.getClickCount() == 2) {
					// 鼠标双击选中的文件或目录
					int index = theList.locationToIndex(e.getPoint());
					// System.out.println(index);
					// 双击空白处时index会置为最后一个选项
					// getCellBounds(index, index).contains(e.getPoint())判断鼠标单击的位置是否在最后一个选项中
					// 若不在该选项中则取消选择状态
					if (index != -1 && !theList.getCellBounds(index, index).contains(e.getPoint())) {
						dirlist.clearSelection();
						index = -1;
					}

					MouseDoubleClick(index);
				} else if (e.getButton() == e.BUTTON1 && e.getClickCount() == 1) {
					// 鼠标单击空白区域时取消选中
					int index = theList.locationToIndex(e.getPoint());
					// System.out.println(index);
					// 单击空白处时index会置为最后一个选项
					// getCellBounds(index, index).contains(e.getPoint())判断鼠标单击的位置是否在最后一个选项中
					// 若不在该选项中则取消选择状态
					if (index != -1 && !theList.getCellBounds(index, index).contains(e.getPoint())) {
						dirlist.clearSelection();
					}
				} else if (e.getButton() == e.BUTTON3) {
					// 鼠标右击选中的文件或目录
					int index = theList.locationToIndex(e.getPoint());
					if (index != -1 && !theList.getCellBounds(index, index).contains(e.getPoint())) {
						index = -1;
					}
					// System.out.println("右击"+index);
					if (index == -1) {
						dirlist.clearSelection();
					} else {
						dirlist.setSelectedIndex(index);
					}
					MouseRightClick(index, e.getX(), e.getY());
				}
			}
		});

访问子目录

客户端通过双击文件列表中的目录向服务器端发送访问指令,服务器端会首先判断该文件是否为目录,如果不是则会返回不能访问的指令,否则首先返回可以访问,再返回该目录下的文件列表,用于客户端列表的更新。
在客户端中双击会调用MouseDoubleClick(int index)函数,函数首先判断双击处是否为空白区域,若非,则调用访问函数VisitDirAction并随后更新文件列表。

	// 处理鼠标双击时的事件
	public void MouseDoubleClick(int index) {
		if (index != -1) {
			VisitDirAction(index);
			// 更新列表
			UpdateList();
		}
	}
	//访问子目录
	void VisitDirAction(int index) {
		// 选中列表范围内
		String s = dirsvect.elementAt(index);
		// System.out.println(s);
		if (index == 0) {
			// 若选中“...”或“返回上一级”
			pStream.println(VisitMsg + "#" + ReturnMsg);
		} else {
			// 访问选中项
			pStream.println(VisitMsg + "#" + s);
		}
	}
	// 更新列表
	void UpdateList() {
		try {
			pStream.println(UpdateMsg + "#");
			String Msg = bReader.readLine();
			String[] dirs = Msg.split("@");
			dirsvect.clear();
			for (String dir : dirs) {
				dirsvect.add(dir);
			}
			dirlist.setListData(dirsvect);
		} catch (Exception e) {
			// TODO: handle exception
		}
	}

弹出菜单

弹出菜单通过JPopupMenu实现,通过右击弹出菜单,右击目录或空白区域时弹出文件夹菜单,可以新建、重命名、删除文件夹,以及上传文件至当前目录下,右击文件时会弹出文件菜单,可以新建文件夹,重命名、删除文件以及下载该文件。
菜单中加入JMenuItem实现菜单选项,并为菜单选项增加监听器,在事件中调用各个函数以实现相应的功能,这里以文件夹目录为例。

	// 显示文件夹菜单
	class DirMenu extends JPopupMenu {
		private JMenuItem newDir = new JMenuItem("新建文件夹");
		private JMenuItem renameDir = new JMenuItem("重命名");
		private JMenuItem deleteDir = new JMenuItem("删除");
		private JMenuItem uploadFile = new JMenuItem("上传文件");

		public DirMenu() {
			this.add(newDir);
			this.add(renameDir);
			this.add(deleteDir);
			this.add(uploadFile);

			newDir.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					try {
						NewDirAction();
					} catch (Exception e1) {

					}
				}
			});

			renameDir.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					try {
						RenameDirAction();
					} catch (Exception e1) {

					}
				}
			});

			deleteDir.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					try {
						DeleteDirAction();
					} catch (Exception e1) {

					}
				}
			});

			uploadFile.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					try {
						new FileChose();
					} catch (Exception e1) {

					}
				}
			});
		}
	}

其余功能

上传和下载功能和服务器端相似,这里以下载功能为例。

	// 下载文件
	void DownloadFileAction() throws Exception {
		msg = dirsvect.elementAt(dirlist.getSelectedIndex());
		pStream.println(DownloadMsg + "#" + msg);
		if (bReader.readLine().equals(NotExistMsg)) {
			return;
		}

		File file = new File(root, msg);
		file.createNewFile(); // 新建文件
		// System.out.println(file.getPath());
		iStream = socket.getInputStream();
		oStream = new FileOutputStream(file);
		byte[] data = new byte[2048]; // 传输文件
		int len = 0;
		while ((len = iStream.read(data)) != -1) {
			oStream.write(data, 0, len);
			oStream.flush();
		}
		oStream.close();
		iStream.close();
		socket.close();
		socket = new Socket("127.0.0.1", 9900);
		pStream = new PrintStream(socket.getOutputStream());
		bReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		JOptionPane.showMessageDialog(null, "文件下载完成");
	}

其余功能只需向服务器端发送指令并更新列表即可,这里以新建文件为例。

	// 在当前目录下新建文件夹
	void NewDirAction() throws Exception {
		String newDirName = JOptionPane.showInputDialog("请输入文件夹名称", "新建文件夹");
		if(newDirName == null) {
			return;
		}
		pStream.println(NewDirMsg + "#" + newDirName);
		if (bReader.readLine().equals(ExistMsg)) {
			JOptionPane.showMessageDialog(null, "当前文件夹已存在");
		}
		UpdateList();
	}

总体来说就是这样,具体代码可以到GitHub项目中去查看
https://github.com/Lemok00/Java_WangPan

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值