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