最近在看一些相關方面的東西,簡單的使用一下 Socket 進行編程是沒有的問題的,但是這樣只是建立了一些基本概念。對于真正的問題,還是無能爲力。當我需要進行文件的傳輸時,我發現我好像只是發送過去了數據(二進制數據),但是關于文件的一些信息卻丟失了(文件的擴展名)。而且每次我只能使用一個 Socket 發送一個文件,沒有辦法做到連續發送文件(因爲我是依靠關閉流來完成發送文件的,也就是說我其實是不知道文件的長度,所以只能以一個 Socket 連接代表一個文件)。 這些問題困擾了我好久,我去網上簡單的查找了一下,沒有發現什麽現成的例子(可能沒有找到吧),有人提了一下,可以自己定義協議進行發送。 這個倒是激發了我的興趣,感覺像是明白了什麽,因爲我剛學過計算機網絡這門課,老實說我學得不怎麽樣,但是計算機網絡的概念我是學習到了。計算機網絡這門課上,提到了很多協議,不知不覺中我也有了協議的概念。所以我找到了解決的辦法:自己在 TCP 層上定義一個簡單的協議。 通過定義協議,這樣問題就迎刃而解了。
協議的作用
從主機1到主機2發送數據,從應用層的角度看,它們只能看到應用程序數據,但是我們通過圖是可以看出來的,數據從主機1開始,每向下一層數據會加上一個首部,然後在網絡上進行傳播,當到達主機2後,每向上一層會去掉一個首部,達到應用層時,就只有數據了。(這裏只是簡單的說明一下,實際上這樣還是不夠嚴謹,但是對于簡單的理解是夠了。)
所以,我可以自己定義一個簡單的協議,將一些必要的信息放在協議頭部,然後讓計算機程序自己解析協議頭部信息,而且每一個協議報文就相當于一個文件。這樣多個協議就是多個文件了。而且協議之間是可以區分的,不然的話,連續傳輸多個文件,如果無法區分屬于每個文件的字節流,那麽傳輸是毫無意義的。
定義數據的發送格式(協議)
這裏的發送格式(我感覺和計算機網絡中的協議有點像,也就稱它爲一個簡單的協議吧)。
發送格式:數據頭+數據體
數據頭:一個長度爲一字節的數據,表示的內容是文件的類型。
注:因爲每個文件的類型是不一樣的,而且長度也不相同,我們知道協議的頭部一般是具有一個固定長度的(對于可變長的那些我們不考慮),所以我采用一個映射關系,即一個字節數字表示一個文件的類型。
舉一個例子,如下:
注:這裏我做的是一個模擬,所以我只要測試幾種就行了。
數據體: 文件的數據部分(二進制數據)。
Talk is cheap, show me your code.
我們來看代碼吧!
客戶端
協議頭部類
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的測試,我喜歡使用圖片和視頻測試,因爲它們是很特殊的文件,如果錯了一點(字節少了、多了),文件基本上就損壞了,表現爲圖片不正常顯示,視頻無法正常播放。
目的文件目錄
總結
這個問題應該是解決了,我這裏經過測試,應該是沒有問題的了。我的代碼寫的不是太好,有時候都沒有怎麽思考,想到哪就寫到哪,這樣看來還是有很大問題。這個例子的代碼很簡單,不過我發現了一個很有趣的問題,因爲我最近看到了一個手寫 Http 服務器的(使用Java簡單的寫一個。),自己也嘗試了一下(還沒看完)。
我們知道 HTTP 協議,也是具有響應頭和響應體,我覺得我這個和 HTTP 協議有點相似,雖然我的想法很簡陋,但是好像確實是有點相似,可能我看到的東西,對我也有了影響。
從基本思考結束,開始動手寫代碼,然後是調試、測試,接著就是寫博客了,大概也用了幾個小時。我挺喜歡這樣的,一次性完成代碼、博客,當然了這也與這個問題本身對于我來說是很合適有關。經常接觸一些知識看來是很有用的,如果我沒有學過計算機網絡的話,我是不會有這種概念的,那麽對于這個問題我就束手無策了,哈哈!
原文鏈接:https://blog.csdn.net/qq_40734247/article/details/104112142