Java简单实现控制台版的聊天室

目录

思路:

实现细节:

        客户端

        服务端

 实际效果:

 具体代码:

客户端:

服务器端:


思路:

        首先我们要创建的聊天室是满足一些基本的要求:

        1.客户端:

        能够显示聊天的界面,并且供用户选择注册和登陆功能,登陆成功后,能接收到其他用户发出的消息,并且自己也能发送消息,发送和接收是能够同时进行的,也就是说只要有别的用户发消息,自己能够立马收到,类似于群聊一样。

       

        2.服务器端;

        服务端要能够支持多个用户同时连接,并且要做到登录验证,信息反馈,消息转发等功能。

       

实现细节:

        总的接收和发送,为了提高效率,我都是使用的缓冲流,BufferedReader和BufferedWriter

并且,用一个文本文件 log.txt 来存储用户名和密码。

        

        对于数据读取,我才用的是readLine()方法,这样一次读取一整行,并且为了防止BufferedReader的死等发生,使用了结束标记"END\n",这里加\n是为了让readLine读取到,每当读取到END这个字符串的时候,就退出读取数据的循环,表示读完了,作为一个结束标记。

        因为是需要多次读取,所以发送端不能使用shutdownOutput直接关闭输出流,否则就收不到下一条消息了

接收处代码:

发送代码:

        这里的t是用来存储message的信息,比如我要发送的是"你好",发送端会发送两行数据:

"你好\n"和"END\n"(结束标记),这里的t会存储"你好",然后重新赋值给message(此时=“END”),然后退出读取循环。

        聊天室中的所有接发消息的代码逻辑都和以上差不多

        客户端:

        为了实现接收消息和发送消息的互不影响的效果,我在登陆成功后的分支,新开了一个线程专门用来接收服务器转发来的其他用户的消息

        代码如下:

        

        我开启了一个新的线程专门用于接收消息并且反馈到控制台上,因为如果单纯放在一个线程中,接收和发送会发生阻塞,会出现: 你发出一条消息后,才能接收到一条消息的情况         

       服务端:

         这里我在一开始使用了线程池ExecutorService,通过工具类Executors.newCachedThreadPool()方法获取,这是一个没有设置具体线程数上限的线程池。用来接收各个客户端的Socket

        代码如下:

        

        注意:

                 这里的获取套接字Socket一定要放在while循环中,因为一个accept对应一个客户端,没接收到一个客户端就要新建一个Socket。并且用线程池实现多线程处理不同客户端的数据,让他们能同时各聊各的。

        此外,还为了实现消息转发,我采用了集合存储来自各个客户端的Socket,并且为了线程安全,使用了CopyOnWriteArrayList<Socket>,后面只要把消息,用集合遍历发送到各个客户端就行了,集合创建在main函数内,方法外,这样可以让多个线程共享同一个集合,同步增加和删除等。

 

         这里的代码是只有当用户名和密码都对的情况下,也就是登陆成功,才会将Socket加入到集合中,因为只有登录成功的Socket才是有效的Socket。

 实际效果:

 具体代码:

客户端:

