首頁 >Java >java教程 >如何在Java中使用單一TCP連線發送多個檔案?

如何在Java中使用單一TCP連線發送多個檔案?

王林
王林轉載
2023-04-27 08:49:061594瀏覽

    使用一個TCP連線發送多個檔案

    為什麼會有這篇部落格? 最近在看一些相關方面的東西,簡單的使用一下 Socket 進行程式設計是沒有的問題的,但是這樣只是建立了一些基本概念。對於真正的問題,還是無能為力。

    當我需要進行檔案的傳輸時,我發現我好像只是發送過去了資料(二進位資料),但是關於檔案的一些資訊卻遺失了(檔案的副檔名)。而且每次我只能使用一個Socket 發送一個文件,沒有辦法做到連續發送文件(因為我是依靠關閉流來完成發送文件的,也就是說我其實是不知道文件的長度,所以只能以一個Socket 連線代表一個檔案)。

    這些問題困擾了我好久,我去網上簡單的查找了一下,沒有發現什麼現成的例子(可能沒有找到吧),有人提了一下,可以自己定義協議進行發送。這個倒是激發了我的興趣,感覺像是明白了什麼,因為我剛學過計算機網絡這門課,老實說我學得不怎麼樣,但是計算機網絡的概念我是學習到了。

    電腦網路這門課上,提到了許多協議,不知不覺中我也有了協議的概念。所以我找到了解決的方法:自己在 TCP 層上定義一個簡單的協定。 透過定義協議,這樣問題就迎刃而解了。

    協定的作用

    從主機1到主機2發送數據,從應用層的角度看,它們只能看到應用程式數據,但是我們透過圖是可以看出來的,資料從主機1開始,每向下一層資料會加上一個首部,然後在網路上傳播,當到達主機2後,每向上一層會去掉一個首部,達到應用層時,就只有資料了。 (這裡只是簡單的說明一下,實際上這樣還是不夠嚴謹,但是對於簡單的理解是夠了。)

    如何在Java中使用單一TCP連線發送多個檔案?

    所以,我可以自己定義一個簡單的協議,將一些必要的資訊放在協議頭部,然後讓電腦程式自己解析協議頭部訊息,而且每一個協議報文就相當於一個文件。這樣多個協定就是多個文件了。而且協定之間是可以區分的,不然的話,連續傳輸多個文件,如果無法區分屬於每個文件的位元組流,那麼傳輸是毫無意義的。

    定義資料的發送格式(協定)

    這裡的發送格式(我感覺和電腦網路中的協定有點像,也就稱它為一個簡單的協定吧) 。

    傳送格式:資料頭 資料體

    資料頭:長度為一位元組的數據,表示的內容是檔案的型別。注意:因為每個文件的類型是不一樣的,而且長度也不相同,我們知道協議的頭部一般是具有一個固定長度的(對於可變長的那些我們不考慮),所以我採用一個映射關係,即一個位元組數字表示一個檔案的類型。

    舉一個例子,如下:

    ##2# jpg3jpeg4avi

    註:這裡我做的是一個模擬,所以我只要測試幾種就行了。

    資料體: 檔案的資料部分(二進位資料)。

    程式碼

    客戶端

    協定頭類別

    package com.dragon;
    
    public class Header {
    	private byte type;      //文件类型
    	private long length;      //文件长度
    	
    	public Header(byte type, long length) {
    		super();
    		this.type = type;
    		this.length = length;
    	}
    
    	public byte getType() {
    		return this.type;
    	}
    	
    	public long getLength() {
    		return this.length;
    	}
    }

    傳送檔案類別

    package com.dragon;
    
    import java.io.BufferedInputStream;
    import java.io.BufferedOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.net.Socket;
    
    /**
     * 模拟文件传输协议:
     * 协议包含一个头部和一个数据部分。
     * 头部为 9 字节,其余为数据部分。
     * 规定头部包含:文件的类型、文件数据的总长度信息。
     * */
    public class FileTransfer {
    	private byte[] header = new byte[9];   //协议的头部为9字节,第一个字节为文件类型,后面8个字节为文件的字节长度。
    	
    	/**
    	 *@param src source folder 
    	 * @throws IOException 
    	 * @throws FileNotFoundException 
    	 * */
    	public void transfer(Socket client, String src) throws FileNotFoundException, IOException {
    		File srcFile = new File(src);
    		File[] files = srcFile.listFiles(f->f.isFile());
    		//获取输出流
    		BufferedOutputStream bos = new BufferedOutputStream(client.getOutputStream());
    		for (File file : files) {
    			try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))){
    				 //将文件写入流中
    				String filename = file.getName();
    				System.out.println(filename);
    				//获取文件的扩展名
    				String type = filename.substring(filename.lastIndexOf(".")+1);
    				long len = file.length();
    				//使用一个对象来保存文件的类型和长度信息,操作方便。
    				Header h = new Header(this.getType(type), len);
    				header = this.getHeader(h);
    				
    				//将文件基本信息作为头部写入流中
    				bos.write(header, 0, header.length);
    				//将文件数据作为数据部分写入流中
    				int hasRead = 0;
    				byte[] b = new byte[1024];
    				while ((hasRead = bis.read(b)) != -1) {
    					bos.write(b, 0, hasRead);
    				}
    				bos.flush();   //强制刷新,否则会出错!
    			}
    		}
    	}
    	
    	private byte[] getHeader(Header h) {
    		byte[] header = new byte[9];
    		byte t = h.getType();  
    		long v = h.getLength();
    		header[0] = t;                  //版本号
    		header[1] = (byte)(v >>> 56);   //长度
    		header[2] = (byte)(v >>> 48);
    		header[3] = (byte)(v >>> 40);
    		header[4] = (byte)(v >>> 32);
    		header[5] = (byte)(v >>> 24);
    		header[6] = (byte)(v >>> 16);
    		header[7] = (byte)(v >>>  8);
    		header[8] = (byte)(v >>>  0);
    		return header;
    	}
    	
    	/**
    	 * 使用 0-127 作为类型的代号
    	 * */
    	private byte getType(String type) {
    		byte t = 0;
    		switch (type.toLowerCase()) {
    		case "txt": t = 0; break;
    		case "png": t=1; break;
    		case "jpg": t=2; break;
    		case "jpeg": t=3; break;
    		case "avi": t=4; break;
    		}
    		return t;
    	}
    }

    附註:

    1. 發送完一個檔案後需要強制刷新一下。因為我是使用的緩衝流,我們知道為了提高發送的效率,並不是一有資料就發送,而是等待緩衝區滿了以後再發送,因為IO 過程是很慢的(相較於CPU),所以如果不刷新的話,當資料量特別小的檔案時,可能會導致伺服器端接收不到資料(這個問題,感興趣的可以去了解一下。),這是一個需要注意的問題。 (我測試的例子有一個文字檔案只有31位元組)。

    2. getLong() 方法將一個long 型數據轉為byte 型數據,我們知道long 佔8個字節,但是這個方法是我從Java源碼裡面抄過來的,有一個類別叫做DataOutputStream,它有一個方法是writeLong(),它的底層實作就是將long 轉為byte,所以我直接借鏡了。 (其實,這個也不是很複雜,它只是涉及了位運算,但是寫出來這個代碼就是很厲害了,所以我選擇直接使用這段代碼,如果對於位運算感興趣,可以參考一個我的博客:位運算)。

    測試類別

    package com.dragon;
    
    import java.io.IOException;
    import java.net.Socket;
    import java.net.UnknownHostException;
    
    //类型使用代号:固定长度
    //文件长度:long->byte 固定长度
    public class Test {
    	public static void main(String[] args) throws UnknownHostException, IOException {
    		FileTransfer fileTransfer = new FileTransfer();
    		try (Socket client = new Socket("127.0.0.1", 8000)) {
    			fileTransfer.transfer(client, "D:/DBC/src");
    		}
    	}
    }
    伺服器端

    協定解析類別

    package com.dragon;
    
    import java.io.BufferedInputStream;
    import java.io.BufferedOutputStream;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.net.Socket;
    import java.util.UUID;
    
    /**
     * 接受客户端传过来的文件数据,并将其还原为文件。
     * */
    public class FileResolve {
    	private byte[] header = new byte[9];  
    	
    	/**
    	 * @param des 输出文件的目录
    	 * */
    	public void fileResolve(Socket client, String des) throws IOException {
    		BufferedInputStream bis = new BufferedInputStream(client.getInputStream());
    		File desFile = new File(des);
    		if (!desFile.exists()) {
    			if (!desFile.mkdirs()) {
    				throw new FileNotFoundException("无法创建输出路径");
    			}
    		}
    		
    		while (true) {	
    			//先读取文件的头部信息
    			int exit = bis.read(header, 0, header.length);
    			
    			//当最后一个文件发送完,客户端会停止,服务器端读取完数据后,就应该关闭了,
    			//否则就会造成死循环,并且会批量产生最后一个文件,但是没有任何数据。
    			if (exit == -1) {
    				System.out.println("文件上传结束!");
    				break;   
    			}
    			
    			String type = this.getType(header[0]);
    			String filename  = UUID.randomUUID().toString()+"."+type;
    			System.out.println(filename);
    			//获取文件的长度
    			long len = this.getLength(header);
    			long count = 0L;
    			try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(des, filename)))){
    				int hasRead = 0;
    				byte[] b = new byte[1024];
    				while (count < len && (hasRead = bis.read(b)) != -1) {
    					bos.write(b, 0, hasRead);
    					count += (long)hasRead;
    					/**
    					 * 当文件最后一部分不足1024时,直接读取此部分,然后结束。
    					 * 文件已经读取完成了。
    					 * */
    					int last = (int)(len-count);
    					if (last < 1024 && last > 0) {
    						//这里不考虑网络原因造成的无法读取准确的字节数,暂且认为网络是正常的。
    						byte[] lastData = new byte[last];
    						bis.read(lastData);
    						bos.write(lastData, 0, last);
    						count += (long)last;
    					}
    				}
    			}
    		}
    	}
    	
    	/**
    	 * 使用 0-127 作为类型的代号
    	 * */
    	private String getType(int type) {
    		String t = "";
    		switch (type) {
    		case 0: t = "txt"; break;
    		case 1: t = "png"; break;
    		case 2: t = "jpg"; break;
    		case 3: t = "jpeg"; break;
    		case 4: t = "avi"; break;
    		}
    		return t;
    	}
    	
    	private long getLength(byte[] h) {
    		return (((long)h[1] << 56) +
                    ((long)(h[2] & 255) << 48) +
                    ((long)(h[3] & 255) << 40) +
                    ((long)(h[4] & 255) << 32) +
                    ((long)(h[5] & 255) << 24) +
                    ((h[6] & 255) << 16) +
                    ((h[7] & 255) <<  8) +
                    ((h[8] & 255) <<  0));
    	}
    }

    註:

    1. 這個將byte 轉為long 的方法,相信大家也能猜出來了。 DataInputStream 有一個方法叫做 readLong(),所以我直接拿來使用了。 (我覺得這兩段程式碼寫的非常好,不過我就看了幾個類別的源碼,哈哈!)

    2. 這裡我使用一個死循環進行檔案的讀取,但是我在測試的時候,發現了一個問題很難解決:什麼時候結束循環。 我一開始使用 client 關閉作為退出條件,但是發現無法起作用。後來發現,對於網路流來說,如果讀取到 -1 說明對面的輸入流已經關閉了,因此使用這個作為退出循環的標誌。如果刪去了這句程式碼,程式會無法自動終止,並且會一直產生最後一個讀取的文件,但是由於無法讀取到數據,所以文件都是 0 位元組的文件。 (這個東西產生文件的速度很快,大概幾秒鐘就會產生幾千個文件,如果有興趣,可以嘗試一下,但是最好快速終止程式的運行,哈哈!

    if (exit == -1) {
    	System.out.println("文件上传结束!");
    	break;   
    }

    測試類別

    這裡只測試一個連線就行了,這只是一個說明的例子。

    package com.dragon;
    
    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class Test {
    	public static void main(String[] args) throws IOException {
    		try (ServerSocket server = new ServerSocket(8000)){
    			Socket client = server.accept();
    			FileResolve fileResolve = new FileResolve();
    			fileResolve.fileResolve(client, "D:/DBC/des");
    		}	
    	}
    }

    測試結果

    Client

    如何在Java中使用單一TCP連線發送多個檔案?

    Server

    如何在Java中使用單一TCP連線發送多個檔案?

    如何在Java中使用單一TCP連線發送多個檔案?

    如何在Java中使用單一TCP連線發送多個檔案?

    ####### ############原始檔目錄### 這裡麵包含了我測試的五種檔案。注意對比文件的大小信息,對於IO的測試,我喜歡使用圖片和視頻測試,因為它們是很特殊的文件,如果錯了一點(字節少了、多了),文件基本上就損壞了,表現為圖片不正常顯示,影片無法正常播放。 ##################目的檔案目錄################
    key value
    0 txt
    1 png

    以上是如何在Java中使用單一TCP連線發送多個檔案?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除