欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

模拟QQ聊天小项目收尾---界面展示服务端与客户端进行信息交互(用到的知识:io,线程,Swing界面,面向对象思想...... )

程序员文章站 2022-05-04 08:54:11
大家好,我是一位在java学习圈中不愿意透露姓名并苟且偷生的小学员,如果文章有错误之处,还望海涵,欢迎多多指正如果你从本文学到有用的干货知识,那么请您尽量点赞,关注,评论,收藏这两天我一直在设计这个小项目,一边跟着老师学,一边自己试着尝试设计,优化代码,以及改bug,接下来为小伙伴们分享我的心路历程以及模拟QQ聊天小项目的实现,具体过程解释已在注释中详细说明,接下来跟我一起手动将小项目做出来吧:这里附上模拟QQ聊天小项目的效果展示图,嘿嘿,先干为敬:若图片无法显示,图片链接如下:https:....

大家好,我是一位在java学习圈中不愿意透露姓名并苟且偷生的小学员,如果文章有错误之处,还望海涵,欢迎多多指正
如果你从本文学到有用的干货知识,那么请您尽量点赞,关注,评论,收藏

这两天我一直在设计这个小项目,一边跟着老师学,一边自己试着尝试设计,优化代码,以及改bug,接下来为小伙伴们分享我的心路历程以及模拟QQ聊天小项目的实现,具体过程解释已在注释中详细说明,接下来跟我一起手动将小项目做出来吧:
这里附上模拟QQ聊天小项目的效果展示图,嘿嘿,先干为敬:

若图片无法显示,图片链接如下:
http://images5.10qianwan.com/10qianwan/20200722/b_0_202007221502157849.png

模拟QQ聊天小项目收尾---界面展示服务端与客户端进行信息交互(用到的知识:io,线程,Swing界面,面向对象思想...... )

先声明,这个小项目的基本功能已经实现,若想要完善功能,感兴趣的小伙伴可以自己尝试嗷;

延续上一篇文章的思想,在此基础上,不难发现正常的QQ聊天服务器并不能自己发消息给我们这些用户(除一些特别信息以外),因为跟我们聊天的是真人呀,而非冷冰冰的服务器(*^▽),另外服务器应该是一个中转站,我们发出的消息都是经过服务器接收然后解析帮我们转发出去的,故而服务器是强大的,能知道我们每个人发的消息内容是什么,什么时间发的等等。但是上一篇文章中有个错误,细心的小伙伴应该知道了,嘿嘿,不过是保护一下我幼小的心灵,所以没有告诉小小陈对吧;哈哈废话不多说,这个错误是因为服务器只有一个吗,而我在上一篇文章中虽然通过暴力解决了java.net.BindException: Address already in use: JVM_Bind,但是现在发现这不合实际,服务器与客户端应该是一对多的关系,故而另一个解决办法就是将服务器的创建移到while循环外即可(呜呜呜,这么简单我居然没想到),好啦下面附上服务器代码:

package server;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;

public class Server {

    //为了找寻用户信息方便,采用HashMap进行存储,静态无需创建对象,类名点即可
    //权限修饰符默认不写方便同包下也能正常访问
    static HashMap<String,User> userBox = new HashMap<>();

