0

0

Java Socket 文件传输中因未校验剩余字节数导致内容重复的解决方案

霞舞

霞舞

发布时间:2026-02-12 10:30:06

|

142人浏览过

|

来源于php中文网

原创

Java Socket 文件传输中因未校验剩余字节数导致内容重复的解决方案

本文详解 java 原生 socket 文件传输时内容重复的根本原因——客户端未严格按预发文件长度终止读取循环,导致缓冲区残留数据被重复写入;并提供健壮、无额外依赖的修复方案。

在使用 Java 原生 DataInputStream/DataOutputStream 实现 Socket 文件传输时,一个隐蔽但高频的问题是:接收端文件内容出现重复(如末尾多出前 N 字节。问题并非源于 ObjectOutputStream 缺失,也不是网络粘包(TCP 本身无消息边界),而在于发送与接收逻辑对“文件结束”的判定不一致

核心症结在于:InputStream.read(byte[], int, int) 方法在到达流末尾时返回 -1,但当文件大小不能被缓冲区整除时(例如文件长 2050 字节,缓冲区为 1024 字节),最后一次 read() 可能只读取 2 字节,随后 size 仍 > 0,循环继续——此时 read() 再次调用将阻塞或(在非阻塞模式下)返回 -1,但若未检查该返回值就直接 write(buffer, 0, bytes),就会把上一次读取的旧数据(如前 1024 字节)再次写出,造成重复。

原客户端代码中这一关键缺陷体现在:

while(size > 0 && (bytes = input.read(buffer,0,(int)Math.min(buffer.length, size)))!=-1)
{
    out.write(buffer, 0, bytes); // ✅ 正确:用实际读取字节数
    out.flush();
    size -= bytes;
}

看似合理,但问题在于:当 size > 0 但 input.read() 返回 -1(即流已关闭或异常)时,循环条件虽退出,但若服务端未及时关闭连接,客户端可能因网络延迟等原因,在下次迭代前 read() 仍返回 -1,而 bytes 保持上一轮值(如 1024),导致 out.write(buffer, 0, 1024) 写入陈旧数据。更本质的是:size > 0 仅表示“预期还有数据”,不能替代对 read() 返回值的严格校验

立即学习Java免费学习笔记(深入)”;

boardmix博思白板
boardmix博思白板

boardmix博思白板,一个点燃团队协作和激发创意的空间,集aigc,一键PPT,思维导图,笔记文档多种创意表达能力于一体,将团队工作效率提升到新的层次。

下载

✅ 正确做法是:以 read() 返回值为唯一 EOF 判定依据,size 仅用于限制单次读取上限,不参与循环控制。修复后的客户端 sendFile 方法如下:

private void sendFile(String path) {
    File file = new File(path);
    try (FileInputStream input = new FileInputStream(file);
         DataOutputStream out = this.out) { // 复用已有 out,确保流一致性

        long fileSize = file.length();
        System.out.println("Sending file size: " + fileSize);
        out.writeLong(fileSize); // 先发送文件总长度
        out.flush();

        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = input.read(buffer)) != -1) { // ✅ 核心:仅靠 read() 返回值判断 EOF
            // 优化:避免发送超出剩余长度的数据(可选,提升安全性)
            int toWrite = (int) Math.min(bytesRead, fileSize);
            out.write(buffer, 0, toWrite);
            out.flush();
            fileSize -= toWrite;

            // 防止 fileSize 为负(理论不应发生,但增强鲁棒性)
            if (fileSize < 0) {
                throw new IOException("File size mismatch: attempted to write more than declared length");
            }
        }
        System.out.println("File sent successfully.");

    } catch (IOException e) {
        e.printStackTrace();
    }
}

服务端 receiveFile 同理需修正循环逻辑,移除 size > 0 的联合判断,仅依赖 in.read() 返回值

private void receiveFile(String path) {
    try (FileOutputStream output = new FileOutputStream(path)) {
        long remaining = in.readLong(); // 读取声明的文件长度
        System.out.println("Expecting " + remaining + " bytes");

        byte[] buffer = new byte[1024];
        int bytesRead;
        while (remaining > 0 && (bytesRead = in.read(buffer, 0, 
                (int) Math.min(buffer.length, remaining))) != -1) {

            output.write(buffer, 0, bytesRead);
            remaining -= bytesRead;
        }

        // ✅ 关键补充:校验是否完整接收
        if (remaining != 0) {
            throw new IOException("Incomplete file transfer: expected " + 
                (remaining + (int)(in.readLong() - remaining)) + 
                " bytes, but received only " + (int)(in.readLong() - remaining));
        }
        System.out.println("File received successfully.");

    } catch (IOException e) {
        e.printStackTrace();
    }
}

⚠️ 重要注意事项

  • 资源管理:务必使用 try-with-resources 确保 FileInputStream/FileOutputStream 在异常时自动关闭,避免句柄泄漏。
  • flush() 调用:DataOutputStream 的 write*() 方法不自动 flush,必须显式调用 flush() 保证数据及时发出(尤其小文件)。
  • 缓冲区安全:Math.min(buffer.length, remaining) 防止 read() 尝试读取超过剩余长度,避免潜在越界(尽管 read() 本身安全,但体现严谨性)。
  • 服务端健壮性:增加 remaining == 0 校验,若提前读完却仍有数据,应视为协议错误而非静默忽略。
  • 字符打印调试:示例中 System.out.print((char)b) 仅用于调试,实际生产环境应避免,因二进制文件含非 UTF-8 字节会抛 StringIndexOutOfBoundsException 或显示乱码。

总结:Socket 文件传输重复的本质,是混淆了“业务层长度声明”与“传输层流状态”。永远以 InputStream.read() 的返回值 -1 作为 EOF 的唯一权威信号,file.length() 仅作预分配和校验用途。此原则适用于所有基于流的协议设计,无需引入 ObjectStream,即可实现简洁、可靠、零依赖的文件传输。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

191

2023.09.27

python print用法与作用
python print用法与作用

本专题整合了python print的用法、作用、函数功能相关内容,阅读专题下面的文章了解更多详细教程。

11

2026.02.03

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

688

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

558

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

193

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

206

2025.08.29

length函数用法
length函数用法

length函数用于返回指定字符串的字符数或字节数。可以用于计算字符串的长度,以便在查询和处理字符串数据时进行操作和判断。 需要注意的是length函数计算的是字符串的字符数,而不是字节数。对于多字节字符集,一个字符可能由多个字节组成。因此,length函数在计算字符串长度时会将多字节字符作为一个字符来计算。更多关于length函数的用法,大家可以阅读本专题下面的文章。

939

2023.09.19

点击input框没有光标怎么办
点击input框没有光标怎么办

点击input框没有光标的解决办法:1、确认输入框焦点;2、清除浏览器缓存;3、更新浏览器;4、使用JavaScript;5、检查硬件设备;6、检查输入框属性;7、调试JavaScript代码;8、检查页面其他元素;9、考虑浏览器兼容性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

192

2023.11.24

2026春节习俗大全
2026春节习俗大全

本专题整合了2026春节习俗大全,阅读专题下面的文章了解更多详细内容。

189

2026.02.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3.5万人学习

C# 教程
C# 教程

共94课时 | 9.2万人学习

Java 教程
Java 教程

共578课时 | 63.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号