☰
从打电话到聊天室:用费曼学习法吃透 Java 网络编程
2026/10/3 12:03:17 网站建设 项目流程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 导语
  • 一、网络编程概述:先有一张总图
    • 1.1 什么是网络编程(通俗版)
    • 1.2 TCP与 UDP:打电话vs明信片
    • 1.3 Java网络编程技术体系
    • 1.4 再补一层画面:数据到底怎么【走】的
    • 1.5 端口再讲清楚一点
  • 二、Java Socket 编程:打电话式可靠通信
    • 2.1 Socket 与 ServerSocket 是干什么的
    • 2.2 最小可运行:一问一答(TCP)
    • 2.3 多客户端:线程池当个【多名柜员】
    • 2.4 半成品聊天室:理解【广播】与协议约定
    • 2.5 运行说明(TCP系列通用)
  • 三、UDP编程:寄明信片式尽力而为
    • 3.1 DatagramSocket 与 DatagramPacket
    • 3.2 可运行示例
    • 3.3 TCP vs UDP(写代码时怎么选)
  • 四、Java NIO:一个前台盯很多窗口
    • 4.1 BIO / NIO / AIO 对比
    • 4.2 三个核心概念
    • 4.3 非阻塞 Echo 服务器(精简可运行)
    • 4.4 Buffer 状态机
    • 4.5 什么时候不该上NIO
  • 五、基于HTTP的网络通信
    • 5.1 Java11+ HttpClient:GET / POST
    • 5.2 和 Socket 编程的关系(别把层搅在一起)
    • 5.3 超时怎么设才不算拍脑袋
  • 六、异常处理与资源管理
    • 6.1 用代码固定「正确关闭」习惯
  • 七、安全与工程实践
    • 7.1 长度头协议:最小可运行拆包示意
  • 八、常见问题与排查建议
    • 补一张「学习时最容易混」的对照表
  • 九、总结与学习路线

导语

网络编程不是「背 API」,而是让两台(或同一台)机器上的程序按约定交换字节。本文面向有 Java 基础、刚开始学网络编程的开发者:先用类比建立总图,再落到 `Socket`、UDP、NIO、`HttpClient`,最后补上异常处理、安全和工程实践。文中代码基于Java 17+,尽量完整可运行;为便于演示,部分示例省略了生产级限流、鉴权等能力,文中会明确说明。

理论脉络可对照 [小林 coding《图解网络》](https://xiaolincoding.com/network/) 中的 TCP/IP、HTTP 等篇章;本文侧重 **Java 怎么写、为什么这样写、踩坑怎么排**。


提示:以下是本篇文章正文内容,下面案例可供参考

一、网络编程概述:先有一张总图

1.1 什么是网络编程(通俗版)

网络编程:用代码控制【本进程 <->对端进程】如何建立通道、发送/接收字节、处理错误与关闭。

一句话:IP找到机器,端口找到进程,协议约定怎么说话。

1.2 TCP与 UDP:打电话vs明信片

1.3.Java网络编程技术体系

应用层 HTTP ──► HttpClient / 各类 Web 框架
传输层 TCP ──► Socket / ServerSocket / NIO / Netty
传输层 UDP ──► DatagramSocket / DatagramPacket
名称解析 ──► InetAddress(DNS)

-BIO(Blocking I/O):一个连接卡住读写,当前线程就等着——像一个柜员只服务一位顾客。
-NIO(New I/O / Non-blocking):少量线程用 `Selector` 盯很多通道——像一个前台同时盯多个窗口灯。
-AIO(Asynchronous I/O):真正异步完成回调;Java 里用得相对少,生产更多见 Netty 等封装。

先掌握 Socket + 多线程,再学 NIO,最后按需上 Netty / Spring WebFlux——这条路线最稳。

1.4 再补一层画面:数据到底怎么【走】的

我们在Java里写 `out.write("你好\n".getBytes(UTF_8))`,脑子里可以走一遍这条路径【不必背死,但要有图):

1.应用把字符串变成字节;

2.操作系统把字节放进TCP发送缓存区;

3.TCP按窗口切成段(segment),带上序号;

4.IP加上源/目的IP,路由出去;

5.对端反向拆包,字节接收缓存区

6.我的`in.readLine()` 从缓冲区读到 `\n` 才返回一行。

