39-Java实现控制台版聊天室

需求文档来自黑马程序员微信公众号网盘资源

控制台版的聊天室

项目名称

​ 利用TCP协议,做一个带有登录,注册的无界面,控制台版的多人聊天室。

使用到的知识点

​ 循环,判断,集合,IO,多线程,网络编程等

准备工作

在当前模块下新建txt文件,文件中保存正确的用户名和密码

文件内容如下:

//左边是用户名
//右边是密码
zhangsan=123
lisi=1234
wangwu=12345

需求描述

① 客户端启动之后,需要连接服务端,并出现以下提示:

服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:

②选择登录之后,出现以下提示:

服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名

③需要输入用户名和密码,输入完毕,没有按回车时,效果如下:

服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhangsan
请输入密码
123

④按下回车,提交给服务器验证

服务器会结合txt文件中的用户名和密码进行判断

根据不同情况,服务器回写三种判断提示:

服务器回写第一种提示:登录成功
服务器回写第二种提示:密码有误  
服务器回写第三种提示:用户名不存在

⑤客户端接收服务端回写的数据,根据三种情况进行不同的处理方案

登录成功的情况, 可以开始聊天,出现以下提示:

服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhangsan
请输入密码
123
1
登录成功,开始聊天
请输入您要说的话

密码错误的情况,需要重新输入,出现以下提示:

服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhangsan
请输入密码
aaa
密码输入错误
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:

用户名不存在的情况,需要重新输入,出现以下提示:

服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhaoliu
请输入密码
123456
用户名不存在
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:

⑥如果成功登录,就可以开始聊天,此时的聊天是群聊,一个人发消息给服务端,服务端接收到之后需要群发给所有人

提示:

​ 此时不能用广播地址,因为广播是UDP独有的

​ 服务端可以将所有用户的Socket对象存储到一个集合中

​ 当需要群发消息时,可以遍历集合发给所有的用户

​ 此时的服务端,相当于做了一个消息的转发

转发核心思想如下图所示:

在这里插入图片描述

其他要求:

用户名和密码要求:

要求1:用户名要唯一,长度:6~18位,纯字母,不能有数字或其他符号。

要求2:密码长度3~8位。第一位必须是小写或者大写的字母,后面必须是纯数字。

客户端:

拥有登录、注册、聊天功能。

① 当客户端启动之后,要求让用户选择是登录操作还是注册操作,需要循环。

  • 如果是登录操作,就输入用户名和密码,以下面的格式发送给服务端

    username=zhangsan&password=123

  • 如果是注册操作,就输入用户名和密码,以下面的格式发送给服务端

    username=zhangsan&password=123

② 登录成功之后,直接开始聊天。

服务端:

① 先读取本地文件中所有的正确用户信息。

② 当有客户端来链接的时候,就开启一条线程。

③ 在线程里面判断当前用户是登录操作还是注册操作。

④ 登录,校验用户名和密码是否正确

⑤ 注册,校验用户名是否唯一,校验用户名和密码的格式是否正确

⑥ 如果登录成功,开始聊天

⑦ 如果注册成功,将用户信息写到本地,开始聊天



实现:

