程序入口
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;
}
}