需求文档来自黑马程序员微信公众号网盘资源
控制台版的聊天室
项目名称
利用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(); } } }
测试
-
先启动服务端: