《综合项目实战-局域网内的沟通软件》

《综合项目实战-局域网内的沟通软件》

以下内容全为黑马程序员的资源:

需求:

  • 展示一个用户的登录界面,这个界面只要求用户输入自己聊天的昵称就可以了。

登录进入后,展示一个群聊的窗口,这个窗口,展示在线人数,展示消息展示框,消息输入框,发送按钮。可以实现群聊。实现实时展示在线人数。完全做到即时通讯功能。

技术选型

1、GUI编程技术:Swing

2、网络编程

3、面向对象设计

4、常用API

思路分析

1、创建一个模块,代表我们的项目

  • 模块名取名为:itheima-chat-system.

2、拿到系统需要的界面:Swing的代码

– 登录界面:这个界面只要求用户输入自己聊天的昵称就可以了。

package com.itheima.ui;

import javax.swing.*;
import java.awt.*;

public class ChatEntryFrame extends JFrame {

    private JTextField nicknameField;
    private JButton enterButton;
    private JButton cancelButton;

    public ChatEntryFrame() {
        setTitle("局域网聊天室");
        setSize(350, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setResizable(false); // 禁止调整大小

        // 设置背景颜色
        getContentPane().setBackground(Color.decode("#F0F0F0"));

        // 创建主面板并设置布局
        JPanel mainPanel = new JPanel(new BorderLayout());
        mainPanel.setBackground(Color.decode("#F0F0F0"));
        add(mainPanel);

        // 创建顶部面板
        JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
        topPanel.setBackground(Color.decode("#F0F0F0"));

        // 标签和文本框
        JLabel nicknameLabel = new JLabel("昵称:");
        nicknameLabel.setFont(new Font("楷体", Font.BOLD, 16));
        nicknameField = new JTextField(10);
        nicknameField.setFont(new Font("楷体", Font.PLAIN, 16));
        nicknameField.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY),
                BorderFactory.createEmptyBorder(5, 5, 5, 5)
        ));

        topPanel.add(nicknameLabel);
        topPanel.add(nicknameField);
        mainPanel.add(topPanel, BorderLayout.NORTH);

        // 按钮面板
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
        buttonPanel.setBackground(Color.decode("#F0F0F0"));

        enterButton = new JButton("进入");
        enterButton.setFont(new Font("楷体", Font.BOLD, 16));
        enterButton.setBackground(Color.decode("#007BFF"));
        enterButton.setForeground(Color.WHITE);
        enterButton.setBorderPainted(false);
        enterButton.setFocusPainted(false);

        cancelButton = new JButton("取消");
        cancelButton.setFont(new Font("楷体", Font.BOLD, 16));
        cancelButton.setBackground(Color.decode("#DC3545"));
        cancelButton.setForeground(Color.WHITE);
        cancelButton.setBorderPainted(false);
        cancelButton.setFocusPainted(false);

        buttonPanel.add(enterButton);
        buttonPanel.add(cancelButton);
        mainPanel.add(buttonPanel, BorderLayout.SOUTH);

        // 添加监听器
        enterButton.addActionListener(e -> {
            String nickname = nicknameField.getText();
            if (!nickname.isEmpty()) {
                // 进入聊天室逻辑
                dispose(); // 关闭窗口
            } else {
                JOptionPane.showMessageDialog(this, "请输入昵称!");
            }
        });

        cancelButton.addActionListener(e -> System.exit(0));

        this.setVisible(true);
    }

    public static void main(String[] args) {
        new ChatEntryFrame();
    }
}
  • 获取系统需要的聊天界面。

  • package com.itheima.ui;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    
    public class ClientChatFrame extends JFrame {
        public JTextArea smsContent = new JTextArea(23, 50);
        private JTextArea smsSend = new JTextArea(4, 40);
        public JList<String> onLineUsers = new JList<>();
        private JButton sendBn = new JButton("发送");
    
        public ClientChatFrame() {
            initView();
            this.setVisible(true);
        }
    
        private void initView() {
            this.setSize(700, 600);
            this.setLayout(new BorderLayout());
            this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭窗口,退出程序
            this.setLocationRelativeTo(null); // 窗口居中
    
            // 设置窗口背景色
            this.getContentPane().setBackground(new Color(0xf0, 0xf0, 0xf0));
    
            // 设置字体
            Font font = new Font("SimKai", Font.PLAIN, 14);
    
            // 消息内容框
            smsContent.setFont(font);
            smsContent.setBackground(new Color(0xdd, 0xdd, 0xdd));
            smsContent.setEditable(false);
    
            // 发送消息框
            smsSend.setFont(font);
            smsSend.setWrapStyleWord(true);
            smsSend.setLineWrap(true);
    
            // 在线用户列表
            onLineUsers.setFont(font);
            onLineUsers.setFixedCellWidth(120);
            onLineUsers.setVisibleRowCount(13);
    
            // 创建底部面板
            JPanel bottomPanel = new JPanel(new BorderLayout());
            bottomPanel.setBackground(new Color(0xf0, 0xf0, 0xf0));
    
            // 消息输入框
            JScrollPane smsSendScrollPane = new JScrollPane(smsSend);
            smsSendScrollPane.setBorder(BorderFactory.createEmptyBorder());
            smsSendScrollPane.setPreferredSize(new Dimension(500, 50));
    
            // 发送按钮
            sendBn.setFont(font);
            sendBn.setBackground(Color.decode("#009688"));
            sendBn.setForeground(Color.WHITE);
    
            // 按钮面板
            JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5));
            btns.setBackground(new Color(0xf0, 0xf0, 0xf0));
            btns.add(sendBn);
    
            // 添加组件
            bottomPanel.add(smsSendScrollPane, BorderLayout.CENTER);
            bottomPanel.add(btns, BorderLayout.EAST);
    
            // 用户列表面板
            JScrollPane userListScrollPane = new JScrollPane(onLineUsers);
            userListScrollPane.setBorder(BorderFactory.createEmptyBorder());
            userListScrollPane.setPreferredSize(new Dimension(120, 500));
    
            // 中心消息面板
            JScrollPane smsContentScrollPane = new JScrollPane(smsContent);
            smsContentScrollPane.setBorder(BorderFactory.createEmptyBorder());
    
            // 添加所有组件
            this.add(smsContentScrollPane, BorderLayout.CENTER);
            this.add(bottomPanel, BorderLayout.SOUTH);
            this.add(userListScrollPane, BorderLayout.EAST);
        }
    
        public static void main(String[] args) {
            new ClientChatFrame();
        }
    }
    

