通信网络编程3.0——JAVA

发布于:2025-06-24 ⋅ 阅读:(21) ⋅ 点赞:(0)

 主要添加了私聊功能

1服务器类定义与成员变量

public class ChatServer {
    int port = 6666;// 定义服务器端口号为 6666
    ServerSocket ss;// 定义一个 ServerSocket 对象用于监听客户端连接
    //List<Socket> clientSockets = new ArrayList<>();// 定义一个列表用于存储已连接的客户端 Socket 对象
    List<Socket> clientSockets = new CopyOnWriteArrayList<>();
    List<String> clientNames = new ArrayList<>(10);

    //迭代时会复制整个底层数组,因此在遍历过程中其他线程对集合的修改不会影响当前遍历,
    // 有效避免了 ConcurrentModificationException 异常。
}

  • 端口号:服务器的端口号被定义为 6666,客户端需要知道这个端口号才能与服务器建立连接。
  • ServerSocket 对象ServerSocket 是 Java 提供的一种用于服务器端编程的类,它负责监听特定端口上的客户端连接请求。一旦有客户端连接,就会创建一个新的 Socket 对象来处理与该客户端之间的通信。
  • 客户端套接字列表clientSockets 使用了 CopyOnWriteArrayList 来存储与客户端建立连接的 Socket 对象。这种数据结构在遍历时会复制整个底层数组,因此在遍历过程中其他线程对集合的修改不会影响当前遍历,有效避免了 ConcurrentModificationException 异常。
  • 客户端名称列表clientNames 用于存储每个连接客户端的名称,方便在消息中区分不同的客户端。

2初始化服务器