public class client {
    /*
    支持多个用户同时聊天,服务器进行转发消息,相当于群聊一样
    有注册和登陆选项
     */
    public static void main(String[] args) throws IOException {
        //功能号
        String function = "";

        //登录界面
        System.out.println("与服务器接连成功");
        System.out.println("============欢迎来到聊天室==========");
        while (!function.equals("3")) {
            //这里把套接字放到循环内来创建,否则第二次循环就还是用的第一次已经关闭的的套接字
            //创建Socket套接字对象
            Socket socket = new Socket("127.0.0.1", 10086);

            System.out.println("请选择功能按钮");
            System.out.println("1.注册");
            System.out.println("2.登陆");
            System.out.println("3.退出");

            //输入功能号
            Scanner sc = new Scanner(System.in);
            function = sc.nextLine();

            //打开连接通道,发送账号密码
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            //获取输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String message = "";
            String t = "";

            //注册分支
            if (function.equals("1")) {
                StringJoiner sj = new StringJoiner("=", "", "");
                //告诉服务器这是注册分支
                sj.add("1");
                System.out.println("请输入你要注册的用户名");
                sj.add(sc.nextLine());
                System.out.println("请输入你要注册的密码");
                sj.add(sc.nextLine());

                writer.write(sj.toString());
                writer.newLine();
                //结束标记
                writer.write("END\n");
                //刷新缓冲区,以发出数据
                writer.flush();

                //获取服务器的反馈信息
                while ((message = br.readLine()) != null) {
                    if (message.equals("END")) {
                        System.out.println(t);
                        break;
                    }
                    t = message;
                }

            }
            //登陆分支
            else if (function.equals("2")) {
                StringJoiner sj = new StringJoiner("=", "", "");
                //告诉服务器这是登陆分支
                sj.add("2");
                System.out.println("请输入你的用户名");
                sj.add(sc.nextLine());
                System.out.println("请输入你的密码");
                sj.add(sc.nextLine());

                //打开连接通道,发送账号密码
                writer.write(sj.toString());
                writer.newLine();
                //结束标记
                writer.write("END\n");
                //刷新缓冲区,以发出数据
                writer.flush();

                //等待服务器的登陆确认

                while ((message = br.readLine()) != null) {
                    if (message.equals("END")) {
                        //获取到上一条信息,因为当前信息是结束标志
                        message = t;
                        break;
                    }
                    t = message;
                }
                System.out.println(message);
                if (message.equals("登陆成功")) {
                    System.out.println("登陆成功,请开始聊天");
                    String temp = "";
                    System.out.println("请输入你要说的话:");
                    while (!temp.equals("886")) {
                        //接下来就是用循环,实现发送群消息
                        temp = sc.nextLine();
                        //发送数据
                    /*
                    重新获取输出流
                    一旦你调用了shutdownOutput()方法关闭了输出流,那么之后尝试使用该输出流进行输出操作将会引发IOException。
                    关闭输出流后,如果你需要再次发送数据,你需要重新获取一个新的输出流对象。关闭输出流是为了告知对方已经发送完毕,不再发送数据,
                    因此需要重新获取输出流来发送新的数据。

                    不过这里我没有采用shutdownOutput 还是用之前创建的缓冲输出流向服务器发送我们说的话
                     */
                        writer.write(temp);
                        writer.newLine();
                        //结束标志
                        writer.write("END\n");
                        //刷新缓冲区,以发出数据
                        writer.flush();

                    /*
                        如果你希望客户端能够同时接收和发送消息,通常需要使用多线程。这是因为客户端在发送消息时需要等待服务器的响应,
                    如果在同一个线程中进行这两个操作,就会导致阻塞,无法同时进行发送和接收。通过将接收消息的逻辑放在一个单独的线程中,
                    可以实现并发地进行发送和接收操作。这样,客户端就能够实时地接收到其他客户端发送的消息,而不会因为发送操作的阻塞而导致无法接收。
                    因此,多线程可以提供更好的用户体验,使客户端能够同时进行发送和接收操作,增加聊天室的实时性和交互性。
                     */
                        // 启动消息接收线程
                        Thread receiveThread = new Thread(() -> {
                            try {
                                String m = "";
                                while (!m.equals("886")) {
                                    // 接收服务器的消息
                                    String receivedMessage = br.readLine();
                                    if (receivedMessage.equals("END")) {
                                        continue;
                                    }
                                    System.out.println(receivedMessage);
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        });
                        receiveThread.start();

                    }
                } else if (message.equals("密码错误")) {
                    System.out.println("密码错误");
                } else if (message.equals("用户名不存在")) {
                    System.out.println("用户名不存在");
                }
            }
            //释放资源
            socket.close();
        }
    }

服务器端:

这里注意把 输入输出流 (也就是BufferedReader和BufferedWriter)的文件路径改为自己本地已有的txt文件路径

package com.itheima.x_网络编程.TCP.大作业_聊天室实现;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.StringJoiner;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class server {
    /*
    支持多个用户同时聊天,服务器进行转发消息,相当于群聊一样
    有注册和登陆选项
     */
    public static void main(String[] args) throws IOException {
        //创建ServerSocket套接字对象
        ServerSocket socket = new ServerSocket(10086);

        //利用线程池
        ExecutorService service = Executors.newCachedThreadPool();
        //用集合存储Socket套接字,对象用于转发,这里采用线程安全的集合类型
        /*
        CopyOnWriteArrayList是Java中的一个线程安全的列表实现,它是ArrayList的线程安全版本。
        CopyOnWriteArrayList的特点是在进行写操作时,它会创建一个底层数组的副本,并在副本上进行修改,而原始数组保持不变。这意味着写操作不会影响到正在进行的读操作,从而实现了并发安全性。
        由于CopyOnWriteArrayList的写操作会创建副本,因此写操作的开销较大,适用于读操作频繁、写操作较少的场景。它适合在多线程环境下进行遍历和读取操作,但不适合频繁的增加、删除和修改操作。
        CopyOnWriteArrayList实现了List接口,因此它具备了List的常用方法,如添加元素、获取元素、删除元素等。它可以作为线程安全的列表容器,用于在多线程环境下进行元素的读取和遍历操作。
        需要注意的是,由于CopyOnWriteArrayList的写操作会创建副本,因此它会占用更多的内存空间。在数据量较大的情况下,应谨慎使用,以避免内存占用过高的问题。
         */
        CopyOnWriteArrayList<Socket> as = new CopyOnWriteArrayList<>();

        while (true) {
            //打开通道获取输入流
            Socket accept = socket.accept();
            service.submit(() -> {
                try {
                    chat(as, accept);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }

    }

    public static void chat(CopyOnWriteArrayList<Socket> as, Socket accept) throws IOException {
        try (
                //括号内的变量创建,在结束后会自动释放资源
                BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream()));
                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()))
        ) {
            //访问log.txt
            BufferedReader fr = new BufferedReader(new FileReader("这里填你存放用户密码的txt文件路径"));
            //创建需要的变量
            String t = "";
            String message = "";
            int flag = 0;

            while ((message = reader.readLine()) != null) {
                if (message.equals("END")) {
                    message = t;
                    break;
                }
                t = message;
            }
            //注册分支
            if(message.split("=")[0].equals("1")) {
                //要保证用户名是唯一的
                String temp;
                while((temp=fr.readLine())!=null)
                {
                    if(temp.split("=")[0].equals(message.split("=")[1]))
                    {
                        flag++;
                        //如果用户名重复了,就返回告诉客户端用户名已存在
                        writer.write("用户名已存在,注册失败");
                        writer.newLine();
                        writer.write("END\n");
                        writer.flush();
                        break;
                    }
                }
                if(flag==0)
                {
                    //注意这里要用append模式,否则会覆盖文件
                    BufferedWriter bw=new BufferedWriter(new FileWriter("这里填你存放用户密码的txt文件路径",true));
                    StringJoiner sj=new StringJoiner("=");
                    sj.add(message.split("=")[1]);
                    sj.add(message.split("=")[2]);
                    //写入文本
                    //注意这里要换行,因为我们后面都是readLine整行整行读取的
                    bw.newLine();
                    bw.write(sj.toString());
                    bw.flush();

                    writer.write("用户创建完成,注册成功");
                    writer.newLine();
                    writer.write("END\n");
                    writer.flush();
                }

            }

            //登录分支
            else if(message.split("=")[0].equals("2")) {
                //从文件中比对信息,验证密码
                String temp;
                while ((temp = fr.readLine()) != null) {
                    //首先比对用户名
                    if (temp.split("=")[0].equals(message.split("=")[1])) {
                        flag++;
                        //进而判断密码
                        if (temp.split("=")[1].equals(message.split("=")[2])) {
                            //将该套接字加入集合
                            as.add(accept);
                            //传递登陆成功返回给客户端
                            writer.write("登陆成功");
                            writer.newLine();
                            //结束标记
                            writer.write("END\n");
                            writer.flush();

                        /*
                        后面接一个聊天方法就行
                         */
                            //获取用户名
                            String username = temp.split("=")[0];

                            while (true) {
                                //接收信息和转发
                                while ((message = reader.readLine()) != null) {
                                    if (message.equals("END")) {
                                        message = t;
                                        break;
                                    }
                                    t = message;
                                }
                                for (int i = 0; i < as.size(); i++) {
                                    //这里不能用之前的输出流了,要对每一个socket新建,还要注意,用as.get(i)获取输出流
                                    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(as.get(i).getOutputStream()));
                                    bw.write(username + "发来的消息: " + message);
                                    bw.newLine();
                                    //结束标记
                                    bw.write("END\n");
                                    bw.flush();
                                }
                            }
                        } else {
                            writer.write("密码错误");
                            writer.flush();
                            break;//直接退出,不需要进入下一层循环
                        }
                    }
                }
                //用户名不匹配的话直接反馈给客户端,用户名错误
                if (flag == 0) {
                    writer.write("用户名不存在");
                    writer.flush();
                }
            }
        } finally {
            accept.close();
        }
    }
}