3、定义一个App启动类:创建进入界面对象并展示。

public class App {
    public static void main(String[] args) {
        new ChatEntryFrame();
    }
}

4、分析系统的整体架构。

1、开发服务端要做的事情大概有这些。

– 接收客户端的管道链接。

– 接收登录消息,接收昵称信息。

– 服务端也可能是接收客户端发送过来的群聊消息。

– 服务端存储全部在线的socket管道,以便到时候知道哪些客户端在线 ,以便为这些客户端转发消息。

– 如果服务端收到了登录消息,接收昵称,然后更新所有客户端的在线人数列表。

– 如果服务端收到了群聊消息,需要接收这个人的消息,再转发给所有客户端展示这个消息。

2、客户端界面已经准备好了。

5、先开发完整的服务端

  • 第一步:创建一个服务端的项目:itheima-chat-server
  • 第二步:创建一个服务端启动类,启动服务器等待客户端的链接。
public class Server {
    public static void main(String[] args) {
        System.out.println("启动服务端系统.....");
        try {
            // 1、注册端口。
            ServerSocket serverSocket = new ServerSocket(Constant.PORT);
            // 2、主线程负责接受客户端的连接请求
            while (true) {
                // 3、调用accept方法,获取到客户端的Socket对象
                System.out.println("等待客户端的连接.....");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接成功.....");
            }
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
}
  • 第三步:把这个管道交给一个独立的线程来处理:以便支持很多客户端可以同时进来通信。
public class Server {
    public static void main(String[] args) {
        System.out.println("启动服务端系统.....");
        try {
            // 1、注册端口。
            ServerSocket serverSocket = new ServerSocket(Constant.PORT);
            // 2、主线程负责接受客户端的连接请求
            while (true) {
                // 3、调用accept方法,获取到客户端的Socket对象
                System.out.println("等待客户端的连接.....");
                
                Socket socket = serverSocket.accept();
                new ServerReaderThread(socket).start();
                
                System.out.println("一个客户端连接成功.....");
            }
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
}
  • 第四步:定义一个集合容器存储所有登录进来的客户端管道,以便将来群发消息给他们.

    – 这个集合只需要一个记住所有的在线的客户端socket

// 定义一个Map集合,键是存储客户端的管道,值是这个管道的用户名称。
public static final Map<Socket, String> onLineSockets = new HashMap<>();

第五步、服务端线程开始接收登录消息/群聊消息

 // 接收的消息可能有很多种类型:1、登录消息(包含昵称) 2、群聊消息 3、私聊消息
            // 所以客户端必须声明协议发送消息
            // 比如客户端先发1,代表接下来是登录消息。
            // 比如客户端先发2,代表接下来是群聊消息。
public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 接收的消息可能有很多种类型:1、登录消息(包含昵称) 2、群聊消息 3、私聊消息
            // 所以客户端必须声明协议发送消息
            // 比如客户端先发1,代表接下来是登录消息。
            // 比如客户端先发2,代表接下来是群聊消息。
            // 先从socket管道中接收客户端发送来的消息类型编号
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            int type = dis.readInt();
            switch (type){
                case 1:
                    // 客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表。
                    break;
                case 2:
                    // 客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息转发给全部在线客户端。
                    break;
                case 3:
                    // 客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息转发给指定客户端。
                    break;
            }
        } catch (Exception e) {
            System.out.println("客户端下线了:"+ socket.getInetAddress().getHostAddress());
        }
    }
}

第六步、实现服务端的登录消息接收

package com.itheima;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Collection;

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 接收的消息可能有很多种类型:1、登录消息(包含昵称) 2、群聊消息 3、私聊消息
            // 所以客户端必须声明协议发送消息
            // 比如客户端先发1,代表接下来是登录消息。
            // 比如客户端先发2,代表接下来是群聊消息。
            // 先从socket管道中接收客户端发送来的消息类型编号
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            int type = dis.readInt(); // 1、2、3
            switch (type){
                case 1:
                    // 客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表。
                    String nickname = dis.readUTF();
                    // 把这个登录成功的客户端socket存入到在线集合。
                    Server.onLineSockets.put(socket, nickname);
                    // 更新全部客户端的在线人数列表
                    updateClientOnLineUserList();
                    break;
                case 2:
                    // 客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息转发给全部在线客户端。
                    break;
                case 3:
                    // 客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息转发给指定客户端。
                    break;
            }
        } catch (Exception e) {
            System.out.println("客户端下线了:"+ socket.getInetAddress().getHostAddress());
            Server.onLineSockets.remove(socket); // 把下线的客户端socket从在线集合中移除
        }
    }

    private void updateClientOnLineUserList() {
        // 更新全部客户端的在线人数列表
        // 拿到全部在线客户端的用户名称,把这些名称转发给全部在线socket管道。
        // 1、拿到当前全部在线用户昵称
        Collection<String> onLineUsers = Server.onLineSockets.values();
        // 2、把这个集合中的所有用户都推送给全部客户端socket管道。
        for (Socket socket : Server.onLineSockets.keySet()) {
            try {
                // 3、把集合中的所有用户名称,通过socket管道发送给客户端
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(1); // 1代表告诉客户端接下来是在线人数列表信息  2 代表发的是群聊消息
                dos.writeInt(onLineUsers.size()); // 告诉客户端,接下来要发多少个用户名称
                for (String onLineUser : onLineUsers) {
                    dos.writeUTF(onLineUser);
                }
                dos.flush();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

第七步: 接收客户端的群聊消息

线程没收一个客户端的群聊消息,就应该把这个消息转发给全部在线的客户端对应的socket管道

package com.itheima;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 接收的消息可能有很多种类型:1、登录消息(包含昵称) 2、群聊消息 3、私聊消息
            // 所以客户端必须声明协议发送消息
            // 比如客户端先发1,代表接下来是登录消息。
            // 比如客户端先发2,代表接下来是群聊消息。
            // 先从socket管道中接收客户端发送来的消息类型编号
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            while (true) {
                int type = dis.readInt(); // 1、2、3
                switch (type){
                    case 1:
                        // 客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表。
                        String nickname = dis.readUTF();
                        // 把这个登录成功的客户端socket存入到在线集合。
                        Server.onLineSockets.put(socket, nickname);
                        // 更新全部客户端的在线人数列表
                        updateClientOnLineUserList();
                        break;
                    case 2:
                        // 客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息转发给全部在线客户端。
                        String msg = dis.readUTF();
                        sendMsgToAll(msg);
                        break;
                    case 3:
                        // 客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息转发给指定客户端。
                        break;
                }
            }
        } catch (Exception e) {
            System.out.println("客户端下线了:"+ socket.getInetAddress().getHostAddress());
            Server.onLineSockets.remove(socket); // 把下线的客户端socket从在线集合中移除
            updateClientOnLineUserList(); // 下线了用户也需要更新全部客户端的在线人数列表。
        }
    }

    // 给全部在线socket推送当前客户端发来的消息
    private void sendMsgToAll(String msg) {
        // 一定要拼装好这个消息,再发给全部在线socket.
        StringBuilder sb = new StringBuilder();
        String name = Server.onLineSockets.get(socket);

        // 获取当前时间
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");
        String nowStr = dtf.format(now);

        String msgResult = sb.append(name).append(" ").append(nowStr).append("\r\n")
                .append(msg).append("\r\n").toString();
        // 推送给全部客户端socket
        for (Socket socket : Server.onLineSockets.keySet()) {
            try {
                // 3、把集合中的所有用户名称,通过socket管道发送给客户端
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(2); // 1代表告诉客户端接下来是在线人数列表信息  2 代表发的是群聊消息
                dos.writeUTF(msgResult);
                dos.flush(); // 刷新数据!
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void updateClientOnLineUserList() {
        // 更新全部客户端的在线人数列表
        // 拿到全部在线客户端的用户名称,把这些名称转发给全部在线socket管道。
        // 1、拿到当前全部在线用户昵称
        Collection<String> onLineUsers = Server.onLineSockets.values();
        // 2、把这个集合中的所有用户都推送给全部客户端socket管道。
        for (Socket socket : Server.onLineSockets.keySet()) {
            try {
                // 3、把集合中的所有用户名称,通过socket管道发送给客户端
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(1); // 1代表告诉客户端接下来是在线人数列表信息  2 代表发的是群聊消息
                dos.writeInt(onLineUsers.size()); // 告诉客户端,接下来要发多少个用户名称
                for (String onLineUser : onLineUsers) {
                    dos.writeUTF(onLineUser);
                }
                dos.flush();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

6、完善整个客户端程序的代码

第一步:从登录界面开始: 完成了登录,完成了socket管道传给消息聊天界面。

给这个进入按钮绑定一个点击事件监听器,让他可以点击,一旦点击了,获取到昵称,然后立即请求与服务器端的socket管道链接。

并立即发送登录信息:发送1,发送昵称。

再展示客户端的聊天界面: 接收到了昵称,接收到了属于自己客户端的socket通信管道。

第二步:立即在消息聊天界面,立即读取客户端socket管道从服务端发来的在线人数更新消息/群聊消息

  • 交给一个独立的线程专门负责读取客户端socket从服务端收到的在线人数更新数据和群聊数据。
  • 收到消息,先判断消息的类型,判断是在线人数更新消息还是群聊消息,分开处理。

第三步:接收群聊消息。

  • 接收消息类型 2 ,接收群聊数据,展示到界面的面板上去即可。

第四步:发送群聊消息。

  • 给发送按钮绑定一个点击事件,获取输入框的消息内容,再先发送2,再把群聊内容发送给服务端就完事了。

完整代码如下:

  • 服务端
package ChatServer;

public class Constant {
    public static final int PORT = 8888;
}
package ChatServer;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class Server {
    // 定义一个Map集合,键是存储客户端的管道,值是这个管道的用户名称。
  public static final Map<Socket, String> onLineSockets = new HashMap<>();

    public static void main(String[] args) {
        System.out.println("启动服务端系统.....");
        try {
            // 1、注册端口。
            ServerSocket serverSocket = new ServerSocket(Constant.PORT);

            // 2、主线程负责接受客户端的连接请求
            while (true) {
                // 3、调用accept方法,获取到客户端的Socket对象
                System.out.println("等待客户端的连接.....");

                Socket socket = serverSocket.accept();
                new ServerReaderThread(socket).start();

                System.out.println("一个客户端连接成功.....");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package ChatServer;
//线程

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;

public class ServerReaderThread extends Thread {
    private Socket socket;

    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try {
            // 接收的消息可能有很多种类型:1、登录消息(包含昵称) 2、群聊消息 3、私聊消息
            // 所以客户端必须声明协议发送消息
            // 比如客户端先发1,代表接下来是登录消息。
            // 比如客户端先发2,代表接下来是群聊消息。

            // 先从socket管道中接收客户端发送来的消息类型编号
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            while (true) {
                int type = dis.readInt();//先读类型编号
                switch (type) {
                    case 1:
                        // 客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表。
                        String nickname = dis.readUTF();//接收名称
                        // 把这个登录成功的客户端socket存入到 在线集合。
                        Server.onLineSockets.put(socket, nickname);
                        // 更新全部客户端的在线人数列表
                        updateClientOnLineUserList();
                        break;
                    case 2:
                        // 客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息转发给全部在线客户端。
                        String msg = dis.readUTF();//接收消息内容
                        sentMsgToAll(msg);
                        break;
                    case 3:
                        // 客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息转发给指定客户端。
                        break;
                }
            }
        } catch (Exception e) {
            System.out.println("客户端下线了:" + socket.getInetAddress().getHostAddress());
            Server.onLineSockets.remove(socket);//把下线的客户端socket从在线集合中移除
            updateClientOnLineUserList();//下线也更新所有人的列表
        }

    }

    private void sentMsgToAll(String mag) {
        // 一定要拼装好这个消息,再发给全部在线socket.
        StringBuilder sb=new StringBuilder();//使用StringBuilder类来拼接内容
        String name=Server.onLineSockets.get(socket);//得到当前socket名称
        LocalDateTime now=LocalDateTime.now();//获取当前时间
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");//指定格式
        String date=now.format(dtf);
        String msgResult=sb.append(name).append(" ").append(date).append("\r\n").append(mag).append("\r\n").toString();//打印消息
        //把这个集合中的所有用户都推送给全部客户端的Socket管道
        for (Socket socket : Server.onLineSockets.keySet()) {

            try {
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(2);// 2代表告诉客户端接下来是群聊消息
                dos.writeUTF(msgResult);//群聊消息内容
                dos.flush();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }


    }

    private void updateClientOnLineUserList() {
        //更新全部客户端的在线人数列表
        //拿到全部在线客户端的用户名称,把这些名称转发给全部在线的Socket管道
        Collection<String> onLineUsers = Server.onLineSockets.values();
        //把这个集合中的所有用户都推送给全部客户端的Socket管道
        for (Socket socket : Server.onLineSockets.keySet()) {

            try {
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(1);// 1代表告诉客户端接下来是在线人数列表信息
                dos.writeInt(onLineUsers.size());
                for (String user : onLineUsers) {
                    dos.writeUTF(user);
                }
                dos.flush();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}
  • 客户端
package UI;

public class Constant {
    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 8888;
}
package UI;

import javax.swing.*;
import java.awt.*;
import java.io.DataOutputStream;
import java.net.Socket;

public class ChatEntryFrame extends JFrame {

    private JTextField nicknameField;
    private JButton enterButton;
    private JButton cancelButton;
    private Socket socket;

    public ChatEntryFrame() {
        setTitle("局域网聊天室");
        setSize(350, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setResizable(false); // 禁止调整大小

        // 设置背景颜色
        getContentPane().setBackground(Color.decode("#F0F0F0"));

        // 创建主面板并设置布局
        JPanel mainPanel = new JPanel(new BorderLayout());
        mainPanel.setBackground(Color.decode("#F0F0F0"));
        add(mainPanel);

        // 创建顶部面板
        JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
        topPanel.setBackground(Color.decode("#F0F0F0"));

        // 标签和文本框
        JLabel nicknameLabel = new JLabel("昵称:");
        nicknameLabel.setFont(new Font("楷体", Font.BOLD, 16));
        nicknameField = new JTextField(10);
        nicknameField.setFont(new Font("楷体", Font.PLAIN, 16));
        nicknameField.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY),
                BorderFactory.createEmptyBorder(5, 5, 5, 5)
        ));

        topPanel.add(nicknameLabel);
        topPanel.add(nicknameField);
        mainPanel.add(topPanel, BorderLayout.NORTH);

        // 按钮面板
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
        buttonPanel.setBackground(Color.decode("#F0F0F0"));

        enterButton = new JButton("进入");
        enterButton.setFont(new Font("楷体", Font.BOLD, 16));
        enterButton.setBackground(Color.decode("#007BFF"));
        enterButton.setForeground(Color.WHITE);
        enterButton.setBorderPainted(false);
        enterButton.setFocusPainted(false);

        cancelButton = new JButton("取消");
        cancelButton.setFont(new Font("楷体", Font.BOLD, 16));
        cancelButton.setBackground(Color.decode("#DC3545"));
        cancelButton.setForeground(Color.WHITE);
        cancelButton.setBorderPainted(false);
        cancelButton.setFocusPainted(false);

        buttonPanel.add(enterButton);
        buttonPanel.add(cancelButton);
        mainPanel.add(buttonPanel, BorderLayout.SOUTH);

        // 添加监听器
        enterButton.addActionListener(e -> {
            String nickname = nicknameField.getText();
            nicknameField.setText("");
            if (!nickname.isEmpty()) {
                try {
                    login(nickname);
                    // 进入聊天室逻辑,把名称传给聊天界面,通信管道也要传给聊天界面
                    new ClientChatFrame(nickname,socket);
                    this.dispose(); // 关闭窗口
                } catch (Exception ex) {
                     ex.printStackTrace();
                }

            } else {
                JOptionPane.showMessageDialog(this, "请输入昵称!");
            }
        });

        cancelButton.addActionListener(e -> System.exit(0));

        this.setVisible(true);
    }

    private void login(String name) throws Exception {
        //立即发送登录消息给服务端程序
        //请求一个socket管道建立与服务端的连接
         socket = new Socket(Constant.SERVER_IP,Constant.SERVER_PORT);
        //立即发送消息类型和名称
        DataOutputStream dos=new DataOutputStream(socket.getOutputStream());
        dos.writeInt(1);
        dos.writeUTF(name);
        dos.flush();

    }

}
package UI;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.List;

public class ClientChatFrame extends JFrame {
    public JTextArea smsContent = new JTextArea(23, 50);
    private JTextArea smsSend = new JTextArea(4, 40);
    public JList<String> onLineUsers = new JList<>();
    private JButton sendBn = new JButton("发送");
    private Socket socket;
    private String name;
    public ClientChatFrame() {
        initView();
        this.setVisible(true);
    }
    public ClientChatFrame(String name, Socket socket) {
        this();//调用上面的无参构造器,初始化界面信息
        //初始化数据
        //立马展示名称到窗口
        this.setTitle(name+"的聊天窗口");
        this.socket = socket;

        //立即把客户端的这个Socket管道交给一个独立的线程专门负责读取客户端socket从服务端收到的在线人数更新数据和群聊数据。
        new ClientReaderThread(socket,this).start();
    }

    private void initView() {
        this.setSize(750, 600);
        this.setLayout(new BorderLayout());
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭窗口,退出程序
        this.setLocationRelativeTo(null); // 窗口居中

        // 设置窗口背景色
        this.getContentPane().setBackground(new Color(0xf0, 0xf0, 0xf0));

        // 设置字体
        Font font = new Font("SimKai", Font.PLAIN, 14);

        // 消息内容框
        smsContent.setFont(font);
        smsContent.setBackground(new Color(0xdd, 0xdd, 0xdd));
        smsContent.setEditable(false);

        // 发送消息框
        smsSend.setFont(font);
        smsSend.setWrapStyleWord(true);
        smsSend.setLineWrap(true);


        // 在线用户列表
        onLineUsers.setFont(font);
        onLineUsers.setFixedCellWidth(120);
        onLineUsers.setVisibleRowCount(13);

        // 创建底部面板
        JPanel bottomPanel = new JPanel(new BorderLayout());
        bottomPanel.setBackground(new Color(0xf0, 0xf0, 0xf0));

        // 消息输入框
        JScrollPane smsSendScrollPane = new JScrollPane(smsSend);
        smsSendScrollPane.setBorder(BorderFactory.createEmptyBorder());
        smsSendScrollPane.setPreferredSize(new Dimension(500, 50));

        // 发送按钮
        sendBn.setFont(font);
        sendBn.setBackground(Color.decode("#009688"));
        sendBn.setForeground(Color.WHITE);

        // 按钮面板
        JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5));
        btns.setBackground(new Color(0xf0, 0xf0, 0xf0));
        btns.add(sendBn);

        //给发送按钮绑定点击事件
        sendBn.addActionListener(e->{
            //获取输入框中的内容
            String msg=smsSend.getText();
            //清空输入框
            smsSend.setText("");
            //发送消息
            sentMsgToServer(msg);
        });

        // 添加组件
        bottomPanel.add(smsSendScrollPane, BorderLayout.CENTER);
        bottomPanel.add(btns, BorderLayout.EAST);

        // 用户列表面板
        JScrollPane userListScrollPane = new JScrollPane(onLineUsers);
        userListScrollPane.setBorder(BorderFactory.createEmptyBorder());
        userListScrollPane.setPreferredSize(new Dimension(120, 500));

        // 中心消息面板
        JScrollPane smsContentScrollPane = new JScrollPane(smsContent);
        smsContentScrollPane.setBorder(BorderFactory.createEmptyBorder());

        // 添加所有组件
        this.add(smsContentScrollPane, BorderLayout.CENTER);
        this.add(bottomPanel, BorderLayout.SOUTH);
        this.add(userListScrollPane, BorderLayout.EAST);
    }
    //发送消息给服务器
    private void sentMsgToServer(String msg) {
        try {
            DataOutputStream dos=new DataOutputStream(socket.getOutputStream());
            //把消息发给服务器
            dos.writeInt(2);
            dos.writeUTF(msg);
            dos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new ClientChatFrame();
    }

    public void updateOnlineUsers(String[] onLineNames) {
        //把线程读取到的在线用户名称展示到界面上
        onLineUsers.setListData(onLineNames);
    }

    public void setMsgToWin(String msg) {
        //更新群聊消息到界面展示
        smsContent.append(msg);
    }
}
package UI;
//线程

import java.io.DataInputStream;
import java.net.Socket;

public class ClientReaderThread extends Thread {
    private Socket socket;
    private DataInputStream dis;
    private ClientChatFrame win;
    public ClientReaderThread(Socket socket, ClientChatFrame win) {
        this.socket = socket;
        this.win = win;
    }

    public void run() {
        try {
            // 接收的消息可能有很多种类型:1、登录消息(包含昵称) 2、群聊消息 3、私聊消息
            dis = new DataInputStream(socket.getInputStream());
            while (true) {
                int type = dis.readInt();//先读类型编号
                switch (type) {
                    case 1:
                        // 服务端发来了在线人数更新信息登录消息
                        updateClientOnLineUserList();
                        break;
                    case 2:
                        // 服务端发来了群聊消息
                        getMsgToWin();
                        break;
                }
            }
        } catch (Exception e) {
           e.printStackTrace();
        }
    }

    private void getMsgToWin() throws Exception {
        //获取群聊消息
        String msg=dis.readUTF();
        win.setMsgToWin(msg);
    }

    //更新全部客户端的在线人数列表
    private void updateClientOnLineUserList() throws Exception {
        //读取有多少个在线用户
        int count = dis.readInt();
        String[] onLineNames= new String[count];
        //循环控制读取多少个用户信息
        for (int i = 0; i < count; i++) {

            String nickname = dis.readUTF();
        //将每个用户添加到集合中
            onLineNames[i]=nickname;
        }
        //将集合中都数据展示到窗口上
        win.updateOnlineUsers(onLineNames);


    }
}
  • 启动客户端
import UI.ChatEntryFrame;

public class APP {
    public static void main(String[] args) {
        new ChatEntryFrame();
    }
}
posted @ 2025-12-11 21:51  芒果学代码  阅读(2)  评论(0)    收藏  举报