Primary Practice h62
构造一个用户通过 socket 访问和控制远程文件的项目
开始理解的时候感觉很困难,但实际上就是一个文件操作的实验,套了一个 socket 的壳。
和 h60 类似,请确保你已经理解 h60。
先来看一下目录结构:
MyRemoteFile
远程文件类,是远程主机下的文件
MyHost
远程主机,即客户端,向服务器发出操作文件的请求
MyDaemon
监听类,即服务器端,接收客户端请求,直接操作文件
MyDaemonConfigVo
服务器配置
my_user.txt
存放用户名和口令,MyHost
登录需要
Test
测试文件
实际上,先配置好服务器,创建监听线程,用于响应主机的请求。然后创建远程主机,根据远程主机和路径访问并控制远程文件。
0x00 MyDaemonConfigVo
服务器配置。
在这里,服务器就是可以直接操作本地文件的一层。根据 Test
,配置项有本地文件目录、端口号和用户信息。其中用户信息可以直接在这里读文件转换为字符串列表(不用 Map)。
1 2 3
| private String root; private int port; private List<String> users = new ArrayList<>();
|
然后创建对应的 setter 和 getter。
读取文件的经典方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private List<String> readLines(String filePath) throws IOException { List<String> result = new ArrayList<>(); String line; Reader reader = new FileReader(filePath); LineNumberReader lineReader = new LineNumberReader(reader);
while (true) { line = lineReader.readLine(); if (line == null) { break; } if (line.trim().length() == 0 || line.startsWith("#")) { continue; } result.add(line); } return result; }
|
0x01 MyDaemon
监听类,服务器端,继承 Thread
。
创建时直接将服务器配置传过来,直接保存三个配置项:
- 本地路径,直接存字符串
- 端口号,直接创建一个
SocketServer
- 用户信息,直接存字符串列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private final ServerSocket server; private Socket socket; private BufferedReader in; private PrintWriter out; private final String root; private final List<String> users;
public MyDaemon(MyDeamonConfigVo config) throws IOException { server = new ServerSocket(config.getPort()); socket = null; root = config.getRoot(); users = config.getUsers(); in = null; out = null; }
|
重写 run
,循环从输入流读入并用 readLine
分析(进一步直接操作本地文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Override public void run() { try { socket = server.accept(); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream()); while (true) { String line = in.readLine(); readLine(line); if (1 == 0) { break; } } in.close(); out.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } }
|
其中 readLine
是自己写的,在下文和客户端放在一起叙述。
0x02 MyHost
远程主机,客户端,需要向输出流写各种操作文件的请求,在服务器那边处理,再从输入流读入服务器输出的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private String ip; private int port; private String username; private String password; private boolean valid; private Socket socket; private BufferedReader in; private PrintWriter out;
public MyHost() { super(); socket = null; in = null; out = null; valid = false; }
|
远程主机需要登录,即输入 用户名\t口令
到服务器端判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public void login() throws IOException { socket = new Socket(ip, port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream()); writeLine("login" + username + "\t" + password);
valid = "success".equals(in.readLine()); }
public boolean isInvalid() { return !valid; }
|
0x03 MyRemoteFile
-> MyHost
-> MyDaemon
MyRemoteFile
是远程文件类,需要一个远程主机再加上路径才能创建一个远程文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private final MyHost host; private final String path;
public MyRemoteFile(MyHost host, String path) throws IOException {
if (host.isInvalid()) { host.login(); if (host.isInvalid()) { throw new IOException("host login failed!"); } } this.host = host; this.path = path; }
|
根据 Test
,需要实现的功能有:
- 登录
- 列出子目录和文件
- 判断文件类型
- 获取远程文件路径(只需返回
path
)
- 写入文件
- 删除文件
- 获取文件大小
- 检查文件是否存在
实现的过程,就是远程文件调用远程主机的方法,向监听发送请求,监听对远程主机的请求作出相应,远程主机根据响应来返回值。这里服务器端(监听)和客户端(远程主机)需要遵从一个协议,即:
操作类型 |
格式 |
登录 |
“login” + username + “\t” + password |
列出子目录和文件 |
“getAscDir” + path |
判断文件类型 |
“type” + path |
写入文件 |
“write” + path + “:” + content |
删除文件 |
“delete” + path |
获取文件大小 |
“length” + path |
检查文件是否存在 |
“exist” + path |
为简化输出,定义方法 writeLine
:
1 2 3 4
| private void writeLine(String line) { out.write(line + "\r\n"); out.flush(); }
|
举例:为实现登录操作,客户端需要:
1
| writeLine("login" + username + "\t" + password);
|
服务器端需要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if (line.startsWith("login")) { checkLogin(line.substring(5)); }
...
private void checkLogin(String line) { String result = "failed"; for (String user: users) { if (line.equals(user)) { result = "success"; break; } } writeLine(result); }
|
如上文所叙述,服务器端始终用 readLine
方法处理所有请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private void readLine(String line) throws IOException { if (line.startsWith("login")) { checkLogin(line.substring(5)); } else if (line.startsWith("getAscDir")) { listFiles(line.substring(9)); } else if (line.startsWith("type")) { getFileType(line.substring(4)); } else if (line.startsWith("write")) { writeFile(line.substring(5)); } else if (line.startsWith("delete")) { delete(line.substring(6)); } else if (line.startsWith("length")) { getLength(line.substring(6)); } else if (line.startsWith("exist")) { isExist(line.substring(5)); } }
|
登录
如上文所叙述,创建一个远程文件时需要登录主机,主机登录时依照协议向服务器端写入用户名和口令。
服务器判断请求为“登录”操作,执行 checkLogin
,遍历用户信息字符串列表,存在则写出 "success"
否则 "failed"
,客户端根据服务器的响应修改 valid
状态。
列出子目录和文件
远程文件类:
直接调用远程主机的方法。
1 2 3
| public MyRemoteFile[] dirByNameAsc() throws IOException, InterruptedException { return host.getDirByNameAsc(path); }
|
远程主机类:
发出请求后,服务器先发来文件个数,据此创建远程文件数组;服务器再依次发来文件路径,据此创建每一个远程文件。
1 2 3 4 5 6 7 8 9
| public MyRemoteFile[] getDirByNameAsc(String path) throws IOException, InterruptedException { writeLine("getAscDir" + path); int count = Integer.parseInt(in.readLine()); MyRemoteFile[] result = new MyRemoteFile[count]; for (int i = 0; i < count; i++) { result[i] = new MyRemoteFile(this, in.readLine()); } return result; }
|
监听类:
根据请求得到文件列表,然后按照名称顺序排序,先输出文件个数再依次输出远程文件路径(远程文件根目录路径+文件名,如果是目录则再加上 /
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| private void listFiles(String filepath) { List<File> files = getFiles(filepath); List<File> resortFiles = sortFiles(files); writeLine(String.valueOf(resortFiles.size())); for (File resortFile : resortFiles) { String path = filepath + resortFile.getName(); if (!resortFile.isFile()) { path += "/"; } writeLine(path); } }
private List<File> getFiles(String filepath) { List<File> result = new ArrayList<>(); File file = new File(root + filepath); if (!file.isFile()) { File[] files = file.listFiles(); if (files != null) { result.addAll(Arrays.asList(files)); } } else { result.add(file); } return result; }
private List<File> sortFiles(List<File> fileList) { List<File> result = new ArrayList<>(); List<File> files = new ArrayList<>(); List<File> dirs = new ArrayList<>();
for (File file: fileList) { if (file.isFile()) { files.add(file); } else { dirs.add(file); } }
int dirCount = dirs.size(); int fileCount = files.size();
for (int i = 0; i < dirCount; i++) { File dir = dirs.get(0); for (File file: dirs) { if (dir.getAbsolutePath().compareTo(file.getAbsolutePath()) > 0) { dir = file; } } result.add(dir); dirs.remove(dir); } for (int i = 0; i < fileCount; i++) { File file = files.get(0); for (File file1: files) { if (file.getAbsolutePath().compareTo(file1.getAbsolutePath()) >= 0) { file = file1; } } result.add(file); files.remove(file); } return result; }
|
判断文件类型
远程文件类:
规定一个文件如果是目录则返回 0,如果是文件则返回 1,否则返回 -1。
1 2 3 4 5 6 7
| public boolean isDirectory() throws IOException { return host.getType(path) == 0; }
public boolean isFile() throws IOException { return host.getType(path) == 1; }
|
远程主机类:
规定一个文件如果是目录则输出 "dir"
,如果是文件则输出 "file"
。
1 2 3 4 5 6 7 8 9 10 11
| public int getType(String path) throws IOException { writeLine("type" + path); String type = in.readLine(); if ("file".equals(type)) { return 1; } else if ("dir".equals(type)) { return 0; } else { return -1; } }
|
监听类:
根据路径创建本地文件对象,判断文件类型。
1 2 3 4 5 6 7 8
| private void getFileType(String filepath) { File file = new File(root + filepath); if (file.isFile()) { writeLine("file"); } else { writeLine("dir"); } }
|
获取远程文件路径
远程文件类:
1 2 3
| public String getPathFileName() { return path; }
|
写入文件
远程文件类:
直接调用远程主机,传入路径和文件内容。
1 2 3
| public void writeByBytes(byte[] bytes) { host.writeByBytes(path, bytes); }
|
远程主机类:
根据字节数组创建字符串。
1 2 3 4
| public void writeByBytes(String path, byte[] bytes) { String content = new String(bytes, StandardCharsets.UTF_8); writeLine("write" + path + ":" + content); }
|
监听类:
根据格式分离路径和文件内容,文件不存在则新建文件,然后通过文件输出流输出文件内容。
1 2 3 4 5 6 7 8 9 10 11 12 13
| private void writeFile(String pathAndContent) throws IOException { String[] pathContent = pathAndContent.split(":"); String path = pathContent[0]; String content = pathContent[1]; File file = new File(root + path); if (!file.exists()) { if (file.createNewFile()) { FileOutputStream outFileStream = new FileOutputStream(file); outFileStream.write(content.getBytes(StandardCharsets.UTF_8)); outFileStream.flush(); } } }
|
删除文件
远程文件类:
直接调用远程主机。
1 2 3
| public void delete() { host.delete(path); }
|
远程主机类:
按照格式发送请求。
1 2 3
| public void delete(String path) { writeLine("delete" + path); }
|
监听类:
由于客户端没有读取操作,这里也不能输出。
1 2 3 4 5 6 7 8
| private void delete(String filepath) { File file = new File(root + filepath);
if (file.delete()) { } }
|
获取文件大小
远程文件类:
直接调用远程主机。
1 2 3
| public int length() throws IOException { return host.getLength(path); }
|
远程主机类:
按照格式发送请求,将返回的字符串转换为数字。
1 2 3 4 5
| public int getLength(String path) throws IOException { writeLine("length" + path); String len = in.readLine(); return Integer.parseInt(len); }
|
监听类:
根据路径新建文件,若存在则输出文件大小(转换为字符串),否则输出 "0"
。
1 2 3 4 5 6 7 8
| private void getLength(String filepath) { File file = new File(root + filepath); if (file.exists()) { writeLine(String.valueOf(file.length())); } else { writeLine("0"); } }
|
检查文件是否存在
远程文件类:
直接调用远程主机。
1 2 3
| public boolean exists() throws IOException { return host.isExist(path); }
|
远程主机类:
按照格式发送请求,对服务器输出的结果进行判断。
1 2 3 4 5
| public boolean isExist(String path) throws IOException { writeLine("exist" + path); String result = in.readLine(); return "exist".equals(result); }
|
监听类:
新建文件,判断是否在本地存在。
1 2 3 4 5 6 7 8
| private void isExist(String filepath) { File file = new File(root + filepath); if (file.exists()) { writeLine("exist"); } else { writeLine("not exist"); } }
|