所有:Java API管的是【进程视角的流】;可靠、重传、乱序重组大多在操作系统协议栈里完成。

我要负责的是:协议边界(什么叫一条消息)、超时、并发、资源关闭和安全校验。

1.5 端口再讲清楚一点

0-1023:常见系统服务(HTTP 80、HTTPS 443),普通用户绑定可能需要权限。

1024-49151:注册端口,应用常用(如 8080,3306)。

49152-65535:临时端口,客户端 connect时OS常自动分配。

一个TCP连接的五元组是:协议+源IP+源端口+目的IP+目的端口。所有【服务端固定8080】不妨碍同时服务成千上万客户端——因为每个客户端源端口不同。

二、Java Socket 编程:打电话式可靠通信

2.1 Socket 与 ServerSocket 是干什么的

- `ServerSocket`:服务端「对外营业」——绑定端口、`accept()` 等客人推门进来。
- `Socket`:一次连接的两端各有一个——拿到它就能用输入/输出流读写字节。

2.2最小可运行:一问一答(TCP)

下面示例在本机 `127.0.0.1:9090` 上跑通「发一行 → 回一行」。先启动服务端,再启动客户端。

服务端

package Exercise.network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; /** 演示用单线程 TCP 回显服务端(一次只认真服务一个连接结束后再接下一个)。 生产环境请改为线程池/NIO,并限制消息长度。 */ public class EchoServer { private static final int PORT = 9090; private static final int MAX_LINE = 1024; public static void main(String[] args) { System.out.println("EchoServer 监听窗口"+ PORT); try (ServerSocket serverSocket = new ServerSocket(PORT)){ while (true){ try (Socket socket = serverSocket.accept(); BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(),StandardCharsets.UTF_8) ); BufferedWriter out = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(),StandardCharsets.UTF_8) )){ String line = in.readLine(); if (line == null){ continue; } if (line.length() > MAX_LINE){ out.write("ERR;message too long\n"); out.flush(); continue; } System.out.println("收到:"+line); out.write("ECHO:"+line+"\n"); out.flush(); } } } catch (IOException e) { System.err.println("处理连接失败: " + e.getMessage()); System.err.println("无法绑定端口: " + e.getMessage()); } } }

客户端:

