JavaSE-11-BIO/NIO/AIO(多人聊天室)

BIO的同步阻塞体现在哪里?

NIO的同步非阻塞体现在哪里?

AIO的异步非阻塞体现在哪里?

这是一个非常核心的问题,本文详细解释这三种I/O模型的本质区别,通过对比,历史演进,可以清晰地看出,随着操作系统对文件IO、网络IO的高效处理,如:epoll事件机制 ,Java也利用其机制,进行更高效率的IO模型设计,类比单核CPU、多核CPU对多线程并发、并行操作的效率提升。Java的发展与时俱进,不断引入新特性,展示其强大的生命力。

一、BIO (Blocking I/O) - 同步阻塞

体现在哪里?

复制代码
public class BIOExample { public void demonstrateBlockingIO () throws IOException { ServerSocket serverSocket = new ServerSocket ( 8080 ); // 1. accept() - 同步阻塞 // 线程会一直阻塞在这里,直到有客户端连接 Socket clientSocket = serverSocket.accept(); System.out.println( "客户端已连接" ); // 这行代码只有在连接建立后才会执行 // 2. read() - 同步阻塞 // 线程会一直阻塞在这里,直到有数据可读 InputStream input = clientSocket.getInputStream(); int data = input.read(); // 阻塞等待数据 System.out.println( "读取到数据: " + data); // 只有读取到数据后才执行 // 3. write() - 同步阻塞 // 线程会阻塞直到数据完全发送 OutputStream output = clientSocket.getOutputStream(); output.write( "Hello" .getBytes()); // 阻塞直到发送完成 System.out.println( "数据发送完成" ); // 发送完成后才执行 } }

同步阻塞的含义

  • 同步:调用I/O操作后,必须等待操作完成才能继续执行后续代码
  • 阻塞:在等待I/O操作完成期间,线程完全停止,不能做其他事情

二、NIO (Non-blocking I/O) - 同步非阻塞

体现在哪里?

复制代码
public class NIOExample { public void demonstrateNonBlockingNIO () throws IOException { // 1. 设置为非阻塞模式 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking( false ); // 关键:设置为非阻塞 serverChannel.bind( new InetSocketAddress ( 8080 )); // 2. 非阻塞accept() SocketChannel clientChannel = serverChannel.accept(); // 如果没有连接,立即返回null,不会阻塞 if (clientChannel == null ) { System.out.println( "当前没有客户端连接" ); // 可以继续执行其他代码 } else { System.out.println( "有客户端连接" ); } // 3. 非阻塞读取 if (clientChannel != null ) { clientChannel.configureBlocking( false ); // 设置为非阻塞 ByteBuffer buffer = ByteBuffer.allocate( 1024 ); int bytesRead = clientChannel.read(buffer); // 立即返回,不会阻塞: // - 如果有数据:返回读取的字节数 // - 如果没有数据:返回0 // - 如果连接断开:返回-1 if (bytesRead == 0 ) { System.out.println( "当前没有数据可读,但可以做其他事情" ); // 线程继续执行,不会阻塞在这里 } else if (bytesRead > 0 ) { System.out.println( "读取到数据" ); } else { System.out.println( "连接已断开" ); } } } // Selector模式的同步非阻塞 public void demonstrateSelectorNIO () throws IOException { Selector selector = Selector.open(); ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking( false ); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while ( true ) { // 同步:必须等待select()调用完成 // 非阻塞:可以设置超时时间 int readyChannels = selector.select( 1000 ); // 最多等待1秒 if (readyChannels == 0 ) { System.out.println( "没有事件发生,但可以做其他事情" ); continue ; // 不会无限期阻塞 } // 处理事件(这里仍然是同步的) Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); keyIterator.remove(); // 这些操作仍然是同步的,但不会阻塞 if (key.isAcceptable()) { // accept操作立即完成或失败 SocketChannel client = serverChannel.accept(); } if (key.isReadable()) { // read操作立即完成或失败 SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); int bytesRead = client.read(buffer); // 立即返回 } } } } }

同步非阻塞的含义

