Skip to content

S07-02 网络编程-TCP与UDP编程

[TOC]

TCP 编程

Java 中的 TCP 编程主要基于 java.net 包提供的 Socket API 实现。TCP 是一种面向连接、可靠的、基于字节流的传输层通信协议。

为了便于系统梳理,本文首先介绍 TCP 编程的核心概念与基础通信实现,后续可以根据实际需求延伸至多线程并发处理与 NIO 高性能架构。

核心概念

在 Java 传统阻塞式 I/O(BIO)模型下,TCP 通信依赖两个关键类:

  • ServerSocket:运行在服务端,负责监听指定端口并接收客户端的连接请求。
  • Socket:代表通信节点。客户端使用它向服务端发起连接,服务端在建立连接后也会获得一个 Socket 对象,双方通过该对象进行双向数据读写。

通信流程

TCP 套接字的通信建立在输入流(InputStream)与输出流(OutputStream)的交叉对接上:

服务端工作流程

ServerSocket 的整个生命周期可以划分为四个阶段:

  1. 绑定端口:创建 ServerSocket 实例并将其绑定到特定的网络端口(如 8080)。

  2. 阻塞等待:调用 accept() 进入阻塞状态,挂起当前线程直到客户端发起 TCP 连接请求。

  3. 完成建立:底层完成 TCP 三次握手后,accept() 方法返回一个代表该连接的 Socket 对象。

  4. 循环与关闭:服务端通常在循环中持续调用 accept() 处理多连接,服务停止时调用 close() 释放端口。

客户端工作流程

Socket 在客户端的使用通常遵循以下生命周期:

  1. 建立连接:指定目标服务器的 IP 地址与端口,发起 TCP 三次握手。

  2. 获取数据流:通过 getInputStream() 获取输入流读取数据,通过 getOutputStream() 获取输出流发送数据。

  3. 数据传输:借由字节流或包装后的字符流/缓冲流进行双向通信。

  4. 半关闭与全关闭:可选择使用 shutdownOutput() / shutdownInput() 仅关闭单向数据通道;传输完毕后调用 close() 关闭套接字,释放底层资源。

代码示例

下面展示一个基础的单次交互示例:客户端发送一条消息,服务端接收并做出响应。

服务端实现