    public static void main(String[] args){

        try {
            System.out.println("服务端启动");
            //创建服务端
            ServerSocket serverSocket = new ServerSocket(9999);
            while(true) {
                Socket socket = serverSocket.accept();
                //每获取到一个用户信息就添加进集合中
                //通过返回的socket获取一个字节型输入流
                InputStream is = socket.getInputStream();
                //将输入流包装成低级的字符型输入流,因为高级流里面不能直接放下字节型输入流
                InputStreamReader isr = new InputStreamReader(is);
                //构建高级字符型输入流 因为用BufferedReader流中有readLine方法可以直接读取一行
                BufferedReader br = new BufferedReader(isr);
                String uid = br.readLine();
               	//提示哪个用户已上线
                System.out.println(uid + "上线了");
                //接收客户端的传过来的uid 并存入集合中
                User user = new User(uid, socket);
                Server.userBox.put(uid, user);
                ServerThread st = new ServerThread(user);
                st.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

同样的用杜曼实体对象来存储用户信息 如下:
package server;

import java.net.Socket;

public class User {

    private String uid;
    private Socket socket;

    public User(String uid, Socket socket) {
        this.uid = uid;
        this.socket = socket;
    }

    public String getUid() {
        return uid;
    }
    public Socket getSocket() {
        return socket;
    }
}

上面已经提到,服务器是一个中转站,那么如何去实现呢?因为消息的传递应该与客户端是同步的,这时考虑到线程,于是通过创建一个线程类来帮服务器接收并解析消息,同时还能将消息转出,但是考虑到实际QQ中的群发消息(即@全体成员)这种效果,于是我们可以将两个人单聊和一群人群聊两种情况分开讨论,代码实现如下:
package server;

import java.io.*;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;

/**
 * 根据分析可知,本类的功能为将服务端读到的信息转发至另一人(单聊)或者一群人(群聊)
 */
public class ServerThread extends Thread {

    private User user;

    public ServerThread(User user){
        this.user = user;
    }

    //为防止代码冗余 设计一个传递信息的方法
    private void writerMessage(Socket socket , String message , String time){
        OutputStream os = null;
        PrintWriter writer = null;
        try {
            //回服务端消息
            os = socket.getOutputStream();
            writer = new PrintWriter(os);
            writer.println(time + "####" + user.getUid() + ":" + message);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //重写run方法  一直读写即可
    public void run(){
        //获取所有用户信息
        HashMap<String,User> messages = Server.userBox;
        try {
            //接收客户端的消息
            InputStream is = user.getSocket().getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            while(true) {
                //根据实际情况 我们单聊假定为:发送的信息要满足 (内容@另一个人的uid)
                //当然可能存在不止@一个人,比如(你在干嘛呀,小老弟@张三@李四)
                //若未解析到 @ 我们假定为群聊
                String message = br.readLine();
                //拼接一个时间
                Date date = new Date();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd kk:mm:ss");
                String time = sdf.format(date);
                System.out.println(time + "####" + user.getUid() + ":" + message);
                //按照@拆分成String数组
                String[] messageAndUid = message.split("@");
                //解析读到信息
                if (messageAndUid.length == 1) {
                    //此时是群聊 获取所有的键(即uid)通过迭代器遍历
                    Iterator<String> uids = messages.keySet().iterator();
                    while (uids.hasNext()) {      //判断是否有元素
                        String uid = uids.next();       //将元素取出来
                        User user1 = messages.get(uid);
受篇幅限制剩余代码分开展示
                       //因为if和else中传递信息的代码一致 故另外封装一个方法负责传递信息
                       this.writerMessage(user1.getSocket(), message,time);
                   }
               } else {
                   //先给自己发一份
                   this.writerMessage(user.getSocket(),messageAndUid[0],time);
                   //此时是单聊 可以做个严谨的判断 @的用户是否存在 但是QQ或者微信中@时弹出的页面显示的人都是存在的
                   //故此处我们假定@的都存在 由于未知@了几个人故此处采用循环
                   for (int i = 1; i < messageAndUid.length; i++) {
                       this.writerMessage(messages.get(messageAndUid[i]).getSocket(), messageAndUid[0],time);
                   }
               }
           }
       } catch (IOException e) {
//      e.printStackTrace();
//      防止客户端下线时出现异常 这里做个输出提示即可
           System.out.println(user.getUid() + "下线了");
       }
   }
}

服务器已经准备就绪,现在开始讨论客户端应该如何实现呢?首先要跟真实的QQ接轨,所以要用Swing简单的画个奇丑无比的小窗口咯,那么细想一下,这个窗口应该有哪些组件和功能呢?起码聊天对话框得有吧(JTextArea),发消息的框得有吧(JTextArea),点击发送的按钮得有吧(JButton),点击取消时发消息的框里面的内容得清空,所以也得有吧(JButton)…那么这个奇丑无比的小窗口出现后是不是还得跟客户端绑一块吧,毕竟是一条绳上的蚂蚱,那么代码实现窗口来了:
package client;

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

public class QQFrame extends JFrame{

    //添加一个属性---表示QQ窗口的名字
    private String uid;
    //添加一个属性---表示客户端连接的socket对象
    private Socket socket;

    private JPanel panel = new JPanel();//无色透明的小容器 小盒子
    //2.有一些组件
    //  文本域(接收信息并展示的 上面部分)
    private JTextArea messArea = new JTextArea();
    private JScrollPane messPane = new JScrollPane(messArea);
    //  文本域(发送信息的 下面部分)
    private JTextArea speakArea = new JTextArea();
    private JScrollPane speakPane = new JScrollPane(speakArea);
    //  按钮(发送)
    private JButton sendButton = new JButton("发送");
    //  按钮(取消)
    private JButton cancelButton = new JButton("取消");

    //构造方法(规定调用的流程)
    public QQFrame(String uid){
        super(uid);
        //加载窗口的组件
        this.setOther();
        this.addElements();
        this.addListener();
        this.setFrameSelf();
        //窗口相当于是一个客户端 产生一个客户端连接
        try {
            //与服务器连接
            socket = new Socket("localhost",9999);
            //通过连接后用socket来获取流
            OutputStream os = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(os);
            writer.println(uid);
            writer.flush();
            //只需要一个读取信息的来为我们做事------客户端读线程
            ClientReader cr = new ClientReader(socket);
            cr.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //设计一个方法 设置那些乱七八糟的东西
    private void setOther(){
        //设置组件的位置
        //将默认布局清空
        panel.setLayout(null);
        //将所有的组件自定义放在panel中
        messPane.setBounds(10,10,320,220);
        speakPane.setBounds(10,240,320,140);
        sendButton.setBounds(180,390,60,30);
        cancelButton.setBounds(260,390,60,30);
受篇幅限制剩余代码分开展示
        //设置一下上面展示的文本域不允许修改了
        messArea.setEditable(false);
        messArea.setFont(new Font("宋体",Font.BOLD,18));
        speakArea.setFont(new Font("宋体",Font.BOLD,18));
    }

    //设计一个方法 添加组件
    private void addElements(){
        //将这些组件放置在窗体里
        panel.add(messPane);
        panel.add(speakPane);
        panel.add(sendButton);
        panel.add(cancelButton);
        this.add(panel);
    }

    //设计一个方法 给组件添加事件(功能)
    private void addListener(){
        //取消按钮绑定一个功能 这里是lanmbda表达式的写法
        cancelButton.addActionListener(e -> {
            speakArea.setText("");
        });

        //给发送按钮绑定一个功能 这里是lanmbda表达式的写法
        sendButton.addActionListener(e -> {
            try {
                OutputStream os = socket.getOutputStream();
                PrintWriter writer = new PrintWriter(os);
                String message = speakArea.getText();
                writer.println(message);
                writer.flush();
                //发送完毕之后,让发送的说话框清空
                speakArea.setText("");
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        });
    }

    //设计一个方法 设置窗体自身的一些元素
    private void setFrameSelf(){
        //2.设置窗体一些样式
        this.setResizable(false);
        //设置窗体点击右上角X 程序结束
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //设置窗体的初始位置
        this.setBounds(500,200,350,480);
        //让窗体可见
        this.setVisible(true);
    }
受篇幅限制剩余代码分开展示
    //内部类写法 这个类帮客户端去接收消息 由于每个客户端之间的消息同步
    //所以这个类需要实现线程
    private class ClientReader extends Thread {

        //通过属性来获取自己客户端的那个socket
        private Socket socket;
        public ClientReader(Socket socket){
            this.socket = socket;
        }

        public void run(){
            StringBuilder result = new StringBuilder();
            try {
                //客户端 接收发来的数据
                InputStream is = socket.getInputStream();
                //将字节流转化成字符流
                InputStreamReader isr = new InputStreamReader(is);
                //字符流基础上 可以读取一行
                BufferedReader reader = new BufferedReader(isr);
                while(true) {
                    //每次读取一行数据
                    String value = reader.readLine();
                    //一行数据进行处理  换行
                    value = value.replace("####","\r\n");
                    //追加到StringBuilder对象中   频繁拼接效果更好
                    result.append(value);
                    result.append("\n");
                    //展示在上面的聊天框中(文本域中)
                    messArea.setText(result.toString());
                }
            } catch (IOException e) {
                //e.printStackTrace();
                //避免异常 简单做个提示即可
                System.out.println("服务器宕机了");
            }
        }
    }
}


接下来看看如何创建客户端窗口吧,哈哈,简单至极啦:
package client;

public class Client {
    public static void main(String[] args){
        new QQFrame("张三");
        new QQFrame("李四");
        new QQFrame("王五");
    }
}

这里附上服务端与客户端的运行结果如下:

服务端启动
张三上线了
李四上线了
王五上线了
2020-07-21 09:35:50####李四:你们在干吗
2020-07-21 09:36:00####李四:我在喝西北风
2020-07-21 09:36:12####张三:啧啧啧@李四
2020-07-21 09:36:51####王五:我笑了,你呢@张三
2020-07-21 09:37:33####张三:李四要是真的喝西北风我直播倒立洗头

细心的小伙伴应该知道为什么客户端的展示为啥什么都没有,因为展示的部分已经和窗口绑一块啦,当然这里由于只有一个电脑,所以直接在创建了三个客户端窗口,感兴趣的小伙伴可以和宿舍里的同学一起玩哦,但是要保证在同一个局域网下嗷。另外特别注意,不管是服务端还是客户端,里面用到的流通道不要放finally里面关掉,一旦关掉就会出现一直发null的消息,因为这些流通道一直都要使用,除非客户下线和服务器宕机。

全剧终

本文地址:https://blog.csdn.net/bw_cx_fd_sz/article/details/107481950