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和用户输入:
- 创建一个Pipe,子线程读取
System.in并写入Pipe的Sink通道 - 将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
相关产品推荐
相关产品推荐