  • 同步:发起I/O操作后,必须等待该操作完成(即使立即完成)才能得到结果
  • 非阻塞:I/O操作不会让线程停止等待,立即返回结果(可能是0字节或null)

三、AIO (Asynchronous I/O) - 异步非阻塞

体现在哪里?

复制代码
public class AIOExample { public void demonstrateAsyncIO () throws Exception { // 1. 异步连接 AsynchronousSocketChannel clientChannel = AsynchronousSocketChannel.open(); // 异步:方法立即返回,不等待连接完成 // 非阻塞:线程可以继续执行其他代码 Future<Void> connectFuture = clientChannel.connect( new InetSocketAddress ( "localhost" , 8080 )); System.out.println( "连接请求已发送,但连接可能还未完成" ); // 可以在这里做其他事情 doOtherWork(); // 可以选择等待连接完成(但这又变成了同步) // connectFuture.get(); // 这样就同步了 // 2. 异步读取(回调方式) ByteBuffer buffer = ByteBuffer.allocate( 1024 ); // 异步:方法立即返回 // 非阻塞:线程继续执行 clientChannel.read(buffer, null , new CompletionHandler <Integer, Void>() { @Override public void completed (Integer result, Void attachment) { // 在另一个线程中被调用,当读取完成时 System.out.println( "异步读取完成,读取了 " + result + " 字节" ); } @Override public void failed (Throwable exc, Void attachment) { // 在另一个线程中被调用,当读取失败时 System.out.println( "异步读取失败: " + exc.getMessage()); } }); System.out.println( "read()方法已返回,但读取可能还未完成" ); // 3. 异步读取(Future方式) Future<Integer> readFuture = clientChannel.read(buffer); System.out.println( "异步读取已启动" ); // 可以做其他事情 doOtherWork(); // 如果需要结果,可以选择等待(但这是可选的) // Integer bytesRead = readFuture.get(); // 等待读取完成 } private void doOtherWork () { System.out.println( "在等待I/O操作完成的同时,可以做其他工作" ); } // 完整的AIO服务器示例 public void demonstrateAIOEchoServer () throws Exception { AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(); serverChannel.bind( new InetSocketAddress ( 8080 )); System.out.println( "AIO服务器启动" ); // 异步接受连接 serverChannel.accept( null , new CompletionHandler <AsynchronousSocketChannel, Void>() { @Override public void completed (AsynchronousSocketChannel clientChannel, Void attachment) { System.out.println( "新客户端连接" ); // 立即准备接受下一个连接(异步非阻塞) serverChannel.accept( null , this ); // 为当前客户端处理数据 handleClient(clientChannel); } @Override public void failed (Throwable exc, Void attachment) { System.out.println( "接受连接失败: " + exc.getMessage()); } }); // 主线程可以继续做其他事情,不会被阻塞 System.out.println( "服务器已启动,等待客户端连接..." ); // 保持程序运行 Thread.sleep(Integer.MAX_VALUE); } private void handleClient (AsynchronousSocketChannel clientChannel) { ByteBuffer buffer = ByteBuffer.allocate( 1024 ); // 异步读取客户端数据 clientChannel.read(buffer, buffer, new CompletionHandler <Integer, ByteBuffer>() { @Override public void completed (Integer result, ByteBuffer attachment) { if (result > 0 ) { attachment.flip(); byte [] data = new byte [attachment.remaining()]; attachment.get(data); String message = new String (data); System.out.println( "收到: " + message); // 异步回显 ByteBuffer response = ByteBuffer.wrap(( "Echo: " + message).getBytes()); clientChannel.write(response, null , new CompletionHandler <Integer, Void>() { @Override public void completed (Integer result, Void attachment) { System.out.println( "回显完成" ); // 继续读取下一个消息 attachment.clear(); clientChannel.read(attachment, attachment, this ); } @Override public void failed (Throwable exc, Void attachment) { System.out.println( "发送失败: " + exc.getMessage()); } }); } } @Override public void failed (Throwable exc, ByteBuffer attachment) { System.out.println( "读取失败: " + exc.getMessage()); } }); } }

异步非阻塞的含义

