Java java.net 教程

java.net 包是 Java 标准库中用于网络编程的核心部分,它提供了丰富的类和接口,使得开发者可以轻松地实现客户端/服务器应用、访问网络资源等。

java.net 教程
(图片来源网络,侵删)

本教程将分为以下几个部分:

  1. 网络基础回顾:理解核心概念。
  2. InetAddress 类:代表 IP 地址。
  3. URL 和 URLConnection 类:访问网络资源的高级方式。
  4. Socket 和 ServerSocket (TCP):面向连接的可靠通信。
  5. DatagramSocket 和 DatagramPacket (UDP):无连接的快速通信。
  6. NIO.2 (New I/O):现代、高性能的网络编程。
  7. 实战案例:构建一个简单的聊天室。

网络基础回顾

在开始编码前,快速回顾几个核心概念:

  • IP 地址:网络中设备的唯一标识,如 168.1.100 (IPv4) 或 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (IPv6)。
  • 端口号:设备上应用程序的标识,范围是 0-65535,Web 服务通常使用 80 或 443 端口。
  • 协议:设备间通信的规则,主要分为两种:
    • TCP (Transmission Control Protocol):面向连接、可靠的协议,通信前需要先建立连接(三次握手),确保数据无丢失、无重复、按序到达,适合要求高可靠性的场景,如文件传输、网页浏览。
    • UDP (User Datagram Protocol):无连接、不可靠的协议,发送方直接发送数据包,不保证对方一定能收到,优点是开销小、传输快,适合对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏。

InetAddress

InetAddressjava.net 包中的一个核心类,它不包含端口信息,仅用于表示 IP 地址(主机名)。

常用方法

  • static InetAddress getByName(String host): 根据主机名或 IP 地址字符串获取 InetAddress 实例。
  • static InetAddress[] getAllByName(String host): 获取主机名对应的所有 IP 地址(一个主机可能有多个网卡,即多个 IP)。
  • String getHostName(): 获取此 IP 地址的主机名。
  • String getHostAddress(): 获取此 IP 地址的字符串形式。
  • boolean isReachable(int timeout): 测试是否能到达该地址。

代码示例

import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressExample {
    public static void main(String[] args) {
        try {
            // 1. 根据主机名获取 InetAddress 对象
            InetAddress address = InetAddress.getByName("www.baidu.com");
            System.out.println("主机名: " + address.getHostName());
            System.out.println("IP 地址: " + address.getHostAddress());
            System.out.println("----------------------------------");
            // 2. 根据IP地址获取 InetAddress 对象
            InetAddress addressByIp = InetAddress.getByName("182.61.200.7");
            System.out.println("IP 对应的主机名: " + addressByIp.getHostName());
            System.out.println("IP 地址: " + addressByIp.getHostAddress());
            System.out.println("----------------------------------");
            // 3. 获取本机的 InetAddress 对象
            InetAddress localHost = InetAddress.getLocalHost();
            System.out.println("本机主机名: " + localHost.getHostName());
            System.out.println("本机IP地址: " + localHost.getHostAddress());
            System.out.println("----------------------------------");
            // 4. 获取一个主机的所有IP地址
            InetAddress[] allAddresses = InetAddress.getAllByName("www.google.com");
            System.out.println("www.google.com 的所有IP地址:");
            for (InetAddress addr : allAddresses) {
                System.out.println(addr.getHostAddress());
            }
        } catch (UnknownHostException e) {
            System.err.println("无法找到主机: " + e.getMessage());
        }
    }
}

URLURLConnection

URL (Uniform Resource Locator) 类提供了一种高级方式来访问互联网上的资源,它封装了网络资源的详细信息。

java.net 教程
(图片来源网络,侵删)

URL 类常用方法

  • String getProtocol(): 获取协议 (e.g., http).
  • String getHost(): 获取主机名。
  • int getPort(): 获取端口号,如果未指定则返回 -1。
  • String getPath(): 获取路径。
  • InputStream openStream(): 打开一个到该 URL 的输入流,用于读取数据。

URLConnection

openStream() 方法返回的是一个 InputStream,如果想进行更复杂的操作,比如获取/设置请求头、发送 POST 请求等,可以使用 URLConnection

代码示例:读取网页内容

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
public class UrlExample {
    public static void main(String[] args) {
        // 使用 try-with-resources 自动关闭流
        try {
            // 创建一个URL对象
            URL url = new URL("https://www.baidu.com");
            // 打开输入流
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(url.openStream()))) {
                String line;
                // 逐行读取并打印
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            }
        } catch (IOException e) {
            System.err.println("读取URL时发生错误: " + e.getMessage());
        }
    }
}

TCP Socket 编程

TCP 是最常用的网络协议,Java 使用 SocketServerSocket 来实现 TCP 通信。

  • Socket (客户端):代表一个客户端套接字,它尝试连接到服务器。
  • ServerSocket (服务器):代表一个服务器套接字,它在指定端口上监听客户端的连接请求。

通信流程

服务器端:

  1. 创建 ServerSocket 对象,并绑定一个端口号。
  2. 调用 accept() 方法,阻塞等待客户端连接,该方法返回一个 Socket 对象,代表与客户端建立的连接。
  3. 通过 Socket 对象的 getInputStream()getOutputStream() 获取输入/输出流。
  4. 使用流进行读写操作(与客户端通信)。
  5. 通信结束后,关闭 SocketServerSocket

客户端:

java.net 教程
(图片来源网络,侵删)
  1. 创建 Socket 对象,指定服务器的 IP 地址和端口号。
  2. 连接建立后,通过 Socket 对象的 getInputStream()getOutputStream() 获取输入/输出流。
  3. 使用流进行读写操作(与服务器通信)。
  4. 通信结束后,关闭 Socket

代码示例:简单的 Echo 服务器和客户端

EchoServer.java (服务器端)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
    public static void main(String[] args) {
        int port = 12345; // 定义服务器监听端口
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,等待客户端连接...");
            // accept() 方法会阻塞,直到有客户端连接
            try (Socket clientSocket = serverSocket.accept();
                 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
                System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
                String inputLine;
                // 读取客户端发送的一行数据
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("收到客户端消息: " + inputLine);
                    // 将收到的消息回写给客户端
                    out.println("服务器回显: " + inputLine);
                    // 如果客户端发送 "bye",则退出循环
                    if ("bye".equalsIgnoreCase(inputLine)) {
                        break;
                    }
                }
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("服务器已关闭。");
    }
}

