Java NIO檔案傳輸
阿新 • • 發佈:2021-01-25
技術標籤: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的完整資料
我尋思緩衝區太小就多迴圈讀取幾次不就行了嗎?為什麼會造成資料得丟失。
如果有大佬能幫忙指教一下,不勝感激,謝謝!!!