4.BIO多线程即时通信

2023年 7月 19日 30.2k 0

基于BIO模式下的即时通信,我们需要解决客户端到客户端的通信,也就是需要实现客户端与客户端的端口消息转发逻辑。

功能清单

1.客户端登陆功能

  • 可以启动客户端进行登录,客户端登陆只需要输入用户名和服务端ip地址即可。

2.在线人数实时更新。

  • 客户端用户户登陆以后,需要同步更新所有客户端的联系人信息栏。

3.离线人数更新

  • 检测到有客户端下线后,需要同步更新所有客户端的联系人信息栏。

4.群聊

  • 任意一个客户端的消息,服务器端收到以后再推送给其他所有客户端接收,即A发了一个消息,将消息转发给所有的在线人员 B C D 即在线的Socket集合。

image.png

5.私聊

  • 可以选择某个员工,点击私聊按钮,然后发出的消息可以被该客户端单独接收。

6.@消息

  • 可以选择某个员工,然后发出的消息可以@该用户,服务器端收到以后再推送其他所有人接收

7.消息用户和消息时间点

  • 服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或者选择。
  • 1代表接收的是登陆消息
  • 2代表群发@消息
  • 3代表了私聊消息

代码实现


    import com.itheima.util.Constants;
    ​
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.HashMap;
    import java.util.Map;
    ​
    /**
     *
     */
    public class ServerChat {
        /**
         * 定义一个集合存放所有在线的socket
         * 在线集合只需要一个:存储客户端socket的同时还需要知道这个Socket客户端的名称
         */
        public static Map onLineSockets = new HashMap();
    ​
        public static void main(String[] args) {
            try {
                /** 注册端口 */
                ServerSocket serverSocket = new ServerSocket(Constants.PORT);
                /** 循环一直等待所有可能的客户端连接 */
                while(true){
                    Socket socket = serverSocket.accept();
                    /** 
                    服务端需要接收多个客户端,目前我们采取的策略是一个客户端对应一个服务端线程。
                    */
                    new ServerReader(socket).start();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    package com.itheima.server;
    import com.itheima.util.Constants;
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.net.Socket;
    import java.text.SimpleDateFormat;
    import java.util.Collection;
    import java.util.Set;
    ​
    public class ServerReader extends Thread {
        /****
        将客户端的socket作为参数传入
        ***/
        private Socket socket;
        public ServerReader(Socket socket) {
            this.socket = socket;
        }
    ​
        @Override
        public void run() {
            DataInputStream dis = null;
            try {
                dis = new DataInputStream(socket.getInputStream());
                /** 1.循环一直等待客户端的消息 */
                while(true){
                    /** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */
                    int flag = dis.readInt();
                    if(flag == 1){
                        /** 先将当前登录的客户端socket存到在线人数的socket集合中   */
                        String name = dis.readUTF() ;
                        System.out.println
                                    (name+"---->"+socket.getRemoteSocketAddress());
                        //放入在线集合
                        ServerChat.onLineSockets.put(socket, name);
                    }
                    //写入的是 flag,dis
                    writeMsg(flag,dis);
                }
            } catch (Exception e) {
                System.out.println("--有人下线了--");
                // 从在线人数中将当前socket移出去  
                ServerChat.onLineSockets.remove(socket);
                try {
                    // 重新更新在线人数并发给所有客户端 
                    writeMsg(1,dis);
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
    ​
        }
    ​
        private void writeMsg(int flag, DataInputStream dis) throws Exception {
    //      DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 
            // 定义一个变量存放最终的消息形式 
            String msg = null ;
            if(flag == 1){
                /** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
                StringBuilder rs = new StringBuilder();
                Collection onlineNames = ServerChat.onLineSockets.values();
                // 判断是否存在在线人数 
                if(onlineNames != null && onlineNames.size() > 0){
                    for(String name : onlineNames){
                        rs.append(name+ Constants.SPILIT);
                    }
                    // 去掉最后的一个分隔符
                    msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));
                    /** 将消息发送给所有的客户端 */
                    sendMsgToAll(flag,msg);
                }
            }else if(flag == 2 || flag == 3){
                // 读到消息  群发的 或者 @消息
                String newMsg = dis.readUTF() ; // 消息
                // 得到发件人 
                String sendName = ServerChat.onLineSockets.get(socket);
                //    内容--
                StringBuilder msgFinal = new StringBuilder();
                // 时间  
                SimpleDateFormat sdf = 
                        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
                if(flag == 2){
                    //@消息 所有人看见
                    msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis()*2)).append("rn");
                    msgFinal.append("    ").append(newMsg).append("rn");
                    //所有人都发送
                    sendMsgToAll(flag,msgFinal.toString());
                }else if(flag == 3){
                    // 私发消息 
                    msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis()*2)).append("对您私发rn");
                    msgFinal.append("    ").append(newMsg).append("rn");
                    // 私发消息 
                    // 得到给谁私发 
                    String destName = dis.readUTF();
                    sendMsgToOne(destName,msgFinal.toString());
                }
            }
        }
        /**
         * @param destName 对谁私发 
         * @param msg 发的消息内容 
         * @throws Exception
         */
        private void sendMsgToOne(String destName, String msg) throws Exception {
            // 拿到所有的在线socket管道 给这些管道写出消息
            Set allOnLineSockets = ServerChat.onLineSockets.keySet();
            for(Socket sk :  allOnLineSockets){
                // 得到当前需要私发的socket 
                // 只对这个名字对应的socket私发消息
                if(ServerChat.onLineSockets.get(sk).trim().equals(destName)){
                    DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
                    dos.writeInt(2); // 消息类型
                    dos.writeUTF(msg);
                    dos.flush();
                }
            }
    ​
        }
    ​
        private void sendMsgToAll(int flag, String msg) throws Exception {
            // 拿到所有的在线socket管道 给这些管道写出消息
            Set allOnLineSockets = ServerChat.onLineSockets.keySet();
            for(Socket sk :  allOnLineSockets){
                DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
                dos.writeInt(flag); // 消息类型
                dos.writeUTF(msg);
                dos.flush();
            }
        }
    }


    package com.itheima.client;
    ​
    import com.itheima.util.Constants;
    ​
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.io.DataOutputStream;
    import java.net.Socket;
    ​
    /**
     * 客户端界面
     */
    public class ClientChat implements ActionListener {
        /** 1.设计界面  */
        private JFrame win = new JFrame();
        /** 2.消息内容框架 */
        public JTextArea smsContent =new JTextArea(23 , 50);
        /** 3.发送消息的框  */
        private JTextArea smsSend = new JTextArea(4,40);
        /** 4.在线人数的区域  */
        /** 存放人的数据 */
        /** 展示在线人数的窗口 */
        public JList onLineUsers = new JList();
    ​
        // 是否私聊按钮
        private JCheckBox isPrivateBn = new JCheckBox("私聊");
        // 消息按钮
        private JButton sendBn  = new JButton("发送");
    ​
        // 登录界面
        private JFrame loginView;
    ​
        private JTextField ipEt , nameEt , idEt;
    ​
        private Socket socket ;
    ​
        public static void main(String[] args) {
            new ClientChat().initView();
    ​
        }
    ​
        private void initView() {
            /** 初始化聊天窗口的界面 */
            win.setSize(650, 600);
    ​
            /** 展示登录界面  */
            displayLoginView();
    ​
            /** 展示聊天界面 */
            //displayChatView();
    ​
        }
    ​
        private void displayChatView() {
    ​
            JPanel bottomPanel = new JPanel(new BorderLayout());
            //-----------------------------------------------
            // 将消息框和按钮 添加到窗口的底端
            win.add(bottomPanel, BorderLayout.SOUTH);
            bottomPanel.add(smsSend);
            JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
            btns.add(sendBn);
            btns.add(isPrivateBn);
            bottomPanel.add(btns, BorderLayout.EAST);
            //-----------------------------------------------
            // 给发送消息按钮绑定点击事件监听器
            // 将展示消息区centerPanel添加到窗口的中间
            smsContent.setBackground(new Color(0xdd,0xdd,0xdd));
            // 让展示消息区可以滚动。
            win.add(new JScrollPane(smsContent), BorderLayout.CENTER);
            smsContent.setEditable(false);
            //-----------------------------------------------
            // 用户列表和是否私聊放到窗口的最右边
            Box rightBox = new Box(BoxLayout.Y_AXIS);
            onLineUsers.setFixedCellWidth(120);
            onLineUsers.setVisibleRowCount(13);
            rightBox.add(new JScrollPane(onLineUsers));
            win.add(rightBox, BorderLayout.EAST);
            //-----------------------------------------------
            // 关闭窗口退出当前程序
            win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            win.pack();  // swing 加上这句 就可以拥有关闭窗口的功能
            /** 设置窗口居中,显示出来  */
            setWindowCenter(win,650,600,true);
            // 发送按钮绑定点击事件
            sendBn.addActionListener(this);
        }
    ​
        private void displayLoginView(){
    ​
            /** 先让用户进行登录
             *  服务端ip
             *  用户名
             *  id
             *  */
            /** 显示一个qq的登录框     */
            loginView = new JFrame("登录");
            loginView.setLayout(new GridLayout(3, 1));
            loginView.setSize(400, 230);
    ​
            JPanel ip = new JPanel();
            JLabel label = new JLabel("   IP:");
            ip.add(label);
            ipEt = new JTextField(20);
            ip.add(ipEt);
            loginView.add(ip);
    ​
            JPanel name = new JPanel();
            JLabel label1 = new JLabel("姓名:");
            name.add(label1);
            nameEt = new JTextField(20);
            name.add(nameEt);
            loginView.add(name);
    ​
            JPanel btnView = new JPanel();
            JButton login = new JButton("登陆");
            btnView.add(login);
            JButton cancle = new JButton("取消");
            btnView.add(cancle);
            loginView.add(btnView);
            // 关闭窗口退出当前程序
            loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            setWindowCenter(loginView,400,260,true);
    ​
            /** 给登录和取消绑定点击事件 */
            login.addActionListener(this);
            cancle.addActionListener(this);
    ​
        }
    ​
        private static void setWindowCenter(JFrame frame, int width , int height, boolean flag) {
            /** 得到所在系统所在屏幕的宽高 */
            Dimension ds = frame.getToolkit().getScreenSize();
    ​
            /** 拿到电脑的宽 */
            int width1 = ds.width;
            /** 高 */
            int height1 = ds.height ;
    ​
            System.out.println(width1 +"*" + height1);
            /** 设置窗口的左上角坐标 */
            frame.setLocation(width1/2 - width/2, height1/2 -height/2);
            frame.setVisible(flag);
        }
    ​
        @Override
        public void actionPerformed(ActionEvent e) {
            /** 得到点击的事件源 */
            JButton btn = (JButton) e.getSource();
            switch(btn.getText()){
                case "登陆":
                    String ip = ipEt.getText().toString();
                    String name = nameEt.getText().toString();
                    // 校验参数是否为空
                    // 错误提示
                    String msg = "" ;
                    // 12.1.2.0
                    // d{1,3}.d{1,3}.d{1,3}.d{1,3}
                    if(ip==null || !ip.matches("d{1,3}.d{1,3}.d{1,3}.d{1,3}")){
                        msg = "请输入合法的服务端ip地址";
                    }else if(name==null || !name.matches("S{1,}")){
                        msg = "姓名必须1个字符以上";
                    }
    ​
                    if(!msg.equals("")){
                        /** msg有内容说明参数有为空 */
                        // 参数一:弹出放到哪个窗口里面
                        JOptionPane.showMessageDialog(loginView, msg);
                    }else{
                        try {
                            // 参数都合法了
                            // 当前登录的用户,去服务端登陆
                            /** 先把当前用户的名称展示到界面 */
                            win.setTitle(name);
                            // 去服务端登陆连接一个socket管道
                            socket = new Socket(ip, Constants.PORT);
    ​
                            //为客户端的socket分配一个线程 专门负责收消息
                            new ClientReader(this,socket).start();
    ​
                            // 带上用户信息过去
                            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                            dos.writeInt(1); // 登录消息
                            dos.writeUTF(name.trim());
                            dos.flush();
    ​
                            // 关系当前窗口 弹出聊天界面
                            loginView.dispose(); // 登录窗口销毁
                            displayChatView(); // 展示了聊天窗口了
    ​
    ​
                        } catch (Exception e1) {
                            e1.printStackTrace();
                        }
                    }
                    break;
                case "取消":
                    /** 退出系统 */
                    System.exit(0);
                    break;
                case "发送":
                    // 得到发送消息的内容
                    String msgSend = smsSend.getText().toString();
                    if(!msgSend.trim().equals("")){
                        /** 发消息给服务端 */
                        try {
                            // 判断是否对谁发消息
                            String selectName = onLineUsers.getSelectedValue();
                            int flag = 2 ;// 群发 @消息
                            if(selectName!=null&&!selectName.equals("")){
                                msgSend =("@"+selectName+","+msgSend);
                                /** 判断是否选中了私法 */
                                if(isPrivateBn.isSelected()){
                                    /** 私法 */
                                    flag = 3 ;//私发消息
                                }
    ​
                            }
    ​
                            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                            dos.writeInt(flag); // 群发消息  发送给所有人
                            dos.writeUTF(msgSend);
                            if(flag == 3){
                                // 告诉服务端我对谁私发
                                dos.writeUTF(selectName.trim());
                            }
                            dos.flush();
    ​
                        } catch (Exception e1) {
                            e1.printStackTrace();
                        }
    ​
                    }
                    smsSend.setText(null);
                    break;
            }
        }
    }


    package com.itheima.client;
    import com.itheima.util.Constants;
    import java.io.DataInputStream;
    import java.net.Socket;
    class ClientReader extends Thread {
        private Socket socket;
        private ClientChat clientChat ;
    ​
        public ClientReader(ClientChat clientChat, Socket socket) {
            this.clientChat = clientChat;
            this.socket = socket;
        }
    ​
        @Override
        public void run() {
            try {
                DataInputStream dis = new DataInputStream(socket.getInputStream());
                /** 循环一直等待客户端的消息 */
                while(true){
                    /** 读取当前的消息类型 :登录,群发,私聊 , @消息 */
                    int flag = dis.readInt();
                    if(flag == 1){
                        // 在线人数消息回来了
                        String nameDatas = dis.readUTF();
                        // 展示到在线人数的界面
                        String[] names = nameDatas.split(Constants.SPILIT);
    ​
                        clientChat.onLineUsers.setListData(names);
                    }else if(flag == 2){
                        //群发,私聊 , @消息 都是直接显示的。
                        String msg = dis.readUTF() ;
                        clientChat.smsContent.append(msg);
                        // 让消息界面滾動到底端
                        clientChat.smsContent.setCaretPosition(clientChat.smsContent.getText().length());
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

项目启动

  • 1.首先需要启动服务端,点击ServerChat类直接右键启动,显示服务端启动成功!

  • 2.其次,点击客户端类ClientChat类,在弹出的方框中输入服务端的ip和当前客户端的昵称

image.png

  • 3.登陆进入后的聊天界面如下,即可进行相关操作。

    • 如果直接点击发送,默认发送群聊消息
  • 如果选中右侧在线列表某个用户,默认发送@消息

    • 如果选中右侧在线列表某个用户,然后选择右下侧私聊按钮默,默认发送私聊消息。

image.png

image.png

贴下视频链接:www.bilibili.com/video/BV1gz…

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论