public void initServer() {// 初始化服务器的方法
        try {

            ss = new ServerSocket(port);// 创建 ServerSocket 对象并绑定到指定端口
            System.out.println("服务器启动,等待客户端连接...");

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
  •  在服务器初始化方法 initServer 中,通过调用 ServerSocket 的构造函数并传入端口号,创建了一个 ServerSocket 对象,绑定了服务器到指定端口,使服务器能够监听该端口上的客户端连接请求。一旦初始化成功,就会打印出 "服务器启动,等待客户端连接..." 的提示信息,告知服务器已正常启动并处于监听状态。

3监听客户端连接

public void listenerConnection() {// 监听客户端连接的方法,返回连接的 Socket 对象
        new Thread(()->{
            while(true){

                try {

                    Socket socket = ss.accept();// 调用 accept() 方法等待客户端连接
                    clientNames.add("Hello");
                    //clientSockets.add(socket);

                    synchronized (clientSockets) {// 同步操作确保线程安全
                        clientSockets.add(socket);// 将连接的客户端 Socket 对象添加到列表中
                    }

                    System.out.println("客户端已连接:" + socket.getInetAddress().getHostAddress());// 输出客户端连接成功提示信息及客户端 IP 地址

                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

            }
        }).start();

    }
  • listenerConnection 方法创建了一个新线程,用于不断地监听客户端的连接请求。在循环中,调用 ss.accept() 方法阻塞等待客户端的连接。一旦有客户端连接,就会创建一个新的 Socket 对象,并将其添加到 clientSockets 列表中。同时,向 clientNames 列表中添加一个默认客户端名称 "Hello",后续可以根据实际情况更新该名称。每当有新的客户端连接成功,就会打印出客户端的 IP 地址,表明该客户端已成功连接到服务器。

4读取客户端消息

public void readMsg(List<Socket> clientSockets, JTextArea msgShow) {// 读取客户端消息的方法

        //System.out.println("clientSockets size: " + clientSockets.size()); // 检查列表大小

        synchronized (clientSockets) {// 对客户端列表进行同步操作

            Thread tt = new Thread(() -> {// 创建一个线程用于读取并处理客户端消息
               //System.out.println("开始读取客户端发送的消息");


                while (true) {// 无限循环持续读取消息


                    InputStream is;// 定义输入流对象用于读取客户端消息
                    Socket socket = null;


                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }


                    for (Socket cSocket : clientSockets) {// 遍历每个客户端 Socket
                       //System.out.println("循环每个socket");
                           socket = cSocket;


                           if(socket == null){
                               continue;
                           }


                           try {

                               is = socket.getInputStream();// 获取客户端 Socket 对象的输入流
                           } catch (IOException e) {
                               throw new RuntimeException(e);
                           }



                        try {

                            int idLen = is.read();// 读取消息中发送方名称长度的字节


                            if(idLen == 0){
                                continue;
                            }


                            byte[] id = new byte[idLen];// 根据读取的长度创建字节数组存储发送方名称
                            is.read(id);// 读取发送方名称字节数组

                            int si = clientSockets.indexOf(socket);

                            clientNames.set(si,new String(id));


                            int msgLen = is.read();// 读取消息内容长度的字节

                            if(msgLen == 0){
                                continue;
                            }

                            byte[] msg = new byte[msgLen];// 根据读取的长度创建字节数组存储消息内容
                            is.read(msg);// 读取消息内容字节数组


                            String clientmsg = new String(msg);
                            //先判断@有没有
                            int start = clientmsg.indexOf('@');
                            if(start != -1){

                                int end = clientmsg.indexOf(' ');
                                String friendID = clientmsg.substring(start+1,end);

                                String message = clientmsg.substring(end);


                                System.out.println(new String(id) + "发送给" + friendID + " 的一条私聊消息:" + message);// 将字节数组转换为字符串并输出消息内容
                                msgShow.append(new String(id) + "发送给" + friendID + " 的一条私聊消息:" + message + "\n");

                                for (Socket clientSocket : clientSockets) {// 遍历所有已连接的客户端 Socket 对象

                                    if (clientSocket == socket) {// 如果是当前发送消息的客户端
                                        continue;
                                    }

                                    int s = clientSockets.indexOf(clientSocket);
                                    String name = clientNames.get(s);

                                    if(name.equals(friendID)){

                                        OutputStream os = null;// 定义输出流对象用于向其他客户端发送消息
                                        os = clientSocket.getOutputStream();// 获取客户端 Socket 对象的输出流
                                        os.write(id.length);// 发送发送方名称长度
                                        os.write(id);// 发送发送方名称字节数组

                                        message += "(这是一条私聊消息)";
                                        os.write(message.getBytes().length);// 发送消息内容长度
                                        os.write(message.getBytes());// 发送消息内容字节数组
                                        os.flush();// 刷新输出流确保数据发送完成
                                        break;
                                    }



                                }


                            }else {
                                System.out.println(new String(id) + "发送的消息:" + new String(msg));// 将字节数组转换为字符串并输出消息内容
                                msgShow.append(new String(id) + "说:" + new String(msg) + "\n");




                                // 转发信息给所有其他客户端
                                for (Socket clientSocket : clientSockets) {// 遍历所有已连接的客户端 Socket 对象

                                    if (clientSocket == socket) {// 如果是当前发送消息的客户端
                                        continue;
                                    }

                                    OutputStream os = null;// 定义输出流对象用于向其他客户端发送消息
                                    os = clientSocket.getOutputStream();// 获取客户端 Socket 对象的输出流
                                    os.write(id.length);// 发送发送方名称长度
                                    os.write(id);// 发送发送方名称字节数组
                                    os.write(msg.length);// 发送消息内容长度
                                    os.write(msg);// 发送消息内容字节数组
                                    os.flush();// 刷新输出流确保数据发送完成

                                }
                            }



                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }

                    }

                }

            });
            tt.start();

        }
    }
  • readMsg 方法的核心功能是读取客户端发送的消息,并根据消息类型(普通消息或私聊消息)进行相应的处理和转发。
  • 首先,创建一个新线程 tt,在无限循环中依次检查 clientSockets 列表中的每个 Socket 对象。对于每个客户端 Socket,通过 socket.getInputStream() 获取输入流,从而读取客户端发送的数据。
  • 客户端发送的数据格式预先约定为:首先是一个字节表示发送方名称的长度,然后是发送方名称对应的字节数组;接下来是一个字节表示消息内容长度,最后是消息内容对应的字节数组。按照这种格式,先读取发送方名称长度 idLen,根据该长度创建字节数组 id 读取发送方名称,再读取消息内容长度 msgLen,创建字节数组 msg 读取消息内容。
  • 随后,将读取到的发送方名称更新到对应的 clientNames 列表位置。接下来对消息内容进行判断,如果消息内容中包含 "@" 符号,则认为这是一条私聊消息。通过解析消息内容,获取私聊目标的名称 friendID 和实际消息内容 message,然后在 clientSockets 中查找对应的私聊目标客户端 Socket,并将私聊消息发送给该目标客户端。
  • 如果消息内容中不包含 "@" 符号,则认为这是一条普通消息,直接将消息转发给除发送方外的所有其他客户端。
  • 无论是普通消息还是私聊消息,都通过输出流 OutputStream 将消息发送给目标客户端。发送时,同样按照约定的数据格式,先发送发送方名称的长度和名称字节数组,再发送消息内容的长度和内容字节数组,并调用 os.flush() 刷新输出流确保数据发送完成。

5服务器启动

public void start() {// 启动服务器的方法

        initServer();// 调用初始化服务器的方法

        //new Thread(()->{
        //startSend();// 启动服务端从控制台向所有客户端发送消息的线程
        //}).start();

        ChatUI ui = new ChatUI("服务端", clientSockets);
        ui.setVisible(true); // 确保 UI 可见
        listenerConnection();// 调用监听客户端连接的方法

        readMsg(clientSockets,ui.msgShow);// 调用读取消息的方法

    }
  • start 方法中,首先调用 initServer 方法初始化服务器,然后创建一个 ChatUI 对象作为服务器端的用户界面(假设存在 ChatUI 类,用于展示聊天内容等信息),使用户界面可见。接着调用 listenerConnection 方法启动监听客户端连接的线程,最后调用 readMsg 方法启动读取消息的线程,并将读取到的消息显示在用户界面中。

6主方法

public static void main(String[] args) {

        ChatServer server = new ChatServer();// 创建 ChatServer 对象
        server.start();// 调用启动服务器的方法

    }
  • main 方法作为程序的入口,创建了一个 ChatServer 对象,并调用其 start 方法启动服务器。 

7其他模块代码不变

public class Client {
    Socket socket;// 定义 Socket 对象用于与服务器建立连接
    String ip;// 定义服务器 IP 地址
    int port;// 定义服务器端口号
    InputStream in;// 定义输入流对象用于读取服务器发送的消息
    OutputStream out;// 定义输出流对象用于向服务器发送消息

    public Client(String ip, int port) {// 构造方法,初始化客户端 IP 地址和端口号
        this.ip = ip;
        this.port = port;
    }

    public void connectServer(String userName) {// 连接服务器的方法
        try {


            socket = new Socket(ip, port);// 创建 Socket 对象连接到指定 IP

            in = socket.getInputStream();// 获取 Socket 对象的输入流用于读取消息
            out = socket.getOutputStream();// 获取 Socket 对象的输出流用于发送消息


            try {
                out.write(userName.length());
                out.write(userName.getBytes());
                out.flush();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }


            System.out.println("连接服务器成功");


        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void readMsg(JTextArea msgShow) {// 读取服务器发送的消息的方法

        new Thread(() -> {// 创建一个线程用于读取并处理服务器消息
            try {
                System.out.println("开始读取消息");

                while (true) { // 无限循环持续读取消息


                    int senderNameLength = in.read();// 读取发送方名称长度的字节
                    byte[] senderNameBytes = new byte[senderNameLength];// 根据读取的长度创建字节数组存储发送方名称
                    in.read(senderNameBytes);// 读取发送方名称字节数组


                    int msgLength = in.read();// 读取消息内容长度的字节
                    byte[] msgBytes = new byte[msgLength];// 根据读取的长度创建字节数组存储消息内容
                    in.read(msgBytes);// 读取消息内容字节数组


                    System.out.println(new String(senderNameBytes) + "发送的消息:" + new String(msgBytes));// 将字节数组转换为字符串并输出消息内容
                    msgShow.append(new String(senderNameBytes) +"说:" + new String(msgBytes) + "\n");
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }

/*

    public void startSend() {// 从控制台向服务器发送消息的方法
        new Thread(() -> {// 创建一个线程用于读取控制台输入并发送消息
            try {

                BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));// 创建缓冲读取器读取控制台输入
                System.out.println("请输入用户名:");
                String userName = reader.readLine();// 读取一行控制台输入作为用户名
                System.out.println("请输入消息(按回车发送):");

                while (true) {// 无限循环持续读取并发送消息

                    String msg = reader.readLine();// 读取一行控制台输入作为消息内容
                    if (msg != null && !msg.isEmpty()) {// 如果输入的消息不为空
                        out.write(userName.getBytes().length);// 发送用户名长度
                        out.write(userName.getBytes());// 发送用户名字节数组
                        out.write(msg.getBytes().length);// 发送消息内容长度
                        out.write(msg.getBytes());// 发送消息内容字节数组
                        out.flush();// 刷新输出流确保数据发送完成

                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }

    */

    public void startClient() {// 启动客户端的方法

        String userName = JOptionPane.showInputDialog("请输入用户名:");

        connectServer(userName);// 调用连接服务器的方法
        ChatUI ui = new ChatUI(userName, out);
        readMsg(ui.msgShow);// 调用读取消息的方法
        //startSend();// 调用发送消息的方法

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


        new Thread() {
            public void run() {
                while (true) {

                    try {

                        out.write(0);
                        out.flush();
                        Thread.sleep(500);

                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                }
            }

        }.start();

    }

    public static void main(String[] args) {
        Client client = new Client("127.0.0.1", 6666);// 创建 Client 对象,连接到本地主机的 6666 端口
        client.startClient();// 调用启动客户端的方法
    }
}
public class ChatUI extends JFrame {
    public JTextArea msgShow = new JTextArea();// 显示消息的文本区域

    public ChatUI(String title, List<Socket> clientSockets) {// 服务器端构造方法
        super(title);// 设置窗口标题
        setSize(500, 500);// 设置窗口大小
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置关闭操作

        JScrollPane scrollPane = new JScrollPane(msgShow);// 创建滚动面板包括消息显示区域
        scrollPane.setPreferredSize(new Dimension(0, 350));
        add(scrollPane, BorderLayout.NORTH);// 添加到窗口北部

        // 创建消息输入面板及组件
        JPanel msgInput = new JPanel();
        JTextArea msg = new JTextArea();
        JScrollPane scrollPane1 = new JScrollPane(msg);
        scrollPane1.setPreferredSize(new Dimension(480, 80));
        msgInput.add(scrollPane1);
        JButton send = new JButton("发送");
        msgInput.add(send);
        msgInput.setPreferredSize(new Dimension(0, 120));
        add(msgInput, BorderLayout.SOUTH);// 添加到窗口南部

        setVisible(true);

        ChatListener cl = new ChatListener();// 创建事件监听器
        send.addActionListener(cl);// 为发送按钮添加监听器

        cl.showMsg = msgShow;// 传递消息显示组件
        cl.msgInput = msg;
        cl.userName = title;
        cl.clientSockets = clientSockets;

    }

    public ChatUI(String title, OutputStream out) {// 客户端构造方法
        super(title);
        setSize(500, 500);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JScrollPane scrollPane = new JScrollPane(msgShow);
        scrollPane.setPreferredSize(new Dimension(0, 350));
        add(scrollPane, BorderLayout.NORTH);

        JPanel msgInput = new JPanel();
        JTextArea msg = new JTextArea();
        JScrollPane scrollPane1 = new JScrollPane(msg);
        scrollPane1.setPreferredSize(new Dimension(480, 80));
        msgInput.add(scrollPane1);
        JButton send = new JButton("发送");
        msgInput.add(send);
        msgInput.setPreferredSize(new Dimension(0, 120));
        add(msgInput, BorderLayout.SOUTH);

        setVisible(true);

        clientListener cl = new clientListener();
        send.addActionListener(cl);

        cl.showMsg = msgShow;
        cl.msgInput = msg;
        cl.userName = title;
        cl.out = out;

    }
}
public class ChatListener implements ActionListener {
    public List<Socket> clientSockets;// 客户端 Socket 列表
    JTextArea showMsg;// 消息显示区域
    JTextArea msgInput;// 消息输入区域
    String userName;// 用户名
    OutputStream out;// 输出流

    public void actionPerformed(ActionEvent e) {// 处理发送按钮点击事件
        String text = msgInput.getText();// 获取输入的消息文本

        showMsg.append(userName + ": " + text + "\n");// 在显示区域追加消息

        for (Socket cSocket : clientSockets) {// 遍历所有客户端
            Socket socket = cSocket;
            try {
                out = socket.getOutputStream();// 获取客户端输出流
                out.write(userName.getBytes().length);// 发送用户名长度
                out.write(userName.getBytes());// 发送用户名
                out.write(text.getBytes().length);// 发送消息内容长度
                out.write(text.getBytes());// 发送消息内容
                out.flush();// 刷新输出流
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }

    }

}
public class clientListener implements ActionListener {
    JTextArea showMsg;// 消息显示区域
    JTextArea msgInput;// 消息输入区域
    String userName;// 用户名
    OutputStream out;// 输出流

    public void actionPerformed(ActionEvent e) {// 处理发送按钮点击
        String text = msgInput.getText();// 获取输入消息

        showMsg.append(userName + ": " + text + "\n");// 显示消息

        try {
            out.write(userName.getBytes().length);// 发送用户名长度
            out.write(userName.getBytes());// 发送用户名
            out.write(text.getBytes().length);// 发送消息长度
            out.write(text.getBytes());// 发送消息内容
            out.flush();// 刷新输出流
            //msgInput.setText(""); // 清空输入框
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }


    }
}

8运行效果


网站公告

今日签到

点亮在社区的每一天
去签到