用JavaSocket编程开发聊天室

用JavaSocket编程开发聊天室

实验要求:

  1. 用Java图形用户界面编写聊天室服务器端和客户端, 支持多个客户端连接到一个服务器。每个客户端能够输入账号。
  2. 可以实现群聊(聊天记录显示在所有客户端界面)。
  3. 完成好友列表在各个客户端上显示。
  4. 可以实现私人聊天,用户可以选择某个其他用户,单独发送信息。
  5. 服务器能够群发系统消息,能够强行让某些用户下线。
  6. 客户端的上线下线要求能够在其他客户端上面实时刷新。

本人目前大二,这是期末课程设计做得一个完整的玩具项目,但是由于水平和时间等问题,这个项目的设计和架构还是有些问题,比如说发送消息和指令都是使用Socket发送的,仅仅使用一些特殊字符来区别消息和指令,如果用户端直接发送和指令相同的字符串,则会导致bug。因此本项目的健壮性和可拓展性都较差,仅能作为课设使用。
分为客户端和服务器端。
服务器端功能:

  • 可以实现查看所有在线用户
  • 可以强制下线在线用户
  • 可以发送系统消息
  • 用户正常登录和退出会通知所有在线用户

客户端功能:

  • 输入服务器、端口和用户名即可登录
  • 可以发生群聊消息
  • 右侧显示所有在线用户
  • 双击右侧在线用户可发送私信

项目地址:
javaSocket聊天室
客户端Client:

