You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java控制台聊天客户端:如何用Selector处理SocketChannel与System.in?

Java控制台聊天客户端:同时处理SocketChannel与System.in的可行方案

针对你的教学项目需求,这里提供两种实用的实现方案,覆盖多线程(入门友好)和NIO Selector(匹配你的初始想法)两种思路:

方案一:多线程拆分职责(最适合教学场景)

直接用两个线程分别处理用户输入和服务器消息接收,逻辑清晰易懂,不需要复杂的NIO知识:

  • 主线程:负责读取System.in的用户输入,将消息发送到服务器的SocketChannel
  • 子线程:专门监听SocketChannel的可读事件,收到服务器消息后立即打印到控制台

代码示例

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;

// 负责读取服务器消息的子线程
class ServerMessageReader extends Thread {
    private final SocketChannel serverChannel;
    private final ByteBuffer buffer = ByteBuffer.allocate(1024);

    public ServerMessageReader(SocketChannel channel) {
        this.serverChannel = channel;
    }

    @Override
    public void run() {
        try {
            while (true) {
                buffer.clear();
                int bytesRead = serverChannel.read(buffer);
                if (bytesRead == -1) {
                    System.out.println("\n服务器连接已断开");
                    break;
                }
                buffer.flip();
                String receivedMsg = StandardCharsets.UTF_8.decode(buffer).toString();
                // 打印消息后重新提示用户输入,避免输入被打断
                System.out.println("\n收到消息:" + receivedMsg);
                System.out.print("请输入消息:");
            }
        } catch (IOException e) {
            System.err.println("读取服务器消息出错:" + e.getMessage());
        }
    }
}

public class ChatClient {
    public static void main(String[] args) throws IOException {
        // 连接服务器
        SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
        // 启动服务器消息读取线程
        new ServerMessageReader(clientChannel).start();

        // 主线程处理用户输入
        Scanner inputScanner = new Scanner(System.in);
        ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
        
        while (true) {
            System.out.print("请输入消息:");
            String userInput = inputScanner.nextLine();
            sendBuffer.clear();
            sendBuffer.put(StandardCharsets.UTF_8.encode(userInput));
            sendBuffer.flip();
            // 确保消息全部发送
            while (sendBuffer.hasRemaining()) {
                clientChannel.write(sendBuffer);
            }
        }
    }
}

优缺点

  • 优点:代码简洁,逻辑直观,适合教学演示线程的基础用法,调试和维护成本低
  • 缺点:每个客户端占用两个线程,但对于教学项目的规模完全可以忽略

方案二:用Pipe桥接System.in与Selector(实现NIO统一监听)

因为System.in是普通输入流,无法直接注册到Selector,我们可以用Java NIO的Pipe做桥接,把用户输入转换成NIO通道的事件,这样就能用Selector同时监听SocketChannel和用户输入:

  1. 创建一个Pipe,子线程读取System.in并写入Pipe的Sink通道
  2. 将Pipe的Source通道和SocketChannel都注册到Selector,统一处理可读事件

代码示例

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.Scanner;
import java.util.Set;

public class NioChatClient {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        // 连接服务器并注册可读事件
        SocketChannel serverChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_READ);

        // 创建Pipe桥接System.in
        Pipe inputPipe = Pipe.open();
        Pipe.SourceChannel inputSource = inputPipe.source();
        inputSource.configureBlocking(false);
        inputSource.register(selector, SelectionKey.OP_READ);

        // 启动线程读取用户输入并写入Pipe
        new Thread(() -> {
            Scanner inputScanner = new Scanner(System.in);
            ByteBuffer inputBuffer = ByteBuffer.allocate(1024);
            try {
                while (true) {
                    System.out.print("请输入消息:");
                    String userInput = inputScanner.nextLine();
                    inputBuffer.clear();
                    inputBuffer.put(StandardCharsets.UTF_8.encode(userInput));
                    inputBuffer.flip();
                    while (inputBuffer.hasRemaining()) {
                        inputPipe.sink().write(inputBuffer);
                    }
                }
            } catch (IOException e) {
                System.err.println("用户输入处理出错:" + e.getMessage());
            }
        }).start();

        // Selector主循环
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (selector.select() > 0) {
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

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

                if (key.isReadable()) {
                    Channel channel = (Channel) key.channel();
                    if (channel instanceof SocketChannel) {
                        // 处理服务器发来的消息
                        SocketChannel sc = (SocketChannel) channel;
                        buffer.clear();
                        int bytesRead = sc.read(buffer);
                        if (bytesRead == -1) {
                            System.out.println("\n服务器连接断开");
                            key.cancel();
                            sc.close();
                            continue;
                        }
                        buffer.flip();
                        String receivedMsg = StandardCharsets.UTF_8.decode(buffer).toString();
                        System.out.println("\n收到消息:" + receivedMsg);
                        System.out.print("请输入消息:");
                    } else if (channel instanceof Pipe.SourceChannel) {
                        // 处理用户输入,发送到服务器
                        Pipe.SourceChannel sc = (Pipe.SourceChannel) channel;
                        buffer.clear();
                        int bytesRead = sc.read(buffer);
                        if (bytesRead == -1) {
                            key.cancel();
                            sc.close();
                            continue;
                        }
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            serverChannel.write(buffer);
                        }
                    }
                }
            }
        }
    }
}

优缺点

  • 优点:符合你想用Selector的需求,能演示NIO中Pipe和Selector的高级用法,统一事件处理模型
  • 缺点:代码复杂度较高,适合已经掌握基础NIO知识的教学环节

内容的提问来源于stack exchange,提问作者YoavKlein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 20:21:02