使用一個TCP連線發送多個檔案
為什麼會有這篇部落格? 最近在看一些相關方面的東西,簡單的使用一下 Socket 進行程式設計是沒有的問題的,但是這樣只是建立了一些基本概念。對於真正的問題,還是無能為力。
當我需要進行檔案的傳輸時,我發現我好像只是發送過去了資料(二進位資料),但是關於檔案的一些資訊卻遺失了(檔案的副檔名)。而且每次我只能使用一個Socket 發送一個文件,沒有辦法做到連續發送文件(因為我是依靠關閉流來完成發送文件的,也就是說我其實是不知道文件的長度,所以只能以一個Socket 連線代表一個檔案)。
這些問題困擾了我好久,我去網上簡單的查找了一下,沒有發現什麼現成的例子(可能沒有找到吧),有人提了一下,可以自己定義協議進行發送。這個倒是激發了我的興趣,感覺像是明白了什麼,因為我剛學過計算機網絡這門課,老實說我學得不怎麼樣,但是計算機網絡的概念我是學習到了。
電腦網路這門課上,提到了許多協議,不知不覺中我也有了協議的概念。所以我找到了解決的方法:自己在 TCP 層上定義一個簡單的協定。 透過定義協議,這樣問題就迎刃而解了。
協定的作用
從主機1到主機2發送數據,從應用層的角度看,它們只能看到應用程式數據,但是我們透過圖是可以看出來的,資料從主機1開始,每向下一層資料會加上一個首部,然後在網路上傳播,當到達主機2後,每向上一層會去掉一個首部,達到應用層時,就只有資料了。 (這裡只是簡單的說明一下,實際上這樣還是不夠嚴謹,但是對於簡單的理解是夠了。)
所以,我可以自己定義一個簡單的協議,將一些必要的資訊放在協議頭部,然後讓電腦程式自己解析協議頭部訊息,而且每一個協議報文就相當於一個文件。這樣多個協定就是多個文件了。而且協定之間是可以區分的,不然的話,連續傳輸多個文件,如果無法區分屬於每個文件的位元組流,那麼傳輸是毫無意義的。
定義資料的發送格式(協定)
這裡的發送格式(我感覺和電腦網路中的協定有點像,也就稱它為一個簡單的協定吧) 。
傳送格式:資料頭 資料體
資料頭:長度為一位元組的數據,表示的內容是檔案的型別。注意:因為每個文件的類型是不一樣的,而且長度也不相同,我們知道協議的頭部一般是具有一個固定長度的(對於可變長的那些我們不考慮),所以我採用一個映射關係,即一個位元組數字表示一個檔案的類型。
舉一個例子,如下:
key |
value |
0 |
txt |
1 |
png |
##2 | # jpg |
3 | jpeg |
4 | avi |
註:這裡我做的是一個模擬,所以我只要測試幾種就行了。
資料體: 檔案的資料部分(二進位資料)。
程式碼
客戶端
協定頭類別
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;
}
}
附註:
發送完一個檔案後需要強制刷新一下。因為我是使用的緩衝流,我們知道為了提高發送的效率,並不是一有資料就發送,而是等待緩衝區滿了以後再發送,因為IO 過程是很慢的(相較於CPU),所以如果不刷新的話,當資料量特別小的檔案時,可能會導致伺服器端接收不到資料(這個問題,感興趣的可以去了解一下。),這是一個需要注意的問題。 (我測試的例子有一個文字檔案只有31位元組)。
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));
}
}
註:
-
這個將byte 轉為long 的方法,相信大家也能猜出來了。 DataInputStream 有一個方法叫做 readLong(),所以我直接拿來使用了。 (我覺得這兩段程式碼寫的非常好,不過我就看了幾個類別的源碼,哈哈!)
這裡我使用一個死循環進行檔案的讀取,但是我在測試的時候,發現了一個問題很難解決:什麼時候結束循環。 我一開始使用 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
Server
####### ############原始檔目錄### 這裡麵包含了我測試的五種檔案。注意對比文件的大小信息,對於IO的測試,我喜歡使用圖片和視頻測試,因為它們是很特殊的文件,如果錯了一點(字節少了、多了),文件基本上就損壞了,表現為圖片不正常顯示,影片無法正常播放。 ##################目的檔案目錄################
以上是如何在Java中使用單一TCP連線發送多個檔案?的詳細內容。更多資訊請關注PHP中文網其他相關文章!