前段时间因任务需要本人这个java渣渣开始研究如何用java实现简单的文件断点续传。所谓的文件断点续传,我的理解是文件在传输过程中因为某些原因程序停止运行文件终止传输,下一次重新传输文件的时候还能从上一次传输的位置开始传输,而不需要重新从头开始。

文件传输的过程分为发送方和接收方,最终我的思路是这样的:

  1. 传输开始之前发送方先向接收方发送一个确认信息,然后再向接收方发送准备发送的文件的文件名
  2. 接收方收到确认信息之后,接收从发送方发送过来的文件名,接收完之后向发送方发送一个确认信息表示文件名接收完毕,然后接收方根据收到的文件名创建一个“.temp”File对象和一个“.temp”RandomAccessFile对象。获取这个File对象所对应文件的长度(大小)(这个长度就是接收方已经接受的长度,如果之前没有接收过这个文件,长度就为0),并把文件长度发送给发送方。
  3. 发送方收到确认信息之后,接收接受方发送的文件长度,然后向接收方发送准备发送的文件的总长度,并向接收方发送一个确认信息。然后根据接收方发送的文件长度,从文件对应长度的位置开始发送。
  4. 接收方收到确认信息之后,接受发送方发送过来的数据,然后从此文件的末尾写入。接受完成之后再将“.temp”文件重命名为正常的文件名。

把过程画成图就是下面这样:

“ok”表示确认信息

能够实现断点续传的关键就是使用了RandomAccessFile,此类的实例支持对随机访问文件的读取和写入。

加入一些如进度条、文件选择器之类的GUI,最终的主要代码如下:

发送方线程:

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.Socket;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;

public class SendFileThread extends Thread{
	
	private Socket socket=null;
	private DataOutputStream dos;
	private DataInputStream dis;
	private RandomAccessFile rad;
	private Container contentPanel;
    private JFrame frame;
    private JProgressBar progressbar;
    private JLabel label;
	
