Java Swing GUI 客户端和服务器聊天应用程序 TextArea 未更新

2023-12-26

我正在使用 Java 和 GUI 的 Swing 类创建一个聊天应用程序。

ChatServer 类将是从客户端接收消息并向所有客户端回显的服务器,但我只想为 2 个客户端进行聊天。

ChatClient 类既是客户端。它们在文本区域上显示从服务器发送的内容。并将文本字段中的文本发送到服务器。

聊天客户端类

package chatclient;

import java.net.Socket;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;

public class ChatClient extends javax.swing.JFrame {


public ChatClient() {
    initComponents();
}

/**
 * This method is called from within the constructor to initialize the form.
 * WARNING: Do NOT modify this code. The content of this method is always
 * regenerated by the Form Editor.
 */
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">                          
private void initComponents() {

    scrollPane = new javax.swing.JScrollPane();
    textArea = new javax.swing.JTextArea();
    btnConnect = new javax.swing.JButton();
    btnDisconnect = new javax.swing.JButton();
    lblStatus = new javax.swing.JLabel();
    lblShowStatus = new javax.swing.JLabel();
    txtInput = new javax.swing.JTextField();

    setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
    setTitle("Chat Client A");

    textArea.setEditable(false);
    textArea.setColumns(20);
    textArea.setRows(5);
    textArea.setText("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");
    textArea.setWrapStyleWord(true);
    textArea.setCaretPosition(textArea.getDocument().getLength());
    scrollPane.setViewportView(textArea);

    btnConnect.setText("Connect");
    btnConnect.setActionCommand("btnConnect");
    btnConnect.addMouseListener(new java.awt.event.MouseAdapter() {
        public void mouseClicked(java.awt.event.MouseEvent evt) {
            btnConnectMouseClicked(evt);
        }
    });

    btnDisconnect.setText("Disconnect");
    btnDisconnect.setActionCommand("btnDisconnect");
    btnDisconnect.addActionListener(new java.awt.event.ActionListener() {
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnDisconnectActionPerformed(evt);
        }
    });

    lblStatus.setText("Status: ");

    lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
    lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
    lblShowStatus.setText("Disconnected");

    txtInput.setToolTipText("");
    txtInput.addActionListener(new java.awt.event.ActionListener() {
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            txtInputActionPerformed(evt);
        }
    });

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
    getContentPane().setLayout(layout);
    layout.setHorizontalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(layout.createSequentialGroup()
            .addContainerGap()
            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addComponent(scrollPane)
                .addGroup(layout.createSequentialGroup()
                    .addComponent(btnConnect)
                    .addGap(18, 18, 18)
                    .addComponent(btnDisconnect)
                    .addGap(42, 42, 42)
                    .addComponent(lblStatus)
                    .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                    .addComponent(lblShowStatus)
                    .addGap(0, 42, Short.MAX_VALUE))
                .addComponent(txtInput))
            .addContainerGap())
    );
    layout.setVerticalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(layout.createSequentialGroup()
            .addContainerGap()
            .addComponent(scrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 213, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 11, Short.MAX_VALUE)
            .addComponent(txtInput, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                .addComponent(btnConnect)
                .addComponent(btnDisconnect)
                .addComponent(lblStatus)
                .addComponent(lblShowStatus))
            .addContainerGap())
    );

    pack();
}// </editor-fold>                        

private void btnConnectMouseClicked(java.awt.event.MouseEvent evt) {                                        
    // TODO add your handling code here:
    lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
    lblShowStatus.setForeground(new java.awt.Color(0, 204, 51));
    lblShowStatus.setText("Connected");

    // ADD CODES FOR CONNECTING TO CHAT SERVER

}                                       

private void btnDisconnectActionPerformed(java.awt.event.ActionEvent evt) {                                              
    // TODO add your handling code here:
    lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
    lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
    lblShowStatus.setText("Disconnected");

    // ADD CODES FOR DISCONNECTING FROM CHAT SERVER

}                                             

private void txtInputActionPerformed(java.awt.event.ActionEvent evt) {                                         
    // TODO add your handling code here:
}                                        

/**
 * @param args the command line arguments
 */
