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" ); } }