  • 异步:发起I/O操作后,立即返回,不需要等待操作完成,结果通过回调或Future获取
  • 非阻塞:线程不会因为I/O操作而停止,可以继续执行其他任务

四、三种模型的本质对比

执行流程对比

复制代码
// BIO - 同步阻塞 public void bioFlow () throws IOException { Socket socket = new Socket ( "localhost" , 8080 ); // 1. 阻塞直到连接建立 InputStream input = socket.getInputStream(); int data = input.read(); // 2. 阻塞直到数据到达 System.out.println(data); // 3. 处理数据 } // NIO - 同步非阻塞 public void nioFlow () throws IOException { SocketChannel channel = SocketChannel.open(); channel.configureBlocking( false ); // 1. 尝试连接(立即返回) channel.connect( new InetSocketAddress ( "localhost" , 8080 )); // 2. 轮询检查连接是否完成(同步检查) while (!channel.finishConnect()) { // 可以做其他事情 } // 3. 尝试读取数据(立即返回) ByteBuffer buffer = ByteBuffer.allocate( 1024 ); int bytesRead = channel.read(buffer); if (bytesRead > 0 ) { // 处理数据 System.out.println( "读取到数据" ); } else { // 没有数据,可以做其他事情 System.out.println( "没有数据可读" ); } } // AIO - 异步非阻塞 public void aioFlow () throws Exception { AsynchronousSocketChannel channel = AsynchronousSocketChannel.open(); // 1. 异步连接(立即返回) channel.connect( new InetSocketAddress ( "localhost" , 8080 ), null , new CompletionHandler <Void, Void>() { @Override public void completed (Void result, Void attachment) { System.out.println( "连接完成" ); // 在回调中处理 } @Override public void failed (Throwable exc, Void attachment) { System.out.println( "连接失败" ); } }); System.out.println( "连接请求已发送" ); // 立即执行 // 2. 线程可以继续做其他事情 doOtherWork(); // 3. 异步读取(立即返回) ByteBuffer buffer = ByteBuffer.allocate( 1024 ); channel.read(buffer, null , new CompletionHandler <Integer, Void>() { @Override public void completed (Integer result, Void attachment) { System.out.println( "读取完成: " + result + " 字节" ); // 在回调中处理 } @Override public void failed (Throwable exc, Void attachment) { System.out.println( "读取失败" ); } }); System.out.println( "读取请求已发送" ); // 立即执行 }

五、精炼总结

模型 同步/异步 阻塞/非阻塞 特点
BIO 同步 阻塞 调用I/O操作必须等待完成,线程停止
NIO 同步 非阻塞 调用I/O操作立即返回,但需要主动检查结果
AIO 异步 非阻塞 调用I/O操作立即返回,结果通过回调通知

关键理解:

