1. 程式人生 > 其它 >Java NIO檔案傳輸

Java NIO檔案傳輸

技術標籤:java計算機網路javaniosocketlinux

上次寫了個OIO的的Sokcet程式設計,現在把最近學習的NIO補上

客戶端:Client

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

/**
 * 使用SocketChannel實現TCP協議的檔案傳輸
 * NIO中的SocketChannel和OIO中的Socket對應
 * NIO中的ServerSocketChannel和OIO中的ServerSocket對應
 */
public class Client{
    public static void main(String[] args) {
        //先啟動服務端,再啟動客戶端
        Client client = new Client();

        //傳送檔案
         String filepath = System.getProperty("user.dir")+"\\resource\\client\\";
         client.upload(filepath,"client1.jpg");

    }

    private final Charset charset = Charset.forName("UTF-8");//java預設編碼為Unicode,有的作業系統不支援,統一編碼格式為UTF-8
    private final String serverAddress = "localhost";
    private final int serverPort = 9111;

    /**
     * 傳遞到服務端的應該有檔案路徑和檔名
     * @param filepath
     * @param filename
     */
    public void upload(String filepath,String filename)  {
        try{
            File sendfile = new File(filepath+filename);
            FileChannel fileChannel = new FileInputStream(sendfile).getChannel();//獲取該檔案的輸入流的通道

            SocketChannel socketChannel = SocketChannel.open();//開啟Socket通道
            socketChannel.connect(new InetSocketAddress(serverAddress,serverPort));//繫結伺服器的連結地址和埠號
            socketChannel.configureBlocking(false);//設定為非阻塞式

            //由於是非阻塞式連線,所以socketChannel.connect()方法不論是否真正的連線成功,都會立即返回,
            while(!socketChannel.finishConnect()){
                //socket沒有真正連線前,不斷的自旋、等待,或者做一些其他的事情
                System.out.println("等待連線中,做其他事....");
            }
            System.out.println("成功連線到伺服器...");
            //將儲存在伺服器的檔名編碼為UTF-8格式的二進位制位元組序列
            ByteBuffer fileNamebuffer = charset.encode(filename);
            socketChannel.write(fileNamebuffer);//將檔名傳過去

            System.out.println("開始傳輸檔案");
            ByteBuffer filebuffer = ByteBuffer.allocate(1024);//開啟緩衝記憶體區域,用於儲存檔案內容
            int len = 0;
            while((len = fileChannel.read(filebuffer)) != -1){//將檔案資料從fileChannel讀取並存儲到filebuffer緩衝區中
                filebuffer.flip();//將記憶體緩衝區翻轉為讀模式
                socketChannel.write(filebuffer);//讀取本次緩衝區所有檔案並寫入通道
                filebuffer.clear();//清空緩衝區
            }

            //單向關閉,表示客戶端資料寫完了,如果需要服務端響應資料,就要用這種方式
//            socketChannel.shutdownOutput();

            //讀完檔案通道關閉
            fileChannel.close();
            socketChannel.close();
            System.out.println("傳輸完成...");
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

服務端Server,對每個客戶端的請求視為一個單獨的物件,所以加了一個靜態內部類作為當前處理的客戶端物件

服務端涉及了Selector選擇器,通過監聽不同的通道來實現IO的多路複用。這樣既能在非阻塞式的監聽客戶端請求,還可以只使用一個執行緒來處理多個客戶端請求,比起以前一個執行緒對應一個客戶端而言,節約了執行緒上下文切換的開銷,效率要高很多。

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 使用SocketChannel實現TCP協議的檔案傳輸
 * NIO中的SocketChannel和OIO中的Socket對應
 * NIO中的ServerSocketChannel和OIO中的ServerSocket對應
 */
public class Server{
    public static void main(String[] args) {
        //先啟動服務端
        Server server = new Server();
        server.startServer();
    }

    private final Charset charset = Charset.forName("UTF-8");
    private final int serverPort = 9111;
    //將客戶端傳遞的資料封裝為一個物件
    static class FileData{
        String clientAddress;
        String filename; //客戶端上傳的檔名稱
        FileChannel fileOutChannel;//輸出的檔案通道
    }

    //使用Map儲存每個客戶端傳輸,當OP_READ通道可讀時,根據channel找到對應的物件
    Map<SelectableChannel, FileData> map = new ConcurrentHashMap<>();

    /**
     * 啟動伺服器
     */
    public void startServer(){
        try{
            // 1、獲取Selector選擇器
            Selector selector = Selector.open();

            // 2、建立一個通道,用於獲取客戶端的請求連線
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            ServerSocket serverSocket = serverChannel.socket();

            // 3.設定為非阻塞
            serverChannel.configureBlocking(false);

            // 4、繫結連線
            // InetSocketAddress只傳入埠號,則自動綁定當前本機IP
            serverSocket.bind(new InetSocketAddress(serverPort));//即服務端開放99埠

            // 5、將該通道註冊到選擇器上,並註冊的IO事件為:“接收新連線”
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服務端已開啟,監聽新連線。。。");

            // 6、遍歷選擇器,輪詢感興趣的I/O就緒事件(選擇鍵集合)
            while (selector.select() > 0) {
                // 7、獲取選擇鍵集合
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    // 8、獲取單個的選擇鍵,並處理
                    SelectionKey key = it.next();

                    // 9、判斷key是具體的什麼事件,是否為新連線事件
                    if (key.isAcceptable()) {
                        // 10、若接受的事件是“新連線”事件,就獲取客戶端新連線
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = server.accept();
                        if (socketChannel == null) continue;
                        // 11、客戶端新連線,切換為非阻塞模式
                        socketChannel.configureBlocking(false);

                        // 12、將獲取到的客戶端socket通道再次註冊到選擇器,並註冊為可讀事件
                        // 這樣下次遍歷選擇器時,就進入可讀事件
                        socketChannel.register(selector, SelectionKey.OP_READ);

                        // 業務處理 - 每次客戶端上傳一個檔案就建立一個物件存到map中
                        // 一個客戶端物件對應一個socket通道
                        FileData fileData = new FileData();
                        fileData.clientAddress = socketChannel.getRemoteAddress().toString();
                        map.put(socketChannel, fileData);
                        System.out.println("與客戶端"+fileData.clientAddress+ "連線成功...");
                    } else if (key.isReadable()) {
                        receiveFile(key);
                    }
                    // NIO的特點只會累加,已選擇的鍵的集合不會刪除
                    // 如果不刪除,下一次又會被select函式選中
                    it.remove();
                }
            }

        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 接收檔案
     */
    private void receiveFile(SelectionKey key){
        ByteBuffer buffer = ByteBuffer.allocate(1024);//開啟記憶體緩衝區域
        FileData fileData = map.get(key.channel());

        SocketChannel socketChannel = (SocketChannel) key.channel();
        String directory = System.getProperty("user.dir")+"\\resource\\server\\";//服務端收到檔案的儲存路徑
        long start = System.currentTimeMillis();
        try {
            int len = 0;
            while ((len = socketChannel.read(buffer)) != -1) {//將客戶端寫入通道的資料讀取並存儲到buffer中
                buffer.flip();//將緩衝區翻轉為讀模式

                //客戶端傳送過來的,首先是檔名
                if (null == fileData.filename) {
                    // 檔名 decode解碼為UTF-8格式,並賦值給client物件的filename屬性
                    fileData.filename = (System.currentTimeMillis()+"_"+charset.decode(buffer).toString()).substring(5);

                    //先檢查儲存的目錄是否存在
                    File dir = new File(directory);
                    if(!dir.exists()) dir.mkdir();

                    //再檢查檔案是否存在,不存在就建立檔案,然後通過FikeChanel寫入資料
                    File file = new File(directory + fileData.filename);
                    if(!file.exists()) file.createNewFile();

                    //將設定要存放的檔案路徑+檔名建立一個輸出流通道
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    fileData.fileOutChannel = fileChannel;//賦值給client物件
                }
                //客戶端傳送過來的,最後是檔案內容
                else{
                    // 通過已經建立的檔案輸出流通道向檔案中寫入資料
                    fileData.fileOutChannel.write(buffer);
                }
                buffer.clear();//清除本次快取區內容
            }
            fileData.fileOutChannel.close();
            key.cancel();
            System.out.println("上傳完畢,費時:"+Long.valueOf(System.currentTimeMillis()-start)+"毫秒");
            System.out.println("檔案在服務端的儲存路徑:" + directory + fileData.filename);
            System.out.println("");
        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
            return;
        }
    }
}

最後留個小問題,希望後面自己能來解決,或者有大佬看到了能夠指點一二

上面的程式碼我在windows本地環境測試是完全沒問題的,但在linux的遠端雲伺服器上測試就有個小問題,就是客戶端的ByteBuffer緩衝區大小會影響服務端接收資料的完整性。

我測試檔案是一個約84kb的圖片檔案

當客戶端的ByteBuffer設為1024時,linux雲伺服器就只能收到64kb的資料

當客戶端的ByteBuffer設為10240時,linux雲伺服器就只能收到70kb的資料

當客戶端的ByteBuffer設為102400時,linux雲伺服器才能收到84kb的完整資料

我尋思緩衝區太小就多迴圈讀取幾次不就行了嗎?為什麼會造成資料得丟失。

如果有大佬能幫忙指教一下,不勝感激,謝謝!!!