客户端

  • 线程类:

    package edu.gxufe.www.client;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.Socket;
    
    /*
        第二种方式实现多线程
        自定义用来接收服务端转发过来的消息的线程类
     */
    public class ClientRunnable implements Runnable{
        //创建Socket对象,用于接收当前客户端的连接对象
        Socket socket;
    
        //有参构造方法:用于创建一个单独的线程来专门接收服务端转发过来的聊天记录
        public ClientRunnable(Socket socket) {
            //初始化当前客户端的连接对象
            this.socket = socket;
        }
    
        //重写run方法
        @Override
        public void run() {
            while (true) {
                try {
                    //接收服务端转发过来的聊天记录,并打印在控制台
                    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String msg = br.readLine();
                    System.out.println(msg);
                } catch (IOException e) {
    //                throw new RuntimeException(e);
                    System.out.println("群聊已解散~");
                    System.exit(0);
                }
            }
        }
    }
    
  • 客户端类:

    package edu.gxufe.www.client;
    
    import edu.gxufe.www.server.Server;
    import edu.gxufe.www.server.ServerRunnable;
    
    import java.io.*;
    import java.net.Socket;
    import java.util.Properties;
    import java.util.Scanner;
    
    public class Client {
        static Scanner sc = new Scanner(System.in);
    
        public static void main(String[] args) throws IOException {
            //连接服务器
            Socket socket = new Socket("127.0.0.1", 10000);
            System.out.println("服务器已经连接成功~");
    
            //主界面
            while (true) {
                System.out.println("==============欢迎来到奥利给聊天室================");
                System.out.println("1.登录");
                System.out.println("2.注册");
                System.out.println("请输入您的选择:");
                String choose = sc.nextLine();
    
                //判断用户输入的选项
                switch (choose) {
                    case "1":
                        //登录
                        login(socket);
                        break;
                    case "2":
                        //注册
                        register(socket);
                        break;
                    default:
                        System.out.println("目前还没有此选项~ 请重试!!");
                        break;
                }
            }
        }
    
        //用户注册操作
        public static void register(Socket socket) throws IOException {
            //获取输出流:用于向服务端写出数据
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
    
            //读取本地文件中所有的正确用户信息(用户名和密码)
            Properties prop = new Properties();
            FileInputStream fis = new FileInputStream("chat_room\\serverdir\\userinfo.txt");
            prop.load(fis);
            fis.close();
    
            //键盘录入用户名和密码
            System.out.println("请输入注册的用户名:");
            String username = sc.nextLine();
            // 要求1:用户名要唯一,
            if (prop.containsKey(username)) {
                System.out.println("用户名已存在!!");
                return;
            }
            // 长度:6~18位,纯字母,不能有数字或其他符号。
            if (!username.matches("[a-zA-Z]{6,18}")) {
                System.out.println("注册的用户名不符合要求(长度6~18位、纯字母、不能有数字或其他符号)");
                return;
            }
    
            System.out.println("请输入注册的密码:");
            String password = sc.nextLine();
            System.out.println("请再次确认注册密码:");
            String okPassword = sc.nextLine();
            // 要求2:密码长度3~8位。第一位必须是小写或者大写的字母,后面必须是纯数字。
            String passwordRegex = "[a-zA-Z]{1}[0-9]{2,7}";
            if (!(password.matches(passwordRegex) || okPassword.matches(passwordRegex))){
                System.out.println("注册的密码不符合要求(长度3~8位,第一位必须是小写或者大写的字母,后面必须是纯数字)");
                return;
            }
    
            //判断两次密码不一致
            if (!password.equals(okPassword)) {
                System.out.println("您输入的两次密码不一致!!");
                return;
            }
    
            //拼接用户名和密码的格式为:username=password
            StringBuilder sb = new StringBuilder();
            sb.append("username=").append(username).append("&password=").append(password);
    
            //第一次写的是执行注册操作
            bw.write("register");
            bw.newLine(); //换行
            bw.flush(); //刷新
    
            //第二次写的是用户名、密码的信息
            //往服务端写出用户名和密码
            bw.write(sb.toString());
            bw.newLine();
            bw.flush();
    
            //接收服务端回写的数据
            //获取输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String message = br.readLine();
            System.out.println("服务端回馈数据:" + message);
            // 1: 注册成功
            if (message.equals("1")) {
                System.out.println("注册成功~");
                //开启一条单独的线程,专门用来接收服务端发送过来的聊天记录
                new Thread(new ClientRunnable(socket)).start();
                //开始聊天:就是把消息写到服务端,然后服务端会将当前客户端的消息群发给所有在线的客户端
                talkServer(bw);
            }
        }
    
        //用户登录操作
        public static void login(Socket socket) throws IOException {
            //获取输出流:用于向服务端写出数据
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
    
            //键盘录入用户名和密码
            System.out.println("请输入用户名:");
            String username = sc.nextLine();
            System.out.println("请输入密码:");
            String password = sc.nextLine();
    
            //拼接用户名和密码的格式为:username=xxx&password=xxx
            StringBuilder sb = new StringBuilder();
            sb.append("username=").append(username).append("&password=").append(password);
    
            //第一次写的是执行登录操作
            bw.write("login");
            bw.newLine(); //换行
            bw.flush(); //刷新
    
            //第二次写的是用户名、密码的信息
            //往服务端写出用户名和密码
            bw.write(sb.toString());
            bw.newLine();
            bw.flush();
    
            //接收服务端回写的数据
            //获取输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String message = br.readLine();
            System.out.println("服务端回馈数据:" + message);
    
            // 1: 登录成功;2: 密码有误;3: 用户名不存在
            if (message.equals("1")) {
                System.out.println("登录成功~");
                //开启一条单独的线程,专门用来接收服务端发送过来的聊天记录
                new Thread(new ClientRunnable(socket)).start();
                //开始聊天:就是把消息写到服务端,然后服务端会将当前客户端的消息群发给所有在线的客户端
                talkServer(bw);
            } else if (message.equals("2")) {
                System.out.println("密码有误!!");
    
            } else if (message.equals("3")) {
                System.out.println("用户名不存在!!");
            }
        }
    
        //将当前客户端的消息发送到服务端
        public static void talkServer(BufferedWriter bw) throws IOException {
            while (true) {
                System.out.println("请输入您想说的话:");
                String message = sc.nextLine();
                //把输入的话写给服务端
                bw.write(message);
                bw.newLine();//换行
                bw.flush();//刷新
            }
        }
    }
    