public static void main(String args[]) {
    /* Set the Nimbus look and feel */
    //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
    /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
     * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
     */
    try {
        for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
            if ("Nimbus".equals(info.getName())) {
                javax.swing.UIManager.setLookAndFeel(info.getClassName());
                break;
            }
        }
    } catch (ClassNotFoundException ex) {
        java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (InstantiationException ex) {
        java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (IllegalAccessException ex) {
        java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (javax.swing.UnsupportedLookAndFeelException ex) {
        java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }
    //</editor-fold>      

    /* Create and display the form */
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
            new ChatClient().setVisible(true);
        }
    });

    String input = "", serverInput = "";
    String host = "localhost";
    int port = 1337;
    Socket client;

    // updateTextArea("TEST UPDATE");

    try {
        client = new Socket(host, port);
        System.out.println("Connected to Server!");

        DataInputStream in = new DataInputStream(client.getInputStream());
        DataOutputStream out = new DataOutputStream(client.getOutputStream());

        System.out.println("Before setting text area");
        updateTextArea("trying to update");

        do {
            // HANDLE INPUT PART HERE
            serverInput = in.readUTF();


            if(serverInput != null) {
                System.out.println("Reached here");
                System.out.println(serverInput);
                updateTextArea(serverInput); 
            }

        } while(!input.equals("/close"));
        System.out.println("Program closed");
    }    

    catch(Exception exc) {
        System.err.println(exc.getMessage());
    }


}

private static void updateTextArea(String temp) {
    textArea.setText(textArea.getText() + "\n" + temp + "\n");
    textArea.setCaretPosition(textArea.getDocument().getLength());
}

// Variables declaration - do not modify                     
private javax.swing.JButton btnConnect;
private javax.swing.JButton btnDisconnect;
private javax.swing.JLabel lblShowStatus;
private javax.swing.JLabel lblStatus;
private javax.swing.JScrollPane scrollPane;
private static javax.swing.JTextArea textArea;
private javax.swing.JTextField txtInput;
// End of variables declaration                   
}

聊天服务器类

package chatserver;

import java.io.BufferedReader;
import java.io.InputStreamReader;
// for testing
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 *
 * @author wacats
 */
public class ChatServer {
public static void main(String args[]) {
    int port = 1337;

    try {
        ServerSocket server = new ServerSocket(port);
        String inMessage = "";

        while(true) {
            Socket clientA = server.accept();
            DataInputStream inA = new DataInputStream(clientA.getInputStream());
            DataOutputStream outA = new DataOutputStream(clientA.getOutputStream());
            // outA.writeUTF("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");

            // for testing
            // BufferedReader user = new BufferedReader(new InputStreamReader(System.in));

            do {
                inMessage = inA.readUTF();

                outA.writeUTF("testing");

                if(inMessage != null) {
                   outA.writeUTF(inMessage); 
                }

            } while(!inMessage.equals("/close"));
            clientA.close();
        }



    }

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

我对这个程序的流程的想法是:

  1. 启动聊天服务器
  2. 启动聊天客户端
  3. 当两个客户端都连接到服务器时,他们就可以开始聊天。
  4. 按“Enter”会将文本字段中的文本发送到服务器。
  5. 服务器将向两个客户端广播文本。
  6. 客户端将更新文本区域,附加从服务器接收到的文本。

基本问题是,textArea is null当你尝试打电话时updateTextArea,这是因为invokeLatercall 尚未执行并构建您的 UI,您基本上有一个竞争条件。

正常的处理方式SocketSwing 中的 s 是,是使用 aSwingWorker

看一下Swing 中的并发 http://docs.oracle.com/javase/tutorial/uiswing/concurrency/ and 工作线程和 SwingWorker http://docs.oracle.com/javase/tutorial/uiswing/concurrency/worker.html更多细节。

有多种方法可以解决您的问题。你可以使用SwingWorker从套接字读取文本并生成更新通知(通过publish/process) 方法,这通常称为“观察者模式”。这很好,因为它可以解耦代码并生成更可重用的解决方案。

SocketReader

这个类所做的就是从Socket并生成ActionEvents来自文字,简单。

public class SocketReader extends SwingWorker<Void, String> {

    private List<ActionListener> actionListeners;

    public SocketReader() {
        actionListeners = new ArrayList<>(25);
    }

    public void addActionListener(ActionListener listener) {
        actionListeners.add(listener);
    }

    public void removeActionListener(ActionListener listener) {
        actionListeners.remove(listener);
    }

    @Override
    protected Void doInBackground() throws Exception {
        System.out.println("Connected to Server!");

        try (DataInputStream in = new DataInputStream(SocketManager.INSTACNE.getInputStream())) {

            System.out.println("Before setting text area");

            String serverInput = null;
            do {
                // HANDLE INPUT PART HERE
                serverInput = in.readUTF();

                if (serverInput != null) {
                    System.out.println("Read " + serverInput);
                    publish(serverInput);
                }

            } while (!serverInput.equals("/close"));
            System.out.println("Program closed");
        }
        return null;
    }

    @Override
    protected void process(List<String> chunks) {
        for (String text : chunks) {
            ActionEvent evt = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, text);
            for (ActionListener listener : actionListeners) {
                listener.actionPerformed(evt);
            }
        }
    }

}

SocketWriter

这个简单的将文本写入Socket, 简单的。从技术上讲,您不需要使用SwingWorker为此,但我喜欢取消相对容易的事实

public class SocketWriter extends SwingWorker<Void, Void> {

