UDP 通信 = 寄快递:DatagramPacket 是贴好地址的包裹,DatagramSocket 是快递员——不握手、不开专线,打包就扔,谁快谁赢。
13.2 TCP通信 学的是打电话:先三次握手、再拉专线、慢慢聊。这节换 13.1 预告过的另一种玩法——寄明信片:地址写在包裹上,扔出去就走,丢了也不补寄。代码量比 TCP 少一大半。
一、定位:TCP vs UDP 一张表
| TCP(上节) | UDP(本节) | |
|---|---|---|
| 比喻 | 打电话 | 寄快递 |
| 连接 | 先握手建专线 | 无连接,地址贴包裹上 |
| 可靠性 | 可靠不丢不乱序 | 可能丢、可能乱序 |
| 速度 | 慢半拍(要握手) | 快 |
| 用在哪 | 聊天、传文件、网页、数据库 | 直播、游戏、广播、DNS |
| Java 对象 | ServerSocket + Socket + 流 | DatagramSocket + DatagramPacket 两类 |
| 代码感受 | 对象多、步骤多、要关流 | 对象少、代码短、只有 close |
丢得起选 UDP,丢不起选 TCP——直播丢一帧没人发现(快比全重要),银行转账丢一笔就出大事(全比快重要)。
二、两个主角:快递员和包裹
DatagramSocket —— 快递员
DatagramSocket socket = new DatagramSocket(); // 发件人:随便拿个端口寄出
DatagramSocket socket = new DatagramSocket(8888); // 收件人:占好 8888 房间等包裹
- 只有两个动作:
send(包裹)寄出、receive(包裹)签收 - 两边用同一个类,没有服务端客户端之分——这是和 TCP 最大的不同
DatagramPacket —— 包裹(注意两种构造)
// ① 发件包裹:内容 + 长度 + 收件地址(IP + 端口)—— 地址贴在包裹上!
byte[] data = "UDP 你好,我是发送方".getBytes();
DatagramPacket sendPacket = new DatagramPacket(
data, data.length,
InetAddress.getByName("127.0.0.1"), 8888);
// ② 收件包裹:只给个空箱子(字节数组),等快递员往里装
byte[] cart = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(cart, cart.length);
TCP 的地址写在 new Socket(ip, 端口) 里(连线时定死);UDP 的地址写在每个包裹上。好处:接收方拆开包裹能查到寄件人是谁(packet.getSocketAddress()),想回信就把回信包裹贴上这个地址——一收一发天然成对。
三、发送方三步 / 接收方三步
发送方(寄快递):
// ① 拿快递员(不用指定端口,系统随便分一个)
DatagramSocket socket = new DatagramSocket();
// ② 打包 + 贴地址
byte[] data = "UDP 你好".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length,
InetAddress.getByName("127.0.0.1"), 8888);
// ③ 扔出去,扔完就能 close,不用等对方
socket.send(packet);
socket.close();
接收方(等快递):
// ① 占好 8888 房间(必须先启动!)
DatagramSocket socket = new DatagramSocket(8888);
// ② 备个空箱子,交给快递员
byte[] cart = new byte[1024];
DatagramPacket packet = new DatagramPacket(cart, cart.length);
// ③ receive 卡在这里等包裹(阻塞,和 TCP 的 accept 一个脾气)
socket.receive(packet);
// 拆包裹
String msg = new String(cart, 0, packet.getLength());
System.out.println("收到:" + msg + ",寄件人:" + packet.getSocketAddress());
socket.close();
和 13.2 的 accept() 一样:没包裹来,程序就停在这一行干等。区别是 TCP 的 accept 在等"电话线接通",UDP 的 receive 在等"包裹进箱"。
四、三条保命规矩
规矩一:先启动接收方,再启动发送方
TCP 客户端拨号没人接会报错(Connection refused,好歹吭一声);UDP 发送方扔包裹没人收——一声不吭,包裹直接丢地上,发送方还以为成功了。这是 UDP 最坑新手的地方:不报错 ≠ 发到了。
规矩二:拆包裹用 new String(cart, 0, packet.getLength())
和 10.2 的 new String(cart, 0, len) 一模一样的道理:箱子 1024 大、包裹只装了 11 字节,不按 getLength() 截取会把箱底的空字节(一堆空格/null)一起打出来。cart.length 是箱子大小,packet.getLength() 才是实际装了多少。
规矩三:接收方的箱子要够大,装不下直接丢
UDP 包裹是整进整出的:箱子 64 字节、包裹 100 字节,不会分两次装,多出来的 36 字节直接扔掉,也不报错。传大内容要么加大箱子,要么分包寄。
五、动手实验:Demo24 发快递 + 签收
建包 com.bozai.network,两个类各带 main:
发送方 Demo24Sender:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Demo24Sender {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
byte[] data = "UDP 你好,我是发送方".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length,
InetAddress.getByName("127.0.0.1"), 8888);
socket.send(packet);
System.out.println("包裹已寄出:" + new String(data));
socket.close();
}
}
接收方 Demo24Receiver:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class Demo24Receiver {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(8888);
System.out.println("【接收方】已就位,等包裹...");
byte[] cart = new byte[1024];
DatagramPacket packet = new DatagramPacket(cart, cart.length);
socket.receive(packet); // 阻塞等
String msg = new String(cart, 0, packet.getLength()); // 按实际长度拆
System.out.println("【接收方】收到:" + msg);
System.out.println("【接收方】寄件人:" + packet.getSocketAddress());
socket.close();
}
}
保姆式跑法(IDEA):
- 先跑
Demo24Receiver,控制台看到已就位,等包裹...算成功(程序会卡住不动——正常,在等包裹) - 再跑
Demo24Sender,接收方控制台立刻打印收到的消息和寄件人地址,算成功
三个 30 秒实验
- 顺序倒着来:只跑发送方不跑接收方 → 发送方说"已寄出"却没有任何报错,接收方后跑也收不到(包裹早丢了)——体会规矩一,UDP 丢了不吭声
- 把拆包裹改成
new String(cart):后面跟一串空白——体会规矩二,箱子 ≠ 包裹 - 连发两个包裹:发送方 send 两次再 close,接收方跑一轮只收到第一个——一次 receive 只收一个包裹,想连收要
while(true)(和 TCP 的 accept 循环一个道理)
六、常见误区
最大的坑。UDP 是"扔了就走",丢件不通知、对方没开机也不通知。要确认送达?让接收方回一个包裹(把 packet.getSocketAddress() 的地址贴上去),自己实现"签收回执"——这就开始自己造 TCP 的轮子了。
没有。两边都是 DatagramSocket,只是接收方构造时要指定端口(new DatagramSocket(8888)),发送方不用(系统随机分配)。角色是临时分的,不是类定的。
发件包裹四件套缺一不可:内容 data、长度 data.length、地址 InetAddress、端口。新手常忘贴地址——包裹没有地址,send() 直接抛异常,算它负责任。
理论上一个 DatagramPacket 最多约 64KB,实际建议远小于它;大文件老老实实用 TCP(10.2 的复制套路)。UDP 的主场是短小、高频、丢得起的数据。
一次 receive() 只收一个包裹就往下走了。想一直收(比如做广播接收器),必须 while(true){ socket.receive(packet); ... }——TCP 的 accept 循环、13.3 前埋的聊天室地基,同款配方。
七、折叠自测
因为无连接:TCP 的分工来自"谁开门谁敲门"(建立专线需要角色);UDP 地址贴在包裹上,谁都能寄、谁都能收,两边用同一个 DatagramSocket,只是接收方要指定端口。
发件包裹带地址(内容 + 长度 + IP + 端口,四件套);收件包裹只给空箱子(字节数组 + 长度)。收完靠 packet.getLength() 知道装了多少、packet.getSocketAddress() 知道寄件人。
都是阻塞的:没等到货就停在那行不动。且都只服务"一个"——accept 一次接一条连接,receive 一次收一个包裹,常驻都要 while(true)。
启动顺序错了——发送方先跑,接收方后跑,包裹扔出去时没人收,直接丢地上且不吭声(规矩一)。UDP 不报错 ≠ 发到了。
参考骨架:
// A:send 发给 B(8888) → 备空箱 receive 等 B 的回包裹 → 拆开打印
// B:receive 收到 A 的包裹 → new DatagramPacket(回话.getBytes(), 长度,
// packet.getSocketAddress()) ← 寄件人地址直接拿来贴 → send 回去
自检三点:①A 发完不能 close(还要听回信,13.2 的"别关耳朵"在 UDP 换了个说法);②B 的回包裹地址取自 packet.getSocketAddress();③箱子拆包用 getLength()。跑通它,你就同时握住了 TCP 和 UDP 两套拳。
下一步学什么
- ☑ 13.4 本章小结 ✅ —— 点此复习:三节串线、速查 12 条、模板 4 段、五大名坑、毕业大题多人聊天室雏形
相关笔记
- 第十三章 网络编程/13.1 网络基础:TCP 打电话 vs UDP 寄明信片的选型依据——本节的理论地基
- 第十三章 网络编程/13.2 TCP通信:对照着学最快——accept 阻塞、-1 哨兵、三条保命规矩在 UDP 里各有镜像
- 第十章 I-O/10.2 字节流:
new String(cart, 0, len)拆包裹的原型,箱子与实际长度的账 - 第十二章 多线程/12.2 线程的创建:while(true) 连收多个包裹时,一包裹一线程的预科班
- 5.3 异常处理及语法:Demo24 的
throws Exception偷懒姿势,正式写要换成具体异常