	public SendFileThread(){
		frame=new JFrame("文件传输");
		try {
			socket=new Socket("localhost", 8080);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void run(){
		
		JFileChooser fc = new JFileChooser();	
		int status=fc.showOpenDialog(null);
		
		if (status==JFileChooser.APPROVE_OPTION) {
			String path=fc.getSelectedFile().getPath();	
			try {

				dos=new DataOutputStream(socket.getOutputStream());
				dis=new DataInputStream(socket.getInputStream());
				dos.writeUTF("ok");

				rad=new RandomAccessFile(path, "r");
				File file=new File(path);
				
				byte[] buf=new byte[1024];
				dos.writeUTF(file.getName());
				dos.flush();
				String rsp=dis.readUTF();
				
				if (rsp.equals("ok")) {
					long size=dis.readLong();//读取文件已发送的大小
					dos.writeLong(rad.length());
					dos.writeUTF("ok");
					dos.flush();
					
					long offset=size;//字节偏移量
					
					int barSize=(int) (rad.length()/1024);
					int barOffset=(int)(offset/1024);
					
					//传输界面
					frame.setSize(380,120);
					contentPanel = frame.getContentPane();
					contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
					progressbar = new JProgressBar();//进度条
					
					label=new JLabel(file.getName()+" 发送中");
					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);
					
					//从文件指定位置开始传输
					int length;
					if (offset<rad.length()) {
						rad.seek(offset);
						while((length=rad.read(buf))>0){
							dos.write(buf,0,length);							
							progressbar.setValue(++barOffset);
							dos.flush();
						}
					}
					label.setText(file.getName()+" 发送完成");
				}
			
				dis.close();
				dos.close();
				rad.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				label.setText(" 取消发送,连接关闭");
			}finally {
				frame.dispose();
			}
			
		}
	}
	
	class cancelActionListener implements ActionListener{
		public void actionPerformed(ActionEvent e3){
			try {
				label.setText(" 取消发送,连接关闭");
				JOptionPane.showMessageDialog(frame, "取消发送给,连接关闭!", "提示:", JOptionPane.INFORMATION_MESSAGE);				
				dis.close();
				dos.close();
				rad.close();
				frame.dispose();
				socket.close();
			} catch (IOException e1) {
				
			}
		}
	}

}

接收方线程:

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 ReceiveFileThread extends Thread{
	
	private ServerSocket connectSocket=null;
	private Socket socket=null;
	private JFrame frame;
	private Container contentPanel;
	private JProgressBar progressbar;
	private DataInputStream dis;
	private DataOutputStream dos;
	private RandomAccessFile rad;
	private JLabel label;
	
	public ReceiveFileThread(){
		frame=new JFrame("接收文件");
		try {
			connectSocket=new ServerSocket(8080);
			socket=connectSocket.accept();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void run(){
		try {
			dis=new DataInputStream(socket.getInputStream());
			dos=new DataOutputStream(socket.getOutputStream());
			dis.readUTF();
			
			int permit=JOptionPane.showConfirmDialog(frame, "是否接收文件","文件传输请求:", JOptionPane.YES_NO_OPTION);
			if (permit==JOptionPane.YES_OPTION) {
				String filename=dis.readUTF();
				dos.writeUTF("ok");
				dos.flush();
				File file=new File(filename+".temp");

				rad=new RandomAccessFile(filename+".temp", "rw");
				
				//获得文件大小
				long size=0;
				if(file.exists()&&file.isFile()){
					size=file.length();
				}
				
				dos.writeLong(size);//发送已接收的大小
				dos.flush();
				long allSize=dis.readLong();
				String rsp=dis.readUTF();
				
				int barSize=(int)(allSize/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);
			    
			    //接收文件
				if (rsp.equals("ok")) {
					rad.seek(size);
					int length;
					byte[] buf=new byte[1024];
					while((length=dis.read(buf, 0, buf.length))!=-1){
						rad.write(buf,0,length);
						progressbar.setValue(++barOffset);
					}
					System.out.println("end");
				}
				
				label.setText(filename+" 结束接收");
				
				
				dis.close();
				dos.close();
				rad.close();
				frame.dispose();
				//文件重命名
				if (barOffset>=barSize) {
					file.renameTo(new File(filename));
				}
				
			}else{
				dis.close();
				dos.close();
				frame.dispose();
			}
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			label.setText(" 已取消接收,连接关闭!");		
		}finally {
			frame.dispose();
		}
	}
	
	class cancelActionListener implements ActionListener{
		public void actionPerformed(ActionEvent e){
			try {
				dis.close();
				dos.close();
				rad.close();
				JOptionPane.showMessageDialog(frame, "已取消接收,连接关闭!", "提示:", JOptionPane.INFORMATION_MESSAGE);	
				label.setText(" 取消接收,连接关闭");
			} catch (IOException e1) {
				
			}
		}
	}
	
}

接下来我们来测试一下,下面分别是文件接收方与发送方的测试代码:

public class FileReceiveTest{//接收方
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ReceiveFileThread rf=new ReceiveFileThread();
		rf.start();
	}
	
}
public class FileSendTest{//发送方

	public static void main(String[] args) {
		// TODO Auto-generated method stub	
		SendFileThread sf=new SendFileThread();
		sf.start();
	}
	
}

先运行接收方代码再运行发送方代码,测试的时候我们选一个大一点的文件,我这里选了个电影文件,运行结果如下:

此时目录下生成一个临时文件:

我们点击取消或者关闭按钮终止文件传输,重新运行时我们发现文件从上次已传输的位置继续传输。当文件传输完成后,我们发现文件被重命名为正常的文件名,打开发现视频可以正常播放,测试成功:

当然,一个完整的文件传输还应该具有暂停和继续的功能,这里因为只是要说明简单文件断点续传的实现核心,省略掉了这些功能。

如果发现文章有什么写得不好的地方还望各位大佬能够批评指出(^_^)