關(guān)于Java單個(gè)TCP(Socket)連接發(fā)送多個(gè)文件的問(wèn)題
使用一個(gè)TCP連接發(fā)送多個(gè)文件
為什么會(huì)有這篇博客? 最近在看一些相關(guān)方面的東西,簡(jiǎn)單的使用一下 Socket 進(jìn)行編程是沒(méi)有的問(wèn)題的,但是這樣只是建立了一些基本概念。對(duì)于真正的問(wèn)題,還是無(wú)能為力。
當(dāng)我需要進(jìn)行文件的傳輸時(shí),我發(fā)現(xiàn)我好像只是發(fā)送過(guò)去了數(shù)據(jù)(二進(jìn)制數(shù)據(jù)),但是關(guān)于文件的一些信息卻丟失了(文件的擴(kuò)展名)。而且每次我只能使用一個(gè) Socket 發(fā)送一個(gè)文件,沒(méi)有辦法做到連續(xù)發(fā)送文件(因?yàn)槲沂且揽筷P(guān)閉流來(lái)完成發(fā)送文件的,也就是說(shuō)我其實(shí)是不知道文件的長(zhǎng)度,所以只能以一個(gè) Socket 連接代表一個(gè)文件)。
這些問(wèn)題困擾了我好久,我去網(wǎng)上簡(jiǎn)單的查找了一下,沒(méi)有發(fā)現(xiàn)什么現(xiàn)成的例子(可能沒(méi)有找到吧),有人提了一下,可以自己定義協(xié)議進(jìn)行發(fā)送。 這個(gè)倒是激發(fā)了我的興趣,感覺(jué)像是明白了什么,因?yàn)槲覄倢W(xué)過(guò)計(jì)算機(jī)網(wǎng)絡(luò)這門(mén)課,老實(shí)說(shuō)我學(xué)得不怎么樣,但是計(jì)算機(jī)網(wǎng)絡(luò)的概念我是學(xué)習(xí)到了。
計(jì)算機(jī)網(wǎng)絡(luò)這門(mén)課上,提到了很多協(xié)議,不知不覺(jué)中我也有了協(xié)議的概念。所以我找到了解決的辦法:自己在 TCP 層上定義一個(gè)簡(jiǎn)單的協(xié)議。 通過(guò)定義協(xié)議,這樣問(wèn)題就迎刃而解了。
協(xié)議的作用
從主機(jī)1到主機(jī)2發(fā)送數(shù)據(jù),從應(yīng)用層的角度看,它們只能看到應(yīng)用程序數(shù)據(jù),但是我們通過(guò)圖是可以看出來(lái)的,數(shù)據(jù)從主機(jī)1開(kāi)始,每向下一層數(shù)據(jù)會(huì)加上一個(gè)首部,然后在網(wǎng)絡(luò)上進(jìn)行傳播,當(dāng)?shù)竭_(dá)主機(jī)2后,每向上一層會(huì)去掉一個(gè)首部,達(dá)到應(yīng)用層時(shí),就只有數(shù)據(jù)了。(這里只是簡(jiǎn)單的說(shuō)明一下,實(shí)際上這樣還是不夠嚴(yán)謹(jǐn),但是對(duì)于簡(jiǎn)單的理解是夠了。)

