当前位置:

首页 > 编程开发 > Java单个TCP连接发送多个文件的问题怎么解决

Java单个TCP连接发送多个文件的问题怎么解决

使用一个TCP连接发送多个文件为什么会有这篇博客?最近在看一些相关方面的东西,简单的使用一下Socket进行编程是没有的问题的,但是这样只是建立了一些基本概念。对于真正的问题,还是无能为力。当我需要进行文件的传输时,我发现我好像只是发送过去了数据(二进制数据),但是关于文件的一些信息却丢失了(文件的扩展名)。而且每次我只能使用一个Socket发送一个文件,没有办法做到连续发送文件(因为我是依靠关闭流来完成发送文件的,也就是说我其实是不知道文件的长度,所以只能以一个Socket连接代表一个文件)。这些问题困

    使用一个TCP连接发送多个文件

    为什么会有这篇博客? 最近在看一些相关方面的东西,简单的使用一下 Socket 进行编程是没有的问题的,但是这样只是建立了一些基本概念。对于真正的问题,还是无能为力。

    当我需要进行文件的传输时,我发现我好像只是发送过去了数据(二进制数据),但是关于文件的一些信息却丢失了(文件的扩展名)。而且每次我只能使用一个 Socket 发送一个文件,没有办法做到连续发送文件(因为我是依靠关闭流来完成发送文件的,也就是说我其实是不知道文件的长度,所以只能以一个 Socket 连接代表一个文件)。

    这些问题困扰了我好久,我去网上简单的查找了一下,没有发现什么现成的例子(可能没有找到吧),有人提了一下,可以自己定义协议进行发送。 这个倒是激发了我的兴趣,感觉像是明白了什么,因为我刚学过计算机网络这门课,老实说我学得不怎么样,但是计算机网络的概念我是学习到了。

    计算机网络这门课上,提到了很多协议,不知不觉中我也有了协议的概念。所以我找到了解决的办法:自己在 TCP 层上定义一个简单的协议。 通过定义协议,这样问题就迎刃而解了。

    协议的作用

    从主机1到主机2发送数据,从应用层的角度看,它们只能看到应用程序数据,但是我们通过图是可以看出来的,数据从主机1开始,每向下一层数据会加上一个首部,然后在网络上进行传播,当到达主机2后,每向上一层会去掉一个首部,达到应用层时,就只有数据了。(这里只是简单的说明一下,实际上这样还是不够严谨,但是对于简单的理解是够了。)

    Java单个TCP连接发送多个文件的问题怎么解决

    所以,我可以自己定义一个简单的协议,将一些必要的信息放在协议头部,然后让计算机程序自己解析协议头部信息,而且每一个协议报文就相当于一个文件。这样多个协议就是多个文件了。而且协议之间是可以区分的,不然的话,连续传输多个文件,如果无法区分属于每个文件的字节流,那么传输是毫无意义的。

    定义数据的发送格式(协议)

    这里的发送格式(我感觉和计算机网络中的协议有点像,也就称它为一个简单的协议吧)。

    发送格式:数据头+数据体

    数据头:一个长度为一字节的数据,表示的内容是文件的类型。 注:因为每个文件的类型是不一样的,而且长度也不相同,我们知道协议的头部一般是具有一个固定长度的(对于可变长的那些我们不考虑),所以我采用一个映射关系,即一个字节数字表示一个文件的类型。

    举一个例子,如下:

    keyvalue
    0txt
    1png
    2jpg
    3jpeg
    4avi

    注:这里我做的是一个模拟,所以我只要测试几种就行了。

    数据体: 文件的数据部分(二进制数据)。

    代码

    客户端

    协议头部类

    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;
    	}
    }

    注:

    1. 发送完一个文件后需要强制刷新一下。因为我是使用的缓冲流,我们知道为了提高发送的效率,并不是一有数据就发送,而是等待缓冲区满了以后再发送,因为 IO 过程是很慢的(相较于 CPU),所以如果不刷新的话,当数据量特别小的文件时,可能会导致服务器端接收不到数据(这个问题,感兴趣的可以去了解一下。),这是一个需要注意的问题。(我测试的例子有一个文本文件只有31字节)。

    2. 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));
    	}
    }

    注:

    1. 这个将 byte 转为 long 的方法,相信大家也能猜出来了。DataInputStream 有一个方法叫 readLong(),所以我直接拿来使用了。(我觉得这两段代码写的非常好,不过我就看了几个类的源码,哈哈!)

    2. 这里我使用一个死循环进行文件的读取,但是我在测试的时候,发现了一个问题很难解决:什么时候结束循环。 我一开始使用 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

    Java单个TCP连接发送多个文件的问题怎么解决

    Server

    Java单个TCP连接发送多个文件的问题怎么解决

    源文件目录 这里面包含了我测试的五种文件。注意对比文件的大小信息,对于IO的测试,我喜欢使用图片和视频测试,因为它们是很特殊的文件,如果错了一点(字节少了、多了),文件基本上就损坏了,表现为图片不正常显示,视频无法正常播放。

    Java单个TCP连接发送多个文件的问题怎么解决

    目的文件目录

    Java单个TCP连接发送多个文件的问题怎么解决

    本文内容来源于互联网,如有侵权请联系删除。
    作者最新文章
    编程开发
    相关文章 更多
    谷歌浏览器Mac版入口
    谷歌浏览器Mac版入口

    谷歌浏览器Mac版官方安装指南 谷歌浏览器Mac版官方安装入口是https://www.google.com/chrome/,需macOS 12+系统、500MB空间,下载.dmg后拖入应用程序安装,支持多设备同步、性能优化与隐私保护功能。 苹果电脑Chrome的安装入口究竟在哪里?这个问题最近可是

    Chrome浏览器JS脚本不运行怎么办
    Chrome浏览器JS脚本不运行怎么办

    Chrome中JavaScript未执行需依次检查:一、移除站点级禁用并添加允许域名;二、开启全局JavaScript开关;三、禁用干扰扩展;四、在开发者工具中启用JavaScript;五、重置内容设置为默认。 有时在Chrome里打开网页,会发现交互按钮点了没反应,数据加载不出来,页面仿佛“静止”

    IE浏览器怀旧版在线网址
    IE浏览器怀旧版在线网址

    IE浏览器怀旧版在线网址:一次精准的技术时光回溯 最近,不少老用户和怀旧爱好者在反复搜索一个问题:那个经典的Internet Explorer,如今还能在哪里原汁原味地体验到?答案指向一个特定的地址:https://ie.microsoft.com/legacy/。 这个网站远不止是一个简单的“皮肤

    火狐浏览器有哪些设置功能
    火狐浏览器有哪些设置功能

    火狐浏览器五大核心设置功能:解锁高效、安全与个性化体验 火狐浏览器功能强大,但如果不仔细挖掘,很多能大幅提升效率和安全性的设置可能就“藏着掖着”了。这就好比拥有一台高性能设备,却只用了基础模式。那么,如何把它调整到最顺手、最安全的状态?接下来,我们就聚焦于当前版本(截至2025年末)最关键的五大设置

    chrome搜索免验证入口
    chrome搜索免验证入口

    Chrome官方免验证入口为https://www.google.cn/chrome/,提供全平台安装包、免登录即用、本地化安全机制及引擎级性能优化。 到底该去哪里找正版、免费且无需繁琐验证的Chrome浏览器入口?这个问题困扰了不少网友。今天,我们就来直通核心,为大家详细拆解Chrome引擎的官方

    java heap space 选型思路:使用场景与区别整理
    java heap space 选型思路:使用场景与区别整理

    Java堆是JVM存储对象的核心内存区域,配置需结合场景:单体应用适中设置;大数据处理需大堆并关注GC停顿;微服务强调快速启动;高并发需精细划分堆区域。关键参数-Xms和-Xmx建议等值以稳定性能。垃圾回收器选择影响效率,如G1适用于大堆,ZGC可实现低停顿。内存错误时需监控堆状态。

    java heap space 使用中遇到的问题怎么解决
    java heap space 使用中遇到的问题怎么解决

    Java堆内存溢出错误通常因内存泄漏、数据处理需求过大或JVM参数配置不当引起。排查时可借助jmap、堆转储及MAT等工具定位问题。解决方案包括调整JVM内存参数(如-Xmx)、修复代码中的内存泄漏、优化大数据处理逻辑,并建立持续监控与预防机制,以保障应用稳定运行。

    java xml 选型思路:使用场景与区别整理
    java xml 选型思路:使用场景与区别整理

    XML在Java开发中用于配置、数据交换等场景。解析方式主要有DOM、SAX、StAX及第三方库。DOM适合操作小文件,SAX/StAX适合处理大文件流,JAXB用于对象与XML映射。选型需结合数据大小、内存、性能及团队熟悉度,现代框架常封装底层解析。

    using namespace 使用中遇到的问题怎么解决
    using namespace 使用中遇到的问题怎么解决

    命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

    c语言函数递归 实操经验总结:这些技巧很实用
    c语言函数递归 实操经验总结:这些技巧很实用

    理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件
    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    Mac软件 更多
    灵活计算器
    灵活计算器
    macOS/iOS/Android

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    赤友清理大师
    赤友清理大师
    macOS

    赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    WINDOWS 更多
    Windows 10
    Windows 10
    Windows

    Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    密码键盘
    密码键盘
    Windows/macOS/iOS/Android

    密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。