简介这份资源面向具备一定Java基础、希望掌握网络编程中文件传输实现的开发者围绕Socket与ServerSocket类展开解决服务端与客户端之间可靠传输二进制文件的问题。压缩包共9个文件约9KB以3个java源码和3个class编译文件为核心另含project工程配置、prefs偏好设置与classpath路径文件可直接导入Eclipse运行调试。源码完整呈现服务端监听端口、accept建立连接、通过输入流接收并写入本地文件以及客户端读取本地文件经输出流发送的全过程并涉及缓冲区提升I/O效率、先发送文件大小、以read返回值判断传输结束、try-catch-finally关闭流防止资源泄露等要点同时提及NIO与线程池的并发优化方向。目前已有679人学习下载适合作为理解Java文件传输流程、排查流操作与异常处理问题的参考范例。1. 从一次文件传输需求说起为什么我选了 Java Socket 而不是现成框架前阵子帮一个做嵌入式设备日志采集的朋友处理数据回传的问题。设备端只能跑一个精简的 JRE没有 HTTP 客户端库也没有任何 RPC 框架的依赖唯一能用的就是最原始的java.net.Socket。需求很朴素设备把本地日志文件按块传到服务端服务端落盘并回一个校验结果。听起来像是 Java socket 编程的入门练习但真动手写的时候坑一个接一个——粘包、半包、大文件内存溢出、传输中断后文件损坏每一个都能让线上采集任务悄悄挂掉。这份资源就是围绕这个场景整理的一套可运行代码一个服务端、一个客户端基于 TCP 原生 Socket 实现文件传输支持大文件分块读写、传输进度反馈、基本的完整性校验。它适合两类人一是刚学完 Java IO 流、想找一个完整例子把InputStream/OutputStream、FileInputStream/FileOutputStream串起来的新手二是需要在受限环境里做点对点文件同步、不想引入 Netty 或 Spring 这套重家伙的开发者。代码不依赖任何第三方库JDK 8 以上就能跑拿来当模板改比从零写省事得多。2. 项目结构与环境准备跑起来之前先把地基看清楚2.1 目录结构与模块划分这份代码的组织方式很直白没有用 Maven 多模块那套就是一个普通的 Java 工程。常见做法是分成三个包server放服务端启动类和连接处理逻辑client放客户端启动类和发送逻辑common放两边共用的常量、协议头和工具方法。这样拆的好处是你改传输协议的时候只需要动common不会把服务端和客户端的代码搅在一起。file-transfer-socket/ ├── src/ │ └── com/ │ └── demo/ │ ├── common/ │ │ ├── TransferProtocol.java // 协议常量缓冲区大小、魔数、状态码 │ │ └── FileUtil.java // 文件读写、校验工具 │ ├── server/ │ │ ├── FileServer.java // 服务端入口监听端口 │ │ └── ClientHandler.java // 每个连接一个线程负责收文件 │ └── client/ │ ├── FileClient.java // 客户端入口连接并发送 │ └── SendTask.java // 封装单个文件的发送逻辑 └── README.mdTransferProtocol里我一般会定义几个关键常量BUFFER_SIZE设成 8192 字节这是磁盘 IO 和网络 IO 之间比较平衡的一个值MAGIC_NUMBER用一个固定的 int 做协议头防止客户端连错端口时服务端把乱七八糟的字节当成文件名STATUS_OK和STATUS_ERROR用来在传输结束后回传结果。这些常量两边共用改一处就够。2.2 环境要求和启动方式运行环境没什么特别的JDK 8 或更高版本两台机器或者同一台机器开两个终端防火墙放行你选的端口。我习惯用 9090避开常见的 8080 冲突。编译命令很朴素进到src目录下# 编译所有 Java 文件到 out 目录 javac -d out $(find . -name *.java) # 启动服务端指定监听端口和文件保存目录 java -cp out com.demo.server.FileServer 9090 /data/received # 另开一个终端启动客户端发送文件 java -cp out com.demo.client.FileClient 127.0.0.1 9090 /home/logs/app.log服务端启动后会阻塞在accept()上每来一个连接就起一个线程处理。客户端连上之后先发协议头再发文件名和文件长度然后循环读文件写 socket。传输完成后服务端回一个状态码客户端根据状态码决定是否删除本地文件或重传。这套流程跑通一次后面改起来心里就有底了。注意如果你在 Windows 上跑find命令换成dir /s /b *.java路径分隔符也要相应调整。服务端的保存目录必须提前存在代码里不会自动创建多级目录。3. 核心模块拆解一个文件从客户端到服务端的完整链路3.1 协议设计先定规矩再传数据裸 TCP 传文件最怕的就是“对面怎么知道我要传什么”。我的做法是在真正传文件内容之前先发一个固定长度的协议头。这个头里包含文件名长度、文件名本身、文件总大小。服务端先读这个头解析出这些元信息再按长度循环读文件体。这样即使传的是空文件服务端也能正确创建一个 0 字节的文件而不是傻等。// TransferProtocol.java public class TransferProtocol { public static final int BUFFER_SIZE 8192; public static final int MAGIC_NUMBER 0xCAFEBABE; // 协议魔数防止误连 public static final byte STATUS_OK 0x01; public static final byte STATUS_ERROR 0x00; }MAGIC_NUMBER这个设计是我踩过坑之后加的。有一次测试时客户端连到了另一个服务的端口对面直接把 HTTP 响应头吐了过来服务端傻乎乎地把HTTP/1.1 200 OK当成文件名创建了一个带斜杠的非法文件直接抛异常。加上魔数之后服务端读到的第一个 int 不等于约定值就直接断开省得后面一堆脏数据。3.2 服务端多线程接收与文件落盘服务端的入口类只做两件事创建ServerSocket然后在一个while(true)里accept()。每拿到一个Socket就丢给一个独立的线程去处理。这里用线程池比裸起Thread更稳防止连接数暴涨时把机器拖垮。// FileServer.java 核心片段 public class FileServer { public static void main(String[] args) throws IOException { int port Integer.parseInt(args[0]); String saveDir args[1]; ExecutorService pool Executors.newFixedThreadPool(10); // 限制并发处理数 try (ServerSocket serverSocket new ServerSocket(port)) { System.out.println(服务端启动监听端口 port); while (true) { Socket socket serverSocket.accept(); pool.submit(new ClientHandler(socket, saveDir)); } } } }ClientHandler是真正干活的地方。它先读魔数校验通过后读文件名长度和文件名再读文件长度。然后创建一个FileOutputStream用一个 8KB 的字节数组循环从socket.getInputStream()里读数据读到多少写多少直到累计写入的字节数等于文件长度。这里有个细节read()方法返回 -1 表示对端关闭了连接但如果文件还没传完就返回 -1说明传输中断了这时候要删掉已经写了一半的文件避免留下损坏的残片。// ClientHandler.java 核心循环 try (DataInputStream dis new DataInputStream(socket.getInputStream()); FileOutputStream fos new FileOutputStream(targetFile)) { int magic dis.readInt(); if (magic ! TransferProtocol.MAGIC_NUMBER) { System.out.println(非法连接断开); return; } String fileName dis.readUTF(); long fileLength dis.readLong(); byte[] buffer new byte[TransferProtocol.BUFFER_SIZE]; long received 0; int len; while (received fileLength (len dis.read(buffer)) ! -1) { fos.write(buffer, 0, len); received len; } if (received fileLength) { // 回传成功状态 DataOutputStream dos new DataOutputStream(socket.getOutputStream()); dos.writeByte(TransferProtocol.STATUS_OK); dos.flush(); } else { // 传输不完整删除残片 fos.close(); targetFile.delete(); } }参数说明fileLength是客户端在协议头里写死的总字节数服务端靠它判断什么时候算传完。BUFFER_SIZE设成 8192 是经验值太小会导致频繁的系统调用太大对内存又是一种浪费。received变量累计已写入的字节数每轮循环后和fileLength比对相等就跳出。3.3 客户端分块读取与进度反馈客户端的逻辑和服务端镜像对称。先连上服务端然后依次写魔数、文件名、文件长度接着从本地文件读数据往 socket 里写。为了能看到进度我在循环里加了一个简单的百分比打印每传完 10% 输出一次。这个功能在传大文件时特别有用不然终端半天没反应你都不知道是卡住了还是在正常传。// SendTask.java 核心发送逻辑 public void sendFile(File file) throws IOException { try (Socket socket new Socket(serverHost, serverPort); DataOutputStream dos new DataOutputStream(socket.getOutputStream()); FileInputStream fis new FileInputStream(file)) { dos.writeInt(TransferProtocol.MAGIC_NUMBER); dos.writeUTF(file.getName()); dos.writeLong(file.length()); dos.flush(); byte[] buffer new byte[TransferProtocol.BUFFER_SIZE]; long total file.length(); long sent 0; int len; int lastPercent 0; while ((len fis.read(buffer)) ! -1) { dos.write(buffer, 0, len); sent len; int percent (int) (sent * 100 / total); if (percent lastPercent 10) { System.out.println(已发送 percent %); lastPercent percent; } } dos.flush(); // 读取服务端回传的状态 DataInputStream dis new DataInputStream(socket.getInputStream()); byte status dis.readByte(); if (status TransferProtocol.STATUS_OK) { System.out.println(传输成功 file.getName()); } else { System.out.println(传输失败服务端返回错误); } } }这里有个容易翻车的地方dos.flush()的时机。如果你在写完文件数据之后忘了 flush数据可能还堵在缓冲区里没发出去服务端就会一直阻塞在read()上两边互相等最后超时断开。我一般在写完协议头和写完文件体之后各 flush 一次确保关键节点数据都推到了网络上。4. 避坑与排查那些让我加班到凌晨的传输问题4.1 粘包和半包为什么服务端读到的文件名是乱码现象服务端解析出来的文件名包含奇怪的字符或者文件长度是个负数。原因TCP 是字节流协议没有消息边界。客户端连续调了writeInt、writeUTF、writeLong这三个操作的数据可能被合并成一个 TCP 包发出去也可能被拆成多个包。如果服务端用readLine()或者按固定长度读就会读串位。解决用DataInputStream和DataOutputStream成对出现它们内部会按类型读取固定字节数readInt一定读 4 个字节readUTF先读长度再读内容不会多读也不会少读。前提是两边写入和读取的顺序必须严格一致。4.2 大文件传输时内存溢出现象传一个 2GB 的文件客户端跑着跑着抛OutOfMemoryError。原因有人图省事用Files.readAllBytes()把整个文件读进内存再写 socket。文件小的时候没事文件一大直接把堆撑爆。解决永远用固定大小的缓冲区循环读写缓冲区大小设成 8KB 到 64KB 之间。我一般用 8192这是大多数操作系统页大小的整数倍磁盘 IO 效率比较好。内存占用始终只有缓冲区那么大跟文件总大小无关。4.3 传输中断后留下损坏文件现象网络闪断或者客户端进程被杀服务端保存目录里出现一个半截文件下次重传时文件名冲突。原因服务端一边收一边写中断时FileOutputStream已经创建了文件并写入了部分数据但没有清理机制。解决在ClientHandler里加一个判断如果实际接收的字节数不等于协议头里声明的文件长度就关闭流并调用File.delete()删掉残片。更稳妥的做法是先写到一个临时文件传输完成后再重命名为正式文件名这样即使中断也不会污染正式目录。4.4 端口被占用或防火墙拦截现象客户端连不上抛Connection refused或者一直卡在connect()上。原因服务端端口被其他进程占了或者服务器防火墙没放行这个端口。解决先用netstat -an | grep 9090确认端口监听状态如果被占用就换一个端口。防火墙方面Linux 上检查iptables或firewalld规则Windows 上检查入站规则。测试阶段可以临时关闭防火墙验证但生产环境一定要按最小权限原则只放行必要端口。4.5 文件名包含中文或特殊字符现象服务端创建的文件名是乱码或者直接抛FileNotFoundException。原因DataOutputStream.writeUTF()用的是修改过的 UTF-8 编码能正确处理中文但如果客户端和服务端的默认字符集不一致或者文件名里包含/、\这些路径分隔符就会出问题。解决发送前用file.getName()只取文件名部分不要带路径。服务端收到文件名后做一次安全过滤把..、/、\这些字符替换掉防止路径穿越攻击。中文文件名用writeUTF是没问题的两边都是 Java 的话编码一致。5. 二次开发与改造思路从能跑到好用还差哪几步这份代码跑通之后你手里就有了一个最基础的文件传输通道。但真实项目里往往还有更多要求我挑几个常见的改造方向说说思路。第一个方向是加断点续传。现在的逻辑是每次从头传传一个 1GB 的文件如果中途断了重来一次代价太大。改造的关键是在协议头里加一个offset字段客户端告诉服务端“我从第 N 个字节开始传”服务端用RandomAccessFile定位到对应位置继续写。客户端这边也要记录已发送的字节数断线重连后从断点继续读文件。这个改动涉及协议格式变更两边必须同步升级否则老客户端连新服务端会解析错位。第二个方向是加多文件批量传输。现在的SendTask一次只发一个文件如果要传一个目录可以在外层加一个循环每个文件传完后服务端回一个状态码客户端收到成功后再发下一个。更高效的做法是在协议头里加一个文件数量字段然后连续发多个文件的元信息和内容服务端按顺序解析。但这样会让协议变复杂我一般建议先用简单循环等性能真的成为瓶颈再优化。第三个方向是加传输加密。裸 TCP 传文件是明文的如果走公网内容可能被截获。最简单的做法是在Socket之上套一层SSLSocket用 JDK 自带的SSLSocketFactory创建连接。代码改动不大主要是配置证书和信任库。如果不想动传输层也可以在应用层用 AES 对文件内容加密后再写 socket服务端收到后解密再落盘。两种方式各有优劣前者对代码侵入小后者更灵活但需要自己管理密钥。第四个方向是加进度条和速度统计。现在的进度打印是每 10% 输出一次比较粗糙。可以改成每传 1MB 或者每秒钟输出一次同时计算瞬时速度。实现方式是在循环里记录System.currentTimeMillis()用已传字节数除以耗时得到平均速度。如果要做成图形界面可以把进度回调抽成一个接口让 UI 层去实现。最后一个方向是资源清理和异常兜底。现在的代码用 try-with-resources 管理流大部分情况下没问题但如果服务端在处理过程中抛了未捕获的异常线程会直接死掉连接也不会正常关闭。我一般会在ClientHandler的run()方法里包一层 try-catch-finally在 finally 里确保socket.close()被调用。另外线程池的队列满了之后submit()会抛RejectedExecutionException需要决定是丢弃任务还是阻塞等待这个策略要根据业务场景来定。从那以后我每次写 Socket 相关的代码都会先把协议头定死再写一个最小的收发测试跑通确认粘包和半包问题不存在之后才往上加业务逻辑。这个习惯帮我省掉了至少三次通宵排查的麻烦。希望这份代码和上面的踩坑记录能帮到你拿去改的时候记得先把协议格式画在纸上两边对齐了再动手写。本文还有配套的精品资源点击获取