Java断点续传(基于socket与RandomAccessFile的简单实现)

  Java断点续传(基于socket与RandomAccessFile的简单实现)
  
  这是一个简单的C/S架构,基本实现思路是将服务器注册至某个空闲端口用来监视并处理每个客户端的传输请求。
  
  客户端先获得用户给予的需传输文件与目标路径,之后根据该文件实例化RandomAccessFile为只读,之后客户端向服务器发送需传输的文件名文件大小与目标路径,服务器没接收到一个客户端的请求就会建立一个新的线程去处理它,根据接收到的文件名到目标路径中去寻找目标路径中是否已经有该文件名的.temp临时文件(如果没有就创建它),之后服务器会将文件已经传输的大小(临时文件大小)返回给客户端(例如临时文件刚刚建立返回的便是0),客户端会将刚刚建立的RandomAccessFile对象的文件指针指向服务器返回的位置,之后以1kb为一组向服务器传输需传输文件的内容数据,服务器则接收数据并将其写入临时文件中,并根据现有数据画出进度条。在文件传输完毕后客户端会将临时文件重命名为最初接收到的文件名。
  
  服务器代码:
  
  复制代码
  
  import java.awt.Color;
  
  import java.awt.Container;
  
  import java.awt.Dimension;
  
  import java.awt.FlowLayout;
  
  import java.awt.event.ActionEvent;
  
  import java.awt.event.ActionListener;
  
  import java.io.DataInputStream;
  
  import java.io.DataOutputStream;
  
  import java.io.File;
  
  import java.io.IOException;
  
  import java.io.RandomAccessFile;
  
  import java.net.ServerSocket;
  
  import java.net.Socket;
  
  import javax.swing.BoxLayout;
  
  import javax.swing.JButton;
  
  import javax.swing.JFrame;
  
  import javax.swing.JLabel;
  
  import javax.swing.JOptionPane;
  
  import javax.swing.JPanel;
  
  import javax.swing.JProgressBar;
  
  public class FileTransferServer extends ServerSocket {
  
  private static final int SERVER_PORT = 8899; // 服务端端口
  
  public FileTransferServer() throws Exception {
  
  super(SERVER_PORT);
  
  }
  
  public void load() throws Exception {
  
  while (true) {
  
  // server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的
  
  Socket socket = this.accept();
  
  // 每接收到一个Socket就建立一个新的线程来处理它
  
  new Thread(new Task(socket)).start();
  
  }
  
  }
  
  //处理客户端传输过来的文件线程类
  
  class Task implements Runnable {
  
  private Socket socket;
  
  private DataInputStream dis;
  
  private DataOutputStream dos;
  
  private RandomAccessFile rad;
  
  private JFrame frame;    //用来显示进度条
  
  private Container contentPanel;
  
  private JProgressBar progressbar;
  
  private JLabel label;
  
  public Task(Socket socket) {
  
  frame = new JFrame("文件传输");
  
  this.socket = socket;
  
  }
  
  @Override
  
  public void run() {
  
  try {
  
  dis = new DataInputStream(socket.getInputStream());
  
  dos = new DataOutputStream(socket.getOutputStream());
  
  String targetPath = dis.readUTF();    //接收目标路径
  
  String fileName = dis.readUTF();    //接收文件名
  
  //System.out.println("服务器:接收文件名");
  
  long fileLength = dis.readLong();    //接收文件长度
  
  //System.out.println("服务器:接收文件长度");
  
  File directory = new File(targetPath);    //目标地址
  
  if(!directory.exists()) {    //目标地址文件夹不存在则创建该文件夹
  
  directory.mkdir();
  
  }
  
  File file = new File(directory.getAbsolutePath() + File.separatorChar + fileName + ".temp");    //建立临时数据文件.temp
  
  //System.out.println("服务器:加载temp文件");
  
  rad = new RandomAccessFile(directory.getAbsolutePath() + File.separatorChar + fileName + ".temp", "rw");
  
  long size = 0;
  
  if(file.exists() && file.isFile()){    //如果目标路径存在且是文件,则获取文件大小
  
  size = file.length();
  
  }
  
  //System.out.println("服务器:获的当前已接收长度");
  
  dos.writeLong(size);    //向客户端发送当前数据文件大小
  
  dos.flush();
  
  //System.out.println("服务器:发送当前以接收文件长度");
  
  int barSize = (int)(fileLength / 1024);    //进度条当前进度
  
  int barOffset = (int)(size / 1024);        //进度条总长
  
  frame.setSize(300,120); //传输界面
  
  contentPanel = frame.getContentPane();
  
  contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
  
  progressbar = new JProgressBar();    //进度条
  
  label = new JLabel(fileName + " 接收中");
  
  contentPanel.add(label);
  
  progressbar.setOrientation(JProgressBar.HORIZONTAL);    //进度条为水平
  
  progressbar.setMinimum(0);    //进度条最小值
  
  progressbar.setMaximum(barSize);    //进度条最大值
  
  progressbar.setValue(barOffset);    //进度条当前值
  
  progressbar.setStringPainted(true); //显示进度条信息
  
  progressbar.setPreferredSize(new Dimension(150, 20));    //进度条大小
  
  progressbar.setBorderPainted(true);    //为进度条绘制边框
  
  progressbar.setBackground(Color.pink);    //进度条颜色为骚粉
  
  JButton cancel = new JButton("取消");    //取消按钮
  
  JPanel barPanel = new JPanel();
  
  barPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
  
  barPanel.add(progressbar);
  
  barPanel.add(cancel);
  
  contentPanel.add(barPanel);
  
  cancel.addActionListener(new cancelActionListener());
  
  //为取消按钮注册监听器
  
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  
  frame.setVisible(true);
  
  rad.seek(size);    //移动文件指针
  
  //System.out.println("服务器:文件定位完成");
  
  int length;
  
  byte[] bytes=new byte[1024];
  
  while((length = dis.read(bytes, 0, bytes.length)) != -1){
  
  rad.write(bytes,0, length);    //写入文件
  
  progressbar.setValue(++barOffset);    //更新进度条(由于进度条每个单位代表大小为1kb,所以太小的文件就显示不出啦)
  
  }
  
  if (barOffset >= barSize) {    //传输完成后的重命名
  
  if(rad != null)
  
  rad.close();
  
  if(!file.renameTo(new File(directory.getAbsolutePath() + File.separatorChar + fileName))) {
  
  file.delete();
  
  //防御性处理删除临时文件
  
  }
  
  //System.out.println("服务器:临时文件重命名完成");
  
  }
  
  } catch (Exception e) {
  
  e.printStackTrace();
  
  } finally {
  
  try {    //关闭资源
  
  if(rad != null)
  
  rad.close();
  
  if(dis != null)
  
  dis.close();
  
  if(dos != null)
  
  dos.close();
  
  frame.dispose();
  
  socket.close();
  
  } catch (Exception e) {}
  
  }
  
  }
  
  class cancelActionListener implements ActionListener{    //取消按钮监听器
  
  public void actionPerformed(ActionEvent e){
  
  try {
  
  //System.out.println("服务器:接收取消");
  
  if(dis != null)
  
  dis.close();
  
  if(dos != null)
  
  dos.close();
  
  if(rad != null)
  
  rad.close();
  
  frame.dispose();
  
  socket.close();
  
  JOptionPane.showMessageDialog(frame, "已取消接收,连接关闭!", "提示:", JOptionPane.INFORMATION_MESSAGE);
  
  label.setText(" 取消接收,连接关闭");
  
  } catch (IOException e1) {
  
  AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(JobService.class);
  
  for (String beanname:context.getBeanDefinitionNames())
  
  {
  
  System.out.println("--------"+beanname);
  
  }
  
  System.out.println("context.getBean(JobService.class) = " + context.getBean(JobService.class));
  
  复制代码
  
  这点代码很简单  初始化bean,然后再来拿bean,我们点进AnnotationConfigApplicationContext来看
  
  复制代码
  
  public AnnotationConfigApplicationContext(Class<?>... annotatedClasses)
  
  {
  
  this();
  
  register(annotatedClasses);
  
  refresh();
  
  }
  
  复制代码
  
  进⼊之后 会调用 this()默认无参构造方法
  
  public AnnotationConfigApplicationContext() {
  
  this.reader = new AnnotatedBeanDefinitionReader(this);
  
  this.scanner = new www.tianjiuyule178.com  ClassPathBeanDefinitionScanner(this);
  
  }
  
  调⽤这个⽆参构造⽅法的同时 他会调用⽗类的构造方法,在调用父类构造⽅方法时 他new了一个对象
  
  public GenericApplicationContext() {
  
  this.beanFactory = new DefaultListableBeanFactory();
  
  }
  
  也就是 DefaultListableBeanFactory,当然 这个就是所谓我们平常所说的 bean工厂,其父类就是 BeanFactory,BeanFactory有很多子类,DefaultListableBeanFactory就是其中一个⼦类。 那么 bean的⽣命周期是围绕那个⽅法呢,就是refresh()⽅法。也就是bean的整个生命周期是围绕refresh() 来进行的
  
  在refresh()我们可以看到
  
  复制代码
  
  public void refresh() throws BeansException, IllegalStateException {
  
  synchronized (this.startupShutdownMonitor)www.dayuzaixianyL.cn {
  
  // 准备好刷新上下文.
  
  prepareRefresh();
  
  // 返回一个Factory 为什么需要返回一个工厂  因为要对工厂进行初始化
  
  ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  
  // 准备bean工厂,以便在此上下文中使用。
  
  prepareBeanFactory(beanFactory);
  
  try {
  
  // 允许在上下文子类中对bean工厂进行后处理。 在spring5  并未对此接口进行实现
  
  postProcessBeanFactory(beanFactory);
  
  // 在spring的环境中去执行已经被注册的 Factory processors
  
  //设置执行自定义的postProcessBeanFactory和spring内部自己定义的
  
  invokeBeanFactoryPostProcessors(beanFactory);
  
  // 注册postProcessor
  
  registerBeanPostProcessors(beanFactory);
  
  // 初始化此上下文的消息源。
  
  initMessageSource();
  
  // 初始化此上下文的事件多播程序。
  
  initApplicationEventMulticaster();
  
  // 在特定上下文子类中初始化其他特殊bean。
  
  onRefresh();
  
  //检查侦听器bean并注册它们。
  
  registerListeners();
  
  // 实例化所有剩余的(非懒加载)单例。
  
  //new 单例对象
  
  finishBeanFactoryInitialization(beanFactory);
  
  // 最后一步:发布相应的事件
  
  finishRefresh();
  
  }
  
  catch (BeansException ex) {
  
  if (logger.isWarnEnabled()) {
  
  logger.warn("Exception encountered during context initialization - " +
  
  "cancelling refresh attempt: " + ex);
  
  }
  
  // Destroy already created singletons to avoid dangling resources.
  
  destroyBeans(www.hnxinhe.cn);
  
  // Reset 'active' flag.
  
  cancelRefresh(ex);
  
  // Propagate exception to caller.
  
  throw ex;
  
  }
  
  finally {
  
  // Reset common introspection caches in Spring's core, since we
  
  // might not ever need metadata for singleton beans anymore...
  
  resetCommonCaches();
  
  }
  
  }
  
  }
  
  复制代码
  
  那么这里面最重要就是finishBeanFactoryInitialization(beanFactory);这个方法就是描述 spring的一个bean如何初始化
  
  复制代码
  
  protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
  
  // Initialize conversion service for this context.
  
  if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
  
  beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
  
  beanFactory.setConversionService(
  
  beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
  
  }
  
  // Register a default embedded value resolver if no bean post-processor
  
  // (such as a PropertyPlaceholderConfigurer bean) registered any before:
  
  // at this point, primarily for resolution in annotation attribute values.
  
  if (!beanFactory.hasEmbeddedValueResolver(www.yunyouuyL.com)) {
  
  beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment(www.chenghgongs.com).resolvePlaceholders(strVal));
  
  }
  
  // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
  
  String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
  
  for (String weaverAwareName : weaverAwareNames) {
  
  getBean(weaverAwareName);
  
  }
  
  // Stop using the temporary ClassLoader for type matching.
  
  beanFactory.setTempClassLoader(null);
  
  // Allow for caching all bean definition metadata, not expecting further changes.
  
  beanFactory.freezeConfiguration();
  
  // 实例化所有单例对象
  
  beanFactory.preInstantiateSingletons();
  
  }
  
  复制代码
  
  可以看到前面是一些判断 最重要的就是最后一个方法 beanFactory.preInstantiateSingletons();我们看下preInstantiateSingletons()方法,它是ConfigurableListableBeanFactory这个接口的一个方法 我们直接来看这个接口的实现 是由DefaultListableBeanFactory这个类 来实现
  
  复制代码
  
  @Override
  
  public void preInstantiateSingletons() throws BeansException {
  
  if (logger.isDebugEnabled()) {
  
  logger.debug("Pre-instantiating singletons in www.baihuiyulegw.com " + this);
  
  }
  
  // Iterate over a copy to allow for init methods which in turn register new bean definitions.
  
  // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
  
  //所有bean的名字
  
  List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
  
  // Trigger initialization of all non-lazy singleton beans...
  
  for (String beanName : beanNames) {
  
  RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
  
  if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
  
  if (isFactoryBean(beanName)) {
  
  Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
  
  if (bean instanceof FactoryBean) {
  
  final FactoryBean<?> factory = (FactoryBean<?>) bean;
  
  boolean isEagerInit;
  
  if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
  
  isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
  
  ((SmartFactoryBean<www.tianscpt.com>) factory)::isEagerInit,
  
  getAccessControlContext());
  
  }
  
  else {
  
  isEagerInit = (factory instanceof SmartFactoryBean &&
  
  ((SmartFactoryBean<?>) factory).isEagerInit());
  
  }
  
  if (isEagerInit) {
  
  getBean(beanName);
  
  }
  
  }
  
  }
  
  else {
  
  getBean(beanName);
  
  复制代码
  
  客户端代码:
  
  复制代码
  
  import java.awt.event.ActionEvent;
  
  import java.awt.event.ActionListener;
  
  import java.io.DataInputStream;
  
  import java.io.DataOutputStream;
  
  import java.io.File;
  
  import java.io.IOException;
  
  import java.io.RandomAccessFile;
  
  import java.net.Socket;
  
  public class FileTransferClient extends Socket {
  
  private static final String SERVER_IP = "127.0.0.1"; // 服务端IP
  
  private static final int SERVER_PORT = 8899; // 服务端端口
  
  private Socket client;
  
  private DataOutputStream dos;
  
  private DataInputStream dis;
  
  private RandomAccessFile rad;
  
  public FileTransferClient() throws Exception {
  
  super(SERVER_IP, SERVER_PORT);
  
  this.client = this;
  
  //System.out.println("客户端:成功连接服务端");
  
  }
  
  public void sendFile(String filePath, String targetPath) throws Exception {
  
  try {
  
  File file = new File(filePath);
  
  if(file.exists()) {
  
  dos = new DataOutputStream(client.getOutputStream());     //发送信息 getOutputStream方法会返回一个java.io.OutputStream对象
  
  dis = new DataInputStream(client.getInputStream());    //接收远程对象发送来的信息  getInputStream方法会返回一个java.io.InputStream对象
  
  dos.writeUTF(targetPath); //发送目标路径
  
  dos.writeUTF(file.getName()); //发送文件名
  
  //System.out.println("客户端:发送文件名");
  
  rad = new RandomAccessFile(file.getPath(), "r");
  
  /*
  
  * RandomAccessFile是Java输入输出流体系中功能最丰富的文件内容访问类,既可以读取文件内容,也可以向文件输出数据。
  
  * 与普通的输入/输出流不同的是,RandomAccessFile支持跳到文件任意位置读写数据,RandomAccessFile对象包含一个记录指针,用以标识当前读写处的位置。
  
  * 当程序创建一个新的RandomAccessFile对象时,该对象的文件记录指针对于文件头 r代表读取
  
  */
  
  dos.flush();    //作用见下方介绍
  
  dos.writeLong(file.length()); //发送文件长度
  
  //System.out.println("客户端:发送文件长度");
  
  dos.flush();
  
  long size = dis.readLong();    //读取当前已发送文件长度
  
  //System.out.println("客户端:开始传输文件 ");
  
  int length = 0;
  
  byte[] bytes = new byte[1024];    //每1kb发送一次
  
  if (size < rad.length()) {
  
  rad.seek(size);
  
  //System.out.println("客户端:文件定位完成");
  
  //移动文件指针
  
  while((length = rad.read(bytes)) > 0){
  
  dos.write(bytes, 0, length);
  
  dos.flush();
  
  //每1kb清空一次缓冲区
  
  //为了避免每读入一个字节都写一次,java的输流有了缓冲区,读入数据时会首先将数据读入缓冲区,等缓冲区满后或执行flush或close时一次性进行写入操作
  
  }
  
  }
  
  //System.out.println("客户端:文件传输成功 ");
  
  }
  
  } catch (Exception e) {
  
  e.printStackTrace();
  
  } finally {    //关闭资源
  
  if(dos != null)
  
  dos.close();
  
  if(dis != null)
  
  dis.close();
  
  if(rad != null)
  
  rad.close();
  
  client.close();
  
  }
  
  }
  
  class cancelActionListener implements ActionListener{    //关闭按钮监听器
  
  public void actionPerformed(ActionEvent e3){
  
  try {
  
  //System.out.println("客户端:文件传输取消");
  
  if(dis != null)
  
  dis.close();
  
  if(dos != null)
  
  dos.close();
  
  if(rad != null)
  
  rad.close();
  
  client.close();
  
  } catch (IOException e1) {
  
  }
  
  }
  
  }
  
  }
  
  复制代码
  
  传输文件是一个耗时操作,若直接实例化客户端对服务器发送数据会造成UI假死的情况,直到文件传输完成后才会恢复,所以建议在实例化客户端时单独建立一个新线程。
  
  测试代码:
  
  复制代码
  
  import javax.swing.JFrame;
  
  import javax.swing.JButton;
  
  import javax.swing.JFileChooser;
  
  import java.awt.event.ActionListener;
  
  import java.awt.event.MouseAdapter;
  
  import java.awt.event.MouseEvent;
  
  import java.awt.event.ActionEvent;
  
  public class MainFrame extends JFrame{
  
  public MainFrame() {
  
  this.setSize(1280, 768);
  
  getContentPane().setLayout(null);
  
  JButton btnNewButton = new JButton("传输文件");    //点击按钮进行文件传输
  
  btnNewButton.addMouseListener(new MouseAdapter() {
  
  @Override
  
  public void mouseClicked(MouseEvent e) {
  
  // TODO 自动生成的方法存根
  
  super.mouseClicked(e);
  
  JFileChooser fileChooser = new JFileChooser();    //fileChooser用来选择要传输的文件
  
  fileChooser.setDialogTitle("选择要传输的文件");
  
  int stFile = fileChooser.showOpenDialog(null);
  
  if(stFile == fileChooser.APPROVE_OPTION){    //选择了文件
  
  JFileChooser targetPathChooser = new JFileChooser();    //targetPathChooser用来选择目标路径
  
  targetPathChooser.setDialogTitle("选择目标路径");
  
  targetPathChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);    //只能选择路径
  
  int stPath = targetPathChooser.showOpenDialog(null);
  
  if(stPath == targetPathChooser.APPROVE_OPTION) {    //选择了路径
  
  //新建一个线程实例化客户端
  
  new Thread(new NewClient( fileChooser.getSelectedFile().getPath(), targetPathChooser.getSelectedFile().getPath())).start();
  
  }
  
  }
  
  }
  
  });
  
  btnNewButton.setBounds(526, 264, 237, 126);
  
  getContentPane().add(btnNewButton);
  
  }
  
  class NewClient implements Runnable {    //用于实例化客户端的线程
  
  private String fileP;    //需复制文件路径
  
  private String targetP;    //目标路径
  
  public NewClient(String fileP, String targetP) {    //构造函数
  
  this.fileP = fileP;
  
  this.targetP = targetP;
  
  }
  
  @Override
  
  public void run() {
  
  // TODO 自动生成的方法存根
  
  try {
  
  @SuppressWarnings("resource")
  
  FileTransferClient ftc = new FileTransferClient();
  
  //实例化客户端
  
  ftc.sendFile(fileP, targetP);
  
  } catch (Exception e1) {
  
  // TODO 自动生成的 catch 块
  
  e1.printStackTrace();
  
  }
  
  }
  
  }
  
  public static void main(String[] args) {
  
  // TODO 自动生成的方法存根
  
  MainFrame mainFrame = new MainFrame();
  
  mainFrame.setVisible(true);
  
  mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  
  try {
  
  @SuppressWarnings("resource")
  
  FileTransferServer server = new FileTransferServer(); // 启动服务端
  
  server.load();
  
  } catch (Exception e) {
  
  e.printStackTrace();
  
  }
  
  }
  
  }
  
  复制代码
  
  演示:
  
  1运行MainFame
  
  2点击传输文件

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值