java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
  public static void main(String[] args) {
    int port = 8080;

    // 1. 绑定端口并监听连接
    try (ServerSocket serverSocket = new ServerSocket(port)) {
      System.out.println("服务端已启动,等待客户端连接...");

      // 2. 阻塞等待客户端建立连接
      try (Socket socket = serverSocket.accept();
         BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
         PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {

        System.out.println("客户端已连接,IP: " + socket.getInetAddress());

        // 3. 读取客户端消息
        String clientMsg = in.readLine();
        System.out.println("收到客户端消息: " + clientMsg);

        // 4. 返回响应消息
        out.println("服务端已收到你的消息: " + clientMsg);
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

客户端实现

java
import java.io.*;
import java.net.Socket;

public class TCPClient {
  public static void main(String[] args) {
    String host = "127.0.0.1";
    int port = 8080;

    // 1. 发起连接并创建输入输出流
    try (Socket socket = new Socket(host, port);
       PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
       BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

      System.out.println("已成功连接到服务端。");

      // 2. 向服务端发送数据
      out.println("Hello, Java TCP Server!");

      // 3. 读取服务端的响应
      String response = in.readLine();
      System.out.println("服务端响应: " + response);

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

关键细节

  1. 阻塞特性accept()read()/readLine() 方法均为阻塞式操作。如果没有连接请求或数据未到达,线程会卡在当前调用处。

  2. 资源管理:使用 try-with-resources 可以确保在抛出异常或通信结束后自动关闭流和套接字,防止端口占用和内存泄漏。

  3. 缓冲区刷盘:在利用 PrintWriterBufferedWriter 发送数据时,建议开启自动刷盘(如 new PrintWriter(out, true))或手动调用 flush(),否则数据可能会滞留在内存缓冲区中未及时发出。

InetAddress~

java.net.InetAddress 是 Java 标准库中用于表示 IP 地址(包含 IPv4 和 IPv6)的核心类。它封装了主机名与 IP 地址之间的映射关系,是所有底层网络通信(如 Socket、DatagramSocket、URL 连接等)的基础。

类定位

在 Java 网络编程中,InetAddress 充当着网络节点标识符的角色:

  • 无公有构造方法:不能通过 new InetAddress() 实例化,必须通过其提供的静态工厂方法来获取实例。
  • 不可变对象(Immutable):实例一旦创建,其包含的 IP 地址和主机名数据不可更改,天然具备线程安全性。
  • 继承体系
    • InetAddress(抽象父类,定义通用接口)
      • Inet4Address(IPv4 地址实现类,如 192.168.1.1
      • Inet6Address(IPv6 地址实现类,如 fe80::1

解析原理

当通过域名或主机名来获取 InetAddress 实例时,Java 会触发域名解析(DNS Lookup)过程

  1. JVM 缓存检索:首先检查 JVM 内部是否已存在该域名的解析缓存。

  2. 操作系统解析:若 JVM 未命中,调用操作系统的解析接口(如 getaddrinfo),检查本地 hosts 文件和系统 DNS 缓存。

  3. 网络 DNS 查询:若本地均无记录,向配置的 DNS 服务器发送网络查询请求。

  4. 封装对象:获取到 IP 地址字节后,JVM 会根据 IP 版本创建对应的 Inet4AddressInet6Address 实例返回。

API:InetAddress

工厂方法
  • InetAddress InetAddress.getByName()(String host)根据主机名/IP 字符串获取地址对象。将传入的主机名(如 "example.com")解析为 IP 地址,或将 IP 文本(如 "192.168.1.1")直接转化为地址对象。

  • InetAddress InetAddress.getLocalHost()()获取本机地址对象。通过查找操作系统的本机主机名解析出本机的 IP 地址。

  • InetAddress[] InetAddress.getAllByName()(String host)根据主机名获取所有关联 IP 地址。当一个域名绑定了多个 IP(如 CDN 或负载均衡)时,返回包含所有对应 IP 地址的数组。

  • InetAddress InetAddress.getByAddress()(byte[] addr)根据原始 IP 字节数组创建对象。依据传入的 4 字节(IPv4)或 16 字节(IPv6)网络字节序(Big-Endians)数组直接构造地址对象,不触发 DNS 解析

  • InetAddress InetAddress.getByAddress()(String host, byte[] addr)根据主机名与原始 IP 字节数组创建对象。同时指定主机名与字节数组 IP,避免触发网络 DNS 校验。

  • InetAddress InetAddress.getLoopbackAddress()()获取本地环回地址。返回代表 IPv4 环回(127.0.0.1)或 IPv6 环回(::1)的地址对象。

注意事项

  1. InetAddress.getByName(String host)InetAddress.getAllByName(String host) 会触发网络 DNS 查询。如果域名不存在、网络断开或 DNS 服务器无响应,会抛出 java.net.UnknownHostException(继承自 IOException),且可能导致当前线程长时间阻塞。
  2. InetAddress.getByAddress(byte[] addr) 必须严格保证传入的字节数组长度为 4 或 16。若传入其他长度的数组,将抛出 UnknownHostException。由于 Java 中 byte 为有符号类型(-128 至 127),表示大于 127 的 IP 字节(如 192)时需显式强转,例如 (byte) 192
  3. InetAddress.getLocalHost() 在多网卡、VPN 或 /etc/hosts 主机名配置不当的环境下,可能抛出 UnknownHostException 或返回非预期的内网/虚拟网卡 IP。
java
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;

public class InetAddressFactoryDemo {
  public static void main(String[] args) {
    try {
      // 1. 根据域名解析单个 IP 地址
      InetAddress singleIp = InetAddress.getByName("example.com");
      System.out.println("example.com 解析结果: " + singleIp);

      // 2. 根据域名解析所有关联 IP 地址(多 IP/负载均衡场景)
      InetAddress[] allIps = InetAddress.getAllByName("baidu.com");
      System.out.println("baidu.com 绑定的 IP 列表: " + Arrays.toString(allIps));

      // 3. 根据原生字节数组构造 IP(无需 DNS 解析,注意 byte 强转)
      byte[] ipv4Bytes = new byte[]{(byte) 192, (byte) 168, 1, 100};
      InetAddress customIp = InetAddress.getByAddress(ipv4Bytes);
      System.out.println("字节数组直接构造结果: " + customIp.getHostAddress());

      // 4. 获取本机 IP 地址
      InetAddress localHost = InetAddress.getLocalHost();
      System.out.println("本机地址: " + localHost);

      // 5. 获取本地环回地址(Loopback)
      InetAddress loopback = InetAddress.getLoopbackAddress();
      System.out.println("环回地址: " + loopback);

    } catch (UnknownHostException e) {
      System.err.println("域名解析失败或网络异常: " + e.getMessage());
    }
  }
}
地址信息获取
  • String getHostName()()获取主机名。返回此 IP 地址的主机名。如果该对象是通过主机名创建的则直接返回;否则会触发反向 DNS 查找(Reverse DNS Lookup)。

  • String getHostAddress()()获取文本格式 IP 地址。返回标准的 IP 地址文本表示(如 IPv4 点分十进制 "192.168.1.1" 或 IPv6 冒分十六进制)。

  • String getCanonicalHostName()()获取全限定域名(FQDN)。强行触发反向 DNS 查询,获取该 IP 地址在系统网络中完全合格的域名。

  • byte[] getAddress()()获取原始 IP 字节数组。返回网络字节序(高位字节在前,Big-Endian)的 IP 原始字节数组。

注意事项

  1. 调用 getHostName()getCanonicalHostName() 时,若对象初始只包含 IP 地址,系统会尝试向 DNS 发起反向域名解析(PTR 记录查询)。如果目标 IP 没有配置 PTR 记录或网络超时,会导致严重的方法调用卡顿,最终超时后回退返回 IP 地址字符串本身。
  2. getAddress() 返回的 byte[] 数组是对象内部字节数组的浅拷贝(Clone),修改该数组不会影响 InetAddress 内部状态,但读取字节数值时需特别注意无符号转换:int unsignedByte = byteValue & 0xFF
java
import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressInfoDemo {
  public static void main(String[] args) {
    try {
      // 通过 IP 文本构建对象
      InetAddress address = InetAddress.getByName("8.8.8.8");

      // 1. 获取字符串格式 IP 地址(不触发网络反查)
      String ipString = address.getHostAddress();
      System.out.println("IP 文本字符串: " + ipString);

      // 2. 获取原始字节数组并转为无符号整数展示
      byte[] rawBytes = address.getAddress();
      System.out.print("原始字节数组(无符号转换): ");
      for (int i = 0; i < rawBytes.length; i++) {
        int unsignedByte = rawBytes[i] & 0xFF;
        System.out.print(unsignedByte + (i < rawBytes.length - 1 ? "." : ""));
      }
      System.out.println();

      // 3. 获取主机名(若无主机名可能触发反向 DNS 查找)
      System.out.println("获取主机名 (getHostName): " + address.getHostName());

      // 4. 获取规范全限定域名(强行反查)
      System.out.println("获取 FQDN (getCanonicalHostName): " + address.getCanonicalHostName());

    } catch (UnknownHostException e) {
      e.printStackTrace();
    }
  }
}
状态判断与检测
  • boolean isReachable()(int timeout)测试地址可达性(Ping)。在指定的超时毫秒数内,尝试检测当前地址是否可达。

  • boolean isReachable()(NetworkInterface netif, int ttl, int timeout)通过指定网卡和 TTL 测试可达性。通过指定的网络接口、生存时间(TTL)与超时毫秒数测试地址是否可达。

  • boolean isLoopbackAddress()()检测是否为环回地址。检查地址是否属于 IPv4(127.0.0.0/8)或 IPv6(::1)环回地址。

  • boolean isAnyLocalAddress()()检测是否为通配符地址。检查地址是否为通配地址(Wildcard Address,即 0.0.0.0::)。

  • boolean isSiteLocalAddress()()检测是否为私有/局域网地址。检查地址是否属于专用网络/局域网地址(如 IPv4 的 10.0.0.0/8172.16.0.0/12192.168.0.0/16)。

  • boolean isMulticastAddress()()检测是否为组播地址。检查地址是否属于 IP 组播地址(IPv4 的 D 类地址 224.0.0.0 ~ 239.255.255.255 或 IPv6 的 FF00::/8)。

注意事项

  1. isReachable(int timeout) 的底层实现取决于运行环境权限:如果 JVM 具有根权限(Root/Administrator),它会尝试发送原生 ICMP ECHO REQUEST 包(标准 Ping);若缺乏根权限,它会退而求其次,尝试连接目标主机的 TCP Port 7 (Echo 端口)。由于现代防火墙普遍屏蔽 Port 7,无 Root 权限下 isReachable() 极易返回 false,不可完全替代系统原生 ping 指令。
java
import java.io.IOException;
import java.net.InetAddress;

public class InetAddressStatusDemo {
  public static void main(String[] args) {
    try {
      InetAddress local = InetAddress.getByName("127.0.0.1");
      InetAddress privateIp = InetAddress.getByName("192.168.1.1");
      InetAddress publicIp = InetAddress.getByName("1.1.1.1");

      // 1. 检查环回地址
      System.out.println("127.0.0.1 是否为环回地址: " + local.isLoopbackAddress());

      // 2. 检查局域网私有地址
      System.out.println("192.168.1.1 是否为局域网私有地址: " + privateIp.isSiteLocalAddress());
      System.out.println("1.1.1.1 是否为局域网私有地址: " + publicIp.isSiteLocalAddress());

      // 3. 检查通配符地址
      InetAddress wildcard = InetAddress.getByName("0.0.0.0");
      System.out.println("0.0.0.0 是否为通配符地址: " + wildcard.isAnyLocalAddress());

      // 4. 检测目标可达性(设置 2000 毫秒超时)
      System.out.println("尝试探测 1.1.1.1 可达性 (超时 2000ms)...");
      boolean reachable = publicIp.isReachable(2000);
      System.out.println("1.1.1.1 可达性测试结果: " + reachable);

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

DNS 缓存

为提升性能,JVM 默认会缓存 DNS 解析结果。缓存时长由两个关键系统参数决定:

  • networkaddress.cache.ttl解析成功记录的缓存有效时间(单位为)。

    • 若设为 -1:表示永久缓存(非安全管理器模式下,不同 JVM 默认值可能不同)。
    • 若设为 0禁用缓存
  • networkaddress.cache.negative.ttl解析失败记录的缓存有效时间(默认 10 秒)。

微服务与动态 IP 注意事项:

在 Kubernetes 或云服务(如 AWS ALB)环境下,由于服务节点的 IP 经常动态变动,如果 JVM 将 DNS 解析永久缓存(TTL = -1),当后端节点更新 IP 后,Java 应用将无法感知,导致持续请求旧 IP 而报错。生产环境中通常建议设置一个较短的 TTL 值(如 -Dnetworkaddress.cache.ttl=10

注意事项

  1. 同步阻塞特性getByName()getAllByName() 需要发起网络 DNS 查询,这属于同步阻塞操作。若 DNS 服务器响应慢,会直接卡住当前调用线程。

  2. isReachable() 的实现缺陷isReachable() 会优先尝试发送 ICMP Echo 报文(需要操作系统 root/admin 权限)。若缺乏权限,它会自动退化为尝试连接目标的 TCP 7 端口(Echo 端口)。如果目标服务器禁用了 7 端口,即使网络通畅,该方法也会返回 false

  3. InetSocketAddress 的区别

    • InetAddress:仅代表 IP 地址(或 IP + 主机名),不包含端口信息。
    • InetSocketAddress:代表 IP 地址 + 端口号(如 127.0.0.1:8080),通常直接传入 Socket.connect()ServerSocket 的绑定方法中。

API:ServerSocket

构造方法

  • ServerSocket ServerSocket()()构造方法。创建一个未绑定本地端口和地址的空套接字对象。常用于在绑定端口前需要提前配置套接字参数(如 SO_REUSEADDR)的场景。

  • ServerSocket ServerSocket()(int port)构造方法。创建套接字并将其自动绑定到指定的本地端口。采用默认的全连接队列长度(Backlog)。

  • ServerSocket ServerSocket()(int port, int backlog)构造方法。创建套接字并绑定到指定端口,同时显式指定内核全连接队列的最大容量(Backlog)。

  • ServerSocket ServerSocket()(int port, int backlog, InetAddress bindAddr)构造方法。创建套接字,绑定到指定的端口及指定的本地 IP 地址(适用于具备多网卡/多 IP 的服务器)。

注意事项

  1. 参数 port 传入 0 时,系统会自动从动态/私有端口范围(Ephemeral Ports)中随机分配一个可用的空闲端口。可通过 getLocalPort() 获取实际分配的端口。
  2. backlog 参数提示操作系统内核全连接队列的最大长度。当高并发场景下队列满载时,新的 TCP 连接请求可能会被拒(收到 RST)或被忽略(导致客户端超时重试)。若传入的值小于等于 0,则系统将使用平台缺省默认值(JDK 内部默认通常为 50)。
java
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;

public class ServerSocketConstructorDemo {
  public static void main(String[] args) {
    try {
      // 1. 无参构造方法:手动绑定,可灵活配置选项
      ServerSocket unboundServer = new ServerSocket();
      unboundServer.bind(new InetSocketAddress("127.0.0.1", 0), 100);
      System.out.println("绑定随机端口: " + unboundServer.getLocalPort());
      unboundServer.close();

      // 2. 指定端口与 Backlog 队列长度
      ServerSocket portServer = new ServerSocket(8081, 200);
      System.out.println("8081 端口服务端已就绪,Backlog=200");
      portServer.close();

      // 3. 绑定到特定网络接口(IP)
      InetAddress localAddr = InetAddress.getByName("127.0.0.1");
      ServerSocket specificIpServer = new ServerSocket(8082, 50, localAddr);
      System.out.println("绑定特定 IP: " + specificIpServer.getInetAddress().getHostAddress());
      specificIpServer.close();

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

连接监听与生命周期

  • Socket accept()()监听并接受客户端连接。阻塞当前线程,直到建立一个新的 TCP 连接,并返回与客户端通信的 Socket 对象。

  • void bind()(SocketAddress endpoint)绑定本地端点。将未绑定的套接字绑定到指定的 IP 地址和端口号。

  • void bind()(SocketAddress endpoint, int backlog)绑定本地端点并设置队列。绑定端点同时设置 TCP 连接挂起队列的最大长度。

  • boolean isBound()()检查绑定状态。返回套接字当前是否已成功绑定到本地端口。

  • boolean isClosed()()检查关闭状态。返回套接字当前是否已被关闭。

  • void close()()关闭套接字。释放该套接字绑定的端口与操作系统资源,同时中断处于阻塞状态的 accept() 调用。

注意事项

  1. accept() 为阻塞式方法(Blocking I/O)。若没有任何客户端尝试连接,当前线程会持续挂起,不占用 CPU 资源。
  2. 当线程被阻塞在 accept() 时,若在另一个线程中调用该 ServerSocketclose() 方法,accept() 会立刻抛出 SocketException 并终止阻塞。
  3. isBound() 方法仅标识该套接字历史上是否成功绑定过端口。即便调用了 close() 方法,isBound() 仍会返回 true。判断套接字是否处于正常可用状态,应综合判定:serverSocket.isBound() && !serverSocket.isClosed()
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

public class ServerSocketLifecycleDemo {
  public static void main(String[] args) {
    ServerSocket serverSocket = null;
    try {
      serverSocket = new ServerSocket();
      System.out.println("绑定前状态 -> isBound: " + serverSocket.isBound() + ", isClosed: " + serverSocket.isClosed());

      serverSocket.bind(new InetSocketAddress(8083));
      System.out.println("绑定后状态 -> isBound: " + serverSocket.isBound() + ", isClosed: " + serverSocket.isClosed());

      final ServerSocket targetServer = serverSocket;
      // 创建子线程模拟异步关闭 ServerSocket 以解除 accept 阻塞
      new Thread(() -> {
        try {
          Thread.sleep(2000);
          System.out.println("异步关闭 ServerSocket...");
          targetServer.close();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }).start();

      System.out.println("开始阻塞在 accept()...");
      Socket socket = serverSocket.accept(); // 将抛出 SocketException
    } catch (SocketException e) {
      System.out.println("捕获到套接字关闭异常(已安全解除 accept 阻塞): " + e.getMessage());
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (serverSocket != null) {
        System.out.println("最终状态 -> isBound: " + serverSocket.isBound() + ", isClosed: " + serverSocket.isClosed());
      }
    }
  }
}

套接字参数配置

  • void setSoTimeout()(int timeout)设置接收超时时间。设置 accept() 方法阻塞等待的最大毫秒数(设置 SO_TIMEOUT 选项)。超时后抛出 SocketTimeoutException,且 ServerSocket 保持有效状态。参数为 0 表示无限等待。

  • int getSoTimeout()()获取超时设置。返回当前 SO_TIMEOUT 选项的毫秒值。

  • void setReuseAddress()(boolean on)设置地址复用。启用或禁用 SO_REUSEADDR 套接字选项。处于 TIME_WAIT 状态的本地地址是否允许被快速重新绑定。

  • boolean getReuseAddress()()获取地址复用状态。返回当前 SO_REUSEADDR 选项的状态。

  • void setReceiveBufferSize()(int size)设置默认接收缓冲区大小。为由此 ServerSocket 接受(accept)建立的所有 Socket 设定默认的 TCP 接收缓冲区字节数大小(TCP Window Size)。

  • int getReceiveBufferSize()()获取接收缓冲区大小。返回当前设置的 TCP 接收缓冲区字节数大小。

注意事项

  1. setReuseAddress(boolean on) 必须在套接字绑定端口之前(即 bind() 调用前)配置,否则配置将不会对底层 Socket 产生任何效果。
  2. setReceiveBufferSize(int size) 也建议在 bind() 之前调用。这样设置的值才能通过 TCP SYN 报文中的 Window Scale 选项向对端通告初始滑动窗口大小(RFC 1323)。
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;

public class ServerSocketOptionsDemo {
  public static void main(String[] args) {
    try {
      ServerSocket serverSocket = new ServerSocket();

      // 1. 设置地址复用(必须在 bind 之前设置)
      serverSocket.setReuseAddress(true);
      System.out.println("SO_REUSEADDR 状态: " + serverSocket.getReuseAddress());

      // 2. 设置接收缓冲区大小(建议在 bind 之前设置)
      serverSocket.setReceiveBufferSize(64 * 1024); // 64 KB
      System.out.println("套接字接收缓冲区大小: " + serverSocket.getReceiveBufferSize() + " 字节");

      // 3. 绑定端口
      serverSocket.bind(new InetSocketAddress(8084));

      // 4. 设置 accept 阻塞超时时间为 1500 毫秒
      serverSocket.setSoTimeout(1500);
      System.out.println("SO_TIMEOUT 毫秒数: " + serverSocket.getSoTimeout());

      try {
        System.out.println("进入 accept 阻塞,等待客户端连接(将在 1.5 秒后超时)...");
        serverSocket.accept();
      } catch (SocketTimeoutException e) {
        System.err.println("accept 触发超时,无需终止服务端,可继续重试:" + e.getMessage());
      }

      serverSocket.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

API:Socket

构造方法

  • Socket Socket()()构造方法。创建一个未连接的套接字对象。常用于在连接前配置套接字参数(如 SO_TIMEOUTTCP_NODELAY)。

  • Socket Socket()(String host, int port)构造方法。创建套接字并自动连接到指定的主机名和端口号。使用默认的系统超时配置。

  • Socket Socket()(InetAddress address, int port)构造方法。创建套接字并自动连接到指定的 IP 地址和端口号。

  • Socket Socket()(String host, int port, InetAddress localAddr, int localPort)构造方法。创建套接字,连接到远程主机和端口,同时显式绑定本地特定的 IP 地址和本地端口号。

  • Socket Socket()(Proxy proxy)构造方法。使用指定的代理服务器(如 SOCKS 或 HTTP 代理)创建一个未连接的套接字对象。

注意事项

  1. 直接使用 Socket(String host, int port) 构造方法会立即触发阻塞式的 TCP 连接请求。如果网络不可达,该方法将持续阻塞直到底层操作系统超时(Linux 默认通常需要数十秒到 2 分钟)。在生产环境中,应推荐使用空参构造方法 Socket() 配合 connect(SocketAddress endpoint, int timeout) 来精准控制建连超时。
  2. 指定 localPort0 时,系统会自动分配任意可用的本地空闲端口。
java
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;

public class SocketConstructorDemo {
  public static void main(String[] args) {
    try {
      // 1. 无参构造方法:方便配置参数后再发起连接
      Socket socket1 = new Socket();
      System.out.println("socket1 创建完成,当前连接状态: " + socket1.isConnected());
      socket1.close();

      // 2. 绑定特定本地接口和本地端口的构造方法
      InetAddress localHost = InetAddress.getByName("127.0.0.1");
      Socket socket2 = new Socket("example.com", 80, localHost, 0);
      System.out.println("socket2 已连接,系统分配的本地端口: " + socket2.getLocalPort());
      socket2.close();

      // 3. 通过 Proxy 代理对象创建 Socket
      Proxy socksProxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", 1080));
      Socket socket3 = new Socket(socksProxy);
      System.out.println("socket3 代理套接字创建成功(未连接)");
      socket3.close();

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

连接建立与状态控制

  • void connect()(SocketAddress endpoint)建立套接字连接。将套接字连接到指定的远端端点。采用系统无限等待或默认超时设置。

  • void connect()(SocketAddress endpoint, int timeout)指定超时的连接建立。将套接字连接到远端端点,并设置连接建立的最大超时时间(单位:毫秒)。

  • boolean isConnected()()获取连接绑定状态。返回套接字历史上是否成功建立过 TCP 连接。

  • boolean isBound()()获取本地绑定状态。返回套接字是否已成功绑定到本地端口。

  • boolean isClosed()()获取关闭状态。返回套接字是否已被主动关闭。

  • void close()()关闭套接字。释放底层操作系统套接字句柄及与之关联的所有输入输出流。

注意事项

  1. isConnected() 返回 true 仅仅代表该 Socket 过去曾经成功与服务端建立过连接,并不代表当前网络链路上 TCP 连接依然健康通畅。套接字关闭后 isConnected() 依然会返回 true。要判断 Socket 当前的有效状态,应使用:socket.isConnected() && !socket.isClosed()
  2. connect() 方法中的 timeout0 时被视为无限期等待。若超时触发,将抛出 java.net.SocketTimeoutException
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;

public class SocketConnectionDemo {
  public static void main(String[] args) {
    Socket socket = new Socket();
    try {
      System.out.println("连接前 -> isConnected: " + socket.isConnected() + ", isClosed: " + socket.isClosed());

      // 设置 2000 毫秒连接超时
      InetSocketAddress address = new InetSocketAddress("10.255.255.1", 80); // 假定不可达 IP
      socket.connect(address, 2000);

    } catch (SocketTimeoutException e) {
      System.err.println("连接指定超时,触发 SocketTimeoutException: " + e.getMessage());
    } catch (IOException e) {
      System.err.println("网络连接发生异常: " + e.getMessage());
    } finally {
      System.out.println("连接尝试后 -> isBound: " + socket.isBound() + ", isClosed: " + socket.isClosed());
      try {
        if (!socket.isClosed()) {
          socket.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
      System.out.println("关闭套接字后 -> isConnected: " + socket.isConnected() + ", isClosed: " + socket.isClosed());
    }
  }
}

数据传输与半关闭控制

  • InputStream getInputStream()()获取套接字输入流。返回用于从该套接字读取字节数据的输入流。

  • OutputStream getOutputStream()()获取套接字输出流。返回用于向该套接字写入字节数据的输出流。

  • void shutdownInput()()半关闭输入通道。终止当前套接字的输入流,后续试图从该流读取数据都将直接返回 -1(EOF)。

  • void shutdownOutput()()半关闭输出通道。终止当前套接字的输出流,并向对端发送 TCP FIN 包,告知对方本端已无数据发送。

  • boolean isInputShutdown()()获取输入流半关闭状态。返回该套接字的输入通道是否已单向关闭。

  • boolean isOutputShutdown()()获取输出流半关闭状态。返回该套接字的输出通道是否已单向关闭。

注意事项

  1. 切勿直接关闭包装套接字的流(如调用 inputStream.close()outputStream.close())。这会导致整个底层 Socket 被直接关闭,从而破坏全双工通信通道。
  2. shutdownOutput() 是实现单向单工通知(如 HTTP 协议或请求应答模型中显式通知对方“请求报文已发送完毕”)的关键机制,它允许在关闭写通道的同时继续保持读通道可用。
java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class SocketDataStreamDemo {
  public static void main(String[] args) {
    try (Socket socket = new Socket()) {
      socket.connect(new InetSocketAddress("httpbin.org", 80), 3000);

      // 获取输出流写入数据
      PrintWriter writer = new PrintWriter(
        new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
      writer.print("GET /get HTTP/1.0\r\nHost: httpbin.org\r\n\r\n");
      writer.flush();

      // 发送 FIN 告知服务端写入完毕(半关闭输出流)
      socket.shutdownOutput();
      System.out.println("输出通道已关闭: isOutputShutdown = " + socket.isOutputShutdown());
      System.out.println("输入通道状态: isInputShutdown = " + socket.isInputShutdown());

      // 输入流依然可用,正常读取服务端返回数据
      BufferedReader reader = new BufferedReader(
        new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
      String line;
      System.out.println("--- 开始接收服务端数据 ---");
      while ((line = reader.readLine()) != null) {
        System.out.println(line);
        break; // 仅读取第一行示例
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

套接字选项配置

  • void setSoTimeout()(int timeout)设置读取超时时间。设置 InputStream.read() 操作的最大阻塞毫秒数(SO_TIMEOUT)。超过该时长抛出 SocketTimeoutException

  • int getSoTimeout()()获取读取超时时间。返回当前的 SO_TIMEOUT 参数毫秒值。

  • void setTcpNoDelay()(boolean on)配置 Nagle 算法。启用或禁用 TCP_NODELAY 选项。设置为 true 时禁用 Nagle 算法,强制小数据包立即发送,大幅降低高频小包的延时。

  • boolean getTcpNoDelay()()获取 TCP_NODELAY 状态。返回当前 Nagle 算法是否已禁用。

  • void setSoLinger()(boolean on, int linger)配置关闭挂起时间。设置 SO_LINGER 选项。控制调用 close() 时,底层缓冲区未发送数据时的阻塞滞留秒数。

  • int getSoLinger()()获取 SO_LINGER 配置。返回当前的 SO_LINGER 滞留时间(秒)。未开启则返回 -1

  • void setKeepAlive()(boolean on)设置 TCP 保持连接探针。启用或禁用 SO_KEEPALIVE 选项。长时间无数据交互时由内核发送心跳包检测链路存活。

  • boolean getKeepAlive()()获取 SO_KEEPALIVE 状态。返回当前保持连接探针的状态。

  • void setSendBufferSize()(int size)设置发送缓冲区容量。设置内核 TCP 发送缓冲区(SO_SNDBUF)的字节数大小。

  • void setReceiveBufferSize()(int size)设置接收缓冲区容量。设置内核 TCP 接收缓冲区(SO_RCVBUF)的字节数大小。

注意事项

  1. setTcpNoDelay(true) 适合游戏、RPC、实时通讯等对延时极度敏感的场景;但对于高吞吐量写密集型应用,禁用 Nagle 算法可能会增加网络小包数量,降低网络整体利用率。
  2. setSoLinger(true, 0) 会在调用 close()直接丢弃内核发送缓冲区中的所有数据,并向对端强行发送 TCP RST(重置连接),跳过正常四次挥手流程。需慎用。
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;

public class SocketOptionsDemo {
  public static void main(String[] args) {
    try (Socket socket = new Socket()) {
      // 1. 配置读取超时 SO_TIMEOUT = 5000ms
      socket.setSoTimeout(5000);
      System.out.println("SO_TIMEOUT: " + socket.getSoTimeout() + " ms");

      // 2. 禁用 Nagle 算法,降低实时交互延迟
      socket.setTcpNoDelay(true);
      System.out.println("TCP_NODELAY (Nagle 禁用): " + socket.getTcpNoDelay());

      // 3. 启用 TCP 存活探针
      socket.setKeepAlive(true);
      System.out.println("SO_KEEPALIVE: " + socket.getKeepAlive());

      // 4. 配置 SO_LINGER,在 close 时最多逗留等待 3 秒以确保缓冲区数据发送完毕
      socket.setSoLinger(true, 3);
      System.out.println("SO_LINGER 滞留秒数: " + socket.getSoLinger());

      // 5. 连接服务端
      socket.connect(new InetSocketAddress("example.com", 80), 3000);

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

UDP 编程

Java 中的 UDP 编程基于 java.net 包提供的 DatagramSocketDatagramPacket 两个类实现。UDP(用户数据报协议) 是一种无连接、不可靠但传输效率极高的传输层协议

与 TCP 的字节流不同,UDP 以数据报(Datagram) 为独立单位进行传输,每个数据报中都包含了完整的目的地址和端口信息

核心概念

Java 将 UDP 通信解耦为“传输通道”与“数据载体”两个部分:

  • DatagramSocket:代表通信的套接字端点,负责在网络中发送和接收数据报。可以将其比作邮筒或信箱
  • DatagramPacket:代表具体的数据报文。包含待传输的字节数组,以及目标的 IP 地址和端口号(发送时)或发送方的 IP 地址和端口号(接收时)。可以将其比作装有内容和地址的信封

通信流程

由于 UDP 无需建立连接,发送端和接收端的地位是相对平等的:

  1. 接收端准备:创建 DatagramSocket 并绑定指定端口;准备一个空字节数组和 DatagramPacket 容器,调用 receive() 阻塞等待。

  2. 发送端打包:创建 DatagramSocket;将要发送的数据转为字节数组,连同目标的 IP 地址与端口封装进 DatagramPacket

  3. 数据传输:发送端调用 send() 方法将数据包投递出去。

  4. 接收与解析:接收端的 receive() 方法收到数据包后返回,随后从 DatagramPacket 中提取数据内容及发送者信息。

示例:UDP 客户端与服务端通信

以下演示一个完整的 UDP 客户端与服务端通信过程。

接收端

java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;

public class UDPServer {
  public static void main(String[] args) {
    int port = 8088;

    // 1. 绑定 8088 端口监听
    try (DatagramSocket socket = new DatagramSocket(port)) {
      System.out.println("UDP 服务端已启动,等待接收数据包...");

      // 2. 准备 1KB 的缓冲区来接收数据
      byte[] buffer = new byte[1024];
      DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

      // 3. 阻塞等待客户端发送的数据
      socket.receive(packet);

      // 4. 解析接收到的字节数据
      String receivedMsg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
      System.out.println("收到来自 [" + packet.getAddress() + ":" + packet.getPort() + "] 的消息: " + receivedMsg);

      // 5. 给客户端发送回执
      String response = "服务端已成功收到数据!";
      byte[] respBytes = response.getBytes(StandardCharsets.UTF_8); // 字符串转字节
      DatagramPacket respPacket = new DatagramPacket(
          respBytes, respBytes.length, packet.getAddress(), packet.getPort()
      );
      socket.send(respPacket);

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

发送端

java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

public class UDPClient {
  public static void main(String[] args) {
    String serverHost = "127.0.0.1";
    int serverPort = 8088;

    // 1. 随机端口初始化 Socket
    try (DatagramSocket socket = new DatagramSocket()) {
      socket.setSoTimeout(3000); // 设置接收超时 3 秒

      // 2. 构建要发送的数据包
      String message = "Hello, Java UDP Server!";
      byte[] data = message.getBytes(StandardCharsets.UTF_8);
      InetAddress serverAddr = InetAddress.getByName(serverHost);

      DatagramPacket packet = new DatagramPacket(data, data.length, serverAddr, serverPort);

      // 3. 发送数据包
      System.out.println("正在向服务端发送 UDP 数据包...");
      socket.send(packet);

      // 4. 接收服务端的响应
      byte[] buffer = new byte[1024];
      DatagramPacket respPacket = new DatagramPacket(buffer, buffer.length);
      socket.receive(respPacket);

      String response = new String(respPacket.getData(), 0, respPacket.getLength(), StandardCharsets.UTF_8);
      System.out.println("收到服务端响应: " + response);

    } catch (IOException e) {
      System.err.println("通信失败或超时: " + e.getMessage());
    }
  }
}

API:DatagramSocket

构造方法

  • DatagramSocket DatagramSocket()()构造方法。创建一个未绑定到任何本地地址和端口的数据报套接字。需配合 bind() 方法使用,或在首次 send() 时由系统自动分配随机空闲端口。

  • DatagramSocket DatagramSocket()(int port)构造方法。创建数据报套接字并将其绑定到指定的本地端口。通常用于 UDP 服务端绑定固定端口

  • DatagramSocket DatagramSocket()(int port, InetAddress laddr)构造方法。创建数据报套接字,绑定到指定的本地端口和特定的本地 IP 地址(适用于多网卡服务器环境)。

  • DatagramSocket DatagramSocket()(SocketAddress bindaddr)构造方法。创建数据报套接字并绑定到指定的 SocketAddress(封装 IP 地址与端口)。

注意事项

  1. 参数 port 传入 0 时,操作系统会自动分配一个未使用的动态/私有端口(Ephemeral Port)。可通过 getLocalPort() 获取实际绑定的端口。
  2. 若指定的端口已经被其他进程(或套接字)占用,构造方法将抛出 java.net.SocketException: Address already in use
java
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;

public class DatagramSocketConstructorDemo {
  public static void main(String[] args) {
    try {
      // 1. 无参构造方法:未绑定状态
      DatagramSocket socket1 = new DatagramSocket(null); // 明确传入 null 表示不绑定
      socket1.bind(new InetSocketAddress("127.0.0.1", 0)); // 手动绑定随机端口
      System.out.println("socket1 绑定成功,自动分配端口: " + socket1.getLocalPort());
      socket1.close();

      // 2. 指定端口构造方法
      DatagramSocket socket2 = new DatagramSocket(9091);
      System.out.println("socket2 成功绑定端口: " + socket2.getLocalPort());
      socket2.close();

      // 3. 绑定特定网卡 IP 与端口
      InetAddress localAddr = InetAddress.getByName("127.0.0.1");
      DatagramSocket socket3 = new DatagramSocket(9092, localAddr);
      System.out.println("socket3 绑定特定 IP: " + socket3.getLocalAddress());
      socket3.close();

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

数据报发送与接收

  • void send()(DatagramPacket p)发送数据包。从给定的 DatagramPacket 中提取数据、目标 IP 和目标端口,通过底层 UDP 套接字直接投递到网络。

  • void receive()(DatagramPacket p)接收数据包阻塞当前线程,直到接收到一个 UDP 数据包并将数据填入传入的 DatagramPacket 缓冲区中。

注意事项

  1. receive(DatagramPacket p)阻塞式方法。若网络上没有数据包到达,当前线程会持续挂起,除非设置了 SO_TIMEOUT 超时在其他线程中显式调用了 close()
  2. 数据截断问题:传入 receive()DatagramPacket 内部缓冲区长度若小于实际收到的 UDP 报文长度,多余的字节会被操作系统内核直接静默丢弃(Truncated),不会抛出异常。因此,接收缓冲区建议预留足够容量(例如 UDP 最大有效载荷为 65507 字节,或根据应用层协议约定设置大小)。
java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

public class DatagramSocketSendReceiveDemo {
  public static void main(String[] args) {
    try (DatagramSocket receiver = new DatagramSocket(9093);
       DatagramSocket sender = new DatagramSocket()) {

      // 线程:异步发送数据
      new Thread(() -> {
        try {
          Thread.sleep(500); // 模拟延迟
          byte[] data = "UDP 通信数据载荷".getBytes(StandardCharsets.UTF_8);
          DatagramPacket packet = new DatagramPacket(
              data, data.length, InetAddress.getByName("127.0.0.1"), 9093);
          sender.send(packet);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }).start();

      // 主线程:接收数据
      byte[] buffer = new byte[512];
      DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
      System.out.println("等待接收数据包...");
      receiver.receive(receivePacket); // 阻塞挂起

      String msg = new String(receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8);
      System.out.println("成功接收数据: " + msg + " (字节长度: " + receivePacket.getLength() + ")");

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

虚拟连接与地址绑定

  • void connect()(InetAddress address, int port)创建虚拟连接。将套接字锁定到远程特定主机的指定端口。连接后,此套接字只能向该远端发送数据,也只能接收来自该远端的数据包。

  • void connect()(SocketAddress addr)创建虚拟连接。使用 SocketAddress 锁定远端目标端点。

  • void disconnect()()断开虚拟连接。解除对特定远端地址的锁定,恢复为可向任意地址发送/接收数据包的自由模式。

  • void bind()(SocketAddress addr)绑定本地端点。将未绑定的套接字绑定到指定的本地 IP 和端口。

  • boolean isConnected()()获取虚拟连接状态。返回套接字当前是否已锁定指定的远程主机与端口。

  • boolean isBound()()获取绑定状态。返回套接字是否已成功绑定到本地端口。

  • boolean isClosed()()获取关闭状态。返回套接字是否已被主动关闭。

  • void close()()关闭套接字。释放底层套接字句柄及端口占用,中断处于阻塞状态的 receive() 调用。

注意事项

  1. UDP 的 connect() 方法完全不涉及网络握手或传输层的逻辑连接,它仅仅是一个操作系统内核级别的过滤限制(Kernel Filter)。
  2. 调用 connect() 后,内核会自动过滤掉非连接目标发来的 UDP 数据包,并且调用 send() 时无须在 DatagramPacket 中再次提供目标 IP 和端口。此外,若发送的目标 ICMP 不可达,内核能够将错误信息反馈给 Java 层(抛出 PortUnreachableException)。
java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

public class DatagramSocketConnectDemo {
  public static void main(String[] args) {
    try {
      DatagramSocket socket = new DatagramSocket();
      socket.bind(new InetSocketAddress("127.0.0.1", 0));

      System.out.println("绑定状态 (isBound): " + socket.isBound());
      System.out.println("虚拟连接状态 (isConnected): " + socket.isConnected());

      // 建立 UDP 虚拟连接(锁定远端 127.0.0.1:9094)
      InetSocketAddress remoteAddr = new InetSocketAddress("127.0.0.1", 9094);
      socket.connect(remoteAddr);
      System.out.println("执行 connect 后 (isConnected): " + socket.isConnected());

      // 试图发送数据包(此时 DatagramPacket 无需显式指定远端地址)
      byte[] data = "Connected UDP Packet".getBytes();
      DatagramPacket packet = new DatagramPacket(data, data.length);
      socket.send(packet);

      // 解除虚拟连接限制
      socket.disconnect();
      System.out.println("执行 disconnect 后 (isConnected): " + socket.isConnected());

      socket.close();
      System.out.println("关闭套接字后 (isClosed): " + socket.isClosed());

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

套接字参数与控制

  • void setSoTimeout()(int timeout)设置读取超时时间。配置 receive() 方法阻塞等待的最大毫秒数(SO_TIMEOUT 选项)。超过该时长将抛出 SocketTimeoutException0 表示无限等待。

  • int getSoTimeout()()获取读取超时时间。返回当前 SO_TIMEOUT 的毫秒值。

  • void setBroadcast()(boolean broadcast)设置广播选项。启用或禁用 SO_BROADCAST 选项,控制是否允许发送 UDP 广播数据包(如发往 255.255.255.255)。

  • boolean getBroadcast()()获取广播选项状态。返回当前是否允许发送广播包。

  • void setReuseAddress()(boolean on)设置地址复用。启用或禁用 SO_REUSEADDR 选项。允许多个套接字绑定到完全相同的本地地址与端口(常用于组播/广播监听)。

  • boolean getReuseAddress()()获取地址复用状态。返回当前 SO_REUSEADDR 选项的状态。

  • void setSendBufferSize()(int size)设置发送缓冲区容量。设置操作系统内核为此套接字分配的 UDP 发送缓冲区(SO_SNDBUF)字节数。

  • void setReceiveBufferSize()(int size)设置接收缓冲区容量。设置操作系统内核为此套接字分配的 UDP 接收缓冲区(SO_RCVBUF)字节数。

注意事项

  1. 发送局域网广播包(针对 255.255.255.255 或网段广播地址)前,必须显式调用 setBroadcast(true),否则在某些操作系统上会直接抛出 Permission denied 异常。
  2. setReuseAddress(true) 必须在套接字显式调用 bind() 之前执行方可生效。
java
import java.net.DatagramSocket;
import java.net.DatagramPacket
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;

public class DatagramSocketOptionsDemo {
  public static void main(String[] args) {
    try {
      DatagramSocket socket = new DatagramSocket(null);

      // 1. 设置地址复用(必须在 bind 前调用)
      socket.setReuseAddress(true);
      System.out.println("SO_REUSEADDR 配置状态: " + socket.getReuseAddress());

      // 2. 绑定本地端口
      socket.bind(new InetSocketAddress(9095));

      // 3. 启用广播支持
      socket.setBroadcast(true);
      System.out.println("SO_BROADCAST 配置状态: " + socket.getBroadcast());

      // 4. 调整内核收发缓冲区
      socket.setSendBufferSize(64 * 1024);
      socket.setReceiveBufferSize(64 * 1024);

      // 5. 设置 1000ms 超时
      socket.setSoTimeout(1000);
      System.out.println("SO_TIMEOUT: " + socket.getSoTimeout() + " ms");

      try {
        byte[] buf = new byte[256];
        DatagramPacket p = new DatagramPacket(buf, buf.length);
        System.out.println("进入 receive() 阻塞,等待 1 秒超时...");
        socket.receive(p);
      } catch (SocketTimeoutException e) {
        System.err.println("成功捕获读取超时异常: " + e.getMessage());
      }

      socket.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

API:DatagramPacket

构造方法

  • DatagramPacket DatagramPacket()(byte[] buf, int length)接收端构造方法。创建一个用于接收数据包的 DatagramPacket,指定接收缓冲区和最大接收字节数 length

  • DatagramPacket DatagramPacket()(byte[] buf, int offset, int length)带偏移量的接收端构造方法。创建一个用于接收数据包的 DatagramPacket,指定接收缓冲区、起始写入偏移量 offset 及最大接收字节数 length

  • DatagramPacket DatagramPacket()(byte[] buf, int length, InetAddress address, int port)发送端构造方法。创建一个用于发送数据包的 DatagramPacket,将缓冲区前 length 个字节发送给指定 IP 地址和端口号的主机。

  • DatagramPacket DatagramPacket()(byte[] buf, int offset, int length, InetAddress address, int port)带偏移量的发送端构造方法。创建一个用于发送数据包的 DatagramPacket,将缓冲区从 offset 开始的 length 个字节发送给指定 IP 地址和端口号的主机。

  • DatagramPacket DatagramPacket()(byte[] buf, int length, SocketAddress address)基于 SocketAddress 的发送端构造方法。使用包含 IP 和端口的 SocketAddress 端点创建发送数据包。

  • DatagramPacket DatagramPacket()(byte[] buf, int offset, int length, SocketAddress address)带偏移量与 SocketAddress 的发送端构造方法。使用 SocketAddress 端点及指定数据偏移量创建发送数据包。

注意事项

  1. 构造方法中的 length 不能大于 buf.length - offset,否则会抛出 IllegalArgumentException 异常。
  2. buf 字节数组不会被复制,DatagramPacket 内部直接持有传入数组的引用。对原数组的修改会直接影响数据包,反之亦然。
java
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;

public class DatagramPacketConstructorDemo {
  public static void main(String[] args) throws Exception {
    byte[] buffer = new byte[1024];

    // 1. 用于接收数据包的构造方法
    DatagramPacket recvPacket1 = new DatagramPacket(buffer, buffer.length);
    DatagramPacket recvPacket2 = new DatagramPacket(buffer, 128, 512);

    // 2. 用于发送数据包的构造方法(指定 InetAddress + 端口)
    InetAddress destination = InetAddress.getByName("127.0.0.1");
    DatagramPacket sendPacket1 = new DatagramPacket(buffer, 256, destination, 9000);
    DatagramPacket sendPacket2 = new DatagramPacket(buffer, 64, 128, destination, 9000);

    // 3. 用于发送数据包的构造方法(指定 SocketAddress)
    SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 9000);
    DatagramPacket sendPacket3 = new DatagramPacket(buffer, buffer.length, socketAddress);
    DatagramPacket sendPacket4 = new DatagramPacket(buffer, 0, 100, socketAddress);

    System.out.println("各类构造方法实例化完成。");
  }
}

缓冲区与数据载荷控制

  • byte[] getData()()获取数据缓冲区。返回数据包内部关联的原始字节数组(注意:返回的是整个缓冲区,并非仅包含有效数据的切片)。

  • int getOffset()()获取数据偏移量。返回有效数据在缓冲区中的起始字节索引。

  • int getLength()()获取有效数据长度。返回发送或接收到的数据有效字节数。

  • void setData()(byte[] buf)重置数据缓冲区。重新设置数据包的缓冲区,并将 offset 重置为 0length 设置为 buf.length

  • void setData()(byte[] buf, int offset, int length)重置数据缓冲区及范围。显式重新指定缓冲区、数据起始偏移量以及有效字节长度。

  • void setLength()(int length)重置有效数据长度。重新设置数据包的有效字节长度。

注意事项

  1. 循环复用陷阱(最常见 Bug):当把一个 DatagramPacket 对象反复传入 DatagramSocket.receive(packet) 循环接收数据时,底层内核每次接收数据后都会将 packetlength 属性修改为本次实际接收到的字节数。如果不显式调用 packet.setLength(buffer.length) 还原最大接收容量,下一次接收时 length 就会变成上一次接收到的数值,导致后续接收的数据包被不断截断!
  2. getData() 获取数组转为字符串时,必须结合 getOffset()getLength(),严禁直接使用 new String(packet.getData()),否则会将缓冲区尾部的无用零字节(0x00)一并输出。
java
import java.net.DatagramPacket;
import java.nio.charset.StandardCharsets;

public class DatagramPacketBufferDemo {
  public static void main(String[] args) {
    byte[] buffer = new byte[64];
    String message = "Java Network Programming";
    byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);

    // 将数据写入缓冲区特定位置
    System.arraycopy(msgBytes, 0, buffer, 10, msgBytes.length);

    // 创建带偏移量的包(offset = 10, length = msgBytes.length)
    DatagramPacket packet = new DatagramPacket(buffer, 10, msgBytes.length);

    // 1. 正确提取有效数据
    byte[] rawBuf = packet.getData();
    int offset = packet.getOffset();
    int length = packet.getLength();

    String extracted = new String(rawBuf, offset, length, StandardCharsets.UTF_8);
    System.out.println("解包提取的字符串: " + extracted);

    // 2. 重置缓冲区长度(常用于循环接收前重置状态)
    packet.setLength(buffer.length);
    System.out.println("重置后的有效长度容量: " + packet.getLength());

    // 3. 重新分配新的数据缓冲区
    byte[] newBuf = "New Buffer Content".getBytes(StandardCharsets.UTF_8);
    packet.setData(newBuf, 0, newBuf.length);
    System.out.println("重新 setData 后提取字符串: " + new String(packet.getData(), packet.getOffset(), packet.getLength()));
  }
}

地址与网络端点控制

  • InetAddress getAddress()()获取目标/源 IP 地址。若为待发送数据包,返回目标 IP 地址;若为已接收数据包,返回发送方的源 IP 地址。

  • int getPort()()获取目标/源端口号。若为待发送数据包,返回目标端口号;若为已接收数据包,返回发送方的源端口号。

  • SocketAddress getSocketAddress()()获取套接字端点地址。返回封装了 IP 地址和端口号的 SocketAddress(通常为 InetSocketAddress)对象。

  • void setAddress()(InetAddress iaddr)设置目标 IP 地址。更改或指定数据包发送的目标 IP 地址。

  • void setPort()(int iport)设置目标端口号。更改或指定数据包发送的目标端口号。

  • void setSocketAddress()(SocketAddress address)设置目标套接字端点。通过 SocketAddress 对象一次性配置目标的 IP 地址与端口号。

注意事项

  1. 在服务端接收到来自客户端的 DatagramPacket 后,若需向该客户端发送响应(ECHO 或数据回复),可直接复用该数据包的 SocketAddress,通过 setSocketAddress(packet.getSocketAddress()) 快速定位客户端。
java
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;

public class DatagramPacketAddressDemo {
  public static void main(String[] args) throws Exception {
    byte[] data = "Address Test".getBytes();

    // 1. 获取目标 IP 和 端口
    DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("127.0.0.1"), 8080); // 初始配置目标为 127.0.0.1:8080
    System.out.println("原始 IP 地址: " + packet.getAddress().getHostAddress());
    System.out.println("原始端口号: " + packet.getPort());

    // 2. 重新设置端口号与 IP 地址
    packet.setPort(9090);
    packet.setAddress(InetAddress.getByName("192.168.1.1"));
    System.out.println("更新后端口号: " + packet.getPort());

    // 3. 使用 SocketAddress 进行整体替换
    SocketAddress newDestination = new InetSocketAddress("10.0.0.1", 9999);
    packet.setSocketAddress(newDestination);

    SocketAddress currentAddress = packet.getSocketAddress();
    System.out.println("基于 SocketAddress 的端点信息: " + currentAddress);
  }
}

注意事项

  1. 数据包大小限制:网络标准的以太网 MTU 一般为 1500 字节。除去 IP 头(20 字节)和 UDP 头(8 字节)后,单包推荐的有效载荷(Payload)控制在 1472 字节以内,以避免 IP 分片带来的丢包风险。

  2. 丢包与乱序:UDP 不保证数据包的到达顺序,也不保证数据包一定能送达。如果业务需要高可靠性,必须在应用层自主实现确认重传(ACK)与序号控制机制。

  3. 设置超时:调用 socket.receive() 是阻塞操作。如果对端未发送数据或数据包在网络中丢失,线程会无限期等待。必须使用 setSoTimeout() 设置合理的超时机制。

URL 编程

Java 中的 URL 编程主要基于 java.net 包提供的 URLURIURLConnectionHttpURLConnection 类实现。它允许 Java 程序通过标准的 URL 地址访问 HTTP、HTTPS、FTP 和本地文件等网络资源

核心概念

在 Java 传统网络 API 中,URL 通信体系由以下核心类协作组成:

  • java.net.URI:代表统一资源标识符(Uniform Resource Identifier),仅负责资源的语法结构校验与解析,不涉及网络连接
  • java.net.URL:代表统一资源定位符(Uniform Resource Locator),既包含资源位置标识,又具备打开网络流的能力
  • java.net.URLConnection:代表与 URL 所指向资源的通信连接抽象类
  • java.net.HttpURLConnectionURLConnection 的子类,增加了对 HTTP/HTTPS 协议特有功能(如请求方法 GET/POST、响应头、状态码等)的支持。

URL 组成

一个完整的标准 URL 遵循以下语法格式:

protocol://host:port/path?query#fragment

  • Protocol(协议):如 httphttpsftpfile
  • Host(主机名):服务器域名或 IP 地址。
  • Port(端口):可选,默认 HTTP 为 80,HTTPS 为 443。
  • Path(资源路径):服务器上的文件或 Endpoint 路径。
  • Query(查询参数):以 key=value 形式传递,多个参数用 & 分隔。
  • Fragment(锚点):客户端定位标记,不随 HTTP 请求发送给服务器。

image-20260725180543826

API:URL

构造方法

  • URL URL()(String spec)构造方法。根据传入的完整 URL 字符串创建对象。

  • URL URL()(String protocol, String host, int port, String file)构造方法。显式指定协议、主机名、端口号与文件路径创建 URL 对象。

  • URL URL()(String protocol, String host, String file)构造方法。显式指定协议、主机名与文件路径(使用该协议缺省端口)创建 URL 对象。

  • URL URL()(URL context, String spec)构造方法。基于基准 URL(Context)与相对路径(Spec)解析并构造绝对 URL 对象。

  • URL URL()(String protocol, String host, int port, String file, URLStreamHandler handler)构造方法。使用指定的自定义 URLStreamHandler 策略对象创建 URL。

注意事项

  1. 弃用告警与替代方案(Java 20+):自 Java 20 起,URL 的所有构造方法已被标记为 @Deprecated(since = "20")。直接使用 new URL(...) 会触发编译期警告。生产环境标准规范是优先使用 URI.create("...").toURL()new URI(...).toURL()URI 能更加严密且符合 RFC 规范地对非法字符(如未转义的空格、中文)进行编码与语法校验。
java
import java.net.URI;
import java.net.URL;

public class UrlConstructorDemo {
  public static void main(String[] args) throws Exception {
    // 1. Java 20+ 推荐的标准构造方式(通过 URI 转换)
    URI baseUri = URI.create("https://example.com:8080/api/v1/");
    URL urlFromUri = baseUri.toURL();
    System.out.println("通过 URI 构造绝对 URL: " + urlFromUri);

    // 2. 解析相对路径构造新的 URL
    URI resolvedUri = baseUri.resolve("users/list?page=1");
    URL relativeUrl = resolvedUri.toURL();
    System.out.println("相对路径解析后的 URL: " + relativeUrl);
  }
}

元素解析与组件获取

  • String getProtocol()()获取协议名称。返回 URL 所使用的通信协议(如 "https""http""file")。

  • String getHost()()获取主机名。返回 URL 中的域名或 IP 地址字符串。

  • int getPort()()获取显式端口号。返回 URL 中显式指定的端口号;若未显式指定则返回 -1

  • int getDefaultPort()()获取协议默认端口。返回当前协议的标准默认端口号(例如 HTTP 为 80,HTTPS 为 443)。

  • String getPath()()获取资源路径。返回 URL 的路径部分(不包含 Query 查询参数)。

  • String getFile()()获取文件路径与查询串。返回路径与 Query 参数的组合(即 path + "?" + query)。

  • String getQuery()()获取查询参数。返回 ? 后面的请求键值对字符串。

  • String getRef()()获取锚点标识。返回 # 后面的片段标识符(Fragment)。

  • String getAuthority()()获取授权部分。返回包含用户信息、主机名与端口号的完整授权字符串。

  • String getUserInfo()()获取用户信息。返回 URL 中包含的身份验证凭据(如 "user:password")。

注意事项

  1. 致命性能陷阱:URL.equals()URL.hashCode() 的阻塞式网络解析URL 类的 equals()hashCode() 实现存在严重的历史设计缺陷——为了判断两个 URL 是否指向同一物理资源,它们会触发底层的 DNS 反向解析(通过 InetAddress.getByName())。若网络卡顿或 DNS 响应缓慢,会导致当前线程严重阻塞。因此,严禁将 URL 对象存入 HashMapHashSet 等依赖哈希码的集合中,应始终使用 URI 作为 Key 或集合元素。
java
import java.net.URI;
import java.net.URL;

public class UrlParsingDemo {
  public static void main(String[] args) throws Exception {
    URL url = URI.create("https://admin:secret123@example.com:8443/docs/index.html?lang=zh_CN&sort=asc#top").toURL();

    System.out.println("协议 (Protocol)   : " + url.getProtocol());
    System.out.println("主机名 (Host)     : " + url.getHost());
    System.out.println("显式端口 (Port)   : " + url.getPort());
    System.out.println("协议默认端口     : " + url.getDefaultPort());
    System.out.println("资源路径 (Path)   : " + url.getPath());
    System.out.println("文件整体 (File)   : " + url.getFile());
    System.out.println("查询参数 (Query)  : " + url.getQuery());
    System.out.println("锚点 (Ref)        : " + url.getRef());
    System.out.println("授权信息(Authority): " + url.getAuthority());
    System.out.println("用户信息(UserInfo) : " + url.getUserInfo());
  }
}

网络连接与数据读取

  • InputStream openStream()()打开资源输入流。直接建立与远程资源的连接,并返回一个 InputStream 用于读取数据。

  • URLConnection openConnection()()打开网络连接对象。返回代表该 URL 所指向远程对象连接的 URLConnection(或其子类如 HttpURLConnection)。

  • URLConnection openConnection()(Proxy proxy)通过代理打开连接。使用指定的网络代理(如 SOCKS 或 HTTP 代理)建立网络连接。

  • Object getContent()()获取 URL 资源内容。由系统匹配的 ContentHandler 自动将资源解析并转换为对应的 Java 对象。

注意事项

  1. url.openStream() 仅仅是 url.openConnection().getInputStream() 的简写。使用 openStream() 无法配置超时时间(Timeout)、请求头(Headers)以及 HTTP 方法。网络不稳定时极易导致线程永久卡死在 read() 调用上。生产环境强烈建议通过 openConnection() 强转为 HttpURLConnection 后显式配置连接超时与读取超时
java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;

public class UrlConnectionDemo {
  public static void main(String[] args) {
    try {
      URL url = URI.create("https://httpbin.org/get").toURL();

      // 1. 获取 URLConnection 并强转为 HTTP 专门实现
      URLConnection connection = url.openConnection();
      if (connection instanceof HttpURLConnection httpConn) {
        // 2. 显式设置连接超时与读取超时
        httpConn.setConnectTimeout(3000); // 3 秒建连超时
        httpConn.setReadTimeout(5000);    // 5 秒读取超时
        httpConn.setRequestMethod("GET");
        httpConn.setRequestProperty("User-Agent", "Java-URL-Client/1.0");

        // 3. 校验 HTTP 响应码
        int responseCode = httpConn.getResponseCode();
        System.out.println("HTTP 状态码: " + responseCode);

        if (responseCode == HttpURLConnection.HTTP_OK) {
          try (BufferedReader reader = new BufferedReader(
              new InputStreamReader(httpConn.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            System.out.println("--- 响应体内容 ---");
            while ((line = reader.readLine()) != null) {
              System.out.println(line);
            }
          }
        }
        // 4. 断开底层连接
        httpConn.disconnect();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

API:URLConnection

建立连接与生命周期控制

  • void connect()()显式建立网络连接。发起与远程 URL 资源的真实 Socket 通信连接。如果尚未配置完请求属性,调用此方法后将锁定配置。

  • void setConnectTimeout()(int timeout)设置建连超时时间。配置建立网络连接(TCP 三路握手)的最大等待毫秒数。设置 0 代表无限等待。

  • int getConnectTimeout()()获取建连超时时间。返回设定的连接超时毫秒值。

  • void setReadTimeout()(int timeout)设置读取超时时间。配置建立连接后,从输入流读取数据时的最大等待毫秒数。超长未读到数据将抛出 SocketTimeoutException

  • int getReadTimeout()()获取读取超时时间。返回设定的数据读取超时毫秒值。

  • URL getURL()()获取关联的 URL 对象。返回当前套接字连接所指向的 URL 资源引用。

注意事项

  1. URLConnection 实例是**一次性(Single-use)**的。当调用输入输出流的 close() 方法关闭连接后,无法复用同一个 URLConnection 实例重新发起 connect()
  2. 所有的连接参数(如超时设置)必须在调用 connect() 方法或获取输入/输出流之前调用设置,否则抛出 IllegalStateException
java
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;

public class UrlConnectionLifecycleDemo {
  public static void main(String[] args) throws Exception {
    URL url = URI.create("https://httpbin.org/delay/1").toURL();
    URLConnection connection = url.openConnection();

    // 1. 配置超时参数(必须在建连前执行)
    connection.setConnectTimeout(3000); // 3 秒建连超时
    connection.setReadTimeout(5000);    // 5 秒读取超时

    System.out.println("目标 URL 地址: " + connection.getURL());
    System.out.println("建连超时配置 (ms): " + connection.getConnectTimeout());
    System.out.println("读取超时配置 (ms): " + connection.getReadTimeout());

    // 2. 显式建立网络链路
    connection.connect();
    System.out.println("已完成网络链路连接。");
  }
}

请求报头与响应元数据配置

  • void setRequestProperty()(String key, String value)设置单值请求头。设置指定键的 HTTP 请求标头属性。如果已有同名键,原有属性值将被覆盖。

  • void addRequestProperty()(String key, String value)添加多值请求头。向指定名称的请求标头追加属性值,不会覆盖已有同名键。

  • String getRequestProperty()(String key)获取指定请求头。根据属性键名称返回配置的请求头字符串。

  • Map<String, List<String>> getRequestProperties()()获取全部请求头集合。返回包含当前连接配置的所有请求标头 Map 映射表。

  • String getHeaderField()(String name)获取指定响应头字段。根据属性名称获取服务端返回的对应响应标头值。

  • Map<String, List<String>> getHeaderFields()()获取全部响应头映射。返回服务端响应报文首部的所有属性映射多值 Map。

  • int getContentLength()()获取响应内容字节长度。读取响应标头中的 Content-Length 字段值;未知或未返回时返回 -1

  • String getContentType()()获取响应媒体类型。读取响应标头中的 Content-Type(MIME 类型与字符集)。

注意事项

  1. 调用 getHeaderField()getContentType() 等获取响应首部的方法会**隐式触发 connect()**。如果此时尚未建立连接,底层会自动发起网络连接。
  2. setRequestPropertyaddRequestProperty 区别在于多值处理:对 AcceptCookie 等允许传多个值的报头,需使用 addRequestProperty 追加。
java
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;

public class UrlConnectionHeadersDemo {
  public static void main(String[] args) throws Exception {
    URL url = URI.create("https://httpbin.org/headers").toURL();
    URLConnection connection = url.openConnection();

    // 1. 设置与添加请求头
    connection.setRequestProperty("User-Agent", "Java-URLConnection-Client/1.0");
    connection.addRequestProperty("Accept", "application/json");
    connection.addRequestProperty("Accept", "text/plain");

    System.out.println("User-Agent 标头: " + connection.getRequestProperty("User-Agent"));

    // 2. 输出发送给服务端的全部请求头
    Map<String, List<String>> reqHeaders = connection.getRequestProperties();
    System.out.println("已配置请求头映射: " + reqHeaders);

    // 3. 隐式建连并读取响应标头
    System.out.println("\n--- 服务端响应首部 ---");
    System.out.println("Content-Type: " + connection.getContentType());
    System.out.println("Content-Length: " + connection.getContentLength());
    System.out.println("Server 头信息: " + connection.getHeaderField("Server"));

    Map<String, List<String>> respHeaders = connection.getHeaderFields();
    System.out.println("所有响应头 Key 列表: " + respHeaders.keySet());
  }
}

I/O 数据流与缓存控制

  • void setDoInput()(boolean doinput)设置输入流读取模式。指定当前连接是否允许从远程获取输入数据。默认值为 true

  • boolean getDoInput()()获取输入流模式状态。返回当前连接是否配置为允许输入模式。

  • void setDoOutput()(boolean dooutput)设置输出流写入模式。指定当前连接是否允许向远程提交数据。默认值为 false

  • boolean getDoOutput()()获取输出流模式状态。返回当前连接是否配置为允许输出模式。

  • InputStream getInputStream()()获取输入字节流。返回从该套接字连接读取响应数据的 InputStream

  • OutputStream getOutputStream()()获取输出字节流。返回向该套接字连接写入请求数据的 OutputStream

  • void setUseCaches()(boolean usecaches)设置缓存机制。配置当前连接是否允许使用系统协议缓存(如 HTTP 缓存)。默认值为 true

  • boolean getUseCaches()()获取缓存机制状态。返回当前连接是否启用了缓存。

注意事项

  1. HTTP 请求方法转换规则:对 HttpURLConnection 而言,一旦调用了 setDoOutput(true),HTTP 的请求方法(Request Method)会被**隐式自动更改为 POST**
  2. 流的写入顺序:必须先获取 getOutputStream() 写入完整的请求体并调用 flush() / close(),然后再获取 getInputStream() 读取响应。如果颠倒顺序直接调用 getInputStream(),请求体将无法正确发送至服务端。
java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;

public class UrlConnectionStreamDemo {
  public static void main(String[] args) throws Exception {
    URL url = URI.create("https://httpbin.org/post").toURL();
    URLConnection connection = url.openConnection();

    // 1. 开启输出模式以发送 POST 报文体,同时关闭缓存
    connection.setDoInput(true);
    connection.setDoOutput(true); // 隐式转换为 POST 请求
    connection.setUseCaches(false);

    System.out.println("DoInput 状态: " + connection.getDoInput());
    System.out.println("DoOutput 状态: " + connection.getDoOutput());
    System.out.println("UseCaches 状态: " + connection.getUseCaches());

    // 2. 写入 JSON 数据至请求输出流
    String jsonPayload = "{\"name\":\"Java\",\"module\":\"URLConnection\"}";
    connection.setRequestProperty("Content-Type", "application/json; utf-8");

    try (OutputStream os = connection.getOutputStream()) {
      byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
      os.write(input, 0, input.length);
      os.flush();
    }

    // 3. 从输入流读取服务端响应体
    try (BufferedReader br = new BufferedReader(
        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
      StringBuilder response = new StringBuilder();
      String responseLine;
      while ((responseLine = br.readLine()) != null) {
        response.append(responseLine.trim());
      }
      System.out.println("服务端返回结果: " + response);
    }
  }
}

代码示例

使用 URL 读取网络资源

如果只是简单下载图片或获取文本页面,可直接使用 URL.openStream()

java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class SimpleURLExample {
  public static void main(String[] args) {
    String urlString = "https://httpbin.org/get";

    try {
      URL url = new URL(urlString);

      // 读取并打印页面内容
      try (BufferedReader reader = new BufferedReader(
          new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
        String line;
        while ((line = reader.readLine()) != null) {
          System.out.println(line);
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

使用 HttpURLConnection 发送 POST 请求

如果需要配置请求头、设置超时时间或提交 JSON 请求体,需使用 HttpURLConnection

java
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class HttpPostExample {
  public static void main(String[] args) {
    String urlString = "https://httpbin.org/post";

    try {
      URL url = new URL(urlString);
      HttpURLConnection conn = (HttpURLConnection) url.openConnection();

      // 1. 配置 HTTP 请求选项
      conn.setRequestMethod("POST");
      conn.setConnectTimeout(5000); // 5秒超时
      conn.setReadTimeout(5000);
      conn.setRequestProperty("Content-Type", "application/json; utf-8");
      conn.setRequestProperty("Accept", "application/json");

      // 开启输出流以发送 Body 数据
      conn.setDoOutput(true);

      // 2. 写入 POST JSON 请求体
      String jsonInputString = "{\"name\": \"Java\", \"type\": \"URL Programming\"}";
      try (OutputStream os = conn.getOutputStream()) {
        byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
        os.write(input, 0, input.length);
      }

      // 3. 获取响应状态码
      int responseCode = conn.getResponseCode();
      System.out.println("HTTP 状态码: " + responseCode);

      // 4. 根据状态码选择读取正常响应流还是错误流
      InputStream is = (responseCode >= 400) ? conn.getErrorStream() : conn.getInputStream();
      try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = br.readLine()) != null) {
          response.append(line.trim());
        }
        System.out.println("服务端响应: " + response.toString());
      }

      // 5. 断开连接释放底层 Socket
      conn.disconnect();

    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

注意事项

  1. 构造函数弃用警告:从 Java 20 开始,直接使用 new URL(String) 构造方法已被标记为 @Deprecated。官方推荐使用 URI.create("https://...").toURL() 的方式构建 URL 对象,以提供更严格的 RFC 语法检验。

  2. URL.equals() 阻塞隐患URL 类的 equals()hashCode() 方法在比较时会触发同步网络 DNS 解析,以判断两个域名是否映射到同一 IP 地址。因此**绝对不要将 URL 实例作为 HashMap 的 Key 或存入 HashSet**,应统一改用 URIString

  3. 错误流处理:当服务器返回 4xx 或 5xx 状态码时,调用 conn.getInputStream() 会抛出 IOException。此时必须使用 conn.getErrorStream() 读取服务端返回的错误信息。

  4. 现代替代方案HttpURLConnection 的 API 设计较为古老且不支持 HTTP/2 和异步调用。在 Java 11 及更高版本中,推荐使用全新的 java.net.http.HttpClient,它支持链式调用、异步 Future、响应式流以及 HTTP/2 和 WebSocket。