溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決

發(fā)布時(shí)間:2023-04-11 15:26:19 來源:億速云 閱讀:106 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決”的相關(guān)知識(shí),小編通過實(shí)際案例向大家展示操作過程,操作方法簡單快捷,實(shí)用性強(qiáng),希望這篇“Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決”文章能幫助大家解決問題。

    使用一個(gè)TCP連接發(fā)送多個(gè)文件

    為什么會(huì)有這篇博客? 最近在看一些相關(guān)方面的東西,簡單的使用一下 Socket 進(jìn)行編程是沒有的問題的,但是這樣只是建立了一些基本概念。對(duì)于真正的問題,還是無能為力。

    當(dāng)我需要進(jìn)行文件的傳輸時(shí),我發(fā)現(xiàn)我好像只是發(fā)送過去了數(shù)據(jù)(二進(jìn)制數(shù)據(jù)),但是關(guān)于文件的一些信息卻丟失了(文件的擴(kuò)展名)。而且每次我只能使用一個(gè) Socket 發(fā)送一個(gè)文件,沒有辦法做到連續(xù)發(fā)送文件(因?yàn)槲沂且揽筷P(guān)閉流來完成發(fā)送文件的,也就是說我其實(shí)是不知道文件的長度,所以只能以一個(gè) Socket 連接代表一個(gè)文件)。

    這些問題困擾了我好久,我去網(wǎng)上簡單的查找了一下,沒有發(fā)現(xiàn)什么現(xiàn)成的例子(可能沒有找到吧),有人提了一下,可以自己定義協(xié)議進(jìn)行發(fā)送。 這個(gè)倒是激發(fā)了我的興趣,感覺像是明白了什么,因?yàn)槲覄倢W(xué)過計(jì)算機(jī)網(wǎng)絡(luò)這門課,老實(shí)說我學(xué)得不怎么樣,但是計(jì)算機(jī)網(wǎng)絡(luò)的概念我是學(xué)習(xí)到了。

    計(jì)算機(jī)網(wǎng)絡(luò)這門課上,提到了很多協(xié)議,不知不覺中我也有了協(xié)議的概念。所以我找到了解決的辦法:自己在 TCP 層上定義一個(gè)簡單的協(xié)議。 通過定義協(xié)議,這樣問題就迎刃而解了。

    協(xié)議的作用

    從主機(jī)1到主機(jī)2發(fā)送數(shù)據(jù),從應(yīng)用層的角度看,它們只能看到應(yīng)用程序數(shù)據(jù),但是我們通過圖是可以看出來的,數(shù)據(jù)從主機(jī)1開始,每向下一層數(shù)據(jù)會(huì)加上一個(gè)首部,然后在網(wǎng)絡(luò)上進(jìn)行傳播,當(dāng)?shù)竭_(dá)主機(jī)2后,每向上一層會(huì)去掉一個(gè)首部,達(dá)到應(yīng)用層時(shí),就只有數(shù)據(jù)了。(這里只是簡單的說明一下,實(shí)際上這樣還是不夠嚴(yán)謹(jǐn),但是對(duì)于簡單的理解是夠了。)

    Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決

    所以,我可以自己定義一個(gè)簡單的協(xié)議,將一些必要的信息放在協(xié)議頭部,然后讓計(jì)算機(jī)程序自己解析協(xié)議頭部信息,而且每一個(gè)協(xié)議報(bào)文就相當(dāng)于一個(gè)文件。這樣多個(gè)協(xié)議就是多個(gè)文件了。而且協(xié)議之間是可以區(qū)分的,不然的話,連續(xù)傳輸多個(gè)文件,如果無法區(qū)分屬于每個(gè)文件的字節(jié)流,那么傳輸是毫無意義的。

    定義數(shù)據(jù)的發(fā)送格式(協(xié)議)

    這里的發(fā)送格式(我感覺和計(jì)算機(jī)網(wǎng)絡(luò)中的協(xié)議有點(diǎn)像,也就稱它為一個(gè)簡單的協(xié)議吧)。

    發(fā)送格式:數(shù)據(jù)頭+數(shù)據(jù)體

    數(shù)據(jù)頭:一個(gè)長度為一字節(jié)的數(shù)據(jù),表示的內(nèi)容是文件的類型。 注:因?yàn)槊總€(gè)文件的類型是不一樣的,而且長度也不相同,我們知道協(xié)議的頭部一般是具有一個(gè)固定長度的(對(duì)于可變長的那些我們不考慮),所以我采用一個(gè)映射關(guān)系,即一個(gè)字節(jié)數(shù)字表示一個(gè)文件的類型。

    舉一個(gè)例子,如下:

    keyvalue
    0txt
    1png
    2jpg
    3jpeg
    4avi

    注:這里我做的是一個(gè)模擬,所以我只要測(cè)試幾種就行了。

    數(shù)據(jù)體: 文件的數(shù)據(jù)部分(二進(jìn)制數(shù)據(jù))。

    代碼

    客戶端

    協(xié)議頭部類

    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;
    	}
    }

    發(fā)送文件類

    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;
    
    /**
     * 模擬文件傳輸協(xié)議:
     * 協(xié)議包含一個(gè)頭部和一個(gè)數(shù)據(jù)部分。
     * 頭部為 9 字節(jié),其余為數(shù)據(jù)部分。
     * 規(guī)定頭部包含:文件的類型、文件數(shù)據(jù)的總長度信息。
     * */
    public class FileTransfer {
    	private byte[] header = new byte[9];   //協(xié)議的頭部為9字節(jié),第一個(gè)字節(jié)為文件類型,后面8個(gè)字節(jié)為文件的字節(jié)長度。
    	
    	/**
    	 *@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);
    				//獲取文件的擴(kuò)展名
    				String type = filename.substring(filename.lastIndexOf(".")+1);
    				long len = file.length();
    				//使用一個(gè)對(duì)象來保存文件的類型和長度信息,操作方便。
    				Header h = new Header(this.getType(type), len);
    				header = this.getHeader(h);
    				
    				//將文件基本信息作為頭部寫入流中
    				bos.write(header, 0, header.length);
    				//將文件數(shù)據(jù)作為數(shù)據(jù)部分寫入流中
    				int hasRead = 0;
    				byte[] b = new byte[1024];
    				while ((hasRead = bis.read(b)) != -1) {
    					bos.write(b, 0, hasRead);
    				}
    				bos.flush();   //強(qiáng)制刷新,否則會(huì)出錯(cuò)!
    			}
    		}
    	}
    	
    	private byte[] getHeader(Header h) {
    		byte[] header = new byte[9];
    		byte t = h.getType();  
    		long v = h.getLength();
    		header[0] = t;                  //版本號(hào)
    		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 作為類型的代號(hào)
    	 * */
    	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. 發(fā)送完一個(gè)文件后需要強(qiáng)制刷新一下。因?yàn)槲沂鞘褂玫木彌_流,我們知道為了提高發(fā)送的效率,并不是一有數(shù)據(jù)就發(fā)送,而是等待緩沖區(qū)滿了以后再發(fā)送,因?yàn)?IO 過程是很慢的(相較于 CPU),所以如果不刷新的話,當(dāng)數(shù)據(jù)量特別小的文件時(shí),可能會(huì)導(dǎo)致服務(wù)器端接收不到數(shù)據(jù)(這個(gè)問題,感興趣的可以去了解一下。),這是一個(gè)需要注意的問題。(我測(cè)試的例子有一個(gè)文本文件只有31字節(jié))。

    2. getLong() 方法將一個(gè) long 型數(shù)據(jù)轉(zhuǎn)為 byte 型數(shù)據(jù),我們知道 long 占8個(gè)字節(jié),但是這個(gè)方法是我從Java源碼里面抄過來的,有一個(gè)類叫做 DataOutputStream,它有一個(gè)方法是 writeLong(),它的底層實(shí)現(xiàn)就是將 long 轉(zhuǎn)為 byte,所以我直接借鑒過來了。(其實(shí),這個(gè)也不是很復(fù)雜,它只是涉及了位運(yùn)算,但是寫出來這個(gè)代碼就是很厲害了,所以我選擇直接使用這段代碼,如果對(duì)于位運(yùn)算感興趣,可以參考一個(gè)我的博客:位運(yùn)算)。

    測(cè)試類

    package com.dragon;
    
    import java.io.IOException;
    import java.net.Socket;
    import java.net.UnknownHostException;
    
    //類型使用代號(hào):固定長度
    //文件長度: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");
    		}
    	}
    }
    服務(wù)器端

    協(xié)議解析類

    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;
    
    /**
     * 接受客戶端傳過來的文件數(shù)據(jù),并將其還原為文件。
     * */
    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("無法創(chuàng)建輸出路徑");
    			}
    		}
    		
    		while (true) {	
    			//先讀取文件的頭部信息
    			int exit = bis.read(header, 0, header.length);
    			
    			//當(dāng)最后一個(gè)文件發(fā)送完,客戶端會(huì)停止,服務(wù)器端讀取完數(shù)據(jù)后,就應(yīng)該關(guān)閉了,
    			//否則就會(huì)造成死循環(huán),并且會(huì)批量產(chǎn)生最后一個(gè)文件,但是沒有任何數(shù)據(jù)。
    			if (exit == -1) {
    				System.out.println("文件上傳結(jié)束!");
    				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;
    					/**
    					 * 當(dāng)文件最后一部分不足1024時(shí),直接讀取此部分,然后結(jié)束。
    					 * 文件已經(jīng)讀取完成了。
    					 * */
    					int last = (int)(len-count);
    					if (last < 1024 && last > 0) {
    						//這里不考慮網(wǎng)絡(luò)原因造成的無法讀取準(zhǔn)確的字節(jié)數(shù),暫且認(rèn)為網(wǎng)絡(luò)是正常的。
    						byte[] lastData = new byte[last];
    						bis.read(lastData);
    						bos.write(lastData, 0, last);
    						count += (long)last;
    					}
    				}
    			}
    		}
    	}
    	
    	/**
    	 * 使用 0-127 作為類型的代號(hào)
    	 * */
    	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. 這個(gè)將 byte 轉(zhuǎn)為 long 的方法,相信大家也能猜出來了。DataInputStream 有一個(gè)方法叫 readLong(),所以我直接拿來使用了。(我覺得這兩段代碼寫的非常好,不過我就看了幾個(gè)類的源碼,哈哈?。?/p>

    2. 這里我使用一個(gè)死循環(huán)進(jìn)行文件的讀取,但是我在測(cè)試的時(shí)候,發(fā)現(xiàn)了一個(gè)問題很難解決:什么時(shí)候結(jié)束循環(huán)。 我一開始使用 client 關(guān)閉作為退出條件,但是發(fā)現(xiàn)無法起作用。后來發(fā)現(xiàn),對(duì)于網(wǎng)絡(luò)流來說,如果讀取到 -1 說明對(duì)面的輸入流已經(jīng)關(guān)閉了,因此使用這個(gè)作為退出循環(huán)的標(biāo)志。如果刪去了這句代碼,程序會(huì)無法自動(dòng)終止,并且會(huì)一直產(chǎn)生最后一個(gè)讀取的文件,但是由于無法讀取到數(shù)據(jù),所以文件都是 0 字節(jié)的文件。 (這個(gè)東西產(chǎn)生文件的速度很快,大概幾秒鐘就會(huì)產(chǎn)生幾千個(gè)文件,如果感興趣,可以嘗試一下,但是最好快速終止程序的運(yùn)行,哈哈!

    if (exit == -1) {
    	System.out.println("文件上傳結(jié)束!");
    	break;   
    }

    測(cè)試類

    這里只測(cè)試一個(gè)連接就行了,這只是一個(gè)說明的例子。

    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");
    		}	
    	}
    }

    測(cè)試結(jié)果

    Client

    Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決

    Server

    Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決

    源文件目錄 這里面包含了我測(cè)試的五種文件。注意對(duì)比文件的大小信息,對(duì)于IO的測(cè)試,我喜歡使用圖片和視頻測(cè)試,因?yàn)樗鼈兪呛芴厥獾奈募?,如果錯(cuò)了一點(diǎn)(字節(jié)少了、多了),文件基本上就損壞了,表現(xiàn)為圖片不正常顯示,視頻無法正常播放。

    Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決

    目的文件目錄

    Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決

    關(guān)于“Java單個(gè)TCP連接發(fā)送多個(gè)文件的問題怎么解決”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI