Socket 阻塞和非阻塞式编程详解(Socket、SocketChannel、DatagramSocket、DatagramChannel)

一、概述

Java 网络编程是构建分布式应用和网络服务的基础。Java 提供了两套主要的网络编程 API:

  • 传统的阻塞式 I/O(BIO - Blocking I/O) :基于 java.net 包,主要类包括 SocketServerSocket
  • 新 I/O(NIO - New I/O) :基于 java.nio.channels 包,主要类包括 SocketChannelServerSocketChannel

1.1 传输协议:TCP vs UDP

在深入 Socket 编程之前,需要理解两种底层传输协议:

TCP(传输控制协议)

  • 面向连接:通信前需要建立连接(三次握手)
  • 可靠传输:保证数据顺序和完整性
  • 流式传输:数据以字节流形式传输
  • 适用场景:需要可靠传输的应用(HTTP、FTP、邮件等)

UDP(用户数据报协议)

  • 无连接:直接发送数据,无需建立连接
  • 不可靠传输:不保证数据到达和顺序
  • 数据报传输:数据以数据报(Datagram)形式传输
  • 适用场景:对实时性要求高、可容忍少量丢失的应用(视频直播、在线游戏、DNS查询等)

1.2 Java Socket 编程分类

根据传输协议和 I/O 模式,Java Socket 编程可以分为:

协议 BIO NIO
TCP Socket / ServerSocket SocketChannel / ServerSocketChannel
UDP DatagramSocket / DatagramPacket DatagramChannel

二、Socket 编程基础

Socket 编程概念

Socket(套接字) 是网络通信的端点,是应用程序与网络协议栈之间的接口。在 Java 中,Socket 编程可以从两个维度进行分类:

  1. 按 I/O 模式分类

    • 阻塞式 I/O(BIO):线程在等待 I/O 操作完成时会被阻塞,直到操作完成
    • 非阻塞式 I/O(NIO):线程在等待 I/O 操作时不会被阻塞,可以继续处理其他任务
  2. 按传输协议分类

    • TCP Socket:面向连接的可靠传输
    • UDP Socket:无连接的快速传输

三、传统 BIO:TCP Socket 编程

3.1 Socket 类(TCP 客户端)

3.1.1 概念

Socket 类代表客户端与服务器之间的一个 TCP 连接端点。它封装了 TCP/IP 协议的细节,提供了面向流的网络通信接口。注意:Socket 类只支持 TCP 协议,不支持 UDP。

3.1.2 主要方法

  • Socket(String host, int port):创建并连接到指定主机和端口
  • connect(SocketAddress endpoint):连接到指定的服务器地址
  • getInputStream():获取输入流,用于接收数据
  • getOutputStream():获取输出流,用于发送数据
  • close():关闭套接字连接

3.1.3 客户端示例

java 复制代码
import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/**
 * Socket 客户端示例
 * 演示如何使用 Socket 连接到服务器并发送/接收数据
 */
public class SocketClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) {
        try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
             // 获取输出流,用于向服务器发送数据
             PrintWriter writer = new PrintWriter(
                 socket.getOutputStream(), true);
             // 获取输入流,用于接收服务器响应
             BufferedReader reader = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()));
             // 从控制台读取用户输入
             Scanner scanner = new Scanner(System.in)) {

            System.out.println("已连接到服务器:" + socket.getRemoteSocketAddress());

            // 启动一个线程用于接收服务器消息
            Thread receiveThread = new Thread(() -> {
                try {
                    String response;
                    while ((response = reader.readLine()) != null) {
                        System.out.println("服务器响应:" + response);
                    }
                } catch (IOException e) {
                    System.out.println("连接已断开");
                }
            });
            receiveThread.start();

            // 主线程用于发送消息
            String userInput;
            while (scanner.hasNextLine()) {
                userInput = scanner.nextLine();
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                writer.println(userInput);
            }

        } catch (IOException e) {
            System.err.println("客户端连接失败:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

3.1.4 使用场景

  • 简单的客户端-服务器通信
  • 连接数量较少的应用
  • 对实时性要求不高的场景
  • 学习和原型开发

3.2 ServerSocket 类(TCP 服务器)

3.2.1 概念

ServerSocket 类用于 TCP 服务器端,监听指定端口的客户端连接请求。当客户端连接时,accept() 方法会返回一个新的 Socket 对象,用于与该客户端进行通信。注意:ServerSocket 类只支持 TCP 协议。

3.2.2 主要方法

  • ServerSocket(int port):创建并绑定到指定端口
  • accept():监听并接受客户端的连接请求,阻塞直到有连接
  • setSoTimeout(int timeout):设置 accept 超时时间
  • close():关闭服务器套接字

3.2.3 服务器端示例(单线程版本)

java 复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * ServerSocket 单线程服务器示例
 * 注意:此版本只能处理一个客户端连接
 */
public class SimpleSocketServer {
    private static final int PORT = 8888;

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("服务器已启动,监听端口:" + PORT);
            System.out.println("等待客户端连接...");

            // 接受客户端连接(阻塞)
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接:" + clientSocket.getRemoteSocketAddress());

            // 处理客户端请求
            handleClient(clientSocket);

        } catch (IOException e) {
            System.err.println("服务器启动失败:" + e.getMessage());
            e.printStackTrace();
        }
    }

    private static void handleClient(Socket socket) {
        try (BufferedReader reader = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()));
             PrintWriter writer = new PrintWriter(
                 socket.getOutputStream(), true)) {

            String message;
            while ((message = reader.readLine()) != null) {
                System.out.println("收到客户端消息:" + message);
                // 回显消息
                writer.println("服务器已收到:" + message);
            }

        } catch (IOException e) {
            System.err.println("处理客户端请求时出错:" + e.getMessage());
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3.2.4 服务器端示例(多线程版本)

  • 主线程仅负责监听端口,收到客户端连接请求(accept)后,立即创建一个子线程专门处理该客户端的完整交互(接收数据、业务处理、发送响应);
  • 主线程无需等待子线程结束,直接返回继续监听下一个连接。
java 复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * ServerSocket 多线程服务器示例
 * 使用线程池处理多个客户端连接
 */
public class MultiThreadSocketServer {
    private static final int PORT = 8888;
    private static final int THREAD_POOL_SIZE = 10;
    private static final ExecutorService executorService = 
        Executors.newFixedThreadPool(THREAD_POOL_SIZE);

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("多线程服务器已启动,监听端口:" + PORT);
            System.out.println("等待客户端连接...");

            while (true) {
                // 接受客户端连接(阻塞)
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端连接:" + clientSocket.getRemoteSocketAddress());

                // 为每个客户端创建一个任务,提交到线程池
                executorService.submit(new ClientHandler(clientSocket));
            }

        } catch (IOException e) {
            System.err.println("服务器启动失败:" + e.getMessage());
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }

    /**
     * 客户端处理器
     */
    static class ClientHandler implements Runnable {
        private final Socket socket;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try (BufferedReader reader = new BufferedReader(
                     new InputStreamReader(socket.getInputStream()));
                 PrintWriter writer = new PrintWriter(
                     socket.getOutputStream(), true)) {

                String message;
                while ((message = reader.readLine()) != null) {
                    System.out.println("[" + Thread.currentThread().getName() + 
                                     "] 收到消息:" + message);
                    
                    // 处理业务逻辑
                    String response = processMessage(message);
                    writer.println(response);
                }

            } catch (IOException e) {
                System.err.println("处理客户端请求时出错:" + e.getMessage());
            } finally {
                try {
                    socket.close();
                    System.out.println("客户端连接已关闭:" + socket.getRemoteSocketAddress());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 处理消息的业务逻辑
         */
        private String processMessage(String message) {   
            // 示例:简单的命令处理
            if (message.startsWith("ECHO:")) {
                return "回显:" + message.substring(5);
            } else if (message.startsWith("TIME")) {
                return "当前时间:" + new java.util.Date();
            } else {
                return "已收到:" + message;
            }
        }
    }
}

3.2.5 使用场景

  • 传统的客户端-服务器应用
  • 连接数量可控的场景(通常 < 1000)
  • 需要简单易用的网络通信
  • 企业内部系统、管理后台等

3.2.6 TCP BIO 的优缺点

优点:

  • 编程简单,易于理解
  • 代码直观,调试方便
  • 适合连接数较少的场景
  • 数据可靠传输,保证顺序

缺点:

  • 每个连接需要一个线程,资源消耗大
  • 高并发场景下性能受限
  • 线程阻塞导致资源浪费
  • 需要建立连接,有额外开销

四、UDP Socket 编程:DatagramSocket 和 DatagramPacket

4.1 UDP 编程概述

UDP(User Datagram Protocol)是一种无连接的传输协议,与 TCP 不同,UDP 不需要建立连接,直接发送数据报。Java 中使用 DatagramSocketDatagramPacket 进行 UDP 编程。

4.2 DatagramSocket 类

4.2.1 概念

DatagramSocket 是 UDP 通信的套接字,用于发送和接收 UDP 数据报。与 TCP 的 Socket 不同,UDP 的客户端和服务器端都使用 DatagramSocket,没有专门的服务器类。

4.2.2 主要方法

  • DatagramSocket(int port):创建并绑定到指定端口
  • DatagramSocket():创建未绑定的套接字
  • send(DatagramPacket packet):发送数据报
  • receive(DatagramPacket packet):接收数据报(阻塞)
  • setSoTimeout(int timeout):设置接收超时时间
  • close():关闭套接字

4.3 DatagramPacket 类

4.3.1 概念

DatagramPacket 表示 UDP 数据报,包含要发送或接收的数据以及目标地址信息。

4.3.2 主要方法

  • DatagramPacket(byte[] buf, int length):创建接收数据报
  • DatagramPacket(byte[] buf, int length, InetAddress address, int port):创建发送数据报
  • getData():获取数据
  • getLength():获取数据长度
  • getAddress():获取发送方/接收方地址
  • getPort():获取发送方/接收方端口

4.3.3 UDP 服务器示例

java 复制代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

/**
 * UDP 服务器示例
 * 注意:UDP 服务器和客户端都使用 DatagramSocket
 */
public class UDPServer {
    private static final int PORT = 8888;
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        try (DatagramSocket socket = new DatagramSocket(PORT)) {
            System.out.println("UDP 服务器已启动,监听端口:" + PORT);

            while (true) {
                // 创建接收缓冲区
                byte[] buffer = new byte[BUFFER_SIZE];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

                // 接收数据报(阻塞)
                socket.receive(packet);

                // 获取客户端信息
                InetAddress clientAddress = packet.getAddress();
                int clientPort = packet.getPort();

                // 解析接收到的数据
                String received = new String(
                    packet.getData(), 
                    0, 
                    packet.getLength(), 
                    StandardCharsets.UTF_8
                );
                System.out.println("收到来自 [" + clientAddress + ":" + clientPort + "] 的消息:" + received);

                // 准备响应数据
                String response = "服务器已收到:" + received;
                byte[] responseData = response.getBytes(StandardCharsets.UTF_8);

                // 创建响应数据报
                DatagramPacket responsePacket = new DatagramPacket(
                    responseData,
                    responseData.length,
                    clientAddress,
                    clientPort
                );

                // 发送响应
                socket.send(responsePacket);
            }

        } catch (Exception e) {
            System.err.println("UDP 服务器错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

4.3.4 UDP 客户端示例

java 复制代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * UDP 客户端示例
 */
public class UDPClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8888;
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        try (DatagramSocket socket = new DatagramSocket();
             Scanner scanner = new Scanner(System.in)) {

            InetAddress serverAddress = InetAddress.getByName(SERVER_HOST);
            System.out.println("UDP 客户端已启动,连接到服务器:" + SERVER_HOST + ":" + SERVER_PORT);

            while (true) {
                System.out.print("请输入消息(输入 'exit' 退出):");
                String message = scanner.nextLine();

                if ("exit".equalsIgnoreCase(message)) {
                    break;
                }

                // 准备发送数据
                byte[] sendData = message.getBytes(StandardCharsets.UTF_8);
                DatagramPacket sendPacket = new DatagramPacket(
                    sendData,
                    sendData.length,
                    serverAddress,
                    SERVER_PORT
                );

                // 发送数据报
                socket.send(sendPacket);
                System.out.println("已发送消息:" + message);

                // 接收响应
                byte[] receiveBuffer = new byte[BUFFER_SIZE];
                DatagramPacket receivePacket = new DatagramPacket(
                    receiveBuffer,
                    receiveBuffer.length
                );

                // 设置超时时间(可选)
                socket.setSoTimeout(5000);

                try {
                    socket.receive(receivePacket);
                    String response = new String(
                        receivePacket.getData(),
                        0,
                        receivePacket.getLength(),
                        StandardCharsets.UTF_8
                    );
                    System.out.println("服务器响应:" + response);
                } catch (java.net.SocketTimeoutException e) {
                    System.out.println("接收超时,服务器可能未响应");
                }
            }

        } catch (Exception e) {
            System.err.println("UDP 客户端错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

4.3.5 UDP 广播示例

UDP 支持广播和组播,可以实现一对多的通信:

java 复制代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * UDP 广播发送示例
 */
public class UDPBroadcastSender {
    private static final int BROADCAST_PORT = 9999;

    public static void main(String[] args) {
        try (DatagramSocket socket = new DatagramSocket()) {
            // 启用广播
            socket.setBroadcast(true);

            String message = "这是一条广播消息";
            byte[] data = message.getBytes();

            // 使用广播地址(255.255.255.255 或子网广播地址)
            InetAddress broadcastAddress = InetAddress.getByName("255.255.255.255");

            DatagramPacket packet = new DatagramPacket(
                data,
                data.length,
                broadcastAddress,
                BROADCAST_PORT
            );

            socket.send(packet);
            System.out.println("广播消息已发送");

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

/**
 * UDP 广播接收示例
 */
public class UDPBroadcastReceiver {
    private static final int BROADCAST_PORT = 9999;

    public static void main(String[] args) {
        try (DatagramSocket socket = new DatagramSocket(BROADCAST_PORT)) {
            System.out.println("开始监听广播消息,端口:" + BROADCAST_PORT);

            byte[] buffer = new byte[1024];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

            while (true) {
                socket.receive(packet);
                String message = new String(
                    packet.getData(),
                    0,
                    packet.getLength()
                );
                System.out.println("收到广播消息 [" + packet.getAddress() + "]:" + message);
            }

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

4.3.6 UDP 使用场景

  • 实时性要求高的应用:视频直播、语音通话
  • 在线游戏:游戏状态同步,可容忍少量丢包
  • DNS 查询:快速查询,无需建立连接
  • 日志收集:日志服务器接收来自多个客户端的日志
  • 服务发现:局域网内服务发现和注册

4.3.7 UDP 的优缺点

优点:

  • 无需建立连接,延迟低
  • 资源消耗少,适合高并发
  • 支持广播和组播
  • 编程相对简单

缺点:

  • 不保证数据到达
  • 不保证数据顺序
  • 没有流量控制和拥塞控制
  • 数据报大小有限制(通常 64KB)

4.4 TCP vs UDP 对比

特性 TCP UDP
连接方式 面向连接(需要握手) 无连接
可靠性 可靠(保证送达和顺序) 不可靠(可能丢包、乱序)
传输方式 字节流 数据报
速度 较慢(有连接开销) 较快(无连接开销)
资源消耗 较高 较低
适用场景 文件传输、Web、邮件 视频直播、游戏、DNS
Java 类 Socket / ServerSocket DatagramSocket / DatagramPacket

五、NIO:SocketChannel 和 ServerSocketChannel(TCP)

5.1 NIO 核心概念

NIO(New I/O)引入了以下核心概念:

  • Channel(通道):类似于流,但可以同时进行读写操作
  • Buffer(缓冲区):数据容器,所有数据都通过 Buffer 进行传输
  • Selector(选择器):用于监听多个通道的事件,实现 I/O 多路复用

5.2 SocketChannel 类(TCP NIO 客户端)

5.2.1 概念

SocketChannel 是 NIO 中用于客户端的通道,支持非阻塞模式的 TCP 连接。它提供了比传统 Socket 更高的性能和更好的并发处理能力。注意:SocketChannel 只支持 TCP 协议。

4.2.2 主要方法

  • open():打开一个新的 SocketChannel
  • configureBlocking(boolean block):设置阻塞/非阻塞模式
  • connect(SocketAddress remote):连接到远程服务器
  • finishConnect():完成非阻塞连接
  • read(ByteBuffer dst):从通道读取数据到缓冲区
  • write(ByteBuffer src):将缓冲区数据写入通道
  • close():关闭通道

5.2.3 客户端示例(阻塞模式)

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * SocketChannel 客户端示例(阻塞模式)
 */
public class SocketChannelClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            // 连接到服务器
            socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
            System.out.println("已连接到服务器:" + socketChannel.getRemoteAddress());

            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Scanner scanner = new Scanner(System.in);

            while (true) {
                System.out.print("请输入消息(输入 'exit' 退出):");
                String message = scanner.nextLine();

                if ("exit".equalsIgnoreCase(message)) {
                    break;
                }

                // 发送数据
                buffer.clear();
                buffer.put(message.getBytes(StandardCharsets.UTF_8));
                buffer.flip(); // 准备读取
                while (buffer.hasRemaining()) {
                    socketChannel.write(buffer);
                }

                // 接收响应
                buffer.clear();
                int bytesRead = socketChannel.read(buffer);
                if (bytesRead > 0) {
                    buffer.flip();
                    String response = StandardCharsets.UTF_8.decode(buffer).toString();
                    System.out.println("服务器响应:" + response);
                }
            }

        } catch (IOException e) {
            System.err.println("客户端连接失败:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

5.2.4 客户端示例(非阻塞模式)

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

/**
 * SocketChannel 客户端示例(非阻塞模式 + Selector)
 */
public class NonBlockingSocketChannelClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open();
             Selector selector = Selector.open();
             Scanner scanner = new Scanner(System.in)) {

            // 设置为非阻塞模式
            socketChannel.configureBlocking(false);
            
            // 注册连接事件
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
            
            // 发起连接
            socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));

            System.out.println("正在连接服务器...");

            while (true) {
                // 等待事件
                int readyChannels = selector.select(1000);
                
                if (readyChannels == 0) {
                    continue;
                }

                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();

                    if (key.isConnectable()) {
                        // 连接就绪
                        SocketChannel channel = (SocketChannel) key.channel();
                        if (channel.finishConnect()) {
                            System.out.println("已连接到服务器");
                            // 注册读事件
                            channel.register(selector, SelectionKey.OP_READ);
                            
                            // 启动发送线程
                            new Thread(() -> sendMessages(channel, scanner)).start();
                        }
                    } else if (key.isReadable()) {
                        // 读就绪
                        SocketChannel channel = (SocketChannel) key.channel();
                        readFromServer(channel);
                    }
                }
            }

        } catch (IOException e) {
            System.err.println("客户端连接失败:" + e.getMessage());
            e.printStackTrace();
        }
    }

    private static void sendMessages(SocketChannel channel, Scanner scanner) {
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (scanner.hasNextLine()) {
                String message = scanner.nextLine();
                if ("exit".equalsIgnoreCase(message)) {
                    channel.close();
                    break;
                }
                buffer.clear();
                buffer.put(message.getBytes(StandardCharsets.UTF_8));
                buffer.flip();
                while (buffer.hasRemaining()) {
                    channel.write(buffer);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void readFromServer(SocketChannel channel) {
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = channel.read(buffer);
            if (bytesRead > 0) {
                //buffet创建之后默认为写入模式,通过flip()切换为读取模式
                buffer.flip();
                String response = StandardCharsets.UTF_8.decode(buffer).toString();
                System.out.println("服务器响应:" + response);
            } else if (bytesRead < 0) {
                // 连接已关闭
                channel.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.2.5 使用场景

  • 需要高性能的网络通信
  • 大量并发连接的客户端
  • 需要非阻塞 I/O 的场景
  • 实时通信应用

5.3 ServerSocketChannel 类(TCP NIO 服务器)

5.3.1 概念

ServerSocketChannel 是 NIO 中用于服务器端的通道,类似于传统的 ServerSocket,但支持非阻塞模式。结合 Selector 可以实现高效的 I/O 多路复用,用一个线程处理多个客户端连接。注意:ServerSocketChannel 只支持 TCP 协议。

4.3.2 主要方法

  • open():打开一个新的 ServerSocketChannel
  • configureBlocking(boolean block):设置阻塞/非阻塞模式
  • bind(SocketAddress local):绑定到本地地址
  • accept():接受客户端连接,返回 SocketChannel
  • register(Selector sel, int ops):注册到选择器

5.3.3 服务器端示例(完整 NIO 实现)

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * ServerSocketChannel 服务器示例(NIO + Selector)
 * 使用单线程处理多个客户端连接
 */
public class NIOServer {
    private static final int PORT = 8888;
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        try (ServerSocketChannel serverChannel = ServerSocketChannel.open();
             Selector selector = Selector.open()) {

            // 设置为非阻塞模式
            serverChannel.configureBlocking(false);
            
            // 绑定端口
            serverChannel.bind(new InetSocketAddress(PORT));
            
            // 注册到选择器,监听连接事件
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            
            System.out.println("NIO 服务器已启动,监听端口:" + PORT);
            System.out.println("等待客户端连接...");

            while (true) {
                // 阻塞等待事件,最多等待 1 秒
                int readyChannels = selector.select(1000);
                
                if (readyChannels == 0) {
                    continue;
                }

                // 获取就绪的通道
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove(); // 移除已处理的键

                    if (key.isAcceptable()) {
                        // 处理连接事件
                        handleAccept(serverChannel, selector);
                    } else if (key.isReadable()) {
                        // 处理读事件
                        handleRead(key);
                    } else if (key.isWritable()) {
                        // 处理写事件
                        handleWrite(key);
                    }
                }
            }

        } catch (IOException e) {
            System.err.println("服务器启动失败:" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 处理客户端连接
     */
    private static void handleAccept(ServerSocketChannel serverChannel, Selector selector) 
            throws IOException {
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        
        // 注册读事件
        clientChannel.register(selector, SelectionKey.OP_READ);
        
        System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
        
        // 发送欢迎消息
        ByteBuffer welcomeBuffer = ByteBuffer.wrap(
            "欢迎连接到服务器!\n".getBytes(StandardCharsets.UTF_8));
        clientChannel.write(welcomeBuffer);
    }

    /**
     * 处理读事件
     */
    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        
        if (buffer == null) {
            buffer = ByteBuffer.allocate(BUFFER_SIZE);
            key.attach(buffer);
        }

        int bytesRead = clientChannel.read(buffer);
        
        if (bytesRead > 0) {
            buffer.flip();
            String message = StandardCharsets.UTF_8.decode(buffer).toString().trim();
            System.out.println("收到客户端消息 [" + clientChannel.getRemoteAddress() + "]:" + message);
            
            // 处理消息并准备响应
            String response = processMessage(message);
            ByteBuffer responseBuffer = ByteBuffer.wrap(
                (response + "\n").getBytes(StandardCharsets.UTF_8));
            
            // 注册写事件
            key.interestOps(SelectionKey.OP_WRITE);
            key.attach(responseBuffer);
            
        } else if (bytesRead < 0) {
            // 客户端断开连接
            System.out.println("客户端断开连接:" + clientChannel.getRemoteAddress());
            clientChannel.close();
        }
    }

    /**
     * 处理写事件
     */
    private static void handleWrite(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        
        if (buffer != null && buffer.hasRemaining()) {
            clientChannel.write(buffer);
        }
        
        // 写完后重新注册读事件
        key.interestOps(SelectionKey.OP_READ);
        key.attach(null);
    }



    /**
     * 处理业务逻辑
     */
    private static String processMessage(String message) {
        if (message.startsWith("ECHO:")) {
            return "回显:" + message.substring(5);
        } else if (message.equals("TIME")) {
            return "当前时间:" + new java.util.Date();
        } else if (message.equals("HELP")) {
            return "可用命令:ECHO:<消息>、TIME、HELP";
        } else {
            return "已收到:" + message;
        }
    }
}

特殊说明: 不能一直注册 OP_WRITE,因为服务器输出缓冲区默认是空闲的,一旦注册 OP_WRITE,Selector 会一直触发该事件(相当于无限循环),导致 CPU 100%。所以必须 "有数据要写时才注册,写完就取消"。

5.3.4 使用场景

  • 高并发服务器应用
  • 需要处理大量连接的场景(> 1000 连接)
  • 实时通信系统(聊天室、游戏服务器等)
  • 需要高性能的网络服务

5.3.5 TCP NIO 的优缺点

优点:

  • 单线程可以处理大量连接
  • 非阻塞 I/O,提高资源利用率
  • 适合高并发场景
  • 性能优于传统 BIO

缺点:

  • 编程复杂度较高
  • 需要理解 Channel、Buffer、Selector 等概念
  • 调试相对困难
  • 对开发人员要求较高

六、NIO UDP:DatagramChannel

6.1 DatagramChannel 类

6.1.1 概念

DatagramChannel 是 NIO 中用于 UDP 通信的通道,支持非阻塞模式。它提供了比传统 DatagramSocket 更高的性能和更好的并发处理能力。

6.1.2 主要方法

  • open():打开一个新的 DatagramChannel
  • configureBlocking(boolean block):设置阻塞/非阻塞模式
  • bind(SocketAddress local):绑定到本地地址
  • send(ByteBuffer src, SocketAddress target):发送数据报
  • receive(ByteBuffer dst):接收数据报
  • connect(SocketAddress remote):连接到远程地址(可选,用于只与特定地址通信)
  • close():关闭通道

6.1.3 UDP NIO 服务器示例

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;

/**
 * DatagramChannel UDP 服务器示例(NIO)
 */
public class NIOUDPServer {
    private static final int PORT = 8888;
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        try (DatagramChannel channel = DatagramChannel.open()) {
            // 绑定端口
            channel.bind(new InetSocketAddress(PORT));
            // 设置为阻塞模式(也可以设置为非阻塞模式配合 Selector 使用)
            channel.configureBlocking(true);

            System.out.println("NIO UDP 服务器已启动,监听端口:" + PORT);

            ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);

            while (true) {
                buffer.clear();
                // 接收数据报
                InetSocketAddress clientAddress = (InetSocketAddress) channel.receive(buffer);

                if (clientAddress != null) {
                    buffer.flip();
                    String message = StandardCharsets.UTF_8.decode(buffer).toString();
                    System.out.println("收到来自 [" + clientAddress + "] 的消息:" + message);

                    // 准备响应
                    String response = "服务器已收到:" + message;
                    buffer.clear();
                    buffer.put(response.getBytes(StandardCharsets.UTF_8));
                    buffer.flip();

                    // 发送响应
                    channel.send(buffer, clientAddress);
                }
            }

        } catch (IOException e) {
            System.err.println("NIO UDP 服务器错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

6.1.4 UDP NIO 客户端示例

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * DatagramChannel UDP 客户端示例(NIO)
 */
public class NIOUDPClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8888;
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        try (DatagramChannel channel = DatagramChannel.open();
             Scanner scanner = new Scanner(System.in)) {

            InetSocketAddress serverAddress = new InetSocketAddress(SERVER_HOST, SERVER_PORT);
            System.out.println("NIO UDP 客户端已启动,连接到服务器:" + SERVER_HOST + ":" + SERVER_PORT);

            while (true) {
                System.out.print("请输入消息(输入 'exit' 退出):");
                String message = scanner.nextLine();

                if ("exit".equalsIgnoreCase(message)) {
                    break;
                }

                // 发送数据
                ByteBuffer sendBuffer = ByteBuffer.wrap(
                    message.getBytes(StandardCharsets.UTF_8)
                );
                channel.send(sendBuffer, serverAddress);
                System.out.println("已发送消息:" + message);

                // 接收响应
                ByteBuffer receiveBuffer = ByteBuffer.allocate(BUFFER_SIZE);
                InetSocketAddress responseAddress = (InetSocketAddress) channel.receive(receiveBuffer);

                if (responseAddress != null) {
                    receiveBuffer.flip();
                    String response = StandardCharsets.UTF_8.decode(receiveBuffer).toString();
                    System.out.println("服务器响应:" + response);
                }
            }

        } catch (IOException e) {
            System.err.println("NIO UDP 客户端错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

6.1.5 UDP NIO 非阻塞模式示例(配合 Selector)

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * DatagramChannel UDP 服务器示例(非阻塞模式 + Selector)
 */
public class NonBlockingNIOUDPServer {
    private static final int PORT = 8888;
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        try (DatagramChannel channel = DatagramChannel.open();
             Selector selector = Selector.open()) {

            channel.bind(new InetSocketAddress(PORT));
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);

            System.out.println("非阻塞 NIO UDP 服务器已启动,监听端口:" + PORT);

            while (true) {
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectedKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();

                    if (key.isReadable()) {
                        handleRead(key);
                    }
                }
            }

        } catch (IOException e) {
            System.err.println("服务器错误:" + e.getMessage());
            e.printStackTrace();
        }
    }

    private static void handleRead(SelectionKey key) throws IOException {
        DatagramChannel channel = (DatagramChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);

        InetSocketAddress clientAddress = (InetSocketAddress) channel.receive(buffer);

        if (clientAddress != null) {
            buffer.flip();
            String message = StandardCharsets.UTF_8.decode(buffer).toString();
            System.out.println("收到来自 [" + clientAddress + "] 的消息:" + message);

            // 发送响应
            String response = "服务器已收到:" + message;
            buffer.clear();
            buffer.put(response.getBytes(StandardCharsets.UTF_8));
            buffer.flip();
            channel.send(buffer, clientAddress);
        }
    }
}

特殊说明: 为什么tcp的多路复用器需要监听写就绪事件,udp的多路复用器不需要监听写就绪事件?

TCP需要监听写就绪事件:

  • TCP 有流量控制(滑动窗口),服务器的输出缓冲区可能会满,导致 write() 阻塞:
  • TCP 是可靠传输,客户端会向服务器返回「接收窗口大小」(告诉服务器 "我还能接收多少数据");
  • 服务器发送数据时,会先写入内核的「输出缓冲区」,再由内核通过网络发给客户端;如果客户端的接收窗口满了(比如客户端处理数据很慢,自己的输入缓冲区满了),服务器的输出缓冲区就会被塞满 ------ 此时调用 clientSocketChannel.write() 会阻塞(非阻塞模式下会返回 "写失败" 或 "写了部分数据")。 因此,TCP 需要 OP_WRITE 事件:当服务器的输出缓冲区有空闲空间(能容纳新数据)时,Selector 触发 OP_WRITE,通知服务器 "可以无阻塞写数据了" UDP 写操作永远不会阻塞(监听了反而出问题):
  • UDP 无流量控制:UDP 是 "尽力交付",不管客户端是否能接收(哪怕客户端断开连接),服务器都能直接发送数据包(不会因为客户端 "忙" 而被阻塞);
  • UDP 输出缓冲区不会 "满到需要等待":UDP 是数据包传输,每个数据包独立发送,内核的 UDP 输出缓冲区通常足够容纳单个数据包(即使缓冲区临时满了,内核也会直接丢弃数据包,不会让应用层的 send() 阻塞);
  • 监听 OP_WRITE 会导致 CPU 飙升:UDP 的输出缓冲区默认是空闲的,一旦注册 OP_WRITE,Selector 会疯狂触发该事件(因为缓冲区一直有空),导致应用层无限循环处理 OP_WRITE,CPU 占用率直接拉满。 简单说:UDP 的 send() 操作在非阻塞模式下「要么立即发送成功,要么立即失败(丢弃数据包)」,永远不会阻塞 ------ 因此不需要 OP_WRITE 事件通知 "可以写了",直接调用 send() 即可。
相关推荐
靠沿4 小时前
Java数据结构初阶——LinkedList
java·开发语言·数据结构
qq_12498707534 小时前
基于springboot的建筑业数据管理系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·毕业设计
一 乐5 小时前
宠物管理|宠物共享|基于Java+vue的宠物共享管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·springboot·宠物
a crazy day5 小时前
Spring相关知识点【详细版】
java·spring·rpc
IT_陈寒5 小时前
Vite 5.0实战:10个你可能不知道的性能优化技巧与插件生态深度解析
前端·人工智能·后端
z***3355 小时前
SQL Server2022版+SSMS安装教程(保姆级)
后端·python·flask
白露与泡影5 小时前
MySQL中的12个良好SQL编写习惯
java·数据库·面试
foundbug9995 小时前
配置Spring框架以连接SQL Server数据库
java·数据库·spring
凯酱5 小时前
@JsonSerialize
java
悦悦子a啊5 小时前
项目案例作业(选做):使用文件改造已有信息系统
java·开发语言·算法