学 Java 网络通信时,不要一上来就背一堆类名。先抓住一个问题:一段数据怎样从一台机器上的程序,发送到另一台机器上的程序?
一、网络通信先看三要素
网络通信离不开三个信息:IP、端口、协议。
IP 用来定位网络中的一台主机;端口用来定位这台主机上的某个应用程序;协议决定双方按什么规则传输数据。
常见架构可以先分成两类:
| 架构 | 说明 | 常见例子 |
|---|---|---|
| C/S | 客户端直接连接服务器 | 桌面聊天软件、游戏客户端 |
| B/S | 浏览器访问服务器 | 网页、后台管理系统 |
不管是 C/S 还是 B/S,底层都绕不开"地址 + 端口 + 协议"。代码里用本机测试时,目的地址一般写成本机地址,服务端监听一个固定端口,客户端向这个端口发送数据。
二、先用 InetAddress 认识主机地址
day05-net 的 demo1inetaddress 示例先演示了 InetAddress。它不负责发送消息,而是负责表示和解析主机地址。
java
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1);
System.out.println(ip1.getHostAddress());
System.out.println(ip1.getHostName());
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostAddress());
System.out.println(ip2.getHostName());
System.out.println(ip2.isReachable(5000));
这段代码可以拆成三件事:
- getLocalHost 获取本机地址对象。
- getByName 根据域名或 IP 字符串获取目标主机地址对象。
- isReachable 尝试判断指定时间内目标主机是否可达。
学习网络通信时,InetAddress 的价值在于让"主机"变成代码里的对象。后面 UDP 和 TCP 示例中,客户端都需要指定目标主机和端口,本机测试时就使用 getLocalHost。
三、UDP:把数据封成包发出去
UDP 的特点是无连接、速度快,但不保证可靠到达。
它适合对实时性要求高、允许少量丢失的场景,比如直播、语音、广播。它的代码模型也很直观:发送端把字节数组封成 DatagramPacket,再通过 DatagramSocket 发出去;接收端绑定端口,准备缓冲区,等待数据包进来。
1. UDP 发送端
demo2udp 的客户端代码完成了一次发送:
java
DatagramSocket socket = new DatagramSocket();
byte[] bytes = "你好,帅哥".getBytes();
DatagramPacket packet = new DatagramPacket(
bytes,
bytes.length,
InetAddress.getLocalHost(),
8080
);
socket.send(packet);
socket.close();
这里的关键点是 DatagramPacket 构造方法中的四个参数:
| 参数 | 作用 |
|---|---|
| bytes | 要发送的数据 |
| bytes.length | 本次发送的有效字节长度 |
| InetAddress.getLocalHost() | 目标主机地址 |
| 8080 | 目标程序监听的端口 |
发送端本身不需要和服务端提前建立连接。只要知道目标 IP 和端口,就可以把这个数据包发出去。
2. UDP 接收端
服务端要先绑定端口,否则客户端发到 8080 的数据没有程序接收。
java
DatagramSocket socket = new DatagramSocket(8080);
byte[] buf = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
int len = packet.getLength();
String message = new String(buf, 0, len);
System.out.println("服务端收到: " + message);
System.out.println("对方IP:" + packet.getAddress().getHostAddress());
System.out.println("对方端口:" + packet.getPort());
接收端的缓冲区用来装收到的数据。真正转成字符串时,不应该直接把整个 buf 转掉,而是要根据 packet.getLength 取本次收到的有效长度。否则缓冲区后面没被写入的空内容也会被带进去。
packet 里还能拿到发送方的地址和端口,这一点很重要。UDP 没有连接对象,如果接收端想知道是谁发来的,就要从数据包本身获取。
3. UDP 多发多收
demo3udp2 在一发一收的基础上加了 while 循环。
客户端循环读控制台输入,输入 exit 时退出:
java
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String text = sc.nextLine();
if (text.equals("exit")) {
break;
}
byte[] bytes = text.getBytes();
DatagramPacket packet = new DatagramPacket(
bytes,
bytes.length,
InetAddress.getLocalHost(),
8080
);
socket.send(packet);
}
服务端也用 while 循环持续接收:
java
while (true) {
socket.receive(packet);
int len = packet.getLength();
String message = new String(buf, 0, len);
System.out.println("服务端收到: " + message);
System.out.println("对方IP:" + packet.getAddress().getHostAddress());
System.out.println("对方端口:" + packet.getPort());
}
这里能看出 UDP 的一个特点:客户端退出只是客户端不再发送,服务端不会自动结束,它仍然会阻塞在 receive,继续等待下一个数据包。
四、TCP:先建立连接,再通过流传输
TCP 的特点是面向连接、可靠传输。和 UDP 不同,TCP 通信前必须先建立连接。
在 Java 代码里,客户端用 Socket 请求连接;服务端用 ServerSocket 监听端口,并通过 accept 等待客户端接入。连接建立后,双方通过输入流和输出流收发数据。
1. TCP 一发一收
demo4tcp1 的客户端连接本机 9999 端口,然后通过输出流发送数据:
java
Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(1);
dos.writeUTF("你好");
socket.close();
服务端先启动 ServerSocket,再调用 accept 等客户端连接:
java
ServerSocket ss = new ServerSocket(9999);
Socket socket = ss.accept();
DataInputStream dis = new DataInputStream(socket.getInputStream());
int id = dis.readInt();
String message = dis.readUTF();
System.out.println("id:" + id + " 内容:" + message);
System.out.println("客户端的ip" + socket.getInetAddress());
System.out.println("客户端的端口" + socket.getPort());
这一组代码最值得注意的是读写顺序。客户端先 writeInt,再 writeUTF;服务端也必须先 readInt,再 readUTF。TCP 传的是连续字节流,不会自动理解业务含义,双方必须约定好数据格式和读取顺序。
2. TCP 多发多收
demo5tcp2 把客户端发送放进循环里:
java
while (true) {
System.out.println("请说:");
String text = sc.nextLine();
if ("exit".equals(text)) {
System.out.println("退出");
dos.close();
break;
}
dos.writeUTF(text);
dos.flush();
}
服务端对应地循环读取:
java
while (true) {
String message = dis.readUTF();
System.out.println("内容:" + message);
System.out.println("客户端的ip" + socket.getInetAddress());
System.out.println("客户端的端口" + socket.getPort());
}
这里的 flush 很关键。输出流可能有缓冲,如果写完后迟迟不刷新,接收端可能一直读不到数据。多发多收时,写一次消息后及时刷新,能让服务端更快拿到本次输入。
3. TCP 支持多个客户端
demo5tcp2 的服务端只能处理一个客户端。因为 accept 接到一个连接后,当前线程就进入读取循环,无法回到 accept 接下一个连接。
demo6tcp3 用多线程解决这个问题:
java
while (true) {
Socket socket = ss.accept();
System.out.println("一个客户端上线了" + socket.getInetAddress().getHostAddress());
new ServerReader(socket).start();
}
每接入一个客户端,就创建一个 ServerReader 线程专门负责读取这个客户端的消息:
java
public class ServerReader extends Thread {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
DataInputStream dis = new DataInputStream(socket.getInputStream());
while (true) {
String message = dis.readUTF();
System.out.println("收到的客户端message:" + message);
System.out.println("客户端的IP" + socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口" + socket.getPort());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
主线程只负责不断接收新连接;子线程负责和某一个客户端持续通信。这就是一个很基础的 TCP 多客户端模型。
五、UDP 和 TCP 怎么选
UDP 和 TCP 的区别可以从"有没有连接"和"可靠性"两条线来记。
| 对比点 | UDP | TCP |
|---|---|---|
| 是否连接 | 不需要连接 | 必须先建立连接 |
| 数据形式 | 数据包 | 字节流 |
| 可靠性 | 不保证可靠到达 | 可靠传输 |
| 接收方式 | 接收一个个数据包 | 从连接流中持续读取 |
| 常见场景 | 直播、语音、广播 | 文件传输、登录、支付 |
选择时可以问自己三个问题:
- 这条数据丢了能不能接受?
- 双方是否需要维持一个稳定连接?
- 数据顺序和完整性是不是必须保证?
如果答案偏向"必须可靠、必须按顺序、不能丢",优先考虑 TCP。如果答案偏向"实时性更重要,偶尔丢一点可以接受",UDP 更合适。
六、运行验证和常见排查
这些示例都可以按"先服务端,后客户端"的顺序验证。
UDP 验证:
- 先运行 demo2udp 或 demo3udp2 中的服务端,让它绑定 8080。
- 再运行对应客户端。
- 如果服务端打印消息、发送方 IP 和端口,说明数据包已经收到。
TCP 验证:
- 先运行服务端,让它监听 9999。
- 再运行客户端发起连接。
- 如果服务端打印客户端消息、IP 和端口,说明连接和读取都正常。
常见问题可以按下面顺序排查:
| 现象 | 优先检查 |
|---|---|
| 客户端连接失败 | 服务端是否先启动,端口是否一致 |
| 服务端没有收到 UDP 消息 | 接收端端口是否绑定为 8080,发送目标地址是否正确 |
| TCP 服务端一直卡住 | accept 或读取方法本来就是阻塞等待,先确认客户端是否真的连接或发送 |
| 读取内容错乱 | DataOutputStream 和 DataInputStream 的写入、读取顺序是否一致 |
| 多客户端只能连一个 | 服务端是否为每个 Socket 单独开线程 |
| 命令行运行客户端失败 | 检查 main 方法是否声明为 public static void main |
这里最后一点来自当前代码细节:部分 ClientDemo1 写成了 static void main。如果用命令行 java 直接运行,标准入口方法需要 public static void main。学习时不一定要急着改代码,但排查运行失败时要先看这个位置。
七、总结
网络通信入门可以按一条链路理解:
IP 找主机,端口找程序,协议决定传输方式。UDP 是把字节封进数据包直接发,写法简单但不保证可靠;TCP 是先建立连接,再通过输入输出流稳定传输,可靠性更强,也需要处理连接、阻塞、读写顺序和多客户端问题。
以后再遇到网络通信代码,可以先判断它在解决哪一层问题:是在找地址、绑定端口、封装数据包,还是建立连接、读写字节流。把这条线分清,InetAddress、DatagramSocket、DatagramPacket、Socket、ServerSocket 这些类就不会乱成一团。
参考资料
- Oracle Java SE 26 API:java.net 包
https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/net/package-summary.html - Oracle Java SE 26 API:DatagramSocket
https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/net/DatagramSocket.html - Oracle Java SE 26 API:DatagramPacket
https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/net/DatagramPacket.html - Oracle Java SE 26 API:Socket
https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/net/Socket.html - Oracle Java SE 26 API:ServerSocket
https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/net/ServerSocket.html