package client;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
    private Socket socket;
    private DataOutputStream outputStream;
    private PrintWriter out;
    private BufferedReader in;
    private String serverAddress;
    private int port;
    private String username;
    private OnMessageReceivedListener listener;

    public Client(String serverAddress, int port, String username, OnMessageReceivedListener listener) throws IOException {
        this.serverAddress = serverAddress;
        this.port = port;
        this.username = username;
        this.listener = listener;
        initConnection();
    }



    private void initConnection() throws IOException {
        if (socket != null && !socket.isClosed()) {
            return;
        }
        socket = new Socket(serverAddress, port);
        outputStream = new DataOutputStream(socket.getOutputStream());
        out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
        in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
    }

    public void sendMessage(String message) {
        if (outputStream != null && !socket.isClosed()) {
            try {
                out.println(message);
                out.flush();
            } catch (Exception e) {
                handleSendError(e);
            }
        } else {
            System.err.println("Socket is not properly initialized or is closed.");
        }
    }

    public void sendPrivateMessage(String recipient, String message) {
        sendMessage("/pm " + recipient + " " + message);
    }

    private void handleSendError(Exception e) {
        e.printStackTrace();
        try {
            socket.close();
        } catch (IOException closeException) {
            closeException.printStackTrace();
        }
        listener.onConnectionLost();
    }

    public void connect() {
        try {
            initConnection();
            sendMessage(username); // 发送用户名以登录

            Thread readerThread = new Thread(() -> {
                String message;
                try {
                    while ((message = in.readLine()) != null) {
                        listener.onMessageReceived(message);
                    }
                } catch (IOException e) {
                    listener.onConnectionLost();
                    e.printStackTrace();
                } finally {
                    try {
                        socket.close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            });
            readerThread.start();
        } catch (UnknownHostException e) {
            System.err.println("Server not found: " + e.getMessage());
            listener.onConnectionLost();
        } catch (IOException e) {
            System.err.println("Error connecting to server: " + e.getMessage());
            listener.onConnectionLost();
        }
    }

    public void disconnect() {
        try {
            if (socket != null) {
                socket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String getUsername() {
        return username;
    }

    public interface OnMessageReceivedListener {
        void onMessageReceived(String message);
        void onConnectionLost();
        void onUpdateOnlineUsers(String userListData);
        void onForceLogout();
    }
}

客户端界面ClientGUI:

package client;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class ClientGUI extends Component {
    private JTextField serverAddressField, portField, usernameField, messageField;
    private JButton connectButton, sendButton;
    private JTextArea chatArea;
    private JList<String> onlineList;
    private DefaultListModel<String> onlineListModel;
    private Client client;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new ClientGUI().initializeUI());
    }

    private void initializeUI() {
        JFrame frame = new JFrame("Chat Client");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);

        chatArea = new JTextArea();
        chatArea.setEditable(false);
        JScrollPane scrollPane = new JScrollPane(chatArea);
        frame.add(scrollPane, BorderLayout.CENTER);

        JPanel southPanel = new JPanel();
        southPanel.setLayout(new BorderLayout());

        JPanel inputPanel = new JPanel();
        inputPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
        messageField = new JTextField(30);
        sendButton = new JButton("Send");
        inputPanel.add(messageField);
        inputPanel.add(sendButton);
        southPanel.add(inputPanel, BorderLayout.CENTER);

        JPanel connectPanel = new JPanel();
        connectPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
        serverAddressField = new JTextField("localhost", 10);
        portField = new JTextField("12345", 5);
        usernameField = new JTextField("User", 10);
        connectButton = new JButton("Connect");
        connectPanel.add(new JLabel("Server: "));
        connectPanel.add(serverAddressField);
        connectPanel.add(new JLabel(" Port: "));
        connectPanel.add(portField);
        connectPanel.add(new JLabel(" Username: "));
        connectPanel.add(usernameField);
        connectPanel.add(connectButton);
        southPanel.add(connectPanel, BorderLayout.SOUTH);

        frame.add(southPanel, BorderLayout.SOUTH);

        onlineListModel = new DefaultListModel<>();
        onlineList = new JList<>(onlineListModel);
        JScrollPane onlineScrollPane = new JScrollPane(onlineList);
        onlineScrollPane.setPreferredSize(new Dimension(150, 0));
        frame.add(onlineScrollPane, BorderLayout.EAST);

        createEvents();

        frame.setVisible(true);
    }

    private void createEvents() {
        connectButton.addActionListener(e -> {
            String serverAddress = serverAddressField.getText();
            int port = Integer.parseInt(portField.getText());
            String username = usernameField.getText();
            connectButton.setEnabled(false);
            try {
                client = new Client(serverAddress, port, username, new GUIListener());
                client.connect();
            } catch (Exception ex) {
                JOptionPane.showMessageDialog(this, "连接失败,请检查输入信息。", "连接错误", JOptionPane.ERROR_MESSAGE);
                connectButton.setEnabled(true);
            }
        });

        sendButton.addActionListener(e -> {
            String message = messageField.getText();
            if (!message.isEmpty()) {
                client.sendMessage(message);
                displaySentMessage(message);
                messageField.setText("");
            }
        });

        onlineList.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() == 2) {
                    String selectedUser = onlineList.getSelectedValue();
                    if (selectedUser != null && client != null) {
                        String privateMessage = JOptionPane.showInputDialog(ClientGUI.this,
                                "输入要发送给 " + selectedUser + " 的消息:", "发送私信", JOptionPane.PLAIN_MESSAGE);
                        if (privateMessage != null && !privateMessage.trim().isEmpty()) {
                            client.sendPrivateMessage(selectedUser, privateMessage);
                            displaySentMessage("[私信给 " + selectedUser + "]: " + privateMessage); // 显示发送的私聊消息
                        }
                    }
                }
            }
        });
    }

    private void displaySentMessage(String message) {
        SwingUtilities.invokeLater(() -> {
            chatArea.append(client.getUsername() + ": " + message + "\n");
            chatArea.setCaretPosition(chatArea.getDocument().getLength());
        });
    }

    private class GUIListener implements Client.OnMessageReceivedListener {
        @Override
        public void onMessageReceived(String message) {
            SwingUtilities.invokeLater(() -> {
                if (message.startsWith("/users ")) { // 检查消息是否以/users开头
                    onUpdateOnlineUsers(message.substring(7)); // 去掉"/users "前缀,然后更新在线用户列表
                }else if(message.equals("/forceLogout")){
                    onForceLogout();
                }else if(message.equals("/server/ERROR: 用户名已被占用,请选择其他用户名。")){
                    JOptionPane.showMessageDialog(ClientGUI.this, "用户名已被占用,请选择其他用户名。", "连接错误", JOptionPane.ERROR_MESSAGE);
                    connectButton.setEnabled(true);
                    client.disconnect();
                }else if(message.equals("/server/SUCCESS: 连接成功")){
                    JOptionPane.showMessageDialog(ClientGUI.this, "连接成功", "连接状态", JOptionPane.INFORMATION_MESSAGE);
                }
                else {
                    chatArea.append(message + "\n");
                    chatArea.setCaretPosition(chatArea.getDocument().getLength());
                }
            });
        }

        @Override
        public void onConnectionLost() {
            SwingUtilities.invokeLater(() -> {
                JOptionPane.showMessageDialog(ClientGUI.this, "连接丢失,请检查网络或重新登录。", "连接错误", JOptionPane.ERROR_MESSAGE);
                connectButton.setEnabled(true);
            });
        }

        @Override
        public void onUpdateOnlineUsers(String userListData) {
            SwingUtilities.invokeLater(() -> {
                String[] usernames = userListData.split(","); // 假设用户列表是以逗号分隔的用户名
                onlineListModel.clear(); // 清空现有在线用户列表
                for (String username : usernames) {
                    onlineListModel.addElement(username.trim()); // 添加每个用户名到在线用户列表模型中
                }
            });
        }

        @Override
        public void onForceLogout(){
            SwingUtilities.invokeLater(() -> {
                JOptionPane.showMessageDialog(null, "抱歉!您已被服务器强制下线!", "强制下线", JOptionPane.WARNING_MESSAGE);
                System.exit(0); // 关闭客户端程序
            });
        }
    }
}

