☕ 第十三章 网络编程
📄 一句话概括

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 房间等包裹

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();
⚠️ receive 也是阻塞的

和 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):

  1. 先跑 Demo24Receiver,控制台看到 已就位,等包裹... 算成功(程序会卡住不动——正常,在等包裹)
  2. 再跑 Demo24Sender,接收方控制台立刻打印收到的消息和寄件人地址,算成功

三个 30 秒实验

  1. 顺序倒着来:只跑发送方不跑接收方 → 发送方说"已寄出"却没有任何报错,接收方后跑也收不到(包裹早丢了)——体会规矩一,UDP 丢了不吭声
  2. 把拆包裹改成 new String(cart):后面跟一串空白——体会规矩二,箱子 ≠ 包裹
  3. 连发两个包裹:发送方 send 两次再 close,接收方跑一轮只收到第一个——一次 receive 只收一个包裹,想连收要 while(true)(和 TCP 的 accept 循环一个道理)

六、常见误区

⚠️ 误区一:UDP 没报错 = 发到了

最大的坑。UDP 是"扔了就走",丢件不通知、对方没开机也不通知。要确认送达?让接收方回一个包裹(把 packet.getSocketAddress() 的地址贴上去),自己实现"签收回执"——这就开始自己造 TCP 的轮子了。

⚠️ 误区二:以为 UDP 也有 ServerSocket 那样的"开门"类

没有。两边都是 DatagramSocket,只是接收方构造时要指定端口(new DatagramSocket(8888)),发送方不用(系统随机分配)。角色是临时分的,不是类定的。

⚠️ 误区三:把内容和地址写混

发件包裹四件套缺一不可:内容 data、长度 data.length、地址 InetAddress、端口。新手常忘贴地址——包裹没有地址,send() 直接抛异常,算它负责任。

⚠️ 误区四:一个包裹想寄很大文件

理论上一个 DatagramPacket 最多约 64KB,实际建议远小于它;大文件老老实实用 TCP(10.2 的复制套路)。UDP 的主场是短小、高频、丢得起的数据。

⚠️ 误区五:receive 收到后忘了循环,以为服务常驻

一次 receive() 只收一个包裹就往下走了。想一直收(比如做广播接收器),必须 while(true){ socket.receive(packet); ... }——TCP 的 accept 循环、13.3 前埋的聊天室地基,同款配方。


七、折叠自测

❓ Q1:UDP 为什么没有 ServerSocket / Socket 的分工?

因为无连接:TCP 的分工来自"谁开门谁敲门"(建立专线需要角色);UDP 地址贴在包裹上,谁都能寄、谁都能收,两边用同一个 DatagramSocket,只是接收方要指定端口。

❓ Q2:发件包裹和收件包裹的构造参数差在哪?

发件包裹带地址(内容 + 长度 + IP + 端口,四件套);收件包裹只给空箱子(字节数组 + 长度)。收完靠 packet.getLength() 知道装了多少、packet.getSocketAddress() 知道寄件人。

❓ Q3:`receive()` 和 TCP 的 `accept()` 有什么共同点?

都是阻塞的:没等到货就停在那行不动。且都只服务"一个"——accept 一次接一条连接,receive 一次收一个包裹,常驻都要 while(true)。

❓ Q4:发送方代码跑完没报错,接收方却没收到,最可能是什么原因?

启动顺序错了——发送方先跑,接收方后跑,包裹扔出去时没人收,直接丢地上且不吭声(规矩一)。UDP 不报错 ≠ 发到了。

❓ Q5(毕业题):写一对"UDP 互发"程序——A 先发一句给 B,B 收到后回一句,A 打印回信。

参考骨架:

// 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 两套拳。


下一步学什么


相关笔记