Rumah >Java >javaTutorial >Bagaimana untuk menghantar berbilang fail menggunakan sambungan TCP tunggal dalam Java?
Mengapa blog ini? Saya telah membaca beberapa perkara yang berkaitan baru-baru ini. Tiada masalah untuk menggunakan Socket untuk pengaturcaraan, tetapi ini hanya menetapkan beberapa konsep asas. Masih tiada apa yang boleh dilakukan tentang masalah sebenar.
Apabila saya perlu memindahkan fail, saya dapati saya nampaknya baru sahaja menghantar data (data binari), tetapi beberapa maklumat tentang fail itu hilang (sambungan fail). Dan saya hanya boleh menggunakan satu Soket untuk menghantar satu fail setiap kali, dan tidak ada cara untuk menghantar fail secara berterusan (kerana saya bergantung pada menutup strim untuk menyelesaikan penghantaran fail, yang bermaksud bahawa saya sebenarnya tidak tahu panjang fail , jadi saya hanya boleh menggunakan satu sambungan Soket mewakili fail).
Masalah ini telah merisaukan saya untuk masa yang lama. Saya melakukan carian mudah di Internet dan tidak menemui contoh yang sudah siap (mungkin saya tidak menjumpainya dan anda boleh tentukan sendiri protokol Hantar. Ini menarik minat saya, dan saya rasa seperti memahami sesuatu, kerana saya baru sahaja mengikuti kursus Rangkaian Komputer Sejujurnya, saya tidak belajar dengan baik, tetapi saya mempelajari konsep rangkaian komputer. .
Dalam rangkaian komputer, banyak protokol disebut, dan saya juga mempunyai konsep protokol tanpa mengetahuinya. Jadi saya menemui penyelesaian: tentukan sendiri protokol mudah pada lapisan TCP. Dengan mentakrifkan protokol, masalah itu diselesaikan.
Hantar data dari hos 1 ke hos 2. Dari perspektif lapisan aplikasi, mereka hanya boleh melihat data aplikasi, tetapi kita boleh melihatnya melalui rajah , Data bermula dari hos 1, dan pengepala ditambahkan pada data untuk setiap lapisan bawah, dan kemudian merebak pada rangkaian Apabila ia mencapai hos 2, pengepala akan dialih keluar untuk setiap lapisan atas. hanya ada data. (Ini hanya penjelasan ringkas. Sebenarnya, ini tidak cukup ketat, tetapi ia cukup untuk pemahaman yang mudah.)
Jadi, saya boleh takrifkan satu sendiri Protokol ringkas meletakkan beberapa maklumat yang diperlukan dalam pengepala protokol, dan kemudian membenarkan program komputer menghuraikan maklumat pengepala protokol dengan sendirinya, dan setiap mesej protokol adalah bersamaan dengan fail. Dengan cara ini, berbilang protokol adalah berbilang fail. Dan protokol boleh dibezakan Jika tidak, jika berbilang fail dihantar secara berterusan, penghantaran tidak bermakna jika aliran bait kepunyaan setiap fail tidak dapat dibezakan.
Format penghantaran di sini (saya rasa ia agak serupa dengan protokol dalam rangkaian komputer, jadi mari kita panggil ia protokol mudah) .
Format penghantaran: pengepala data + badan data
Pengepala data: data dengan panjang satu bait, yang menunjukkan jenis fail. Nota: Oleh kerana jenis setiap fail adalah berbeza, dan panjangnya juga berbeza, kita tahu bahawa pengepala protokol secara amnya mempunyai panjang tetap (kami tidak menganggap mereka yang mempunyai panjang berubah), jadi saya menggunakan hubungan pemetaan , itu ialah, nombor bait mewakili jenis fail.
Berikan contoh seperti berikut:
key | value |
0 | txt |
1 | png |
2 | jpg |
3 | jpeg |
4 | avi |
Nota: Apa yang saya lakukan di sini adalah simulasi, jadi saya hanya perlu menguji beberapa jenis.
Isi data: Bahagian data fail (data binari).
Kelas pengepala protokol
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; } }
Hantar kelas fail
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; } }
Nota:
Selepas menghantar fail, anda perlu memaksa muat semula. Oleh kerana saya menggunakan aliran penampan, kami tahu bahawa untuk meningkatkan kecekapan penghantaran, kami tidak menghantar data sebaik sahaja ada data, tetapi tunggu sehingga penampan penuh sebelum menghantar, kerana proses IO sangat perlahan ( berbanding dengan CPU), jadi Jika anda tidak menyegarkan semula, apabila volum data sangat kecil, pelayan mungkin tidak menerima data (, jika anda berminat, anda boleh belajar mengenainya. ), ini adalah masalah yang memerlukan perhatian. (Contoh yang saya uji mempunyai fail teks yang hanya 31 bait). Kaedah
getLong()
menukar data jenis panjang kepada data jenis bait Kami tahu bahawa panjang menduduki 8 bait, tetapi kaedah ini disalin daripada kod sumber Java dipanggil DataOutputStream Ia mempunyai kaedah yang dipanggil writeLong(). (Sebenarnya, ini tidak terlalu rumit. Ia hanya melibatkan operasi bit, tetapi menulis kod ini sangat berkuasa, jadi saya memilih untuk menggunakan kod ini secara langsung. Jika anda berminat dengan operasi bit, anda boleh rujuk blog saya: Operasi Operasi Bit ).
Kelas ujian
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"); } } }
Kelas analisis protokol
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)); } }
Nota:
Saya percaya semua orang boleh meneka kaedah menukar bait ini kepada panjang. DataInputStream mempunyai kaedah yang dipanggil readLong(), jadi saya menggunakannya secara langsung. (Saya rasa dua keping kod ini ditulis dengan sangat baik, tetapi saya hanya melihat kod sumber beberapa kelas, haha!)
Di sini saya menggunakan gelung tak terhingga untuk membaca Tetapi semasa saya menguji, saya mendapati masalah yang sukar diselesaikan: Bila hendak menamatkan gelung. Saya pada mulanya menggunakan penutupan pelanggan sebagai syarat keluar, tetapi mendapati ia tidak berfungsi. Kemudian didapati bahawa untuk aliran rangkaian, jika -1 dibaca, ia bermakna aliran input bertentangan telah ditutup, jadi ini digunakan sebagai tanda untuk keluar dari gelung. Jika kod ini dipadamkan, atur cara tidak akan ditamatkan secara automatik, dan fail bacaan terakhir akan sentiasa dijana Walau bagaimanapun, kerana data tidak boleh dibaca, semua fail adalah fail 0-bait. (Perkara ini menjana fail dengan sangat cepat. Ia akan menghasilkan beribu-ribu fail dalam masa lebih kurang beberapa saat. Jika anda berminat, anda boleh mencubanya, tetapi yang terbaik adalah menamatkan program dengan cepat, haha! )
if (exit == -1) { System.out.println("文件上传结束!"); break; }
Kelas ujian
Hanya uji satu sambungan di sini. Ini hanyalah contoh ilustrasi.
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"); } } }
Pelanggan
Pelayan
Direktori fail sumber Ini mengandungi lima fail yang saya uji. Beri perhatian untuk membandingkan maklumat saiz fail Untuk ujian IO, saya suka menggunakan ujian gambar dan video, kerana ia adalah fail yang sangat istimewa Jika terdapat sedikit ralat (kurang atau lebih bait), fail itu pada dasarnya akan rosak , dan prestasi Gambar tidak dipaparkan dengan betul dan video tidak boleh dimainkan secara normal.
Direktori fail destinasi
Atas ialah kandungan terperinci Bagaimana untuk menghantar berbilang fail menggunakan sambungan TCP tunggal dalam Java?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!