Common包下Message类:

package common;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Message {
    private String sender;
    private String recipient;
    private String content;
    private LocalDateTime timestamp;

    // 构造函数
    public Message(String sender, String recipient, String content) {
        this.sender = sender;
        this.recipient = recipient;
        this.content = content;
        this.timestamp = LocalDateTime.now(); // 当前时间作为发送时间
    }

    public Message(String sender, String content) {
        this(sender, "Everyone", content);
    }

    // Getter 和 Setter 方法
    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public String getRecipient() {
        return recipient;
    }

    public void setRecipient(String recipient) {
        this.recipient = recipient;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public LocalDateTime getTimestamp() {
        return timestamp;
    }

    // 格式化时间戳的字符串表示
    public String getFormattedTimestamp() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return timestamp.format(formatter);
    }

    // 重写toString方法,便于打印或显示消息详情
    @Override
    public String toString() {
        return String.format("[%s] %s -> %s: %s",
                getFormattedTimestamp(),
                sender,
                recipient,
                content);
    }
}

服务器端Server:

package server;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import common.Message;


public class Server {
    private static final int PORT = 12345; // 服务器端口
    private final List<ServerThread> clients = new ArrayList<>(); // 存储所有连接的客户端线程
    private ServerGUI gui;

    public Server(ServerGUI gui) {
        this.gui = gui;
    }
    public static void main(String[] args) {
        ServerGUI gui = new ServerGUI();
        Server server = new Server(gui);
        gui.setServer(server);
        server.startServer();
    }

    public void startServer() {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("服务器启动,正在监听端口: " + PORT);

            while (true) {
                Socket socket = serverSocket.accept(); // 阻塞等待客户端连接
                ServerThread serverThread = new ServerThread(socket, this);
                serverThread.start(); // 启动线程处理客户端请求
                System.out.println("新客户端连接: " + socket.getInetAddress());
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("服务器启动失败");
        }
    }

    public synchronized void addClient(ServerThread client) {
        clients.add(client);
        updateOnlineUsers();
        gui.updateUserList(clients);
    }

    public synchronized boolean removeClient(ServerThread client) {
        boolean removed = clients.remove(client);
        if (removed) {
            updateOnlineUsers();
            gui.updateUserList(clients);
        }
        return removed;
    }

    public synchronized void broadcast(Message message, ServerThread excludeClient) {
        for (ServerThread client : clients) {
            if (client != excludeClient) {
                client.send(message);
            }
        }
    }

    public synchronized void updateOnlineUsers() {
        StringBuilder userList = new StringBuilder("/users ");
        for (ServerThread client : clients) {
            userList.append(client.getUsername()).append(",");
        }
        String userListMessage = userList.toString();
        if (userListMessage.endsWith(",")) {
            userListMessage = userListMessage.substring(0, userListMessage.length() - 1);
        }
        for (ServerThread client : clients) {
            client.sendRawMessage(userListMessage);
        }
    }

    public List<ServerThread> getClients() {
        return clients;
    }

    public void forceLogout(String username) {
        for (ServerThread client : clients) {
            if (client.getUsername().equals(username)) {
                client.interrupt(); // 中断客户端线程以强制下线
                removeClient(client); // 从列表中移除客户端
                client.forceLogout();
                gui.updateUserList(clients);
                break;
            }
        }
    }
    public synchronized boolean isUsernameTaken(String username) {
        for (ServerThread client : clients) {
            if (client.getUsername().equals(username)) {
                return true;
            }
        }
        return false;
    }

    public synchronized void sendSystemMessage(String content) {
        Message systemMessage = new Message("Server", "Everyone", content);
        broadcast(systemMessage, null);
    }
}

客户端界面ServerGUI:

package server;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;

public class ServerGUI extends JFrame {
    private JList<String> userList;
    private DefaultListModel<String> userListModel;
    private Server server;