  • 30
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
CSDN是一个面向程序员的技术交流社区,主要提供程序开发相关的技术文章、博客和论坛。在CSDN上,有很多不同的技术块,其中包含了Java编程语言相关的内容。而Java聊天室则是其中的一个功能,在这个聊天室里,Java程序员可以进行实时交流、讨论问题、分享经验和解决困惑。 Java聊天室的好处有很多。首先,它为Java程序员提供了一个交流的平台,使得他们可以与其他共同兴趣的人沟通,相互学习和进步。在聊天室中,程序员们可以提问有关Java的问题,寻求帮助和建议,也可以分享自己的经验和心得。这样的交流有助于加深理解,解决难题,提高编程能力。 其次,Java聊天室也可以帮助Java程序员扩展人脉。在聊天室中,程序员们可以结识到来自不同地区、不同背景的其他程序员,从而建立起一个更广泛的社交网络。通过与各种人交流,程序员们可以获得不同的观点和思路,认识到自己的不足与不同领域之间的联系,从而更好地提高自己的技术水平。 最后,Java聊天室也可以帮助Java程序员跟上技术的最新动态。聊天室中的程序员们经常会分享最新的技术文章、工具或库的使用经验,讨论热门的技术话题和趋势。这样有助于程序员们保持对技术的敏感度,及时了解行业的新发展,从而更好地适应技术的变化,提高自己的竞争力。 总之,CSDN Java聊天室是一个非常有价值的平台,为Java程序员提供了交流、学习、社交和领先技术的机会。对于想要在Java领域不断进步的人来说,参与Java聊天室的活动是非常有益的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值