package Exercise.network; import java.io.*; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; public class EchoClient { public static void main(String[] args) { String host = "127.0.0.1"; int port = 9090; String message = args.length>0 ? args[0] : "hello-network"; try(Socket socket = new Socket()) { //连接超时 3秒,避免永远卡住connect socket.connect(new InetSocketAddress(host,port),3000); socket.setSoTimeout(3000);//读超时 BufferedWriter out = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8) ); BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(),StandardCharsets.UTF_8) ); out.write(message + "\n"); out.flush(); String response = in.readLine(); System.out.println("服务端回复:"+response); } catch (IOException e) { System.out.println("通信失败:"+e.getMessage()); } } }

逐步解释:

1.`ServerSocket(PORT)`:占用本机 9090,别人连这个端口就是连你。

2.·accept()·:阻塞等到一个客户端完成TCP三次握手后返回业务·Socket·。

3.用 **UTF-8 + 按行协议**(`\n` 结尾),双方约定「一行一条消息」,先避开粘包难题。

4.`try-with-resources`:自动关流和 Socket,减少泄漏。
5. 客户端 `connect` / `setSoTimeout`:把「连不上、对端不回」变成可预期的超时异常。

2.3 多客户端:线程池当个【多名柜员】

单线程·accept·→ 处理 → 再 `accept`,第二个客户会排长队。常见做法是:主线程只负责 accept,业务丢给线程池。

package Exercise.network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; public class MultiClientEchoServer { private static final int PORT = 9091; private static final int MAX_LINE = 1024; public static void main(String[] args) throws InterruptedException { //固定大小线程池:防止恶意海量连接把机器打满 ExecutorService pool = Executors.newFixedThreadPool(16); try (ServerSocket serverSocket = new ServerSocket(PORT)){ System.out.println("多客户端Echo监听 "+PORT); while (!Thread.currentThread().isInterrupted()){ Socket socket = serverSocket.accept(); try { pool.execute(()->handle(socket)); }catch (RejectedExecutionException ex){ closeQuietly(socket); System.err.println("线程池繁忙,拒绝新连接"); } } } catch (IOException e) { System.out.println("服务异常:"+e.getMessage()); }finally { pool.shutdown(); pool.awaitTermination(6, TimeUnit.SECONDS); } } private static void closeQuietly(Socket socket) { try (socket; BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8) ); BufferedWriter out = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(),StandardCharsets.UTF_8) ) ){ String line = in.readLine(); if (line == null || line.length() &gt; MAX_LINE){ return; } out.write("ECHO "+line+"\n"); out.flush(); } catch (IOException e) { System.out.println("客户端处理失败:"+e.getMessage()); } } private static void handle(Socket socket) { try { socket.close(); } catch (IOException e) { throw new RuntimeException(e); } } }

主线程是门口保安(只开门),线程池是柜员(真正办事)。柜员数量有限,客太多就拒绝或排队,避免【无限 new Thread】把内存和FD耗尽。

说明:这仍是BIO。连接数到几千且多数空闲时,线程成本会很高——那是NIO/Netty的舞台。

2.4 半成品聊天室:理解【广播】与协议约定

下面把【一问一答】升级成【多人广播】骨架:每个客户端一个读线程;服务端把某人说的化转发给其他人。为控制篇幅,这里只给服务端核心结构(完整工程可自行补客户端UI)。仍是教学示例:无登录、无加密、无背压。

package Exercise.network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** 简易聊天室服务端骨架:按行协议,行首可约定 "NAME:xxx" / "MSG:内容"。 */ public class TinyChatServer { private static final int PORT = 9094; private static final int MAX_LINE = 512; private static final Set<ClientSession> CLIENTS = ConcurrentHashMap.newKeySet(); public static void main(String[] args) { ExecutorService pool = Executors.newCachedThreadPool(); try (ServerSocket serverSocket = new ServerSocket(PORT)){ System.out.println("聊天室端口 "+PORT); while (true){ Socket socket = serverSocket.accept(); pool.execute(()->serve(socket)); } } catch (IOException e) { throw new RuntimeException(e); } } private static void serve(Socket socket) { ClientSession session = null; try { BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8) ); BufferedWriter out = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(),StandardCharsets.UTF_8) ); session = new ClientSession(socket,out); CLIENTS.add(session); String line; while ((line = in.readLine()) !=null){ if (line.length() >MAX_LINE){ session.send("ERR:too long"); break; } if (line.startsWith("NAME:")){ session.name = line.substring(5).trim(); broadcast("系统",session.name+"加入了聊天室"); }else if (line.startsWith("MSG")){ String name = session.name == null ?"匿名":session.name; broadcast(name,line.substring(4)); }else { session.send("ERR:unknown command"); } } } catch (IOException e) { System.out.println("会话结束:"+e.getMessage()); }finally { if(session !=null){ CLIENTS.remove(session); session.close(); } } } private static void broadcast(String from, String content) { String payload = from +": "+content; for (ClientSession c : CLIENTS){ c.send(payload); } } static final class ClientSession { final Socket socket; final BufferedWriter out; volatile String name; ClientSession(Socket socket,BufferedWriter out){ this.socket = socket; this.out = out; } synchronized void send(String msg){ try { out.write(msg+"\n"); out.flush(); } catch (IOException e) { throw new RuntimeException(e); } } void close(){ try { socket.close(); } catch (IOException e) { throw new RuntimeException(e); } } } }

我们从这段代码应带走三件事:

1.协议先行:`NAME:` / `MSG:` 是应用层约定,不是 TCP 自带的。

2.共享集合要线程安全:多线程广播用`ConcurrentHashMap.newKeySet()`。

3.写要同步:同一 `BufferedWriter` 并发写会乱,所以 `send` 加了 `synchronized`。

2.5 运行说明(TCP系列通用)

1.使用JDK 17+:`javac EchoServer.java && java EchoServer`。

2.另开中断跑客户端。防火墙若拦截,先放行本地端口。

3.Windows上看端口:`netstat -ano | findstr 9090`。

4.若报端口占用,换端口或结束旧Java进程。

三、UDP编程:寄明信片式尽力而为

3.1 DatagramSocket 与 DatagramPacket

- `DatagramSocket`:UDP「邮箱」。
- `DatagramPacket`:一封有长度、有目标地址的「明信片」。

没有连接状态,也没有【对端一定收到】的保证——DNS、心跳,实时音频视频常选它。

3.2可运行示例

服务端:

package Exercise.network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; import java.nio.charset.StandardCharsets; public class UdpEchoServer { public static void main(String[] args) { final int port = 9092; final int maxLen = 1024; byte[] buf = new byte[maxLen]; try (DatagramSocket socket = new DatagramSocket(port)){ System.out.println("UDP Echo 监听 "+port); while (true){ DatagramPacket request = new DatagramPacket(buf,buf.length); socket.receive(request);//阻塞收——包 String msg = new String(request.getData(),0,request.getLength(), StandardCharsets.UTF_8); System.out.println("来自 "+request.getAddress() + ":" + request.getPort()+"->" +msg); byte[] resp = ("ECHO:" +msg).getBytes(StandardCharsets.UTF_8); DatagramPacket response = new DatagramPacket( resp,resp.length,request.getAddress(),request.getPort() ); socket.send(response); } } catch (SocketException e) { throw new RuntimeException(e); } catch (IOException e) { System.out.println("UDP服务失败:"+e.getMessage()); } } }

客户端:

package Exercise.network; import java.io.IOException; import java.net.*; import java.nio.charset.StandardCharsets; public class UdpEchoClient { public static void main(String[] args) { String message = args.length > 0 ? args[0]: "ping-udp"; byte[] data = message.getBytes(StandardCharsets.UTF_8); try (DatagramSocket socket = new DatagramSocket()){ socket.setSoTimeout(3000); InetAddress address = InetAddress.getByName("127.0.0.1"); DatagramPacket request =new DatagramPacket(data,data.length,address,9092); socket.send(request); byte[] buf = new byte[1024]; DatagramPacket response = new DatagramPacket(buf, buf.length); socket.receive(response); String reply = new String(response.getData(),0,response.getLength(),StandardCharsets.UTF_8); System.out.println("收到:"+reply); }catch (IOException e) { System.out.println("UDP 通信失败(可能丢包或超时):"+e.getMessage()); } } }

3.3 TCP vs UDP(写代码时怎么选)

四、Java NIO:一个前台盯很多窗口

4.1 BIO / NIO / AIO 对比

4.2 三个核心概念

1.Channel:双向通道(如 `ServerSocketChannel`、`SocketChannel`)。
2.Buffer:字节的暂存区;读写都围绕 Buffer 的 `position` / `limit` / `capacity`。
3.Selector:多路复用器——一次 `select()` 找出「可读/可写/可接受」的 Channel。

4.3 非阻塞 Echo 服务器(精简可运行)

package Exercise.network; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.Set; /** 演示用 NIO 回显服务:单线程 Selector 驱动。 未实现完整拆包/粘包与写半包缓冲,生产请用 Netty 等框架。 */ public class NioEchoServer { public static void main(String[] args) throws IOException { Selector selector = Selector.open(); ServerSocketChannel server = ServerSocketChannel.open(); server.bind(new InetSocketAddress(9093)); server.configureBlocking(false); server.register(selector, SelectionKey.OP_ACCEPT); System.out.println("NIP Echo 监听 9093"); while (true){ selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()){ SelectionKey key = it.next(); it.remove(); if(!key.isValid()){ continue; } if (key.isAcceptable()){ accept(key,selector); } else if (key.isReadable()) { read(key); } } } } private static void read(SelectionKey key) throws IOException{ SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); int n = client.read(buffer); if(n == -1){ client.close(); return; } buffer.flip(); String msg = StandardCharsets.UTF_8.decode(buffer).toString().trim(); buffer.clear(); if (msg.isEmpty()){ return; } ByteBuffer out = ByteBuffer.wrap(("ECHO:"+msg + "\n").getBytes(StandardCharsets.UTF_8)); while (out.hasRemaining()){ client.write(out);//演示简化;未处理写不完的情况 } } private static void accept(SelectionKey key, Selector selector) throws IOException { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel client = server.accept(); if (client == null){ return; } client.configureBlocking(false); client.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024)); System.out.println("接入:"+client.getRemoteAddress()); } }

NIO 解决什么:用少量线程扛大量连接(尤其是「连着但不说话」的连接)。
复杂性:Buffer 状态、半包写、协议拆包、线程模型都要自己管——所以业务项目多直接上Netty。
局限:不是「无脑比 BIO 快」;业务很重、连接很少时,BIO + 线程池往往更简单够用。

4.4 Buffer 状态机

很多人写 NIO 时在 `flip` / `clear` / `compact` 上栽跟头。记住三个数:

-capacity:容量,固定。
-position:下一个读写位置。
-limit:可读写的上限。

写完数据要改成「读模式」→ `flip()`(limit=position,position=0)。读完要继续写 → `clear()`(丢弃未读)或 `compact()`(把未读挪到前面)。
类比:Buffer 是一张可擦写草稿纸;`flip` 是「翻到另一面开始读你刚写的字」。

4.5什么时候不该上NIO

- 连接数几十、业务逻辑复杂(DB、RPC 串行)——瓶颈不在 I/O 模型。
- 团队不熟 NIO,却硬造「自研协议框架」——维护成本远高于收益。
- 已经有 Spring MVC + 合理线程池能满足 SLA——先优化慢 SQL 和超时,再谈模型升级。

五、基于HTTP的网络通信

应用层协议HTTP跑在TCP(HTTP/3则基于QUIC/UDP)之上。请求/响应可粗看成:

请求行 GET /api/hello HTTP/1.1
头部 Host: example.com
Content-Type: application/json
空行
可选正文 {"name":"Ada"}

状态码直觉:2xx成功,4xx客户端问题,5xx服务端问题。

5.1Java11+ HttpClient:GET / POST

优先用 JDK 自带 `java.net.http.HttpClient`,无需额外依赖。

package Exercise.network; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class HttpClientDemo { public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); //GET HttpRequest get = HttpRequest.newBuilder() .uri(URI.create("https://httpbin.org/get")) .timeout(Duration.ofSeconds(5)) .header("Accept","application/json") .GET() .build(); HttpResponse&lt;String&gt; getResp = client.send(get,HttpResponse.BodyHandlers.ofString()); System.out.println("GET status="+getResp.statusCode()); System.out.println(getResp.body().substring(0,Math.min(200,getResp.body().length()))); //POST JSON(正文仅演示:真实项目用可好JSON库并检验响应) String json="{\"msg\":\"hello\"}"; HttpRequest post = HttpRequest.newBuilder() .uri(URI.create("https://httpbin.org/post")) .timeout(Duration.ofSeconds(5)) .header("Content-Type","application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse&lt;String&gt; postResp = client.send(post,HttpResponse.BodyHandlers.ofString()); System.out.println("PSOT status="+postResp.statusCode()); } }

要点:

- `connectTimeout`:建连超时;`timeout`:整次请求超时。
- 看 `statusCode()`,不要假设一定是 200。
- JSON:JDK 无强推标准库解析;可用 Jackson / Gson,引入要写清依赖。例如 Maven:

<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.2</version> </dependency>

(版本请按项目统一管理;此处仅为说明「第三方库要显式依赖」。)

5.2 和 Socket 编程的关系(别把层搅在一起)

- 写 `Socket`:你在**传输层**视角搬字节,自己定消息格式。
- 写 `HttpClient`:你在**应用层**发「有语义的请求」,TCP/TLS 由实现代劳。
- 写 Servlet / Spring MVC:容器帮你把 HTTP 解析成 `HttpServletRequest`,你更关心业务。

学习路径上,先碰 Socket 是为了理解「流、阻塞、粘包」;做业务时优先 HTTP 生态,不要用裸 Socket 重复造 HTTP。

5.3 超时怎么设才不算拍脑袋

经验:对内网 RPC 可较短;跨公网或依赖第三方要结合 P99 留余量,并配合熔断/舱壁,而不是无限加大超时。

六、异常处理与资源管理

原则:

1. 网络对象用 `try-with-resources` 或 `finally` 关闭。
2. 线程池要 `shutdown`,任务要能响应中断,避免线程泄漏。
3. 读写都设超时,禁止无限阻塞拖垮线程池。
4. 日志记:对端地址、耗时、异常类型;不要把密码、Token 打进日志。

6.1 用代码固定「正确关闭」习惯

下面展示「先设超时、再通信、异常分类日志」的客户端模板,可直接当脚手架改:

package Exercise.network; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; import java.nio.charset.StandardCharsets; public class SafeSocketClientTemplate { public static void main(String[] args) { try (Socket socket = new Socket()){ socket.connect(new InetSocketAddress("127.0.0.1",9090),2000); socket.setSoTimeout(2000); socket.getOutputStream().write("ping\n".getBytes(StandardCharsets.UTF_8)); socket.getOutputStream().flush(); byte[] buf = new byte[256]; int n = socket.getInputStream().read(buf); if (n>0){ System.out.println(new String(buf,0,n,StandardCharsets.UTF_8)); } } catch (SocketException e) { System.err.println("超时: " + e.getMessage()); } catch (IOException e) { System.out.println("I/0失败:"+e.getMessage()); } } }

排查口诀:先确认「对端听没听」→ 再确认「协议是否一致(换行、编码)」→ 再确认「超时与线程是否被堵」。

七、安全与工程实践

1.输入校验+长度限制:防止超大包占满内存(前面`MAX_LINE` 就是最小实践)。

2.限制连接与线程:固定线程池、必要时限流;防简单资源耗尽。

3.敏感信息:Token、Cookie、密码脱敏

4.生产传输:公网用HTTPS/TLS,配合身份认证(Token、mTLS等)。

5.粘包/拆包:TCP是字节流,没有【消息边界】。工程上常用:

v- 固定长度;
- 分隔符(如 `\n`);
- 长度头 + body(最常见)。

6.字符编码:双方统一UTF-8,避免「半个中文」乱码。

7.1 长度头协议:最小可运行拆包示意

约定:前 4 字节是大端 int,表示后面 UTF-8 正文长度。这是生产里非常常见的做法(Netty 的 LengthFieldBasedFrameDecoder 就是这类思想)。

package Exercise.network; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; public class LengthPrefixedEchoServer { private static final int MAX = 4096; public static void main(String[] args) { try (ServerSocket server = new ServerSocket(9095)){ System.out.println("长度头协议监听 9095"); while (true){ try (Socket socket = server.accept(); DataInputStream in = new DataInputStream(socket.getInputStream()); DataOutputStream out = new DataOutputStream(socket.getOutputStream())){ int len =in.readInt(); if (len <=0 || len > MAX){ throw new IOException("非法长度:"+len); } byte[] body = in.readNBytes(len); if (body.length !=len){ throw new IOException("正文不完整"); } String msg = new String(body, StandardCharsets.UTF_8); byte[] resp = ("ECHO:"+msg).getBytes(StandardCharsets.UTF_8); out.writeInt(resp.length); out.write(resp); out.flush(); } } } catch (IOException e) { System.out.println("连接处理失败:"+e.getMessage()); } } }

一句话复述:TCP 只保证「字节按序到达」,不保证「一次 read 正好一条业务消息」。长度头相当于先告诉对方「后面还有几页说明书」,对方就能精确拆开。

八、常见问题与排查建议

1. 客户端卡住不动:是否忘了 `flush`?是否在等 `\n` 而对方没发?是否没设读超时?
2.只连得上第一个客户:是不是单线程同步处理?改线程池或 NIO。
3.中文乱码:检查两端 `Charset` 是否都是 UTF-8。
4.偶发丢消息(UDP):正常现象;关键数据不要裸用 UDP,或自己做确认重传。
5.本机调试:`curl`/`telnet` 测端口;日志打出 `getRemoteAddress()`;抓包用 Wireshark(有需要时再上)。

补一张「学习时最容易混」的对照表

把这张表贴在显示器边上,排障时先问自己:「我现在卡在哪一层?」——这比上来就改代码更快。


九、总结与学习路线

核心知识一句话串联起来:

用 IP+端口找到进程 → 用 TCP/UDP 约定怎么传 → 用 Socket/NIO 在 Java 里操作字节 → 用 HTTP 表达业务语义 → 用超时、线程池、校验和 TLS 让程序在真实网络里活得下去。

学习路线建议:

1. BIO `Socket` / `ServerSocket` + 按行协议;
2. 线程池多客户端;
3. UDP 对比体验;
4. NIO 三件套(理解即可);
5. `HttpClient` + JSON;
6. 进阶:**Netty** 或 **Spring WebFlux**(响应式/高并发网关场景)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询