服务端

  • 线程类:

    package edu.gxufe.www.server;
    
    import java.io.*;
    import java.net.Socket;
    import java.util.Properties;
    
    /*
        第二种方式实现多线程;
        自定义处理客户端登录或注册请求的线程类
     */
    public class ServerRunnable implements Runnable{
        //创建Socket、Properties对象
        //socket: 用于接收每一个客户端的socket连接对象
        //prop: 用于接收所有的正确用户信息
        Socket socket;
        public static Properties prop;
    
        //线程类的有参构造方法:当有客户端来连接时,开启一条线程
        //并将该客户端的socket连接对象 和 所有的正确用户信息初始化给当前线程对象的socket、prop
        public ServerRunnable(Socket socket, Properties prop) {
            this.socket = socket;
            this.prop = prop;
        }
    
        //重写run方法
        @Override
        public void run() {
            try {
                //获取输入流:用于读取客户端的数据
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                //读取用户名的选项
                while (true) {
                    String choose = br.readLine();
                    //判断当前用户是选择了登录操作还是注册操作
                    switch (choose) {
                        case "login": //用户选择了登录
                            login(br);
                            break;
                        case "register": //用户选择了注册
                            register(br);
                            break;
                    }
                }
            } catch (IOException e) {
    //            throw new RuntimeException(e);
                System.out.println("有客户端下线了~");
            }
        }
    
        //用户选择了注册
        public void register(BufferedReader br) throws IOException {
            System.out.println("用户选择了登录~");
            //读取该用户输入的用户名和密码信息:username=xxx&password=xxx
            String userInfo = br.readLine();
    
            //根据 "&" 将用户名和密码进行分裂
            //分裂后得到一个字符串数组:
            // 0索引就是:username=xxx
            // 1索引就是:password=xxx
            String[] userInfoArr = userInfo.split("&");
    
            // 得到用户输入的用户名
            String usernameInput = userInfoArr[0].split("=")[1];
            System.out.println("用户输入的用户名为:" + usernameInput);
    
            // 得到用户输入的密码
            String passwordInput = userInfoArr[1].split("=")[1];
            System.out.println("用户输入的密码为:" + passwordInput);
    
            //拼接用户信息的格式为:username=password
            StringBuilder sb = new StringBuilder();
            sb.append(usernameInput).append("=").append(passwordInput);
    
            //将新注册的用户名和密码写入userInfo.txt文件中
            BufferedWriter bw = new BufferedWriter(new FileWriter("chat_room\\serverdir\\userinfo.txt", true));
            bw.write(sb.toString());
            bw.newLine();
            bw.flush();
    
            //新用户注册成功,回写一个1给客户端
            writerMessageClient("1");
            //把当前注册成功的客户端的socket连接对象存储到socketList集合中
            Server.socketList.add(socket);
    
            //接收客户端发过来的消息,群发给每一个客户端
            talkAllClient(br, usernameInput);
        }
    
        //用户选择了登录:校验用户名和密码是否正确
        public void login(BufferedReader br) throws IOException {
            System.out.println("用户选择了登录~");
            //读取该用户输入的用户名和密码信息:username=xxx&password=xxx
            String userInfo = br.readLine();
    
            //根据 "&" 将用户名和密码进行分裂
            //分裂后得到一个字符串数组:
            // 0索引就是:username=xxx
            // 1索引就是:password=xxx
            String[] userInfoArr = userInfo.split("&");
            
            //再根据 "=" 将0索引的用户名
            //分裂后得到一个字符串数组
            // 0索引就是:username
            // 1索引就是:xxx
            String[] usernameArr = userInfoArr[0].split("=");
            // 得到用户输入的用户名
            String usernameInput = usernameArr[1];
            System.out.println("用户输入的用户名为:" + usernameInput);
    
            //再根据 "=" 将1索引的密码
            //分裂后得到一个字符串数组
            // 0索引就是:password
            // 1索引就是:xxx
            String[] passwordArr = userInfoArr[1].split("=");
            // 得到用户输入的密码
            String passwordInput = passwordArr[1];
            System.out.println("用户输入的密码为:" + passwordInput);
    
    
            //校验用户名是否正确(其实就是判断用户名是否存在)
            if (prop.containsKey(usernameInput)) {
                //用户名存在,则校验密码是否正确
                // 根据用户名获取对应的密码,并转换成字符串类型(方便比较)
                String rightPassword = prop.get(usernameInput) + "";
                if (rightPassword.equals(passwordInput)) {
                    //程序到这里,说明用户输入用户名和密码都正确了,回写一个1(登录成功)给客户端
                    writerMessageClient("1");
                    //把当前登录成功的客户端的socket连接对象存储到socketList集合中
                    Server.socketList.add(socket);
                    //接收客户端发过来的消息,群发给每一个客户端
                    talkAllClient(br, usernameInput);
    
                }else {
                    //密码错误,回写一个2给客户端
                    writerMessageClient("2");
                }
    
            }else {
                //用户名不存在,回写一个3给客户端
                writerMessageClient("3");
            }
        }
    
        //将当前客户端发送过来的消息群发给所有客户端(转发)
        public void talkAllClient(BufferedReader br, String username) throws IOException {
            while (true) {
                String message = br.readLine();
                System.out.println(username + "说:" + message);
    
                //将当前客户端发送过来的消息群发给所有在线的客户端
                for (Socket s : Server.socketList) {
                    //s表示每一个客户端的连接对象
                    writerMessageClient(s, username + "说:" + message);
                }
            }
        }
    
        //回写数据给客户端
        public void writerMessageClient(String message) throws IOException {
            //获取输出流
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write(message);
            bw.newLine();//换行
            bw.flush();//刷新
        }
    
        //回写数据给客户端
        public void writerMessageClient(Socket socket, String message) throws IOException {
            //获取输出流
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write(message);
            bw.newLine();//换行
            bw.flush();//刷新
        }
    }
    
  • 服务端类:

    package edu.gxufe.www.server;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.ArrayList;
    import java.util.Properties;
    
    public class Server {
        //定义集合用于存储每一个客户端的连接对象
        public static ArrayList<Socket> socketList = new ArrayList<>();
    
        public static void main(String[] args) throws IOException {
            //创建ServerSocket对象,并绑定端口号
            ServerSocket ss = new ServerSocket(10000);
    
            //先读取本地文件中所有的正确用户信息(用户名和密码)
            Properties prop = new Properties();
            FileInputStream fis = new FileInputStream("chat_room\\serverdir\\userinfo.txt");
            prop.load(fis);
            fis.close();
    
            //当有客户端来连接的时候,就开启一条线程
            while (true) {
                Socket socket = ss.accept();
                System.out.println("有客户端来连接了~");
                new Thread(new ServerRunnable(socket, prop)).start();
            }
        }
    }
    


测试

  • 先启动服务端:

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

其他功能大家写完程序后可自行测试!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值