恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java Socket 文件传输实战:从原理到避坑指南
首页
资讯中心
/
Java Socket 文件传输实战:从原理到避坑指南
Java Socket 文件传输实战:从原理到避坑指南
发布时间:2026/10/19 5:43:28
简介这是一份面向Java初学者与网络编程入门者的Socket文件传输示例源码围绕服务端与客户端之间的文件收发场景展开帮助读者理解如何用ServerSocket监听端口、用Socket建立连接并通过字节流完成二进制文件的可靠传输。压缩包共9个文件约9KB包含3个java源文件、3个class编译文件以及project、prefs、classpath等Eclipse工程配置便于直接导入IDE运行调试。源码覆盖服务端accept接收、客户端发送、缓冲区读写、文件大小预传、传输进度跟踪与异常处理等关键环节并留有NIO与线程池优化的延伸思路适合对照学习网络I/O与资源关闭规范。目前已有679人学习下载可作为课程实验或分布式文件传输练手的参考实现。1. 从一次文件传输翻车说起这个 Java Socket 项目到底能干什么有个做后端的朋友接了个内部需求要把一台机器上的日志文件定时搬到另一台机器上。他第一反应是上 MQ、上对象存储结果被运维一句“内网环境别引入外部依赖”怼了回来。最后他用 Java 原生 Socket 写了个服务端和客户端两百多行代码搞定跑了大半年没出过问题。这件事说明一个道理文件传输这种需求不一定非要上重型中间件把 Java socket 编程 的基本功打扎实很多场景反而更轻、更可控。这份资源要讲的就是这么一件事——用 Java 的 Socket API 实现一个服务端和客户端之间的文件传输通道。服务端监听端口客户端连接上来之后把本地文件推过去服务端接收并落盘。它适合谁适合刚学完 Java IO 流、想找一个能跑通的完整例子练手的开发者也适合手头有内网小工具需求、不想引入 Netty 或 FTP 组件的熟手。核心用到的是 ServerSocket、Socket、InputStream、OutputStream 这几样东西没有框架没有额外依赖JDK 自带就能跑。2. 架构拆解一个文件从客户端到服务端经历了什么2.1 整体通信模型这个项目的结构非常朴素就是经典的 C/S 模型。服务端启动后在一个固定端口上阻塞等待客户端发起 TCP 连接连接建立之后双方通过各自的输入输出流交换数据。文件传输的本质是把文件拆成字节数组一段一段从客户端的输出流写出去服务端从输入流读进来再写进本地文件。这里有一个容易被忽略的点TCP 是流式协议它不保证你写一次对方就读一次。客户端 write 了 1024 个字节服务端可能分两次读到也可能一次读到 2048 个字节如果客户端连续写了两次。所以接收端必须循环读取直到读到的字节数符合预期或者流结束。这个认知是后面所有代码的基础。常见的做法是客户端先发送文件名和文件长度再发送文件内容。服务端先读文件名和长度然后按长度循环接收直到收满为止。这样服务端就知道什么时候该停不会一直阻塞在那里等一个永远不会来的字节。2.2 服务端职责与关键类服务端的核心类是ServerSocket它负责绑定端口和接受连接。每接受一个连接就拿到一个Socket对象从这个 Socket 上取InputStream用来读数据取OutputStream用来回响应。一个合格的服务端至少要处理三件事第一正确解析客户端发来的文件名和文件长度第二按长度精确接收文件内容并写入磁盘第三处理异常和连接关闭避免文件句柄或 Socket 泄漏。我一般会把接收逻辑单独抽成一个方法主循环只负责 accept这样结构清晰也方便后面加多线程。// 服务端接收文件的核心逻辑 public void receiveFile(Socket socket, String saveDir) throws IOException { DataInputStream dis new DataInputStream(socket.getInputStream()); // 先读文件名长度和文件名避免中文乱码用 UTF-8 int nameLen dis.readInt(); byte[] nameBytes new byte[nameLen]; dis.readFully(nameBytes); String fileName new String(nameBytes, StandardCharsets.UTF_8); // 再读文件长度 long fileLen dis.readLong(); File target new File(saveDir, fileName); try (FileOutputStream fos new FileOutputStream(target)) { byte[] buf new byte[4096]; long received 0; while (received fileLen) { int toRead (int) Math.min(buf.length, fileLen - received); int n dis.read(buf, 0, toRead); if (n -1) break; // 对端提前关闭 fos.write(buf, 0, n); received n; } } }这段代码里DataInputStream是对InputStream的包装提供了readInt()和readLong()这种按类型读的方法省得自己手动拼字节。readFully保证把指定长度的字节全部读满不会出现读一半的情况。接收循环里用fileLen - received算出剩余需要读的字节数再和缓冲区大小取最小值这样最后一次读取不会多读。received变量是判断是否收完的唯一依据不能靠read返回 -1因为对端正常关闭时也会返回 -1但那时候可能还没收完。2.3 客户端职责与关键类客户端用Socket连接服务端地址和端口拿到OutputStream之后先把文件名长度、文件名、文件长度写过去再循环写文件内容。这里同样要用DataOutputStream来写保证和服务端的读取顺序一致。// 客户端发送文件的核心逻辑 public void sendFile(String host, int port, File file) throws IOException { try (Socket socket new Socket(host, port); DataOutputStream dos new DataOutputStream(socket.getOutputStream()); FileInputStream fis new FileInputStream(file)) { byte[] nameBytes file.getName().getBytes(StandardCharsets.UTF_8); dos.writeInt(nameBytes.length); // 文件名长度 dos.write(nameBytes); // 文件名 dos.writeLong(file.length()); // 文件长度 dos.flush(); byte[] buf new byte[4096]; int n; while ((n fis.read(buf)) ! -1) { dos.write(buf, 0, n); } dos.flush(); } }客户端的顺序必须和服务端严格对应先writeInt写文件名长度再write写文件名再writeLong写文件长度最后写内容。任何顺序错位都会导致服务端解析出错误的文件名或长度进而卡死或者写出损坏的文件。flush()在关闭流之前调用一次确保缓冲区里的数据全部发出去。用 try-with-resources 管理 Socket 和流方法结束时自动关闭不用手动写 finally。2.4 目录结构建议这个项目不需要复杂的工程结构但建议至少分成三个文件FileServer.java负责监听和接收FileClient.java负责连接和发送FileTransferTest.java作为入口分别启动两端。如果要做成可配置的可以加一个config.properties放端口号和保存目录。常见做法是把服务端和客户端放在同一个包下方便调试时直接在一个 IDE 里跑两个 main 方法。3. 跑起来从零到一次完整传输的操作步骤3.1 环境准备与编译这个项目对 JDK 版本没有特殊要求JDK 8 及以上都能跑。不需要 Maven 或 Gradle直接用javac编译就行。如果你习惯用 IDE新建一个普通 Java 项目把三个类放进去即可。# 假设源码在 src 目录下 javac -d out src/com/example/transfer/*.java编译完成后out目录下会有对应的 class 文件。如果代码里用了StandardCharsets确认 JDK 是 7 以上JDK 8 肯定没问题。3.2 启动服务端服务端需要先启动因为它要绑定端口并进入监听状态。启动命令里传入端口号和保存目录两个参数。java -cp out com.example.transfer.FileServer 9000 /data/received服务端启动后会打印一行日志类似“服务端已启动监听端口 9000保存目录 /data/received”。这时候它阻塞在accept()上等待客户端连接。如果端口被占用会抛出BindException换一个端口就行。保存目录如果不存在代码里应该先mkdirs()创建否则写文件时会报FileNotFoundException。3.3 启动客户端并发送文件客户端在另一个终端启动传入服务端地址、端口和要发送的文件路径。java -cp out com.example.transfer.FileClient 127.0.0.1 9000 ./test.log客户端连接成功后开始发送发送完毕关闭连接。服务端收到文件后写入指定目录然后继续等待下一个连接。你可以在服务端目录下用ls -l确认文件大小和源文件一致。如果文件比较大比如几百 MB传输过程中可以用watch -n 1 ls -l观察文件大小增长确认数据在持续写入。3.4 验证传输完整性传输完成后最直接的验证方式是对比文件大小和 MD5。在客户端机器上算一次 MD5在服务端机器上算一次两个值一致就说明传输没有丢字节。# 客户端 md5sum ./test.log # 服务端 md5sum /data/received/test.log如果 MD5 不一致大概率是接收端没有按长度精确接收或者发送端在写文件名和长度之后没有 flush 就写内容导致服务端读到的长度字段是脏数据。这种情况在本地回环地址上不容易复现因为本地传输太快缓冲区不会积压一旦跨机器问题就暴露了。提示测试时先用小文件几 KB验证流程再用大文件几百 MB验证稳定性和内存占用。大文件传输时注意观察 JVM 内存如果代码里是一次性把文件读进 byte 数组大文件会直接 OOM。4. 避坑指南文件传输里最容易翻车的几个点4.1 文件名乱码现象服务端收到的文件名是乱码比如“测试.log”这种。原因客户端用file.getName().getBytes()默认平台编码服务端用new String(bytes)默认平台编码两边编码不一致时就乱了。解决两端统一用StandardCharsets.UTF_8发送端getBytes(UTF_8)接收端new String(bytes, UTF_8)。不要依赖默认编码那是个玄学。4.2 接收端提前退出循环现象文件传了一半就停了服务端文件大小比源文件小。原因接收循环里用while ((n dis.read(buf)) ! -1)作为条件但 TCP 连接在数据传输过程中可能因为网络抖动触发一次 read 返回 -1实际上对端还在发。解决不要用 -1 作为唯一判断而是用已接收字节数和文件总长度比较收满fileLen才退出。read返回 -1 只在对端真正关闭连接时出现但为了健壮性还是按长度控制更可靠。4.3 大文件导致内存溢出现象传一个 500MB 的文件客户端或服务端抛OutOfMemoryError。原因代码里用了Files.readAllBytes()或者一次性把整个文件读进byte[]。解决永远用固定大小的缓冲区循环读写比如 4096 或 8192 字节。缓冲区大小不是越大越好4KB 到 8KB 在大多数场景下足够再大对吞吐量提升有限反而占内存。4.4 端口被占用或防火墙拦截现象客户端连接时报Connection refused或ConnectException。原因服务端没启动、端口写错、或者中间有防火墙。解决先用telnet 服务端IP 端口确认端口通不通不通就检查服务端进程是否在跑、端口是否被其他程序占用netstat -tlnp | grep 端口。如果是跨机器确认防火墙规则允许该端口入站。4.5 传输完成后连接没有关闭现象服务端处理完一个文件后客户端进程不退出或者服务端 accept 阻塞住不再接受新连接。原因Socket 没有关闭或者流没有关闭导致底层连接保持。解决用 try-with-resources 包裹 Socket 和所有流方法结束自动关闭。服务端在处理完一个连接后显式调用socket.close()然后回到 accept 循环。不要在一个连接上处理多个文件除非你设计了明确的分隔协议。5. 二次开发把这个 Demo 改成能用的工具5.1 加多线程支持并发传输现在的服务端是单线程的一次只能处理一个客户端。如果多个客户端同时连上来后面的会排队等待。改造方式很简单在 accept 之后把处理逻辑丢给一个线程池。ExecutorService pool Executors.newFixedThreadPool(10); while (true) { Socket socket serverSocket.accept(); pool.submit(() - { try { receiveFile(socket, saveDir); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException ignored) {} } }); }线程池大小根据实际并发量调整10 到 50 都算常见。注意每个线程里要单独处理异常和关闭 Socket不能让一个连接的异常影响主循环。5.2 加进度显示和断点续传进度显示比较简单客户端每写一块就累加已发送字节数除以文件总长度得到百分比打印到控制台。断点续传要复杂一些客户端在发送前先问服务端“这个文件你已经收了多少”服务端返回已接收的字节数客户端从那个偏移量开始续传。这需要设计一个简单的请求-响应协议比如客户端先发一个命令字服务端回一个 long 值。// 断点续传的协议设计示例 // 客户端发送CMD_RESUME(1字节) 文件名长度(4字节) 文件名 // 服务端返回已接收字节数(8字节) // 客户端根据返回值决定从哪个位置开始读文件这个改造的关键是服务端要记录每个文件的接收进度可以用一个MapString, Long存在内存里也可以落盘到一个进度文件。注意并发情况下要加锁否则两个客户端同时续传同一个文件会写乱。5.3 加校验和重传传输完成后客户端把源文件的 MD5 发给服务端服务端对比本地文件的 MD5不一致就通知客户端重传。这个机制在不可靠网络下很有用。实现上就是在文件内容发送完之后再发一个 16 字节的 MD5 值服务端接收后计算本地文件 MD5 并比较。如果不一致服务端回一个失败标记客户端重新发送整个文件。虽然简单粗暴但在内网环境下足够用。5.4 我踩过的一个坑早期我在客户端写完文件内容后直接关闭了 Socket没有调用flush()。本地测试一切正常因为数据量小缓冲区自动刷出去了。后来传一个 200MB 的文件服务端收到的文件总是少最后几 KB。排查了半天才意识到DataOutputStream的缓冲区在close()时会 flush但如果代码里先关了FileInputStream再关 Socket顺序不对就可能丢数据。从那以后我每次写完数据都强制flush()一次再关闭流。这个习惯看起来多余但能省掉很多莫名其妙的“文件损坏”问题。希望帮到你。本文还有配套的精品资源点击获取