    public ServerGUI() {
        setTitle("Chat Server");
        setSize(400, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);

        userListModel = new DefaultListModel<>();
        userList = new JList<>(userListModel);
        JScrollPane scrollPane = new JScrollPane(userList);

        JButton forceLogoutButton = new JButton("Force Logout");
        forceLogoutButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String selectedUser = userList.getSelectedValue();
                if (selectedUser != null) {
                    server.forceLogout(selectedUser);
                }
            }
        });

        // 系统消息输入框和发送按钮
        JTextField systemMessageField = new JTextField();
        JButton sendSystemMessageButton = new JButton("发送系统消息");
        sendSystemMessageButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String message = systemMessageField.getText();
                if (message != null && !message.trim().isEmpty()) {
                    server.sendSystemMessage(message);
                    systemMessageField.setText(""); // 清空输入框
                }
            }
        });

        JPanel panel = new JPanel(new BorderLayout());

        panel.add(scrollPane, BorderLayout.CENTER);
        panel.add(forceLogoutButton, BorderLayout.SOUTH);

        // 系统消息面板
        JPanel systemMessagePanel = new JPanel(new BorderLayout());
        systemMessagePanel.add(systemMessageField, BorderLayout.CENTER);
        systemMessagePanel.add(sendSystemMessageButton, BorderLayout.EAST);

        // 添加到主窗口
        add(panel, BorderLayout.CENTER);
        add(systemMessagePanel, BorderLayout.SOUTH);

        setVisible(true);
    }

    public void updateUserList(List<ServerThread> clients) {
        SwingUtilities.invokeLater(() -> {
            userListModel.clear();
            for (ServerThread client : clients) {
                userListModel.addElement(client.getUsername());
            }
        });
    }

    public void setServer(Server server) {
        this.server = server;
    }
}

客户端ServerThread:

package server;

import common.Message;

import java.io.*;
import java.net.Socket;

public class ServerThread extends Thread {
    private Socket socket;
    private PrintWriter out;
    private BufferedReader in;
    private String username;
    private Server server;
    private volatile boolean running = true;

    public ServerThread(Socket socket, Server server) {
        this.socket = socket;
        this.server = server;
        try {
            out = new PrintWriter(socket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("Error initializing streams for client: " + e.getMessage());
        }
    }

    @Override
    public void run() {
        try {
            this.username = in.readLine();
            if (server.isUsernameTaken(this.username)) {
                sendRawMessage("/server/ERROR: 用户名已被占用,请选择其他用户名。");
                socket.close();
                return;
            }

            sendRawMessage("/server/SUCCESS: 连接成功");
            System.out.println("账号" + this.username + "已经登录");
            server.addClient(this);  // 注意不要在构造函数中重复调用 addClient
            server.broadcast(new Message(username, "Server", username + " has joined the chat!"), this);

            String inputLine;
            while (running && (inputLine = in.readLine()) != null) {
                if (inputLine.startsWith("/pm ")) {
                    handlePrivateMessage(inputLine);
                }
                else {
                    server.broadcast(new Message(username, "Everyone", inputLine), this);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            boolean removed = server.removeClient(this);
            if (removed) {
                server.broadcast(new Message(username, "Server", username + " has left the chat!"), null);
            }
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void send(Message message) {
        if (out != null && !out.checkError()) {
            out.println(message.toString());
            out.flush();
        }
    }

    public void sendRawMessage(String message) {
        try {
            if (out != null && !out.checkError()) {
                out.println(message);
                out.flush();
            } else {
                System.err.println("Output stream is closed, cannot send message.");
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("Error sending message: " + e.getMessage());
        }
    }

    private void handlePrivateMessage(String inputLine) {
        int firstSpace = inputLine.indexOf(" ");
        int secondSpace = inputLine.indexOf(" ", firstSpace + 1);
        if (secondSpace != -1) {
            String recipient = inputLine.substring(firstSpace + 1, secondSpace);
            String message = inputLine.substring(secondSpace + 1);
            for (ServerThread client : server.getClients()) {
                if (client.getUsername().equals(recipient)) {
                    client.send(new Message(username, recipient, message));
                    break;
                }
            }
        }
    }

    public String getUsername() {
        return username;
    }
    public void forceLogout() {
        running = false; // 停止主循环
        sendRawMessage("/forceLogout");
        try {
            socket.close(); // 关闭Socket以触发IOException并停止线程
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用方法

首先启动服务器端Server,再启动客户端界面ClientGUI,关于同时启动多个实例比较简单,大家可以自行搜索。

其他

个人主页:
张明宇的个人主页

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值