關(guān)于Java單個TCP(Socket)連接發(fā)送多個文件的問題
使用一個TCP連接發(fā)送多個文件
為什么會有這篇博客? 最近在看一些相關(guān)方面的東西,簡單的使用一下 Socket 進(jìn)行編程是沒有的問題的,但是這樣只是建立了一些基本概念。對于真正的問題,還是無能為力。
當(dāng)我需要進(jìn)行文件的傳輸時,我發(fā)現(xiàn)我好像只是發(fā)送過去了數(shù)據(jù)(二進(jìn)制數(shù)據(jù)),但是關(guān)于文件的一些信息卻丟失了(文件的擴展名)。而且每次我只能使用一個 Socket 發(fā)送一個文件,沒有辦法做到連續(xù)發(fā)送文件(因為我是依靠關(guān)閉流來完成發(fā)送文件的,也就是說我其實是不知道文件的長度,所以只能以一個 Socket 連接代表一個文件)。
這些問題困擾了我好久,我去網(wǎng)上簡單的查找了一下,沒有發(fā)現(xiàn)什么現(xiàn)成的例子(可能沒有找到吧),有人提了一下,可以自己定義協(xié)議進(jìn)行發(fā)送。 這個倒是激發(fā)了我的興趣,感覺像是明白了什么,因為我剛學(xué)過計算機網(wǎng)絡(luò)這門課,老實說我學(xué)得不怎么樣,但是計算機網(wǎng)絡(luò)的概念我是學(xué)習(xí)到了。
計算機網(wǎng)絡(luò)這門課上,提到了很多協(xié)議,不知不覺中我也有了協(xié)議的概念。所以我找到了解決的辦法:自己在 TCP 層上定義一個簡單的協(xié)議。 通過定義協(xié)議,這樣問題就迎刃而解了。
協(xié)議的作用
從主機1到主機2發(fā)送數(shù)據(jù),從應(yīng)用層的角度看,它們只能看到應(yīng)用程序數(shù)據(jù),但是我們通過圖是可以看出來的,數(shù)據(jù)從主機1開始,每向下一層數(shù)據(jù)會加上一個首部,然后在網(wǎng)絡(luò)上進(jìn)行傳播,當(dāng)?shù)竭_(dá)主機2后,每向上一層會去掉一個首部,達(dá)到應(yīng)用層時,就只有數(shù)據(jù)了。(這里只是簡單的說明一下,實際上這樣還是不夠嚴(yán)謹(jǐn),但是對于簡單的理解是夠了。)
所以,我可以自己定義一個簡單的協(xié)議,將一些必要的信息放在協(xié)議頭部,然后讓計算機程序自己解析協(xié)議頭部信息,而且每一個協(xié)議報文就相當(dāng)于一個文件。這樣多個協(xié)議就是多個文件了。而且協(xié)議之間是可以區(qū)分的,不然的話,連續(xù)傳輸多個文件,如果無法區(qū)分屬于每個文件的字節(jié)流,那么傳輸是毫無意義的。
定義數(shù)據(jù)的發(fā)送格式(協(xié)議)
這里的發(fā)送格式(我感覺和計算機網(wǎng)絡(luò)中的協(xié)議有點像,也就稱它為一個簡單的協(xié)議吧)。
發(fā)送格式:數(shù)據(jù)頭+數(shù)據(jù)體
數(shù)據(jù)頭:一個長度為一字節(jié)的數(shù)據(jù),表示的內(nèi)容是文件的類型。 注:因為每個文件的類型是不一樣的,而且長度也不相同,我們知道協(xié)議的頭部一般是具有一個固定長度的(對于可變長的那些我們不考慮),所以我采用一個映射關(guān)系,即一個字節(jié)數(shù)字表示一個文件的類型。
舉一個例子,如下:
key | value |
0 | txt |
1 | png |
2 | jpg |
3 | jpeg |
4 | avi |
注:這里我做的是一個模擬,所以我只要測試幾種就行了。
數(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é)議包含一個頭部和一個數(shù)據(jù)部分。 * 頭部為 9 字節(jié),其余為數(shù)據(jù)部分。 * 規(guī)定頭部包含:文件的類型、文件數(shù)據(jù)的總長度信息。 * */ public class FileTransfer { private byte[] header = new byte[9]; //協(xié)議的頭部為9字節(jié),第一個字節(jié)為文件類型,后面8個字節(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); //獲取文件的擴展名 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); //將文件數(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(); //強制刷新,否則會出錯! } } } 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; } }
注:
- 發(fā)送完一個文件后需要強制刷新一下。因為我是使用的緩沖流,我們知道為了提高發(fā)送的效率,并不是一有數(shù)據(jù)就發(fā)送,而是等待緩沖區(qū)滿了以后再發(fā)送,因為 IO 過程是很慢的(相較于 CPU),所以如果不刷新的話,當(dāng)數(shù)據(jù)量特別小的文件時,可能會導(dǎo)致服務(wù)器端接收不到數(shù)據(jù)(這個問題,感興趣的可以去了解一下。),這是一個需要注意的問題。(我測試的例子有一個文本文件只有31字節(jié))。
getLong()
方法將一個 long 型數(shù)據(jù)轉(zhuǎn)為 byte 型數(shù)據(jù),我們知道 long 占8個字節(jié),但是這個方法是我從Java源碼里面抄過來的,有一個類叫做 DataOutputStream,它有一個方法是 writeLong(),它的底層實現(xiàn)就是將 long 轉(zhuǎn)為 byte,所以我直接借鑒過來了。(其實,這個也不是很復(fù)雜,它只是涉及了位運算,但是寫出來這個代碼就是很厲害了,所以我選擇直接使用這段代碼,如果對于位運算感興趣,可以參考一個我的博客:位運算)。
測試類
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"); } } }
服務(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)最后一個文件發(fā)送完,客戶端會停止,服務(wù)器端讀取完數(shù)據(jù)后,就應(yīng)該關(guān)閉了, //否則就會造成死循環(huán),并且會批量產(chǎn)生最后一個文件,但是沒有任何數(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時,直接讀取此部分,然后結(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 作為類型的代號 * */ 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)); } }
注:
- 這個將 byte 轉(zhuǎn)為 long 的方法,相信大家也能猜出來了。DataInputStream 有一個方法叫 readLong(),所以我直接拿來使用了。(我覺得這兩段代碼寫的非常好,不過我就看了幾個類的源碼,哈哈!)
- 這里我使用一個死循環(huán)進(jìn)行文件的讀取,但是我在測試的時候,發(fā)現(xiàn)了一個問題很難解決:什么時候結(jié)束循環(huán)。 我一開始使用 client 關(guān)閉作為退出條件,但是發(fā)現(xiàn)無法起作用。后來發(fā)現(xiàn),對于網(wǎng)絡(luò)流來說,如果讀取到 -1 說明對面的輸入流已經(jīng)關(guān)閉了,因此使用這個作為退出循環(huán)的標(biāo)志。如果刪去了這句代碼,程序會無法自動終止,并且會一直產(chǎn)生最后一個讀取的文件,但是由于無法讀取到數(shù)據(jù),所以文件都是 0 字節(jié)的文件。 (這個東西產(chǎn)生文件的速度很快,大概幾秒鐘就會產(chǎn)生幾千個文件,如果感興趣,可以嘗試一下,但是最好快速終止程序的運行,哈哈!)
if (exit == -1) { System.out.println("文件上傳結(jié)束!"); 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"); } } }
測試結(jié)果
Client
Server
源文件目錄 這里面包含了我測試的五種文件。注意對比文件的大小信息,對于IO的測試,我喜歡使用圖片和視頻測試,因為它們是很特殊的文件,如果錯了一點(字節(jié)少了、多了),文件基本上就損壞了,表現(xiàn)為圖片不正常顯示,視頻無法正常播放。
目的文件目錄
總結(jié)
這個問題應(yīng)該是解決了,我這里經(jīng)過測試,應(yīng)該是沒有問題的了。我的代碼寫的不是太好,有時候都沒有怎么思考,想到哪就寫到哪,這樣看來還是有很大問題。這個例子的代碼很簡單,不過我發(fā)現(xiàn)了一個很有趣的問題,因為我最近看到了一個手寫 Http 服務(wù)器的(使用Java簡單的寫一個。),自己也嘗試了一下(還沒看完)。 我們知道 HTTP 協(xié)議,也是具有響應(yīng)頭和響應(yīng)體,我覺得我這個和 HTTP 協(xié)議有點相似,雖然我的想法很簡陋,但是好像確實是有點相似,可能我看到的東西,對我也有了影響。
到此這篇關(guān)于關(guān)于Java單個TCP(Socket)連接發(fā)送多個文件的問題的文章就介紹到這了,更多相關(guān)單個(Socket)TCP發(fā)送多個文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis-plus指定字段模糊查詢的實現(xiàn)方法
最近項目中使用springboot+mybatis-plus來實現(xiàn),所以下面這篇文章主要給大家介紹了關(guān)于mybatis-plus實現(xiàn)指定字段模糊查詢的相關(guān)資料,需要的朋友可以參考下2022-04-04Java Predicate及Consumer接口函數(shù)代碼實現(xiàn)解析
這篇文章主要介紹了Java Predicate及Consumer接口函數(shù)代碼實現(xiàn)解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06java使用hadoop實現(xiàn)關(guān)聯(lián)商品統(tǒng)計
本篇文章java使用hadoop實現(xiàn)關(guān)聯(lián)商品統(tǒng)計,可以實現(xiàn)商品的關(guān)聯(lián)統(tǒng)計,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2016-10-10java并發(fā)編程專題(一)----線程基礎(chǔ)知識
這篇文章主要介紹了java并發(fā)編程線程的基礎(chǔ)知識,文中講解非常詳細(xì),幫助大家更好的學(xué)習(xí)JAVA并發(fā)編程,感興趣想學(xué)習(xí)JAVA的可以了解下2020-06-06