所以,我可以自己定義一個(gè)簡(jiǎn)單的協(xié)議,將一些必要的信息放在協(xié)議頭部,然后讓計(jì)算機(jī)程序自己解析協(xié)議頭部信息,而且每一個(gè)協(xié)議報(bào)文就相當(dāng)于一個(gè)文件。這樣多個(gè)協(xié)議就是多個(gè)文件了。而且協(xié)議之間是可以區(qū)分的,不然的話,連續(xù)傳輸多個(gè)文件,如果無(wú)法區(qū)分屬于每個(gè)文件的字節(jié)流,那么傳輸是毫無(wú)意義的。
定義數(shù)據(jù)的發(fā)送格式(協(xié)議)
這里的發(fā)送格式(我感覺(jué)和計(jì)算機(jī)網(wǎng)絡(luò)中的協(xié)議有點(diǎn)像,也就稱(chēng)它為一個(gè)簡(jiǎn)單的協(xié)議吧)。
發(fā)送格式:數(shù)據(jù)頭+數(shù)據(jù)體
數(shù)據(jù)頭:一個(gè)長(zhǎng)度為一字節(jié)的數(shù)據(jù),表示的內(nèi)容是文件的類(lèi)型。 注:因?yàn)槊總€(gè)文件的類(lèi)型是不一樣的,而且長(zhǎng)度也不相同,我們知道協(xié)議的頭部一般是具有一個(gè)固定長(zhǎng)度的(對(duì)于可變長(zhǎng)的那些我們不考慮),所以我采用一個(gè)映射關(guān)系,即一個(gè)字節(jié)數(shù)字表示一個(gè)文件的類(lèi)型。
舉一個(gè)例子,如下:
| key | value |
| 0 | txt |
| 1 | png |
| 2 | jpg |
| 3 | jpeg |
| 4 | avi |
注:這里我做的是一個(gè)模擬,所以我只要測(cè)試幾種就行了。
數(shù)據(jù)體: 文件的數(shù)據(jù)部分(二進(jìn)制數(shù)據(jù))。
代碼
客戶(hù)端
協(xié)議頭部類(lèi)
package com.dragon;
public class Header {
private byte type; //文件類(lèi)型
private long length; //文件長(zhǎng)度
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ā)送文件類(lèi)
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ī)定頭部包含:文件的類(lèi)型、文件數(shù)據(jù)的總長(zhǎng)度信息。
* */
public class FileTransfer {
private byte[] header = new byte[9]; //協(xié)議的頭部為9字節(jié),第一個(gè)字節(jié)為文件類(lèi)型,后面8個(gè)字節(jié)為文件的字節(jié)長(zhǎng)度。
/**
*@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))){
//將文件寫(xiě)入流中
String filename = file.getName();
System.out.println(filename);
//獲取文件的擴(kuò)展名
String type = filename.substring(filename.lastIndexOf(".")+1);
long len = file.length();
//使用一個(gè)對(duì)象來(lái)保存文件的類(lèi)型和長(zhǎng)度信息,操作方便。
Header h = new Header(this.getType(type), len);
header = this.getHeader(h);
//將文件基本信息作為頭部寫(xiě)入流中
bos.write(header, 0, header.length);
//將文件數(shù)據(jù)作為數(shù)據(jù)部分寫(xiě)入流中
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); //長(zhǎng)度
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 作為類(lèi)型的代號(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;
}
}
注:
- 發(fā)送完一個(gè)文件后需要強(qiáng)制刷新一下。因?yàn)槲沂鞘褂玫木彌_流,我們知道為了提高發(fā)送的效率,并不是一有數(shù)據(jù)就發(fā)送,而是等待緩沖區(qū)滿(mǎn)了以后再發(fā)送,因?yàn)?IO 過(guò)程是很慢的(相較于 CPU),所以如果不刷新的話,當(dāng)數(shù)據(jù)量特別小的文件時(shí),可能會(huì)導(dǎo)致服務(wù)器端接收不到數(shù)據(jù)(這個(gè)問(wèn)題,感興趣的可以去了解一下。),這是一個(gè)需要注意的問(wèn)題。(我測(cè)試的例子有一個(gè)文本文件只有31字節(jié))。
getLong()方法將一個(gè) long 型數(shù)據(jù)轉(zhuǎn)為 byte 型數(shù)據(jù),我們知道 long 占8個(gè)字節(jié),但是這個(gè)方法是我從Java源碼里面抄過(guò)來(lái)的,有一個(gè)類(lèi)叫做 DataOutputStream,它有一個(gè)方法是 writeLong(),它的底層實(shí)現(xiàn)就是將 long 轉(zhuǎn)為 byte,所以我直接借鑒過(guò)來(lái)了。(其實(shí),這個(gè)也不是很復(fù)雜,它只是涉及了位運(yùn)算,但是寫(xiě)出來(lái)這個(gè)代碼就是很厲害了,所以我選擇直接使用這段代碼,如果對(duì)于位運(yùn)算感興趣,可以參考一個(gè)我的博客:位運(yùn)算)。
測(cè)試類(lèi)
package com.dragon;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
//類(lèi)型使用代號(hào):固定長(zhǎng)度
//文件長(zhǎng)度:long->byte 固定長(zhǎng)度
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é)議解析類(lèi)
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;
/**
* 接受客戶(hù)端傳過(guò)來(lái)的文件數(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("無(wú)法創(chuàng)建輸出路徑");
}
}
while (true) {
//先讀取文件的頭部信息
int exit = bis.read(header, 0, header.length);
//當(dāng)最后一個(gè)文件發(fā)送完,客戶(hù)端會(huì)停止,服務(wù)器端讀取完數(shù)據(jù)后,就應(yīng)該關(guān)閉了,
//否則就會(huì)造成死循環(huán),并且會(huì)批量產(chǎn)生最后一個(gè)文件,但是沒(méi)有任何數(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);
//獲取文件的長(zhǎng)度
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ò)原因造成的無(wú)法讀取準(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 作為類(lèi)型的代號(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));
}
}
注:
- 這個(gè)將 byte 轉(zhuǎn)為 long 的方法,相信大家也能猜出來(lái)了。DataInputStream 有一個(gè)方法叫 readLong(),所以我直接拿來(lái)使用了。(我覺(jué)得這兩段代碼寫(xiě)的非常好,不過(guò)我就看了幾個(gè)類(lèi)的源碼,哈哈!)
- 這里我使用一個(gè)死循環(huán)進(jìn)行文件的讀取,但是我在測(cè)試的時(shí)候,發(fā)現(xiàn)了一個(gè)問(wèn)題很難解決:什么時(shí)候結(jié)束循環(huán)。 我一開(kāi)始使用 client 關(guān)閉作為退出條件,但是發(fā)現(xiàn)無(wú)法起作用。后來(lái)發(fā)現(xiàn),對(duì)于網(wǎng)絡(luò)流來(lái)說(shuō),如果讀取到 -1 說(shuō)明對(duì)面的輸入流已經(jīng)關(guān)閉了,因此使用這個(gè)作為退出循環(huán)的標(biāo)志。如果刪去了這句代碼,程序會(huì)無(wú)法自動(dòng)終止,并且會(huì)一直產(chǎn)生最后一個(gè)讀取的文件,但是由于無(wú)法讀取到數(shù)據(jù),所以文件都是 0 字節(jié)的文件。 (這個(gè)東西產(chǎn)生文件的速度很快,大概幾秒鐘就會(huì)產(chǎn)生幾千個(gè)文件,如果感興趣,可以嘗試一下,但是最好快速終止程序的運(yùn)行,哈哈!)
if (exit == -1) {
System.out.println("文件上傳結(jié)束!");
break;
}
測(cè)試類(lèi)
這里只測(cè)試一個(gè)連接就行了,這只是一個(gè)說(shuō)明的例子。
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

Server

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

目的文件目錄

總結(jié)
這個(gè)問(wèn)題應(yīng)該是解決了,我這里經(jīng)過(guò)測(cè)試,應(yīng)該是沒(méi)有問(wèn)題的了。我的代碼寫(xiě)的不是太好,有時(shí)候都沒(méi)有怎么思考,想到哪就寫(xiě)到哪,這樣看來(lái)還是有很大問(wèn)題。這個(gè)例子的代碼很簡(jiǎn)單,不過(guò)我發(fā)現(xiàn)了一個(gè)很有趣的問(wèn)題,因?yàn)槲易罱吹搅艘粋€(gè)手寫(xiě) Http 服務(wù)器的(使用Java簡(jiǎn)單的寫(xiě)一個(gè)。),自己也嘗試了一下(還沒(méi)看完)。 我們知道 HTTP 協(xié)議,也是具有響應(yīng)頭和響應(yīng)體,我覺(jué)得我這個(gè)和 HTTP 協(xié)議有點(diǎn)相似,雖然我的想法很簡(jiǎn)陋,但是好像確實(shí)是有點(diǎn)相似,可能我看到的東西,對(duì)我也有了影響。
到此這篇關(guān)于關(guān)于Java單個(gè)TCP(Socket)連接發(fā)送多個(gè)文件的問(wèn)題的文章就介紹到這了,更多相關(guān)單個(gè)(Socket)TCP發(fā)送多個(gè)文件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot 靜態(tài)資源導(dǎo)入及首頁(yè)設(shè)置問(wèn)題
本節(jié)了解一下 SpringBoot 中 Web 開(kāi)發(fā)的靜態(tài)資源導(dǎo)入和首頁(yè)設(shè)置,對(duì)應(yīng) SpringBoot-03-Web 項(xiàng)目,本節(jié)主要是從源碼的角度,研究了一下靜態(tài)資源導(dǎo)入和首頁(yè)設(shè)置的問(wèn)題2021-09-09
SpringBoot實(shí)現(xiàn)本地上傳文件到resources目錄
Java后端項(xiàng)目上傳文件是一個(gè)很常見(jiàn)的需求,這篇文章主要為大家介紹了SpringBoot如何實(shí)現(xiàn)本地上傳文件到resources目錄永久保存下載,需要的可以參考一下2023-07-07
MyBatis-Plus 分頁(yè)查詢(xún)以及自定義sql分頁(yè)的實(shí)現(xiàn)
這篇文章主要介紹了MyBatis-Plus 分頁(yè)查詢(xún)以及自定義sql分頁(yè)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
Java 使用Axis調(diào)用WebService的示例代碼
這篇文章主要介紹了Java 使用Axis調(diào)用WebService的示例代碼,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-09-09
Java使用itextpdf實(shí)現(xiàn)生成PDF并添加圖片,水印和文字
這篇文章主要為大家詳細(xì)介紹了Java在使用itextpdf實(shí)現(xiàn)生成PDF時(shí)如何實(shí)現(xiàn)添加圖片,水印和文字等效果,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02
IDEA修改java文件后 不用重啟Tomcat服務(wù)便可實(shí)現(xiàn)自動(dòng)更新
這篇文章主要介紹了IDEA修改java文件后 不用重啟Tomcat服務(wù)便可實(shí)現(xiàn)自動(dòng)更新,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
BlockingQueue隊(duì)列處理高并發(fā)下的日志
這篇文章主要介紹了BlockingQueue隊(duì)列處理高并發(fā)下的日志示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03

