
简介《Java socket字节流传输示例解析》是面向Java网络编程初学者及需要排查字节流通信问题的开发者的PDF学习笔记通过一个可运行的ServerSocket示例帮助理解TCP/IP网络通信流程与字节级数据处理方式解决Socket编程中流包装、数据读取和资源关闭等常见问题。资源仅含1个PDF文档压缩后大小约38KB便于随时打开查阅已有2629人学习下载。内容包含完整服务端TalkServer4Byte与客户端TalkClient4Byte代码围绕ServerSocket指定端口监听、accept()阻塞接收、BufferedInputStream与DataInputStream装饰流组合、单字节循环读取并转换为16进制字符串、available()判断一次请求结束以及finally中关闭Socket的规范写法逐一展开并补充了IOException处理和装饰流使用要点。适合课程设计、接口联调或面试前快速回顾Java网络编程基础。1. 从一个卡死的文件传输说起为什么字节流才是网络传输的主角两个 Java 进程之间传一个文件传着传着就卡死了或者数据只收到一半程序就抛了 EOFException——这类问题十次里有八次出在 socket 字节流的使用姿势上。所谓 Java socket 字节流传输示例解析就是把 InputStream / OutputStream 这条最基础的数据通道讲透连接怎么建、数据怎么读怎么写、连接关闭时“半关闭”到底意味着什么。它适合两类人一类是刚开始做 socket 网络编程、照着 demo 能跑通但说不清 read(byte[]) 返回值含义的开发者另一类是已经在写接口联调、遇到粘包和端口占用却只能靠搜索引擎碰运气的工程师。这篇文章不绕原理直接从一次真实会话的完整动作讲起再给两个能直接编译运行的示例。2. 把概念立住再动手字节流、缓冲区与 socket 会话的完整动作2.1 为什么是字节流而不是字符流TCP 这一层给应用暴露的接口本质就是“没有边界的字节管道”。你调用 write 塞进去多少数据内核不保证对端一次 read 就能完整取出来它只保证字节顺序不变。所以 socket 编程里真正的主角永远是 InputStream 和 OutputStream 这对字节流。字符流Reader / Writer多做了两层事编码和解码。本地文件用字符流没问题因为两端都是同一个 JVM、同一套默认字符集但网络传输不行。对端可能是 C 写的服务可能是 Python 写的网关它拿到的就是裸字节。你用 PrintWriter 写出一句中文本质上也是先按 JVM 默认编码转成字节再上线路对端一旦按另一套编码解读乱码马上出现。更麻烦的是字符流在读多字节字符时如果 TCP 包被切成两半读到的第一个半截字符会直接报错这属于典型的“编码状态被网络包边界打断”。所以常见做法是把编码问题放在应用层解决要么用 DataOutputStream.writeUTF 这类自带长度和编码约定的包装流要么自己在消息头里声明字节长度和字符集。socket 这层只负责搬运 byte[]不碰字符。2.2 一次完整交互从 bind 到 close 的六个动作一次 socket 会话无论代码怎么写底层都绕不开这六个动作动作关键方法阻塞点常见误解服务端绑定端口new ServerSocket(port)无绑定立即返回端口被占用时抛 BindException等待客户端连接accept()阻塞直到有连接进来以为 accept 返回后连接就“稳定”了客户端发起连接new Socket(host, port)阻塞到三次握手完成连上不代表对端应用已调用 read写数据OutputStream.write(buf, off, len)发送缓冲满时阻塞write 成功等于对端已收到读数据InputStream.read(buf)接收缓冲无数据时阻塞read 返回少了就是粘包关闭连接shutdownOutput() / close()半关闭只关发送方向把 close 和 shutdownOutput 混为一谈注意 write 的语义它只保证数据进入了本机内核的发送缓冲区不保证对端应用已经 read。TCP 的确认机制在内核里完成对端应用还没调用 read 时数据也可能已经在接收缓冲区里躺着了。所以排查“我发了对方没收到”时先确认对方是否真的在读而不是怀疑 write 没生效。read 的语义同样容易被低估。read(byte[] buf) 的返回值是“本次实际读到的字节数”这个数可能小于 buf.length也可能一次读到多个业务消息拼在一起。阻塞发生在“缓冲区里一个字节都没有”的时候只要缓冲区里还有数据read 就会立刻返回不会傻等凑满 buf 长度。这两个细节是整个排错的根基。还有一个动作很多人会漏半关闭。Socket.shutdownOutput() 表示“我的数据发完了发送方向关闭但接收方向还开着”对端的 read 会读到 -1流结束。而 close() 是双向关闭并且会立即释放文件描述符发送缓冲区里没发完的数据可能会被丢弃。所以写完数据想优雅结束会话顺序应该是 flush → shutdownOutput → 等对方回包 → close。3. 用字节流跑通一次真实传输echo 示例与带长度前缀的文件收发3.1 最小 echo单文件即可运行的 Server 与 Client先给一个能直接编译运行的完整示例这段代码同时启动服务端和客户端用两个线程在同一个 JVM 里完成回环测试不需要额外开进程import java.io.*; import java.net.*; public class SocketByteDemo { public static void main(String[] args) throws Exception { new Thread(SocketByteDemo::runServer).start(); Thread.sleep(500); runClient(); } static void runServer() { try (ServerSocket ss new ServerSocket(9090)) { System.out.println(server listening on 9090); try (Socket s ss.accept(); InputStream in s.getInputStream(); OutputStream out s.getOutputStream()) { byte[] buf new byte[1024]; int n; while ((n in.read(buf)) ! -1) { out.write(buf, 0, n); out.flush(); } System.out.println(server: client closed, echo done); } } catch (Exception e) { e.printStackTrace(); } } static void runClient() throws Exception { try (Socket s new Socket(127.0.0.1, 9090); OutputStream out s.getOutputStream(); InputStream in s.getInputStream()) { out.write(hello socket byte stream.getBytes()); out.flush(); s.shutdownOutput(); byte[] buf new byte[1024]; int n; while ((n in.read(buf)) ! -1) { System.out.write(buf, 0, n); } System.out.println(); System.out.println(client: received echo done); } } }这段代码有两个关键用法。第一个是服务端的读取循环in.read(buf) 返回 -1 表示对端发送方向关闭循环退出。这里用 System.out.write 直接输出原始字节避免用 new String(buf) 时把整个缓冲区的尾部空字节也打出来这是新手常见的输出脏数据问题。第二个是客户端的 shutdownOutput()它让服务端的 read 能读到 -1从而知道“数据完了”。参数方面缓冲区 1024 是演示用的最小值实际建议 8192 或 16384。缓冲区越大系统调用次数越少但超过 64KB 对吞吐提升就不明显了因为 TCP 窗口和内核缓冲会先成为瓶颈。flush 在裸 SocketOutputStream 上意义不大因为 write 直接进内核缓冲区但如果外面包了 BufferedOutputStreamflush 就是必须的否则数据会憋在 JVM 内存里。3.2 传文件用长度前缀解决数据边界echo 没有边界问题因为服务端读到 -1 就结束。但真实场景里服务端还要继续服务下一个请求不可能等客户端关闭连接。传文件的标准做法是在数据前面先写一个长度头专业说法叫“长度前缀协议”。import java.io.*; import java.net.*; public class FileTransferDemo { static final int PORT 9091; public static void main(String[] args) throws Exception { new Thread(FileTransferDemo::startServer).start(); Thread.sleep(500); startClient(new File(hello.txt)); } static void startServer() { try (ServerSocket ss new ServerSocket(PORT)) { System.out.println(server ready); try (Socket s ss.accept()) { DataInputStream dis new DataInputStream(s.getInputStream()); long len dis.readLong(); try (FileOutputStream fos new FileOutputStream(received.bin)) { byte[] buf new byte[8192]; long remaining len; while (remaining 0) { int toRead (int) Math.min(remaining, buf.length); int n dis.read(buf, 0, toRead); if (n -1) { throw new EOFException(connection closed before full length); } fos.write(buf, 0, n); remaining - n; } } System.out.println(server received len bytes); } } catch (Exception e) { e.printStackTrace(); } } static void startClient(File file) throws Exception { try (Socket s new Socket(127.0.0.1, PORT); DataOutputStream dos new DataOutputStream(s.getOutputStream())) { long len file.length(); dos.writeLong(len); try (FileInputStream fis new FileInputStream(file)) { byte[] buf new byte[8192]; int n; while ((n fis.read(buf)) ! -1) { dos.write(buf, 0, n); } } dos.flush(); s.shutdownOutput(); System.out.println(client sent len bytes); } } }服务端最值得讲的细节是循环里的 two-step 读取。dis.read(buf, 0, toRead) 不是“读满 toRead 字节”它只是“最多读 toRead 字节”。假设文件剩余 10000 字节缓冲区 8192第一次调用可能只读回 4096 字节这时 remaining 变成 5904循环继续读。绝不能写成 if (dis.read(buf, 0, toRead) -1) 然后直接落盘那会漏数据。DataInputStream 的 readFully 可以省掉这段循环但为了让你看清底层语义这里保留手写循环。还有个细节readLong() 会阻塞直到 8 个字节全部到达这是 DataInputStream 内部用 readFully 实现的所以长度头不会出现“只读了 4 字节就继续执行”的问题。客户端这边的 shutdownOutput 其实不是必须的因为服务端靠长度判断结束但保留它能让对端在异常断连时更快感知 FIN也算是个防御习惯。4. 排查粘包、卡死、端口占用几个高频故障的排查顺序socket 网络编程里最不缺的就是“看起来很玄学”的问题但拆开看基本都是那几个固定套路。这里按我在实际联调里遇到的频率排序逐条给出现象、原因和解决。4.1 对方发了 100 字节我这里一次只读到 40 字节粘包与半包现象两端明明约定好“一次请求一条消息”结果服务端 read 有时读到 40 字节有时读到 150 字节两条消息拼一起解析直接错位。客户端发了两条完整消息服务端却把它们当成了一条。原因TCP 是字节流没有消息边界。内核只保证字节顺序不保证每次 write 对应一次 read。网络拥塞、内核缓冲调度都会把一次 write 拆成多次 read半包或者把多次 write 合并成一次 read粘包。这不是 Java 的问题是所有 socket 网络编程的通病。解决应用层必须自己定义消息边界。常见方案就三种定长消息、分隔符、长度前缀。文件传输用长度前缀3.2 里的 writeLong 循环文本协议可以用换行符但内容里不能出现换行需要转义定长消息最简单但对不定长数据浪费空间。这道题在 Java 面试题里几乎必问代码里遇到时原因和答案一样直白——别试图用“多 sleep 一会”来解决那是把不可控因素当可控因素。4.2 我 flush 了对面迟迟收不到Nagle 算法与延迟确认现象客户端 write 一小段数据后立刻 flush服务端那边 read 卡住好几秒才返回。数据量一多吞吐掉得离谱。原因TCP 默认开了 Nagle 算法它会把小包攒在一起等前一个包的 ACK 回来后才发下一个。而接收方内核又可能启用延迟 ACK故意等一会儿再回 ACK两个机制叠加上去小包交互的延迟能被放大到几十毫秒甚至上百毫秒。这个现象在“请求-响应”这类一来一回的交互里尤其明显。解决对延迟敏感的交互场景连接建立后立刻调用 s.setTcpNoDelay(true)关闭 Nagle 算法。注意要在连接建立后、第一次 write 之前设置。另一个很容易忽略的点是接收方要尽快把数据从内核缓冲读走否则接收窗口变小发送方也会被反压。如果你在服务端做了耗时的业务处理才去 read 下一个请求那就是在人为制造延迟 ACK。4.3 对方已经 close 了我这边 read 却一直卡住半关闭与 FIN现象客户端主动 close服务端线程却一直阻塞在 read 上既不返回 -1 也不抛异常。原因这里面有个常见误解——以为对端 close 了本端 read 就一定会立刻返回 -1。实际上 close 会发送 FIN但 FIN 的处理依赖 TCP 状态机。如果对端进程还有别的线程持着同一个 Socket 引用没关或者程序用了连接池复用了底层连接FIN 可能不会被正确处理。更常见的情况是你调用的是某个包装流外层的 BufferedInputStream 已经读取了超出业务需要的数据把 FIN 之前的字节都消费掉了但流的结束标志没有正确传递。解决自己掌控关闭语义。发送方写完数据用 shutdownOutput() 而不是直接 close()这会让对端 read 稳定地返回 -1。接收方不要指望“读一次”就能感知结束要么用 while ((n read(buf)) ! -1) 循环要么用带长度前缀的协议并在长度读完后主动跳出。兜底方案是给 Socket 设置 setSoTimeoutread 超时抛 SocketTimeoutException至少不会让线程无限期挂着——这个超时在排查“是不是死锁了”的时候特别有用能让问题以异常形式暴露出来。4.4 服务端重启报“Address already in use”TIME_WAIT 与端口复用现象服务端程序停止后立刻重启bind 同一个端口报 java.net.BindException: Address already in use。在 Windows 上对应的报错文案是“通常每个套接字地址(协议/网络地址/端口)只允许使用一次”。原因主动关闭连接的一方会进入 TIME_WAIT 状态持续 2MSL一般约 1 到 4 分钟。TIME_WAIT 的作用是保证最后一个 ACK 能到达对端以及让旧连接的迟到报文不会污染新连接。服务端如果先 close就会把自己置于 TIME_WAIT端口在这段时间内不能被重新 bind。解决服务端在 ServerSocket 绑定端口之前调用 setReuseAddress(true)。注意必须在 bind 之前设置也就是不能先 new ServerSocket(port)再调用 setReuseAddress那已经晚了。正确写法ServerSocket ss new ServerSocket(); ss.setReuseAddress(true); ss.bind(new InetSocketAddress(port));另一个容易踩的场景是客户端用固定端口连接服务端客户端主动 close 后马上重连同样会撞 TIME_WAIT。这种场景下 setReuseAddress 在客户端也有效但要记得它只对“主动关闭方”有意义。如果你希望彻底避开 TIME_WAIT 的纠缠可以让对端服务端先关闭连接或者干脆把服务端设计成“客户端 close 后自己再 close”把 TIME_WAIT 留给客户端。5. 验证的最后一步双线程自测一个带长度前缀的收发闭环写了半天代码最后一定得有一个能证明“这条路真的通”的自测方法。我常用的做法是写一个回环测试服务端绑定端口 0让系统自动分配可用端口再在同一个进程里用两个线程完成一次完整收发。端口 0 这个技巧能避免 CI 环境里端口冲突比写死 9090 靠谱得多。import java.io.*; import java.net.*; import java.security.MessageDigest; import java.util.concurrent.CountDownLatch; public class LoopbackTest { public static void main(String[] args) throws Exception { byte[] payload new byte[1024 * 1024 17]; for (int i 0; i payload.length; i) { payload[i] (byte) (i % 251); } CountDownLatch ready new CountDownLatch(1); CountDownLatch done new CountDownLatch(1); byte[][] received {null}; try (ServerSocket ss new ServerSocket(0)) { int port ss.getLocalPort(); Thread server new Thread(() - { try (Socket s ss.accept()) { ready.countDown(); DataInputStream in new DataInputStream(s.getInputStream()); long len in.readLong(); ByteArrayOutputStream bos new ByteArrayOutputStream(); byte[] buf new byte[4096]; long remaining len; while (remaining 0) { int n in.read(buf, 0, (int) Math.min(remaining, buf.length)); bos.write(buf, 0, n); remaining - n; } received[0] bos.toByteArray(); done.countDown(); } catch (Exception e) { e.printStackTrace(); } }); server.start(); ready.await(); try (Socket s new Socket(127.0.0.1, port); DataOutputStream out new DataOutputStream(s.getOutputStream())) { out.writeLong(payload.length); out.write(payload); out.flush(); } done.await(); boolean match MessageDigest.isEqual(payload, received[0]); System.out.println(send payload.length , recv received[0].length , match match); } } }payload 我特意用了 1MB 17 字节这种不规则长度就是为了验证循环读取逻辑在“最后一段凑不满缓冲区”时依然正确。MessageDigest.isEqual 做字节数组比对比用 Arrays.equals 更直观地表达“内容一致”这个意图。验证时你可以跑三组数据1KB 小包、16KB 临界值、1MB 以上大包分别验证 Nagle 影响、缓冲区边界和循环读取。测试输入预期结果观察点1KBmatchtrue秒回是否有无谓的延时检查 tcpNoDelay16KBmatchtrue是否出现半包后依然能读全1MB17matchtrue循环是否正确处理剩余不足缓冲区的数据这个自测脚本我在本地跑了几十次没出过幺蛾子后来真出问题的地方全在疏于验证的细节上——比如忘了设置 tcpNoDelay 导致小包延迟比如 read 循环少减了 remaining 导致文件最后一个块被重复写。我现在每写一套 socket 接口都会先把这个回环测试跑绿了再发出去联调算是交过多次学费换来的习惯写下来希望帮到你。本文还有配套的精品资源点击获取