文件上传分析图解
- 【客户端】输入流,从硬盘读取文件数据到程序中。
- 【客户端】输出流,写出文件数据到服务端。
- 【服务端】输入流,读取文件数据到服务端程序。
- 【服务端】输出流,写出文件数据到服务器硬盘中。
原理:客户端读取本地的文件,把文件上传到服务器,服务器再把上传的文件保存到服务器的硬盘上。
信息回写分析图解
5. 【服务端】获取网络字节缓冲输出流,回写数据。
6. 【客户端】获取网络字节缓冲输入流,解析回写数据。
基本实现步骤
- 客户端使用本地字节输入流,读取要上传的文件。
- 客户端使用网络字节输出流,把读取到的文件上传到服务器。
- 服务器使用网络字节输入流,读取客户端上传的文件。
- 服务器使用本地字节输出流,把读取到的文件,保存到服务器的硬盘上。
- 服务器使用网络字节输出流给客户端回写一个"上传成功”。
- 客户端使用网络字节输入流读取服务器回写的数据。
- 释放资源。
服务端实现
读取客户端上传的文件,保存到服务器的硬盘,给客户端回写"上传成功"。
保存目的地:
"/Users/liyihua/IdeaProjects/Study/src/view/study/demo38/CopyFile.jpg"
实现代码:
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TcpFileUploadServer { public static void main(String[] args) throws IOException { System.out.println("服务器启动时间:" + System.currentTimeMillis()); method(); System.out.println("服务器关闭时间:" + System.currentTimeMillis()); } private static void method() throws IOException { // 1. 创建服务端ServerSocket ServerSocket serverSocket = new ServerSocket(8888); // 2. 建立连接 Socket accept = serverSocket.accept(); // 3. 创建流对象 // 3.1 获取网络字节缓冲输入流,读取文件数据 BufferedInputStream bis1 = new BufferedInputStream(accept.getInputStream()); // 3.2 创建本地字节缓冲输出流,保存到本地 BufferedOutputStream bos1 = new BufferedOutputStream(new FileOutputStream("/Users/liyihua/IdeaProjects/Study/src/view/study/demo38/CopyFile.jpg")); // 4. 读写上传文件的数据 byte[] b = new byte[1024]; int len; while ((len = bis1.read(b)) != -1) { bos1.write(b, 0, len); } // 5. 上传成功 // 5.1 服务端,创建网络字节缓冲输出流 BufferedOutputStream bos2 = new BufferedOutputStream(accept.getOutputStream()); // 5.2 给客户回写数据:"上传成功"。 bos2.write("上传成功".getBytes()); // 5. 释放资源 bos1.close(); bis1.close(); bos2.close(); accept.close(); } }
客户端实现
读取本地文件,上传到服务器,读取服务器回写的数据。
本地文件地址:
"/Users/liyihua/IdeaProjects/Study/src/view/study/demo38/LocalFile.jpg"
实现代码:
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.net.Socket; public class TcpFileUploadClient { public static void main(String[] args) throws IOException { System.out.println("客户端启动时间:" + System.currentTimeMillis()); method(); System.out.println("客户端关闭时间:" + System.currentTimeMillis()); } private static void method() throws IOException { // 1. 创建流对象 // 1.1 创建本地字节缓冲输入流,读取本地文件 BufferedInputStream bis1 = new BufferedInputStream(new FileInputStream("/Users/liyihua/IdeaProjects/Study/src/view/study/demo38/LocalFile.jpg")); // 1.2 创建网络字节缓冲输出流,写到服务端 Socket socket = new Socket("localhost", 8888); BufferedOutputStream bos1 = new BufferedOutputStream(socket.getOutputStream()); // 2. 写出本地文件数据 byte[] bytes1 = new byte[1024]; int len1; while (( len1 = bis1.read(bytes1)) != -1) { bos1.write(bytes1, 0, len1); bos1.flush(); } // 3. 上传成功 // 3.1 客户端,创建网络字节缓冲输入流 BufferedInputStream bis2 = new BufferedInputStream(socket.getInputStream()); // 3.2 读取服务器回写的数据 byte[] bytes2 = new byte[1024]; int len2; while (( len2 = bis2.read(bytes2)) != -1) { bos1.write(bytes2, 0, len2); bos1.flush(); } // 3. 释放资源 bos1.close(); socket.close(); bis1.close(); } }
程序运行
文件上传优化分析
文件名称写死问题
服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一,代码如下:
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg"); // 文件名称 BufferedOutputStream bos = new BufferedOutputStream(fis);
循环接收的问题
服务端,只保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件,代码如下:
// 每次接收新的连接,创建一个Socket while(true){ Socket accept = serverSocket.accept(); ...... }
效率问题
服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化,代码如下:
while (true) { Socket accept = serverSocket.accept(); // accept交给子线程处理 new Thread(new Runnable() { @Override public void run() { ...... BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); ...... } }).start(); }
这里使用了匿名内部类实现Runnable接口,由于该接口中只有一个抽象方法,所以也可以使用Lambda表达式来简化代码:
while (true) { Socket accept = serverSocket.accept(); // accept交给子线程处理 new Thread( () -> { ...... BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); ...... } ).start(); }
优化实现
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class Tcp01FileUploadServer { public static void main(String[] args) throws IOException { System.out.println("服务器启动时间:" + System.currentTimeMillis()); method(); System.out.println("服务器关闭时间:" + System.currentTimeMillis()); } private static void method() throws IOException { // 1. 创建服务端ServerSocket ServerSocket serverSocket = new ServerSocket(8888); // 2. 循环接收,建立连接 while (true) { // 2.1 建立连接 Socket accept = serverSocket.accept(); new Thread( () -> { try ( // 3. 创建流对象 // 3.1 获取网络字节缓冲输入流,读取文件数据 BufferedInputStream bis1 = new BufferedInputStream(accept.getInputStream()); // 3.2 创建本地字节缓冲输出流,保存到本地 BufferedOutputStream bos1 = new BufferedOutputStream(new FileOutputStream("/Users/liyihua/IdeaProjects/Study/src/view/study/demo38/CopyFile.jpg")); // 4. 上传成功 // 4.1 服务端,创建网络字节缓冲输出流 BufferedOutputStream bos2 = new BufferedOutputStream(accept.getOutputStream()); ) { // 5. 读写上传文件的数据 byte[] b = new byte[1024]; int len; while ((len = bis1.read(b)) != -1) { bos1.write(b, 0, len); } bos1.flush(); // 6. 给客户回写数据:"上传成功"。 bos2.write("上传成功".getBytes()); bos2.flush(); // 7. 释放资源 bos1.close(); bis1.close(); bos2.close(); accept.close(); } catch (IOException e) { e.printStackTrace(); } } ).start(); } } }
这里虽然实现了多个文件同时上传,不过会在内存中不断的创建线程和销毁线程,造成效率低下,浪费内存资源。线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
线程池具体如何实现,可以查看:Java线程池概念、原理、简单实现