【JAVASE | 第十七篇】Java 网络通信

学 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));

这段代码可以拆成三件事:

  1. getLocalHost 获取本机地址对象。
  2. getByName 根据域名或 IP 字符串获取目标主机地址对象。
  3. 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
是否连接 不需要连接 必须先建立连接
数据形式 数据包 字节流
可靠性 不保证可靠到达 可靠传输
接收方式 接收一个个数据包 从连接流中持续读取
常见场景 直播、语音、广播 文件传输、登录、支付

选择时可以问自己三个问题:

  1. 这条数据丢了能不能接受?
  2. 双方是否需要维持一个稳定连接?
  3. 数据顺序和完整性是不是必须保证?

如果答案偏向"必须可靠、必须按顺序、不能丢",优先考虑 TCP。如果答案偏向"实时性更重要,偶尔丢一点可以接受",UDP 更合适。

六、运行验证和常见排查

这些示例都可以按"先服务端,后客户端"的顺序验证。

UDP 验证:

  1. 先运行 demo2udp 或 demo3udp2 中的服务端,让它绑定 8080。
  2. 再运行对应客户端。
  3. 如果服务端打印消息、发送方 IP 和端口,说明数据包已经收到。

TCP 验证:

  1. 先运行服务端,让它监听 9999。
  2. 再运行客户端发起连接。
  3. 如果服务端打印客户端消息、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 这些类就不会乱成一团。

参考资料

相关推荐
执于代码1 小时前
Java交互打印的问题
java
飞舞哲2 小时前
三维点云最小二乘拟合MATLAB程序
开发语言·算法·matlab
有点。2 小时前
C++(贪心算法二)
开发语言·c++·贪心算法
meilindehuzi_a2 小时前
透视 V8 底部:从物理内存到函数式哲学,重新解构 JavaScript 数组
开发语言·javascript·ecmascript
jllllyuz2 小时前
HVDC 高压直流输电系统 MATLAB/Simulink 仿真全集
开发语言·matlab
我命由我123452 小时前
Windows 操作系统 - Windows 查看防火墙是否开启、Windows 查看防火墙放行端口
java·运维·开发语言·windows·java-ee·操作系统·运维开发
fly spider2 小时前
Spring 原理总览:从启动到请求执行
java·数据库·spring
天天进步20152 小时前
Python全栈项目--基于Python的数据库管理工具
开发语言·数据库·python
YHHLAI2 小时前
JavaScript 数据结构精讲:数组底层与实战避坑
开发语言·javascript·数据结构