程序入口

package main;

import service.YunPanClientImp;
import service.YunPanService;

/**
 * 客户端程序的入口
 * @author WangJunHui
 * @date 2020/12/16 20:45
 */
public class ClouDiskClient {
    public static void main(String[] args) {
        //创建程序对象
        YunPanService yunPanService = new YunPanClientImp();
        //初始化程序
        yunPanService.init();
        //启动程序
        yunPanService.start();
    }
}


package main;

import service.YunPanService;
import service.YunPanServiceImp;

/**
 * 服务器程序的入口
 * @author WangJunHui
 * @date 2020/12/15 22:05
 */
public class ClouDiskServer {
    public static void main(String[] args) {
        //创建入口程序对象
        YunPanService yunPanService = new YunPanServiceImp();
        //调用初始化方法
        yunPanService.init();
        //启动程序
        yunPanService.start();
    }
}

客户端和服务器业务类接口

为了方便就用了一个接口,一般都是分开的,毕竟服务器和客户端属于不同的工程。

public interface YunPanService {
    //初始化方法,初始化程序,从配置文件中读取端口号根目录等
    void init();
    //启动程序
    void start();
}

客户端业务类

package service;

import bean.Protocol;

import java.io.*;
import java.net.Socket;
import java.util.ResourceBundle;
import java.util.Scanner;

/**
 * 客户端的业务类,包含了程序初始化。和start业务方法,以及每个功能对应的业务方法。
 * @author WangJunHui
 * @date 2020/12/15 22:06
 */
public class YunPanClientImp implements YunPanService {
    //默认链接服务器的端口,如果配置文件中配置了,则使用配置文件中的。
    private static int port = 6667;//默认端口
    //默认的服务器IP,配置文件中可以配置。
    private static String ip = "127.0.0.1";//默认IP
    //默认客户端的下载根目录。配置文件中可以配置修改。
    private static File rootDir = new File("download");
    //用于记录当前路径,初始为根目录root
    private File file = new File("root");

    /**
     * 程序的初始化方法,用于读取配置文件,配置程序运行时的各种配置信息。
     */
    @Override
    public void init() {
        //通过配置文件获取根目录,连接端口号IP地址等云盘配置
        ResourceBundle bundle = ResourceBundle.getBundle("client");
        //获取配置文件中的端口号
        port = Integer.parseInt((String) bundle.getObject("port"));
        //获取配置文件中的服务器IP
        ip = (String) bundle.getObject("ip");
        //获取配置文件中的客户端下载根目录
        rootDir = new File((String) bundle.getObject("rootDir"));
    }

    /**
     * 程序的业务主界面,程序启动时,自动请求服务器根目录的目录列表,并显示到屏幕。
     * 然后读取用户输入的选项,根据选项,执行不同的业务方法。
     */
    @Override
    public void start() {
        //用于读取用户屏幕录入信息
        Scanner scanner = new Scanner(System.in);
        //欢迎词
        System.out.println("*******欢迎进入黑马网盘*******");
        wc:while (true){
            //显示云盘服务器当前目录文件列表
            scanDirectory();
            //显示选项信息
            System.out.println("*************************************************************************");
            System.out.println("1)浏览子目录 \t2)返回上一级目录 \t3)下载文件 \t4)上传文件 \t5)退出系统");
            System.out.println("*************************************************************************");
            //读取用户输入的选择序号
            String line = scanner.nextLine();
            //根据选择执行对应的业务方法
            switch (line){
                case "1":
                    //浏览子目录
                    scanChildDirectory();
                    break;
                case "2":
                    //返回上一级
                    scanPreviousDirectory();
                    break;
                case "3":
                    //下载文件
                    downloadFile();
                    break;
                case "4":
                    //上传文件
                    uploadFile();
                    break;
                case "5":
                    //退出系统
                    System.out.println("谢谢使用");
                    break wc;
                default:
                    //处理错误输入
                    System.out.println("输入的序号有误!");
            }
        }
    }

    /**
     * 浏览子目录业务功能,让用户输入子目录名称,然后和当前目录拼接为子目录路径,并作为当前路径,
     * start方法的死循环,每次都会调用浏览方法,获取当前目录的文件列表,所以我们在这里只需要更改当前目录即可。
     */
    private void scanChildDirectory(){
        System.out.println("请输入子文件夹:");
        Scanner scanner = new Scanner(System.in);
        //获取用户输入的子目录名称
        String line = scanner.nextLine();
        //将子目录和当前路径拼接,然后作为新的当前目录
        file = new File(file,line);
    }

