Java斷點續傳(基於socket與RandomAccessFile的簡單實現)

suvvm發表於2019-05-09

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) {
                    
                }
            }
        }
    }  
}

  客戶端程式碼:

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點選傳輸檔案

  3選擇要傳輸的檔案

  4選擇目標路徑

  5點選開啟

  點選取消

  之後重複2 - 5的操作。

  啊我手速慢,問題不大,你會發現斷點續傳已經實現了。

相關文章