  • 同步:调用者需要主动等待或检查结果
  • 异步:系统在完成后主动通知调用者
  • 阻塞:线程停止执行,等待I/O完成
  • 非阻塞:线程继续执行,不等待I/O完成

六、NIO多人GUI聊天室

使用NIO+GUI技术实现多人聊天室。

先启动服务端,再启动客户端,可以启动多个客户端,这样就是多人聊天了。

成果展示:

服务端

复制代码
public class EnhancedNIOChatServer { private Selector selector; private ServerSocketChannel serverSocketChannel; private ConcurrentHashMap<SocketChannel, String> clientMap; private static final int PORT = 9080 ; private DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "HH:mm:ss" ); public EnhancedNIOChatServer () { clientMap = new ConcurrentHashMap <>(); } public void start () throws IOException { selector = Selector.open(); serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind( new InetSocketAddress (PORT)); serverSocketChannel.configureBlocking( false ); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println( "聊天室服务端启动,监听端口:" + PORT); while ( true ) { int select = selector.select(); if (select == 0 ) continue ; Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); try { if (key.isAcceptable()) { handleAccept(key); } else if (key.isReadable()) { handleRead(key); } } catch (Exception e) { key.channel().close(); if (key.channel() instanceof SocketChannel) { handleClientDisconnect((SocketChannel) key.channel()); } } } } } private void handleAccept (SelectionKey key) throws IOException { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = serverChannel.accept(); if (clientChannel != null ) { clientChannel.configureBlocking( false ); clientChannel.register(selector, SelectionKey.OP_READ); String username = "用户" + (System.currentTimeMillis() % 10000 ); clientMap.put(clientChannel, username); System.out.println(getCurrentTime() + " " + username + " 连接到聊天室" ); broadcastMessage( "系统消息" , username + " 加入聊天室" , null ); } } private void handleRead (SelectionKey key) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); int bytesRead = clientChannel.read(buffer); if (bytesRead > 0 ) { buffer.flip(); byte [] data = new byte [buffer.remaining()]; buffer.get(data); String message = new String (data).trim(); String username = clientMap.get(clientChannel); if (username == null ) { username = "未知用户" ; } if (message.startsWith( "/name " )) { String newName = message.substring( 6 ); if (!newName.isEmpty()) { clientMap.put(clientChannel, newName); sendPrivateMessage(clientChannel, "系统消息" , "用户名已更改为: " + newName); } } else if (message.equals( "/quit" )) { broadcastMessage( "系统消息" , username + " 离开聊天室" , clientChannel); handleClientDisconnect(clientChannel); } else if (message.startsWith( "/pm " )) { // 私聊功能: /pm username message String[] parts = message.split( " " , 3 ); if (parts.length >= 3 ) { String targetUser = parts[ 1 ]; String privateMessage = parts[ 2 ]; sendPrivateMessage(findUserChannel(targetUser), username + " [私聊]" , privateMessage); } } else { broadcastMessage(username, message, clientChannel); } } else if (bytesRead == - 1 ) { handleClientDisconnect(clientChannel); } } private void handleClientDisconnect (SocketChannel clientChannel) throws IOException { String username = clientMap.get(clientChannel); if (username != null ) { broadcastMessage( "系统消息" , username + " 离开聊天室" , clientChannel); System.out.println(getCurrentTime() + " " + username + " 断开连接" ); } clientMap.remove(clientChannel); clientChannel.close(); } private void broadcastMessage (String username, String message, SocketChannel excludeChannel) throws IOException { String formattedMessage = "[" + getCurrentTime() + "] [" + username + "]: " + message; System.out.println(formattedMessage); ByteBuffer buffer = ByteBuffer.wrap(formattedMessage.getBytes()); for (SocketChannel channel : clientMap.keySet()) { if (channel != excludeChannel && channel.isOpen()) { buffer.rewind(); channel.write(buffer); } } } private void sendPrivateMessage (SocketChannel targetChannel, String username, String message) throws IOException { if (targetChannel != null && targetChannel.isOpen()) { String formattedMessage = "[" + getCurrentTime() + "] [" + username + "]: " + message + "\n" ; ByteBuffer buffer = ByteBuffer.wrap(formattedMessage.getBytes()); targetChannel.write(buffer); } } private SocketChannel findUserChannel (String username) { for (SocketChannel channel : clientMap.keySet()) { if (username.equals(clientMap.get(channel))) { return channel; } } return null ; } private String getCurrentTime () { return LocalDateTime.now().format(formatter); } public static void main (String[] args) { try { new EnhancedNIOChatServer ().start(); } catch (IOException e) { e.printStackTrace(); } } }

启动之后:

客户端

复制代码
public class NIOChatGUIClient extends JFrame { private static final String HOST = "localhost" ; private static final int PORT = 9080 ; private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern( "HH:mm:ss" ); // UI组件 private JTextField serverField; private JTextField portField; private JTextField usernameField; private JButton connectButton; private JButton disconnectButton; private JTextPane chatArea; private JTextField messageField; private JButton sendButton; private JButton clearButton; // NIO相关 private Selector selector; private SocketChannel socketChannel; private boolean connected = false ; private String myUsername = "" ; // 网络处理线程 private Thread networkThread; public NIOChatGUIClient () { initializeComponents(); setupLayout(); setupEventHandlers(); updateUIState(); } /** * 初始化UI组件 */ private void initializeComponents () { setTitle( "NIO聊天室客户端" ); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); setSize( 800 , 600 ); setLocationRelativeTo( null ); // 服务器设置面板组件 serverField = new JTextField (HOST, 10 ); portField = new JTextField (String.valueOf(PORT), 5 ); usernameField = new JTextField ( "用户" + ( int )(Math.random() * 1000 ), 10 ); connectButton = new JButton ( "连接" ); disconnectButton = new JButton ( "断开" ); // 聊天显示区域 chatArea = new JTextPane (); chatArea.setEditable( false ); chatArea.setContentType( "text/html" ); chatArea.setFont( new Font ( "微软雅黑" , Font.PLAIN, 13 )); // 初始化HTML内容 chatArea.setText( "<html><body style='margin: 10px;'></body></html>" ); // 设置编辑器工具包 HTMLEditorKit editorKit = new HTMLEditorKit (); chatArea.setEditorKit(editorKit); // 消息输入组件 messageField = new JTextField (); sendButton = new JButton ( "发送" ); clearButton = new JButton ( "清屏" ); } /** * 设置布局 */ private void setupLayout () { // 主面板 JPanel mainPanel = new JPanel ( new BorderLayout ( 5 , 5 )); mainPanel.setBorder( new EmptyBorder ( 10 , 10 , 10 , 10 )); setContentPane(mainPanel); // 顶部设置面板 JPanel topPanel = new JPanel ( new FlowLayout (FlowLayout.LEFT)); topPanel.setBorder(BorderFactory.createTitledBorder( "服务器设置" )); topPanel.add( new JLabel ( "服务器:" )); topPanel.add(serverField); topPanel.add( new JLabel ( "端口:" )); topPanel.add(portField); topPanel.add( new JLabel ( "用户名:" )); topPanel.add(usernameField); topPanel.add(connectButton); topPanel.add(disconnectButton); // 中部聊天区域 JScrollPane scrollPane = new JScrollPane (chatArea); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); // 底部输入面板 JPanel bottomPanel = new JPanel ( new BorderLayout ( 5 , 5 )); bottomPanel.add(messageField, BorderLayout.CENTER); JPanel buttonPanel = new JPanel ( new FlowLayout (FlowLayout.RIGHT)); buttonPanel.add(sendButton); buttonPanel.add(clearButton); bottomPanel.add(buttonPanel, BorderLayout.EAST); // 添加到主面板 mainPanel.add(topPanel, BorderLayout.NORTH); mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(bottomPanel, BorderLayout.SOUTH); } /** * 设置事件处理器 */ private void setupEventHandlers () { // 连接按钮事件 connectButton.addActionListener( new ActionListener () { @Override public void actionPerformed (ActionEvent e) { connectToServer(); } }); // 断开按钮事件 disconnectButton.addActionListener( new ActionListener () { @Override public void actionPerformed (ActionEvent e) { disconnectFromServer(); } }); // 发送按钮事件 sendButton.addActionListener( new ActionListener () { @Override public void actionPerformed (ActionEvent e) { sendMessage(); } }); // 清屏按钮事件 clearButton.addActionListener( new ActionListener () { @Override public void actionPerformed (ActionEvent e) { clearChatArea(); } }); // 回车发送消息 messageField.addActionListener( new ActionListener () { @Override public void actionPerformed (ActionEvent e) { sendMessage(); } }); // 窗口关闭事件 addWindowListener( new WindowAdapter () { @Override public void windowClosing (WindowEvent e) { disconnectFromServer(); System.exit( 0 ); } }); } /** * 更新UI状态 */ private void updateUIState () { serverField.setEnabled(!connected); portField.setEnabled(!connected); usernameField.setEnabled(!connected); connectButton.setEnabled(!connected); disconnectButton.setEnabled(connected); messageField.setEnabled(connected); sendButton.setEnabled(connected); } /** * 连接到服务器 */ private void connectToServer () { if (connected) return ; String host = serverField.getText().trim(); int port; try { port = Integer.parseInt(portField.getText().trim()); } catch (NumberFormatException e) { JOptionPane.showMessageDialog( this , "端口号格式错误!" , "错误" , JOptionPane.ERROR_MESSAGE); return ; } myUsername = usernameField.getText().trim(); if (myUsername.isEmpty()) { JOptionPane.showMessageDialog( this , "请输入用户名!" , "提示" , JOptionPane.WARNING_MESSAGE); return ; } try { // 创建选择器 selector = Selector.open(); // 创建客户端通道 socketChannel = SocketChannel.open(); socketChannel.configureBlocking( false ); // 连接到服务器 boolean isConnected = socketChannel.connect( new InetSocketAddress (host, port)); if (isConnected) { socketChannel.register(selector, SelectionKey.OP_READ); } else { socketChannel.register(selector, SelectionKey.OP_CONNECT); } // 启动网络处理线程 startNetworkThread(); connected = true ; appendSystemMessage( "连接到服务器 " + host + ":" + port); updateUIState(); } catch (IOException e) { JOptionPane.showMessageDialog( this , "连接服务器失败: " + e.getMessage(), "错误" , JOptionPane.ERROR_MESSAGE); } } /** * 断开服务器连接 */ private void disconnectFromServer () { if (!connected) return ; try { if (socketChannel != null && socketChannel.isOpen()) { // 发送退出消息 sendQuitMessage(); socketChannel.close(); } if (selector != null && selector.isOpen()) { selector.close(); } connected = false ; appendSystemMessage( "已断开与服务器的连接" ); updateUIState(); } catch (IOException e) { appendSystemMessage( "断开连接时发生错误: " + e.getMessage()); } } /** * 发送退出消息 */ private void sendQuitMessage () { if (!connected || socketChannel == null ) return ; try { ByteBuffer buffer = ByteBuffer.wrap( "/quit\n" .getBytes()); socketChannel.write(buffer); } catch (IOException e) { // 忽略发送退出消息时的错误 } } /** * 启动网络处理线程 */ private void startNetworkThread () { networkThread = new Thread ( new Runnable () { @Override public void run () { try { while (connected && selector != null && selector.isOpen()) { int select = selector.select( 1000 ); // 1秒超时 if (select == 0 ) continue ; Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (key.isConnectable()) { handleConnect(key); } else if (key.isReadable()) { handleRead(key); } } } } catch (Exception e) { if (connected) { SwingUtilities.invokeLater(() -> { appendSystemMessage( "网络错误: " + e.getMessage()); }); } } } }); networkThread.setDaemon( true ); networkThread.start(); } /** * 处理连接完成事件 */ private void handleConnect (SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); if (channel.finishConnect()) { key.interestOps(SelectionKey.OP_READ); SwingUtilities.invokeLater(() -> { appendSystemMessage( "成功连接到服务器!" ); }); } } /** * 处理读取事件 */ private void handleRead (SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); int bytesRead = channel.read(buffer); if (bytesRead > 0 ) { buffer.flip(); byte [] data = new byte [buffer.remaining()]; buffer.get(data); String message = new String (data); SwingUtilities.invokeLater(() -> { displayReceivedMessage(message); }); } else if (bytesRead == - 1 ) { SwingUtilities.invokeLater(() -> { appendSystemMessage( "与服务器断开连接" ); try { disconnectFromServer(); } catch (Exception e) { // 忽略断开连接时的异常 } }); } } /** * 发送消息 */ private void sendMessage () { if (!connected) return ; String message = messageField.getText().trim(); if (message.isEmpty()) return ; try { // 先在本地显示发送的消息 String currentTime = LocalTime.now().format(TIME_FORMATTER); displaySentMessage(myUsername, message, currentTime); ByteBuffer buffer = ByteBuffer.wrap((message + "\n" ).getBytes()); socketChannel.write(buffer); messageField.setText( "" ); // 如果是修改用户名命令,更新界面显示 if (message.startsWith( "/name " )) { String newName = message.substring( 6 ).trim(); if (!newName.isEmpty()) { myUsername = newName; usernameField.setText(newName); } } } catch (IOException e) { SwingUtilities.invokeLater(() -> { appendSystemMessage( "发送消息失败: " + e.getMessage()); }); } } /** * 显示发送的消息 */ private void displaySentMessage (String username, String message, String time) { String htmlMessage = formatMessageHtml(username, message, time, true ); appendMessageHtml(htmlMessage); } /** * 显示接收到的消息 */ private void displayReceivedMessage (String serverMessage) { // 解析服务器消息格式: [时间] [用户名]: 消息内容 Pattern pattern = Pattern.compile( "\\[(.*?)\\] \\[(.*?)\\]: (.*)" ); Matcher matcher = pattern.matcher(serverMessage.trim()); if (matcher.matches()) { String time = matcher.group( 1 ); String username = matcher.group( 2 ); String message = matcher.group( 3 ); // 判断是否是系统消息 if ( "系统消息" .equals(username)) { appendSystemMessage(message); } else { String htmlMessage = formatMessageHtml(username, message, time, myUsername.equals(username)); appendMessageHtml(htmlMessage); } } else { // 无法解析的格式,直接显示 appendMessageHtml( "<div style='text-align: center; color: gray;'>" + serverMessage.replace( "\n" , "<br>" ) + "</div>" ); } } /** * 格式化消息为HTML */ private String formatMessageHtml (String username, String message, String time, boolean isMyMessage) { String align = isMyMessage ? "right" : "left" ; String bgColor = isMyMessage ? " #95ec69 " : " #ffffff " ; String avatar = isMyMessage ? "😊" : "😀" ; // 简单的表情符号作为头像 String nameColor = isMyMessage ? " #008000 " : " #0000ff " ; return "<div style='text-align: " + align + "; margin: 5px 0;'>" + "<table style='width: 100%;'>" + "<tr>" + (isMyMessage ? "" : "<td style='width: 1%; vertical-align: top; padding-right: 10px;'>" + avatar + "</td>" ) + "<td style='width: " + (isMyMessage ? "99%" : "98%" ) + ";'>" + "<div style='display: inline-block; background: " + bgColor + "; " + "padding: 8px 12px; border-radius: 8px; max-width: 70%; text-align: " + align + ";'>" + "<div style='font-size: 12px; color: " + nameColor + "; margin-bottom: 2px;'>" + username + "</div>" + "<div>" + message.replace( "<" , "&lt;" ).replace( ">" , "&gt;" ).replace( "\n" , "<br>" ) + "</div>" + "<div style='font-size: 10px; color: gray; text-align: " + align + "; margin-top: 3px;'>" + time + "</div>" + "</div>" + "</td>" + (isMyMessage ? "<td style='width: 1%; vertical-align: top; padding-left: 10px;'>" + avatar + "</td>" : "" ) + "</tr>" + "</table>" + "</div>" ; } /** * 添加系统消息 */ private void appendSystemMessage (String message) { String htmlMessage = "<div style='text-align: center; color: gray; margin: 10px 0;'>" + "<span style='background: #f0f0f0 ; padding: 3px 8px; border-radius: 10px;'>" + message + "</span></div>" ; appendMessageHtml(htmlMessage); } /** * 清空聊天区域 */ private void clearChatArea () { chatArea.setText( "<html><body style='margin: 10px;'></body></html>" ); } /** * 追加HTML消息到聊天区域 */ private void appendMessageHtml (String htmlMessage) { SwingUtilities.invokeLater(() -> { try { HTMLDocument doc = (HTMLDocument) chatArea.getDocument(); Element element = doc.getElement( "body" ); if (element != null ) { doc.insertBeforeEnd(element, htmlMessage); // 自动滚动到底部 chatArea.setCaretPosition(doc.getLength()); } else { // 如果body元素不存在,重新设置整个文档 String currentText = chatArea.getText(); if (currentText != null && currentText.contains( "</body>" )) { int bodyEndIndex = currentText.lastIndexOf( "</body>" ); String newText = currentText.substring( 0 , bodyEndIndex) + htmlMessage + currentText.substring(bodyEndIndex); chatArea.setText(newText); } else { chatArea.setText( "<html><body style='margin: 10px;'>" + htmlMessage + "</body></html>" ); } // 自动滚动到底部 chatArea.setCaretPosition(chatArea.getDocument().getLength()); } } catch (Exception e) { e.printStackTrace(); } }); } public static void main (String[] args) { // 解决libpng警告问题 System.setProperty( "sun.java2d.uiScale" , "1.0" ); System.setProperty( "sun.java2d.cmm" , "sun.java2d.cmm.kcms.KcmsServiceProvider" ); SwingUtilities.invokeLater( new Runnable () { @Override public void run () { try { // 设置系统外观 UIManager.setLookAndFeel(UIManager.getLookAndFeel()); } catch (Exception e) { e.printStackTrace(); } new NIOChatGUIClient ().setVisible( true ); } }); } }

成果展示

相关推荐
恣艺1 小时前
Python 实用工具与机器学习入门:Rich + Tqdm + Faker + Schedule + Scikit-learn
python·机器学习·scikit-learn
为何创造硅基生物1 小时前
C 语言 typedef 结构体私有化
c语言·开发语言·算法
计算机安禾1 小时前
【c++面向对象编程】第43篇:可变参数模板(C++11):优雅处理不定长参数
java·开发语言·c++
测试员周周1 小时前
【Appium 系列】第14节-断言与验证 — Validator 的设计
android·人工智能·python·功能测试·ios·单元测试·appium
Hanniel1 小时前
Python __slots__ 入门指南
开发语言·python·性能优化
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第69题】【JVM篇】第29题:GC Roots 有哪些?
java·开发语言·jvm·面试
小白|1 小时前
tensorflow:昇腾CANN的TensorFlow适配层
人工智能·python·tensorflow
William Dawson1 小时前
【通俗易懂!Spring四大核心注解源码解读:@Configuration、@ComponentScan、@Import、@EnableXXX实战】
java·后端·spring
Matlab程序猿小助手2 小时前
【MATLAB源码-第319期】基于matlab的帝王蝶优化算法(MBO)无人机三维路径规划,输出做短路径图和适应度曲线.
开发语言·算法·matlab