1 つの TCP 接続を使用して複数のファイルを送信する
なぜこのブログがあるのですか? 最近、関連する書籍を読んでいるのですが、単に Socket を使ってプログラミングするのは問題ありませんが、これはいくつかの基本的な概念を確立するだけです。本当の問題に対してはまだ何もできません。
ファイルを転送する必要がある場合、データ (バイナリ データ) を送信しただけのように見えますが、ファイルに関する一部の情報 (ファイル拡張子) が失われていることがわかります。また、毎回 1 つのファイルを送信するには 1 つのソケットしか使用できず、連続してファイルを送信する方法はありません (ファイルの送信を完了するにはストリームを閉じることに依存しているため、実際にはファイルの長さがわかりません)したがって、1 つのソケット接続がファイルを表すため、ファイルのみを送信できます。
これらの問題は長い間私を悩ませてきました。インターネットで簡単に検索しましたが、既成の例は見つかりませんでした (私が見つけられなかったのかもしれません)。 自分でプロトコルを定義して送信できると述べました。 コンピュータネットワークという科目を受講したばかりだったので、とても興味が湧き、なんとなくわかったような気がしました。正直、あまり勉強はできませんでしたが、コンピュータネットワークの概念については学びました。 . .
コンピュータネットワークの授業ではプロトコルがたくさん出てきますが、私も知らず知らずのうちにプロトコルという概念を持っていました。そこで私は解決策を見つけました。
自分で TCP 層に単純なプロトコルを定義します。 プロトコルを定義することで、問題は解決されます。
プロトコルの役割
ホスト 1 からホスト 2 にデータを送信します。アプリケーション層の観点からは、アプリケーション データしか見ることができませんが、図を通してそれを見ることができます。データはホスト1から始まり、下位層ごとにヘッダが付加されてネットワーク上に広がり、ホスト2に到達すると上位層ごとにヘッダが削除され、アプリケーション層に到達すると、データしかありません。 (これは単なる簡単な説明です。実際、これは十分厳密ではありませんが、簡単に理解するには十分です。)
したがって、次のように定義できます。単純なプロトコルでは、プロトコル ヘッダーに必要な情報がいくつか配置され、コンピューター プログラムが独自にプロトコル ヘッダー情報を解析します。各プロトコル メッセージはファイルに相当します。このように、複数のプロトコルは複数のファイルになります。また、プロトコルの区別もできますが、複数のファイルを連続して送信する場合、各ファイルに属するバイトストリームが区別できなければ送信の意味がありません。
データ送信形式 (プロトコル) を定義します
ここでの送信形式 (コンピュータ ネットワークのプロトコルに少し似ていると思います。単純なプロトコルと呼びます) 。
送信形式:データヘッダ データ本体
データヘッダ:ファイルの種類を示す1バイト長のデータ。注: 各ファイルのタイプが異なり、長さも異なるため、プロトコルのヘッダーは一般に固定長であることがわかっています (可変長のものは考慮しません)。そのため、マッピング関係を使用します。つまり、バイト番号はファイルのタイプを表します。
次のような例を示します。
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 というクラスがあり、そのメソッドの 1 つである writeLong() の基礎となる実装は、long を byte に変換することなので、それを直接借用しました。 (実際には、これはそれほど複雑ではありません。必要なのはビット演算だけですが、このコードを記述することは非常に強力なので、このコードを直接使用することにしました。ビット演算に興味がある場合は、私のブログの 1 つを参照してください: Bit操作操作)。
テスト クラス
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));
}
}
注:
-
バイトをロングに変換するこの方法は誰もが推測できると思います。 DataInputStream には readLong() というメソッドがあるので、これを直接使用しました。 (これら 2 つのコードは非常によく書かれていると思いますが、私はいくつかのクラスのソース コードを見ただけです、笑!)
ここでは、無限ループを使用して、しかし、テスト中に、解決が難しい問題を発見しました: いつループを終了するか。 最初は終了条件としてクライアントのシャットダウンを使用していましたが、それが機能しないことがわかりました。その後、ネットワーク ストリームの場合、-1 が読み取られると、反対側の入力ストリームが閉じられたことを意味することが判明したため、これはループを終了するための標識として使用されます。このコードを削除してもプログラムは自動終了せず、常に最後に読み込んだファイルが生成されますが、データを読み込むことができないため、ファイルはすべて0バイトのファイルになります。 (これは非常に高速にファイルを生成します。約数秒で数千のファイルが生成されます。興味がある場合は試してみてください。しかし、プログラムをすぐに終了するのが最善です、笑! )
if (exit == -1) {
System.out.println("文件上传结束!");
break;
}
テスト クラス
ここで 1 つの接続をテストするだけです。これは単なる例です。
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");
}
}
}
テスト結果
クライアント
##サーバー
ソース ファイル ディレクトリ これには、テストした 5 つのファイルが含まれています。ファイルのサイズ情報の比較に注意してください。IO テストでは、非常に特殊なファイルであるため、画像とビデオのテストを使用するのが好きです。わずかなエラー (バイト数の多寡) があると、基本的にファイルは破損します。 、およびパフォーマンス 画像が正しく表示されず、ビデオが正常に再生できません。
#宛先ファイル ディレクトリ
以上がJava で単一の TCP 接続を使用して複数のファイルを送信するにはどうすればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。