EchoClient.java (客户端)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class EchoClient {
    public static void main(String[] args) {
        String hostName = "localhost"; // 服务器地址,本机测试用
        int portNumber = 12345;       // 服务器端口
        try (Socket socket = new Socket(hostName, portNumber);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
            System.out.println("已连接到服务器。");
            System.out.println("请输入消息,输入 'bye' 退出:");
            String userInput;
            // 从控制台读取用户输入
            while ((userInput = stdIn.readLine()) != null) {
                // 将用户输入发送给服务器
                out.println(userInput);
                // 从服务器读取回显
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
                if ("bye".equalsIgnoreCase(userInput)) {
                    break;
                }
            }
        } catch (UnknownHostException e) {
            System.err.println("不知道主机: " + hostName);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 对主机 " + hostName + " 的连接失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

如何运行:

  1. 先运行 EchoServer
  2. 再运行 EchoClient
  3. 在客户端的控制台输入消息,按回车,你会在服务器和客户端的控制台看到相应的输出。

UDP Socket 编程

UDP 是一种无连接的协议,通信更快速,但不保证可靠性。

  • DatagramSocket:用于发送和接收数据报包。
  • DatagramPacket:数据报包,包含了要发送的数据、目标地址和端口,或接收到的数据和源地址。

通信流程

发送方:

  1. 创建 DatagramSocket 对象(可以指定端口,也可以不指定,让系统分配)。
  2. 准备要发送的数据,并将其打包到 DatagramPacket 中,同时指定接收方的 IP 和端口。
  3. 调用 DatagramSocketsend() 方法发送数据包。
  4. 关闭 DatagramSocket

接收方:

  1. 创建 DatagramSocket 对象,并绑定一个监听端口。
  2. 创建一个空的 DatagramPacket 对象,用于接收数据。
  3. 调用 DatagramSocketreceive() 方法阻塞等待数据包,当有数据包到达时,数据会被填充到空的 DatagramPacket 中。
  4. DatagramPacket 中解析出数据和发送方信息。
  5. 关闭 DatagramSocket

代码示例:UDP 通信

UDPServer.java

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPServer {
    public static void main(String[] args) {
        int port = 9876;
        try (DatagramSocket serverSocket = new DatagramSocket(port)) {
            System.out.println("UDP 服务器已启动,监听端口 " + port);
            byte[] receiveData = new byte[1024];
            while (true) { // 持续监听
                // 创建接收数据包
                DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
                // 接收数据包 (阻塞)
                serverSocket.receive(receivePacket);
                // 提取数据
                String sentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
                System.out.println("收到消息: " + sentence);
                // 获取客户端地址和端口
                InetAddress clientAddress = receivePacket.getAddress();
                int clientPort = receivePacket.getPort();
                // 创建响应数据
                String capitalizedSentence = sentence.toUpperCase();
                byte[] sendData = capitalizedSentence.getBytes();
                // 创建并发送响应数据包
                DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, clientAddress, clientPort);
                serverSocket.send(sendPacket);
                System.out.println("已向客户端发送响应: " + capitalizedSentence);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

UDPClient.java

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class UDPClient {
    public static void main(String[] args) {
        String hostName = "localhost";
        int port = 9876;
        try (DatagramSocket clientSocket = new DatagramSocket();
             Scanner scanner = new Scanner(System.in)) {
            InetAddress IPAddress = InetAddress.getByName(hostName);
            byte[] sendData;
            byte[] receiveData = new byte[1024];
            System.out.println("UDP 客户端已启动。");
            System.out.println("请输入消息,输入 'exit' 退出:");
            while (true) {
                String sentence = scanner.nextLine();
                if ("exit".equalsIgnoreCase(sentence)) {
                    break;
                }
                // 发送数据
                sendData = sentence.getBytes();
                DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
                clientSocket.send(sendPacket);
                // 接收响应
                DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
                clientSocket.receive(receivePacket);
                String modifiedSentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
                System.out.println("服务器响应: " + modifiedSentence);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO.2 (New I/O)

传统的 I/O (BIO - Blocking I/O) 是阻塞式的,当一个线程在读写数据时,如果数据还没准备好,线程就会被阻塞,直到操作完成,在高并发场景下,这会导致大量的线程被创建和阻塞,消耗大量资源。

NIO.2 (Java 1.4 引入,在 Java 7 中得到极大增强) 提供了非阻塞 I/O 的能力,是构建高性能网络服务器的关键技术。

核心概念

  • Channel (通道):类似流,但可以双向读写(InputStream 只能读,OutputStream 只能写)。SocketChannelServerSocketChannel 是网络编程中常用的通道。
  • Buffer (缓冲区):数据被读取到 Buffer 中,或从 Buffer 写出,所有读写操作都通过 Buffer 进行。
  • Selector (选择器):这是 NIO 的核心,一个 Selector 可以同时监控多个 Channel 的事件(如连接、接受、读、写),当某个 Channel 有事件发生时,Selector 会通知我们,这使得我们可以用一个线程管理多个连接,大大提高了效率。

简单流程 (NIO Server)

  1. 创建 ServerSocketChannel 并设置为非阻塞模式。
  2. ServerSocketChannel 注册到 Selector 上,监听 OP_ACCEPT(新连接)事件。
  3. 循环调用 Selector.select(),它会阻塞直到至少一个注册的通道上有事件发生。
  4. 获取 SelectorKeys(发生事件的通道集合)。
  5. 遍历 SelectorKeys,对每个事件进行处理:
    • 如果是 OP_ACCEPT,则接受新连接,并将新的 SocketChannel 也注册到 Selector 上,监听 OP_READ 事件。
    • 如果是 OP_READ,则从 SocketChannel 读取数据到 Buffer,处理数据,然后可以注册 OP_WRITE 事件以便后续写入。
  6. 处理完毕后,将 SelectorKey 从集合中移除。

NIO 的 API 相对复杂,但性能优势巨大,Netty、Mina 等知名框架都是基于 NIO 构建的。


实战案例:简单的多人聊天室

我们将使用 TCP Socket 来构建一个简单的命令行聊天室。

功能:

  • 服务器端:接收所有客户端的消息,并广播给所有其他客户端。
  • 客户端:可以发送消息给服务器,并接收服务器转发的其他客户端的消息。

ChatServer.java

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
public class ChatServer {
    private static final int PORT = 8888;
    // 使用一个线程安全的集合来存储所有客户端的输出流
    private static Set<PrintWriter> clientWriters = ConcurrentHashMap.newKeySet();
    public static void main(String[] args) throws Exception {
        System.out.println("聊天室服务器启动...");
        // 使用线程池来处理每个客户端的连接
        ExecutorService pool = Executors.newFixedThreadPool(200); // 假设最多200个客户端
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                // 接受新客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
                // 为每个客户端创建一个处理线程
                pool.execute(new ClientHandler(clientSocket));
            }
        }
    }
    /**
     * 客户端处理器
     */
    private static class ClientHandler implements Runnable {
        private Socket socket;
        private PrintWriter out;
        private BufferedReader in;
        public ClientHandler(Socket socket) {
            this.socket = socket;
        }
        @Override
        public void run() {
            try {
                // 获取输入输出流
                out = new PrintWriter(socket.getOutputStream(), true);
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // 将新客户端的输出流添加到集合中
                clientWriters.add(out);
                String inputLine;
                // 读取客户端发送的消息
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("收到消息: " + inputLine);
                    // 将消息广播给所有客户端
                    for (PrintWriter writer : clientWriters) {
                        writer.println(inputLine);
                    }
                }
            } catch (IOException e) {
                System.out.println("与客户端 " + socket.getInetAddress() + " 的连接出现错误或中断。");
            } finally {
                // 客户端断开连接后,将其输出流从集合中移除
                if (out != null) {
                    clientWriters.remove(out);
                }
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("客户端 " + socket.getInetAddress() + " 已断开连接。");
            }
        }
    }
}

ChatClient.java

import java.io.*;
import java.net.*;
import java.util.Scanner;
public class ChatClient {
    private static final String SERVER_ADDRESS = "localhost";
    private static final int SERVER_PORT = 8888;
    public static void main(String[] args) {
        try (
            // 创建Socket连接服务器
            Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
            // 用于发送消息的流
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 用于接收消息的流
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 用于读取用户控制台输入的流
            Scanner scanner = new Scanner(System.in)
        ) {
            System.out.println("已连接到聊天室服务器。");
            System.out.println("请输入您的消息,输入 'quit' 退出:");
            // 启动一个线程来异步接收服务器消息
            Thread receiveThread = new Thread(() -> {
                try {
                    String serverMessage;
                    while ((serverMessage = in.readLine()) != null) {
                        System.out.println(serverMessage);
                    }
                } catch (IOException e) {
                    System.out.println("与服务器连接已断开。");
                }
            });
            receiveThread.start();
            // 主线程用于读取用户输入并发送
            while (scanner.hasNextLine()) {
                String userInput = scanner.nextLine();
                if ("quit".equalsIgnoreCase(userInput)) {
                    break;
                }
                out.println(userInput);
            }
        } catch (UnknownHostException e) {
            System.err.println("不知道主机: " + SERVER_ADDRESS);
        } catch (IOException e) {
            System.err.println("I/O 对主机 " + SERVER_ADDRESS + " 的连接失败: " + e.getMessage());
        }
        System.out.println("客户端已退出。");
    }
}

如何运行:

  1. 运行 ChatServer
  2. 运行多个 ChatClient(可以在多个命令行窗口中运行)。
  3. 在任何一个客户端输入消息,所有连接的客户端都会收到这条消息。

特性 TCP (Socket/ServerSocket) UDP (DatagramSocket/DatagramPacket) NIO.2 (Channels, Buffers, Selector)
类型 面向连接 无连接 面向连接/非阻塞
可靠性 高,保证数据顺序和完整性 低,不保证,可能丢包或重复 高,但通过非阻塞实现高吞吐
速度 较慢,有连接开销 快,直接发送 极快,单线程管理多连接
复杂性 简单,易于理解 较简单 复杂
适用场景 文件传输、网页浏览、邮件 视频会议、在线游戏、DNS 高性能服务器、Netty框架
  • 对于简单的网络应用,TCP Socket 是最常用和最容易上手的选择。
  • 如果对实时性要求高且能容忍少量丢包,UDP 是不错的选择。
  • 当你需要构建能够处理成千上万个并发连接的高性能服务器时,NIO.2 是必经之路。

希望这份详细的教程能帮助你理解并掌握 Java 的 java.net 编程!