    /**
     * 返回上一级业务功能,判断上一级是否存在,如返回到root时就不存在上一级目录,存在时获取当前目录的父目录,并作为当前路径,
     * 不存在时,提示用户已经处于根目录,没有上一级目录了。
     * start方法的死循环,每次都会调用浏览方法,获取当前目录的文件列表,所以我们在这里只需要更改当前目录即可。
     */
    private void scanPreviousDirectory(){
        //判断当前目录是否是root根目录
        if ("root".equals(file.getPath())){
            //提示用户已经处于根目录,没有上级目录了
            System.out.println("已返回到根目录,没有上级目录了");
        }else {
            //存在上级目录,将上级目录设置为当前目录。
            file = new File(file.getParent());
        }
    }

    /**
     * 浏览当前目录,每次在用户做出选择功能前都会显示当前目录供用户选择。
     */
    private void scanDirectory(){
        //将当前目录的路径写入到协议中
        Protocol protocol = Protocol.getScanDirProtocol(file.getPath());
        try {
            //获取和服务器的连接对象然后传递功能类的构造函数中创建对象,然后调用功能类的浏览方法,并传递浏览协议对象。
            //成功返回true,失败返回false。
            boolean b = new FileUpDownClientImp(connection()).scanDirectory(protocol);
            if (!b){
                /*
                浏览目录失败,获取父目录再次调用浏览功能。
                    1、当是浏览子目录功能改变当前目录后调用浏览失败时(子目录不存在),起到回退作用,
                    将当前目录回退到父目录中,然后显示父目录文件列表,父目录肯定存在(从父目录选择的子目录)。
                    2、当是返回上一级功能改变当前目录后调用浏览失败时(进入子目录后,父目录被删除),会退到上上一级,
                    如果上上一级不存在,还是会再次回到此处,然后继续向上返回直到返回到存在的上级目录或者最后返回到root根目录,
                    如果root目录不存在(也被删了),那云盘服务器已经废了(根目录都没有),可以报空指针异常(通过root获取的父目录为null)退出客户端了。
                 */
                file = file.getParentFile();
                scanDirectory();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 下载文件的业务方法,提示用户输入下载的文件名,然后将当前路径和文件名拼接为下载的文件路径,放于协议中。
     * 然后在客户端下载的根目录中创建文件父路径,创建不存在的父文件夹,然后调用功能类的下载方法,将请求协议和获取的客户端套接字传递过去。
     * 然后接受下载是否成功,失败时,删除创建的空文件,由于怕麻烦就没有删除创建的空的父文件夹,也可以通过循环获取父路径删除,
     * 直到删除到父目录不为空或者为根目录(因为有可能根目录也为空,但是不能删除)为止。
     */
    private void downloadFile(){
        System.out.println("请输入下载的文件名:");
        Scanner scanner = new Scanner(System.in);
        //获取客户输入的下载文件名称
        String line = scanner.nextLine();
        //将文件名和当前路径拼接为下载文件的路径。并设置到请求协议中
        Protocol protocol = Protocol.getDownloadProtocol(file.getPath() + "\\" + line);
        try {
            //将root替换为下载根目录。
            File root = new File(file.getPath().replaceFirst("root", rootDir.getPath()));
            //创建不存在的父目录
            root.mkdirs();
            File file = new File(root, line);
            //判断下载的文件在本地文件中是否是文件夹
            if (file.isDirectory()){
                //提示不能下载文件夹
                System.out.println("不能直接下载文件夹");
                //结束下载功能
                return;
            }
            //创建下载文件的字节输出流对象
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            //调用方法传递下载协议和文件输出流对象。
            boolean b = new FileUpDownClientImp(connection()).downloadFile(protocol, fileOutputStream);
            //获取下载执行结果
            if (!b){
                //下载失败,提示用户
                System.out.println("下载失败");
                fileOutputStream.close();
                //删除创建的空文件。
                file.delete();
            }
            //关闭文件输出字节流
            fileOutputStream.close();
        } catch (Exception e) {
            return;
           // e.printStackTrace();
        }
    }

    /**
     *上传功能的业务方法,提示用户输入要上传文件的路径,判断在本地硬盘上该路径是否存在,并且是否是文件类型,
     * 如果符合要求,将文件名拼接到当前目录上,组成在云盘上的文件路径,然后设置到协议里,创建文件字节读取流,
     * 调用方法传递文件字节读取流、协议对象、获取的客户端套接字。接受boolean类型的返回值,判断文件是否上传成功
     * 如果上传失败,给出提示信息。
     */
    private void uploadFile(){
        Scanner scanner = new Scanner(System.in);
        //死循环,直到用户输入的文件路径符合要求:存在、不是文件夹。
        while(true){
            System.out.println("请输入上传的文件路径:");
            String line = scanner.nextLine();
            //接受用户输入的文件路径。
            File file = new File(line);
            //判断是否符合上传要求
            if (file.exists()&&file.isFile()){
                //给出开始上传的提示信息
                System.out.println("开始上传"+file.getName());
                //将上传的文件路径封装到协议里。
                Protocol protocol = Protocol.getUploadProtocol(new File(this.file,file.getName()));
                try {
                    //创建文件字节读取流,用于读取要上传的文件数据。
                    FileInputStream fileInputStream = new FileInputStream(file);
                    //调用方法,传递文件字节读取流、协议对象、获取的客户端套接字。并接受返回值。
                    boolean b = new FileUpDownClientImp(connection()).uploadFile(protocol, fileInputStream);
                    //返回值为false,提示上传失败。
                    if (!b){
                        System.out.println("上传失败");
                    }
                    //关闭文件读取流。
                    fileInputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //文件上传完毕结束死循环
                break;
            }else if(!file.exists()){
                //提示用户输入的文件路径不存在
                System.out.println("文件不存在!");
            }else if(file.isDirectory()){
                //提示用户输入的路径是文件夹
                System.out.println("不能直接上传文件夹");
            }
        }
    }

    /**
     * 获取客户端套接字,通过成员变量的配置属性,链接服务器,并返回客户端套接字对象。
     * @return
     * @throws Exception
     */
    private Socket connection() throws Exception{
        return new Socket(ip,port);
    }
}

客户端传输功能类接口

package service;

import bean.Protocol;

import java.io.InputStream;
import java.io.OutputStream;

/**
 * @author WangJunHui
 * @date 2020/12/15 21:56
 */
public interface FileUpDownClient {
    boolean uploadFile(Protocol protocol, InputStream inputStream) throws Exception;
    boolean downloadFile(Protocol protocol, OutputStream outputStream) throws Exception;
    boolean scanDirectory(Protocol protocol) throws Exception;
    Protocol parseProtocol(InputStream inputStream) throws Exception;
}

客户端传输功能类

package service;

import bean.Protocol;
import utils.IOUtils;
import java.io.*;
import java.net.Socket;

/**
 * 客户端用于上传下载浏览的专门工具类,只涉及和服务器交互,不涉及业务逻辑。
 * @author WangJunHui
 * @date 2020/12/16 21:04
 */
public class FileUpDownClientImp implements FileUpDownClient {
    //网络字节输入流,用于读取服务器发来的字节数据
    private InputStream netInputStream;
    //网络字节输出流,用于向服务器发送字节数据
    private OutputStream netOutputStream;
    //构造方法接受的客户端套接字,用于获取网络IO流对象
    private Socket socket;

    public FileUpDownClientImp(Socket socket) {
        //接受传递的客户端套接字
        this.socket = socket;
        try {
            //通过套接字获取网络字节输入流
            this.netInputStream = socket.getInputStream();
            //通过套接字获取网络字节输出流
            this.netOutputStream = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 上传文件方法,向服务器发送请求协议,然后接受服务器回送的响应协议,判断协议status(状态)字段的内容,
     * OK:上传,通过IOUtils工具类的copy方法将文件字节输入流中的数据写入到网络字节输出流,否则打印协议中的失败信息。
     * @param protocol
     * @param inputStream
     * @return
     * @throws Exception
     */
    @Override
    public boolean uploadFile(Protocol protocol, InputStream inputStream) throws Exception {
        //向服务器发送请求协议
        sendProtocol(protocol, netOutputStream);
        //解析服务器发送的回送协议
        protocol = parseProtocol(netInputStream);
        //获取协议中的status字段内容
        String status = protocol.getStatus();
        //判断是否是OK(可以上传)
        if (Protocol.Status.OK.equals(status)) {
            //调用copy方法上传数据
            /*
                copy(inputStream,outputStream)将源数据流中数据写入到目标流中
                inputStream:源字节流,用于读取字节数据
                outputStream:目标字节流,用于写入字节数据。
             */
            boolean b = IOUtils.copy(inputStream, netOutputStream);
            //如果读写失败,返回false,告知用户上传失败。
            if (!b){
                return false;
            }
            //返回值为true提示上传成功
            System.out.println("上传成功");
            //写入结束标记。
            socket.shutdownOutput();
            socket.shutdownInput();
            //返回true
            return true;
        } else {
            //服务器不能接受该文件。打印服务器失败原因。
            System.out.println(protocol.getMessage());
            //关闭资源
            socket.shutdownOutput();
            socket.shutdownInput();
            //返回false
            return false;
        }
    }

    /**
     * 下载文件方法,向服务器发送请求协议,然后接受服务器回送的响应协议,判断协议status(状态)字段的内容,
     * 如果为OK下载网络流中的字节数据,否则提示用户下载失败(返回false),打印响应协议的失败信息。
     * @param protocol
     * @param outputStream
     * @return
     * @throws Exception
     */
    @Override
    public boolean downloadFile(Protocol protocol, OutputStream outputStream) throws Exception {
        //向服务器发送请求协议
        sendProtocol(protocol,netOutputStream);
        //解析服务器的回送协议
        protocol = parseProtocol(netInputStream);
        //获取回送协议中的状态字段
        String status = protocol.getStatus();
        //判断字段是否是OK
        if (Protocol.Status.OK.equals(status)) {
            //提示用户开始下载
            System.out.println(new File(protocol.getFileName()).getName()+"开始下载");
            //调用方法读写字节数据
            boolean b = IOUtils.copy(netInputStream, outputStream);
            //判断是否读写成功
            if (!b){
                //不成功返回false
                return false;
            }
            //成功,提示下载成功
            System.out.println("下载成功");
            //关闭资源。
            socket.shutdownOutput();
            socket.shutdownInput();
            //成功返回true
            return true;
        } else {
            //状态字段不为OK,服务器不能传递下载的文件,打印失败信息
            System.out.println(protocol.getMessage());
            //关闭资源
            socket.shutdownOutput();
            socket.shutdownInput();
            //失败返回false
            return false;
        }
    }

    /**
     * 浏览文件方法,向服务器发送请求协议,然后接受服务器回送的响应协议,判断协议status(状态)字段的内容,
     * 状态字段是OK,向屏幕打印分割条和当前目录(即要浏览的目录,存储在协议的fileName字段中),
     * 从网络流中读取数据(要浏览目录中的文件列表),然后转换为字符打印到屏幕,返回true。
     * 状态字段不是Ok,打印协议中的浏览失败信息。返回false
     * @param protocol
     * @return
     * @throws Exception
     */
    @Override
    public boolean scanDirectory(Protocol protocol) throws Exception {
        //向服务器发送请求协议
        sendProtocol(protocol, netOutputStream);
        //解析服务器回送的响应协议
        protocol = parseProtocol(netInputStream);
        //获取状态字段内容
        String status = protocol.getStatus();
        //判断是否为Ok
        if (Protocol.Status.OK.equals(status)) {
            //创建字节缓冲流,并将网络字节输入流绑定到缓冲流对象。
            BufferedInputStream bufferedInputStream = new BufferedInputStream(netInputStream);
            //打印分割条
            System.out.println("---------------------------------------------------");
            //打印当前目录
            System.out.println("当前目录:" + protocol.getFileName());
            //创建缓冲字节数组
            byte[] bytes = new byte[1024 * 20];
            //从缓冲流读取字节信息
            int len = bufferedInputStream.read(bytes);
            //将字节信息转换成字符串打印到屏幕。
            System.out.println(new String(bytes, 0, len));
            //关闭资源
            socket.shutdownOutput();
            socket.shutdownInput();
            //浏览成功返回true
            return true;
        } else {
            //浏览失败,浏览的文件夹不存在或者不是文件夹。打印失败信息
            System.out.println(protocol.getMessage());
            //关闭资源
            socket.shutdownOutput();
            socket.shutdownInput();
            //失败返回false
            return false;
        }

    }

    /**
     * 用于向服务器发送请求协议。
     * @param protocol
     * @param outputStream
     * @throws IOException
     */
    private void sendProtocol(Protocol protocol, OutputStream outputStream) throws IOException {
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
        //将协议内容转化为字节输出到网络输出流。
        bufferedOutputStream.write(protocol.toString().getBytes());
        //刷新缓存区内容
        bufferedOutputStream.flush();
    }

    /**
     * 用于从网络字节输入流中解析服务器回送的响应协议。调用了Protocol的parseProtocol方法。
     * 返回解析的协议对象。
     * @param inputStream
     * @return
     * @throws Exception
     */
    @Override
    public Protocol parseProtocol(InputStream inputStream) throws Exception {
        return Protocol.parseProtocol(inputStream);
    }
}

服务器启动程序类

package service;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * 服务器端的初始化和启动类
 * @author WangJunHui
 * @date 2020/12/15 22:06
 */
public class YunPanServiceImp implements YunPanService {
    //默认端口,当配置文件不存在时,使用默认端口,配置文件存在时,使用配置文件中配置的端口
    private static int port = 6667;
    //默认根目录,当配置文件存在时,使用配置的根目录。
    private static File rootDir = new File("root");
    //定义服务器套接字变量
    private ServerSocket serverSocket;

    /**
     * 程序初始化方法,用于读取配置文件,然后根据配置文件的内容,初始化服务器套接字和程序运行的根目录
     * 配置文件名:server.properties
     * 读取配置信息:
     *      端口号:port
     *      根目录:rootDir
     */
    @Override
    public void init() {
        //通过配置文件获取根目录,监听端口号等云盘配置
        ResourceBundle bundle = ResourceBundle.getBundle("server");
        //获取端口号
        port = Integer.parseInt((String) bundle.getObject("port"));
        //获取根目录
        rootDir = new File((String) bundle.getObject("rootDir"));
        //检查根目录是否是文件夹
        if (rootDir.isFile()){
            System.out.println("跟目录是一个文件,不是文件夹,请检查配置文件");
            //根目录不是文件夹,初始化失败,给出提示,停止程序。
            System.exit(0);
        }
        //如果根目录不存在,创建根目录文件夹
        rootDir.mkdirs();
        try {
            //使用配置信息创建服务器套接字,用于监听来自服务器的请求。
            serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 程序启动的方法,创建线程池对象,然后监听客户端请求,为每个任务请求分配一个线程运行。
     * 线程池对象:executorService,初始化线程个数:10
     * 线程任务对象类:FileUpDownServiceImp(Socket socket),该类继承了Runnable接口,重写了run方法,
     *          创建对象时需要传递一个监听到的客户端接口。保存在该类对象的成员变量中。
     *
     */
    @Override
    public void start() {
        Socket s = null;
        //根据配置信息,创建服务器套接字
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //监听客户端套接字连接请求
        try {
            while(true) {
                //监听客户端的链接请求,并获取对应的客户端套接字
                s = serverSocket.accept();
                //FileUpDownServiceImp继承了Runnable接口,将客户端套接字传递给该类对象的成员变量。
                //每获取一个连接,创建一个多线程任务,然后从线程池中获取线程,submit提交任务FileUpDownServiceImp类
                executorService.submit(new FileUpDownServiceImp(s));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取根目录的File对象,用于在FileUpDownServiceImp对象的上传下载和浏览时,获得服务器的根目录。
     *      静态的,可以通过类名直接调用
     * @return
     */
    public static File getRootDir() {
        return rootDir;
    }
}

服务器业务类

package service;

import bean.Protocol;
import utils.IOUtils;

import java.io.*;
import java.net.Socket;

/**
 * 服务器端的业务类,包含了浏览、下载、上传等业务功能
 * @author WangJunHui
 * @date 2020/12/15 22:07
 */
public class FileUpDownServiceImp implements FileUpDownService , Runnable {
    //成员变量中存储客户端套接字。
    private Socket socket;
    //网络输入流
    private InputStream netInputStream=null;
    //网络输出流
    private OutputStream netOutputStream=null;
    //程序根目录
    private File rootdir = YunPanServiceImp.getRootDir();
    //构造方法,接受客户端套接字。
    public FileUpDownServiceImp(Socket socket) {
        this.socket = socket;
    }

    /**
     * 实现了Runnable接口中的run方法,用于多线程处理客户端的功能请求。
     * 每个客户端功能请求分配一个单独的线程。
     */
    @Override
    public void run() {
        //通过套接字获取网络流对象
        try {
            //获取网络字节输入流,用于读取客户端发送的数据。
            netInputStream = socket.getInputStream();
            //获取网络字节输出流,用于向客户端传递数据。
            netOutputStream = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
            //然后通过分析方法获取协议对象,根据协议对象的type字段值,选择对应的方法。
            try {
                //解析客户端的请求协议对象,判断是何种业务请求。
                Protocol protocol = parseProtocol(netInputStream);
                //获取业务类型字段
                String type = protocol.getType();
                switch (type){
                    case "scan":
                        //浏览指定目录
                        scanDirectory(protocol,netInputStream,netOutputStream);
                        break;
                    case "upload":
                        //客户端上传文件
                        uploadFile(protocol,netInputStream,netOutputStream);
                        break;
                    case "download":
                        //客户端下载文件
                        downloadFile(protocol,netOutputStream);
                        break;
                }
            } catch (Exception e) {
                return;
            }finally {
                //业务结束,关闭网络流和客户端套接字。
                if (socket!=null) {
                    try {
                        socket.shutdownOutput();
                        socket.getInputStream();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    }

    /**
     * 上传文件业务相应方法,根据客户端的请求协议,获取上传文件名和路径,
     * 判断文件是否存在,如果存在,为防止覆盖,为文件名添加系统毫秒值,保证文件名的唯一性。如果不存在,则使用原本的文件名称。
     * 然后向客户端回送响应请求。并调用IOUtils工具类的copy方法,将网络输入流中的文件数据,写入到文件输出流中。
     * @param protocol
     * @param inputStream
     * @param outputStream
     * @throws Exception
     */
    @Override
    public void uploadFile(Protocol protocol, InputStream inputStream, OutputStream outputStream) throws Exception {
        //获取协议中的filename字段,得到客户端要上传的文件路径root\...\文件名.后缀名
        String fileName = protocol.getFileName();
        //将root替换为服务器的根目录名称。如upload\...\文件名.后缀名,只替换root,其余路径保持不变。
        File file = new File(fileName.replaceFirst("root",rootdir.getPath()));
        //为防止文件重复时覆盖,判断服务器中是否已经存在该文件。至于是否是文件夹,已经在客户端判断。
        if (file.exists()){
            try {
                //文件名重复时,为文件名添加系统毫秒值。保证文件名的唯一性。
                file = new File(file.getParentFile(),System.currentTimeMillis()+"_"+file.getName());
                //创建空白文件,等待接受文件内容。
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //将协议字段的status设置为OK,表示服务器已经准备好接受文件数据。
        protocol.setStatus(Protocol.Status.OK);
        //将响应协议发送给客户端。
        sendProtocol(protocol,outputStream);
        //创建文件输出流,用于向文件中写入字节信息。
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        //调用IOUtils工具类中的copy方法,读取网络字节输入流中的文件数据,写入到文件字节输出流中。
        IOUtils.copy(inputStream,fileOutputStream);
        //关闭文件输出流。
        fileOutputStream.close();
    }

    /**
     * 下载文件业务相应方法,根据客户端的请求协议,获取下载文件的文件名名和路径,
     * 判断要下载的文件是否存在并且是否是文件,如果不存在或者不是文件,回送失败响应协议,并告知失败信息。
     * 如果文件存在并且是文件,回送OK响应协议,并发送文件数据。
     * @param protocol
     * @param outputStream
     * @throws Exception
     */
    @Override
    public void downloadFile(Protocol protocol, OutputStream outputStream) throws Exception {
        //获取要下载的文件路径和文件名
        String fileName = protocol.getFileName();
        //将root替换为服务器的根目录,并获取文件对象
        File file = new File(fileName.replaceFirst("root",rootdir.getPath()));
        //判断文件是否存在并且是否是文件类型
        if (file.exists()&&file.isFile()){
            //修改协议的status字段为OK
            protocol.setStatus(Protocol.Status.OK);
            //向客户端发送响应协议
            sendProtocol(protocol,outputStream);
            //创建文件字节输入流,用于读取文件数据
            FileInputStream fileInputStream = new FileInputStream(file);
            //调用方法,从文件字节输入流中读取文件数据,然后写到网络字节输出流中。
            IOUtils.copy(fileInputStream,outputStream);
            //关闭文件字节输入流。
            fileInputStream.close();
        }else{
            /*
            要下载的文件不存在或者不是文件类型。
             */

            //设置失败响应协议。
            protocol.setStatus(Protocol.Status.FAILED);
            //设置失败信息
            if (file.isDirectory()){
                //下载的文件是文件夹类型
                protocol.setMessage("不能直接下载文件夹");
            }else{
                //下载的文件不存在
                protocol.setMessage("文件不存在");
            }
            //向客户端发送失败响应协议。
            sendProtocol(protocol,outputStream);
        }
    }

    /**
     *浏览文件夹的业务方法,从客户端发来的协议中获取要浏览的文件夹,然后判断是否是文件夹,文件夹是否存在。
     * 如果不是文件夹或者文件夹不存在,回送失败的响应协议。
     * 如果存在并且是文件夹类型,回送成功的响应协议,调用listFile方法获取该文件夹的文件列表字符串。
     * 然后将字符串转化为字节数组,并写入到网络字节输出流中。
     * @param protocol
     * @param inputStream
     * @param outputStream
     * @throws Exception
     */
    @Override
    public void scanDirectory(Protocol protocol, InputStream inputStream, OutputStream outputStream) throws Exception {
        //获取浏览的文件夹,并将路径中的第一个root替换为服务器根目录。
        File file = new File(protocol.getFileName().replaceFirst("root",rootdir.getPath()));
        //判断文件夹是否存在,浏览的文件是否是文件夹类型
        if (file.exists()&&file.isDirectory()){
            //创建缓冲流对象,将网络字节输出流封装到缓冲流中。
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
            //设置响应协议的状态为OK
            protocol.setStatus(Protocol.Status.OK);
            //向客户端发送响应协议。
            sendProtocol(protocol, outputStream);
            //获取文件夹的目录列表字符串,并转换为字节数组,写入到网络字节输出流中。
            bufferedOutputStream.write(listFile(file).getBytes());
            bufferedOutputStream.flush();
        }else{
            /*
            文件夹不存在,或者不是文件夹
             */
            //设置协议状态为失败
            protocol.setStatus(Protocol.Status.FAILED);
            //设置协议的失败信息
            protocol.setMessage("文件不存在或者不是文件夹");
            //回送失败的协议响应
            sendProtocol(protocol, outputStream);
        }
    }

    /**
     * 用来向客户端发送响应协议的方法
     *  protocol:响应协议对象
     *  outputStream:网络字节输出流。
     * @param protocol
     * @param outputStream
     * @throws IOException
     */
    private void sendProtocol(Protocol protocol, OutputStream outputStream) throws IOException {
        //获取网络字节输出缓冲流。
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
        //将参数传递的协议对象转换为字节数组,写到网络字节输出流中。
        bufferedOutputStream.write(protocol.toString().getBytes());
        //刷新缓冲流中的内容。
        bufferedOutputStream.flush();
    }

    /**
     * 获取指定目录的内容列表,只包含一层的文件和文件夹,不递归。
     * @param file
     * @return
     */
    private String listFile(File file) {
        //创建字符串拼接对象
        StringBuilder sb = new StringBuilder();
        //获取该目录下的子文件和文件夹对象集合。
        File[] files = file.listFiles();
        //遍历集合,获得文件和文件夹的名称。
        for (File file1 : files) {
            //如果是目录就拼接"目录:"+文件夹名,如果是文件就接"文件:"+文件名。
            sb.append(file1.isDirectory() ? "目录:"+file1.getName()+"\r\n" : "文件:"+file1.getName()+"\r\n");
        }
        //返回拼接的目录列表字符串。
        return sb.toString();
    }

    /**
     * 用于从网络字节输入流中解析客户端发送的请求协议
     * @param inputStream
     * @return
     * @throws Exception
     */
    @Override
    public Protocol parseProtocol(InputStream inputStream) throws Exception {
        //调用协议类中的解析方法,返回解析的协议对象。
        return Protocol.parseProtocol(inputStream);
    }
}

工具类

package utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * 用于IO操作
 *      方法
 *          copy(InputStream inputStream,OutputStream outputStream)
 *          从一个流中读取数据然后写到另一个流
 * @author WangJunHui
 * @date 2020/12/15 22:19
 */
public class IOUtils {
    /**
     * 从输入流中读取数据,然后写到输出流中,可以改进copy完毕后返回true,发生异常返回false。
     * @param inputStream:源字节流
     * @param outputStream:目标字节流
     * @return boolean
     */
    public static boolean copy(InputStream inputStream, OutputStream outputStream){
        byte[] bytes = new byte[1024*100];
        int len;
        try {
            while((len = inputStream.read(bytes))!=-1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

自定义传输协议类

package bean;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;

/**
 * 协议类,用于客户端和服务器之间的请求与应答的格式规范。
 * 包含:
 * 成员变量:
 * type:浏览、上传、下载
 * fileName:请求的文件或文件夹名称
 * status:服务器对此次请求是否成功的状态提示,成功、失败
 * message:对于此次请求或应答的详细信息描述。
 * 内部类:
 * Type:包含静态变量,封装了type的各个功能
 * SCAN    //浏览
 * UPLOAD  //上传
 * DOWNLOAD//下载
 * Status:包含服务器应答请求的各个状态
 * OK      //成功
 * FAILED  //失败
 * 方法:
 * 无参构造
 * get/set
 * toString:
 * 各字段和值作为键值对,键值对之间用逗号隔开,键值对之间用等号连接。
 * 以下方法返回值都是本类对象。
 * getScanDirProtocol(String path)//返回浏览文件夹的协议对象
 * 将参数传递的文件路径封装到fileName属性中,type指定为浏览其余值为空
 * getDownloadProtocol(String path)//返回下载文件的协议对象
 * 将参数传递的文件路径封装到fileName属性中,type指定为下载其余值为空
 * getUploadProtocol(File file)//返回上传文件的协议对象
 * 将参数传递的文件的路径封装到fileName属性中,type指定为上传其余值为空
 * parseProtocol(String str)//根据协议头解析协议的各个字段内容
 * 将参数解析为各个字段的key和value,然后设置到协议对象中,返回设置好的协议对象
 * parseProtocol(InputStream netIn)//根据网络流解析出报头
 * 将报头获得并转换为字符串形式,调用parseProtocol(String str)方法进行解析,然后返回得到的协议对象。
 *
 * @author WangJunHui
 * @date 2020/12/15 20:43
 */
public class Protocol {
    private String type;
    private String fileName;
    private String status;
    private String message;

    public Protocol() {
    }

    /**
     * 静态内部类Type:使用成员常量列举四种操作类型
     */
    public static class Type {
        public static final String SCAN = "scan";//浏览
        public static final String UPLOAD = "upload";//上传
        public static final String DOWNLOAD = "download";//下载
    }

    /**
     * 静态内部类Status:使用静态成员常量列举两种请求状态
     */
    public static class Status {
        public static final String OK = "ok";//成功
        public static final String FAILED = "failed";//失败
    }

    /**
     *返回浏览文件夹的协议对象
     * @param path
     * @return Protocol
     */
    public static Protocol getScanDirProtocol(String path){
        Protocol protocol = new Protocol();
        protocol.setFileName(path);
        protocol.setType(Type.SCAN);
        return protocol;
    }

    /**
     *根据传递的文件名,获取下载的协议对象
     * @param path
     * @return Protocol
     */
    public static Protocol getDownloadProtocol(String path){
        Protocol protocol = new Protocol();
        protocol.setFileName(path);
        protocol.setType(Type.DOWNLOAD);
        return protocol;
    }

    /**
     * 根据参数传递的文件对象,获取上传文件的协议对象。
     * @param file
     * @return Protocol
     */
    public static Protocol getUploadProtocol(File file){
        Protocol protocol = new Protocol();
        protocol.setType(Type.UPLOAD);
        //文件上传路径
        protocol.setFileName(file.getPath());
        return protocol;
    }

    /**
     *根据协议头解析协议的各个字段内容,然后返回解析的协议对象
     * @param str
     * @return Protocol
     */
   public static Protocol parseProtocol(String str){
        //创建map集合,用于存储解析的键值对。
       if (str==null){
           return null;
       }
       HashMap<String, String> map = new HashMap<>();
       String[] split = str.split(",");
       for (String s : split) {
           //获取每个键值对
           String[] kv = s.split("=");
           //存放到集合中。
           map.put(kv[0],kv[1]);
       }
       //使用反射技术获取Protocol的各个字段然后封装对应的值
       //其实可以直接通过set方法设置,使用反射只是为了练习反射的使用。
       Protocol protocol = new Protocol();
       //使用该方法是因为成员字段都是private修饰,需要暴力反射
       Field[] declaredFields = protocol.getClass().getDeclaredFields();
       for (Field field : declaredFields) {
           //开启暴力反射
           field.setAccessible(true);
           try {
               //通过字段getName方法获取字段名称作为key值查找map集合中对应的value值,然后将该value值设置给对象的对应字段。
               field.set(protocol,map.get(field.getName()));
           } catch (IllegalAccessException e) {
               e.printStackTrace();
           }
       }
       return protocol;

   }

    /**
     * 根据网络流解析出报头,然后返回解析的协议对象
     * @param inputStream
     * @return Protocol
     */
   public static Protocol parseProtocol(InputStream inputStream) throws Exception{
       InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
       BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
       String line =null;

            line= bufferedReader.readLine();
       return parseProtocol(line);

   }

    /**
     * 返回协议的字符串形式,即报头。
     * @return String
     */
    public String toString() {
        Field[] fields = getClass().getDeclaredFields();
        StringBuilder sb = new StringBuilder();
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                sb.append(field.getName()).append("=").append(field.get(this)).append(",");
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return sb.toString() + "\r\n";
    }
    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}