    private List<String> messages;
    private ReentrantLock lock;
    private Condition waitCon;

    public SocketWriter() {
        messages = Collections.synchronizedList(new ArrayList<String>(25));
        lock = new ReentrantLock();
        waitCon = lock.newCondition();
    }

    public void write(String text) {
        System.out.println("Write " + text);
        messages.add(text);
        try {
            lock.lock();
            waitCon.signalAll();
        } finally {
            lock.unlock();
        }
    }

    @Override
    protected Void doInBackground() throws Exception {
        try (DataOutputStream out = new DataOutputStream(SocketManager.INSTACNE.getOutputStream())) {
            while (!isCancelled()) {
                while (messages.isEmpty() && !isCancelled()) {
                    try {
                        lock.lock();
                        waitCon.await();
                    } finally {
                        lock.unlock();
                    }
                }
                List<String> cache = new ArrayList<>(messages);
                messages.clear();
                for (String text : cache) {
                    System.out.println("Send " + text);
                    out.writeUTF(text);
                }
            }
        }
        return null;
    }

}

SocketManager

好吧,这对我来说有点过分了,但我想要一个中央控制器Socket,您不必使用单例,您可以简单地使其成为一个简单的类并将其引用传递给您的ChatClient并一直到SocketReader/Writer,但是已经晚了而且我很懒

public enum SocketManager {
    INSTACNE;

    private String host = "localhost";
    private int port = 1337;
    private Socket socket;

    public Socket open() throws IOException {
        if (socket != null) {
            close();
        }
        socket = new Socket(host, port);
        return socket;
    }

    public void close() throws IOException {
        if (socket == null) {
            return;
        }
        socket.close();
    }

    public boolean isOpen() {
        return socket != null
            && socket.isConnected()
            && !socket.isClosed()
            && !socket.isInputShutdown()
            && !socket.isOutputShutdown();
    }

    public InputStream getInputStream() throws IOException {
        Objects.requireNonNull(socket, "Socket is not open");
        return socket.getInputStream();
    }

    public OutputStream getOutputStream() throws IOException {
        Objects.requireNonNull(socket, "Socket is not open");
        return socket.getOutputStream();
    }
}

ChatClient

这一切都很棒,但是你打算如何使用它呢?

基本上,您将创建一个实例SocketReader and SocketWriter在你的ChatClient,您附加一个ActionListener给读者并更新JTextArea当它被触发并将您想要发送的文本发送到SocketWriter, 例如...

public class ChatClient extends javax.swing.JFrame {

    public ChatClient() {
        initComponents();
        socketReader = new SocketReader();
        socketReader.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String text = e.getActionCommand();
                textArea.append(text);
                textArea.append("\n");
                textArea.setCaretPosition(textArea.getDocument().getLength());
            }
        });
        socketReader.execute();

        socketWriter = new SocketWriter();
        socketWriter.execute();
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        scrollPane = new javax.swing.JScrollPane();
        textArea = new javax.swing.JTextArea();
        btnConnect = new javax.swing.JButton();
        btnDisconnect = new javax.swing.JButton();
        lblStatus = new javax.swing.JLabel();
        lblShowStatus = new javax.swing.JLabel();
        txtInput = new javax.swing.JTextField();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("Chat Client A");

        textArea.setEditable(false);
        textArea.setColumns(20);
        textArea.setRows(5);
        textArea.setText("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");
        textArea.setWrapStyleWord(true);
        textArea.setCaretPosition(textArea.getDocument().getLength());
        scrollPane.setViewportView(textArea);

        btnConnect.setText("Connect");
        btnConnect.setActionCommand("btnConnect");
        btnConnect.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                btnConnectMouseClicked(evt);
            }
        });

        btnDisconnect.setText("Disconnect");
        btnDisconnect.setActionCommand("btnDisconnect");
        btnDisconnect.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                btnDisconnectActionPerformed(evt);
            }
        });

        lblStatus.setText("Status: ");

        lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
        lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
        lblShowStatus.setText("Disconnected");

        txtInput.setToolTipText("");
        txtInput.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                txtInputActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(scrollPane)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(btnConnect)
                        .addGap(18, 18, 18)
                        .addComponent(btnDisconnect)
                        .addGap(42, 42, 42)
                        .addComponent(lblStatus)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(lblShowStatus)
                        .addGap(0, 42, Short.MAX_VALUE))
                    .addComponent(txtInput))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(scrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 213, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 11, Short.MAX_VALUE)
                .addComponent(txtInput, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(btnConnect)
                    .addComponent(btnDisconnect)
                    .addComponent(lblStatus)
                    .addComponent(lblShowStatus))
                .addContainerGap())
        );

        pack();
    }// </editor-fold>                        

    private void btnConnectMouseClicked(java.awt.event.MouseEvent evt) {
        // TODO add your handling code here:
        lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
        lblShowStatus.setForeground(new java.awt.Color(0, 204, 51));
        lblShowStatus.setText("Connected");

        // ADD CODES FOR CONNECTING TO CHAT SERVER
    }

    private void btnDisconnectActionPerformed(java.awt.event.ActionEvent evt) {
        // TODO add your handling code here:
        lblShowStatus.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
        lblShowStatus.setForeground(new java.awt.Color(255, 51, 51));
        lblShowStatus.setText("Disconnected");

        // ADD CODES FOR DISCONNECTING FROM CHAT SERVER
    }

    private void txtInputActionPerformed(java.awt.event.ActionEvent evt) {
        if (SocketManager.INSTACNE.isOpen()) {
            socketWriter.write(txtInput.getText());
        } else {
            System.out.println("!! Not open");
        }
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {

        try {
            /* Set the Nimbus look and feel */
            //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
            /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
            * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
             */
            try {
                for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                    if ("Nimbus".equals(info.getName())) {
                        javax.swing.UIManager.setLookAndFeel(info.getClassName());
                        break;
                    }
                }
            } catch (ClassNotFoundException ex) {
                java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            } catch (InstantiationException ex) {
                java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            } catch (IllegalAccessException ex) {
                java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            } catch (javax.swing.UnsupportedLookAndFeelException ex) {
                java.util.logging.Logger.getLogger(ChatClient.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            }
            //</editor-fold>

            SocketManager.INSTACNE.open();

            /* Create and display the form */
            java.awt.EventQueue.invokeLater(new Runnable() {
                public void run() {
                    new ChatClient().setVisible(true);
                }
            });

        } catch (IOException ex) {
            ex.printStackTrace();
        }
        //</editor-fold>      

    }

    private SocketWriter socketWriter;
    private SocketReader socketReader;

    // Variables declaration - do not modify                     
    private javax.swing.JButton btnConnect;
    private javax.swing.JButton btnDisconnect;
    private javax.swing.JLabel lblShowStatus;
    private javax.swing.JLabel lblStatus;
    private javax.swing.JScrollPane scrollPane;
    private javax.swing.JTextArea textArea;
    private javax.swing.JTextField txtInput;
    // End of variables declaration                   
}

你会注意到,我用过SocketManager#open in the main,抱歉,错过了您的“连接”代码。我建议将其转移到该方法;)

ChatServer

我没有对此做太多改变,但以防万一......

public class ChatServer {

    public static void main(String args[]) {
        int port = 1337;

        try {
            ServerSocket server = new ServerSocket(port);
            String inMessage = "";

            while (true) {
                System.out.println("Waiting");
                Socket clientA = server.accept();
                System.out.println("Connected");
                DataInputStream inA = new DataInputStream(clientA.getInputStream());
                DataOutputStream outA = new DataOutputStream(clientA.getOutputStream());
                // outA.writeUTF("Welcome to the Chat Server. Type '/close' or Click 'Disconnect' to close.");

                // for testing
                // BufferedReader user = new BufferedReader(new InputStreamReader(System.in));
                do {
                    inMessage = inA.readUTF();

                    if (inMessage != null) {
                        outA.writeUTF(inMessage);
                    }

                } while (!inMessage.equals("/close"));
                clientA.close();
            }

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

通常,当客户端连接时,您会启动一个新的Thread并让它处理客户端Socket,但这不是我的重点。

因此,基于此,您需要阅读大量内容,包括Java 中的并发 http://docs.oracle.com/javase/tutorial/essential/concurrency/ and 关于套接字的一切 http://docs.oracle.com/javase/tutorial/networking/sockets/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java Swing GUI 客户端和服务器聊天应用程序 TextArea 未更新 的相关文章

随机推荐