1.功能展示:
2.源码:
2.1 客户端:
2.1.1 Client_main.java:
package Client;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Scanner;
public class Client_main {
public static void main(String[] args) throws IOException {
//Socket socket=new Socket("172.25.211.95",10086);
Socket socket=new Socket("127.0.0.1",10086);
System.out.println("已连接到服务器");
while (true) {
System.out.println("+--------------------------------------------------+");
System.out.println("| 欢迎使用CMD聊天室 |");
System.out.println("+--------------------------------------------------+");
System.out.println("| 1.登陆 |");
System.out.println("| 2.注册 |");
System.out.println("+--------------------------------------------------+");
System.out.println("| 请选择功能: |");
System.out.println("+--------------------------------------------------+");
Scanner sc = new Scanner(System.in);
String choose = sc.nextLine();
switch (choose) {
case "1" -> login(socket);
case "2" -> register(socket);
default -> System.out.println("没有这个选项");
}
}
}
//1.登陆
private static void login(Socket socket) throws IOException {
BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//录入信息
Scanner sc=new Scanner(System.in);
System.out.println("请输入账号:");
String username=sc.nextLine();
System.out.println("请输入密码:");
String password=sc.nextLine();
StringBuilder sb=new StringBuilder();
//username=zhangsan&password=123
sb.append("username=").append(username).append("&password=").append(password);
//对服务器请求登陆
bw.write("request: 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();
//1=登陆成功 2=密码错误 3=账户不存在
if(message.equals("1")){
System.out.println("登陆成功!");
//开一条单独的线程,专门用来接收服务端发送过来的聊天记录
Thread listener = new Thread(new ClientRunnable(socket));
listener.start();
//开始聊天
Chat(socket,listener);
} else if (message.equals("2")) {
System.out.println("密码输入错误");
}else if (message.equals("3")) {
System.out.println("账户不存在");
}
}
//2.注册
private static void register(Socket socket) throws IOException {
BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//录入信息
Scanner sc=new Scanner(System.in);
System.out.println("请输入账号:");
String username=sc.nextLine();
String password1="";
while (true){
System.out.println("请输入密码:");
password1 = sc.nextLine();
System.out.println("请再次输入密码:");
String password2 = sc.nextLine();
if(!password2.equals(password1))
System.out.println("两次密码输入不一致请重新输入");
else
break;
}
System.out.println("请输入邀请码:");
String key=sc.nextLine();
StringBuilder sb=new StringBuilder();
//username=zhangsan&password=123
sb.append("username=").append(username).append("&password=").append(password1);
//对服务器请求登陆
bw.write("request: register");
bw.newLine();
bw.flush();
//发送账号密码
bw.write(sb.toString());
bw.newLine();
bw.flush();
//发送邀请码
bw.write(key);
bw.newLine();
bw.flush();
//接收服务器的返回信息
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message=br.readLine();
//1=注册成功 2=账户已存在 3=邀请码不正确或已失效
if(message.equals("1")){
System.out.println("注册成功");
} else if (message.equals("2")) {
System.out.println("账户已存在");
}else if (message.equals("3")) {
System.out.println("邀请码不正确或已失效");
}
//bw.close();关闭缓冲流也会导致socket连接关闭,致使不能连续使用功能
}
private static void Chat(Socket socket,Thread listener) throws IOException {
BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Scanner scanner=new Scanner(System.in);
System.out.println("上线!输入 quit 退出!");
while (true){
//System.out.println("请输入你要发送的信息:");
String message = scanner.nextLine();
if(message.equals("quit")){
System.out.println("下线!");
bw.write("_socket_close");//下线时告诉服务端
bw.newLine();
bw.flush();
break;
}
bw.write(message);
bw.newLine();
bw.flush();
}
bw.close();
socket.close();
}
}
2.1.2 ClientRunnable.java:
package Client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ClientRunnable implements Runnable{
Socket socket;
ClientRunnable(Socket socket){
this.socket=socket;
}
@Override
public void run() {
while (true){
try {
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message=br.readLine();
System.out.println(message);
} catch (IOException e) {
break;//客户端中quit会导致socket关闭异常,直接break,即可
}
}
}
}
2.2 服务端:
2.2.1 Server_main.java:
package Server;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Server_main {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(10086);
System.out.println("服务器上线!");
//加载用户信息文件
Properties prop=new Properties();
FileInputStream fis=new FileInputStream("src/Server/userinfo.txt");
prop.load(fis);
fis.close();
//创建用户集合
ArrayList<Socket> userList=new ArrayList<>();
//创建线程池
ThreadPoolExecutor socket_pool=new ThreadPoolExecutor(
3,
10,
180,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
//来一个连接,给线程池提交一个任务
while (true){
Socket socket=serverSocket.accept();
// 获取当前时间并格式化为字符串
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = sdf.format(new Date());
System.out.println(socket.getInetAddress().getHostAddress()+":"+socket.getInetAddress().getHostName()+"尝试连接!---"+currentTime);
socket_pool.submit(new ServerRunnable(socket,prop,userList));
}
}
}
2.2.2 ServerRunnable.java:
package Server;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Properties;
public class ServerRunnable implements Runnable{
//邀请码:
public static final String KEY ="CMD137";
Socket socket;
Properties prop;
ArrayList<Socket> userList=new ArrayList<>();//注意竞争问题:不能同时有多个线程操作userlist,使用同步锁
ServerRunnable(Socket socket,Properties prop,ArrayList<Socket> userList){
this.prop=prop;
this.socket=socket;
this.userList=userList;
}
@Override
public void run() {
try {
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (true){
String request=br.readLine();
if(request.equals("request: login"))
login(br);
else if (request.equals("request: register"))
register(br);
else {
System.out.println("未知的请求:"+request);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//处理登陆请求
private void login(BufferedReader br) throws IOException {
System.out.println("正在处理"+socket.getInetAddress().getHostAddress()+":"+socket.getInetAddress().getHostName()+"的登陆请求");
String userinfo = br.readLine();
//username=zhangsan&password=123
String[] userInfoArr = userinfo.split("&");
String usernameInput = userInfoArr[0].split("=")[1];
String passwordInput = userInfoArr[1].split("=")[1];
System.out.println("用户输入的用户名为:" + usernameInput);
System.out.println("用户输入的密码为:" + passwordInput);
//1=登陆成功 2=密码错误 3=账户不存在
if (prop.containsKey(usernameInput)) {
String rightPassword = prop.get(usernameInput) + "";
if (rightPassword.equals(passwordInput)) {
writeMessage2Client("1");
//登录成功的时候,就需要把客户端的连接对象Socket保存起来
userList.add(socket);
talk2All(br,usernameInput);
} else {
//密码输入有误
writeMessage2Client("2");
}
} else {
//3=账户不存在
writeMessage2Client("3");
}
}
//处理注册请求
private void register(BufferedReader br) throws IOException {
System.out.println("正在处理"+socket.getInetAddress().getHostAddress()+":"+socket.getInetAddress().getHostName()+"的注册请求");
String userinfo = br.readLine();
String key=br.readLine();
//username=zhangsan&password=123
String[] userInfoArr = userinfo.split("&");
String usernameInput = userInfoArr[0].split("=")[1];
String passwordInput = userInfoArr[1].split("=")[1];
System.out.println("用户输入的用户名为:" + usernameInput);
System.out.println("用户输入的密码为:" + passwordInput);
System.out.println("用户输入的邀请码为:"+key);
//1=注册成功 2=账户已存在 3=邀请码不正确或已失效
if (key.equals(KEY)) {
if (!prop.containsKey(usernameInput)) {
writeMessage2Client("1");
//注册成功,写入userinfo.txt
prop.setProperty(usernameInput,passwordInput);
FileOutputStream fos=new FileOutputStream("src/Server/userinfo.txt");
prop.store(fos,null);
System.out.println("用户注册成功");
fos.close();
} else {
//2=账户已存在
writeMessage2Client("2");
}
} else {
//3=邀请码不正确或已失效
writeMessage2Client("3");
}
}
//用于回复请求
public void writeMessage2Client(String message) throws IOException {
//获取输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write(message);
bw.newLine();
bw.flush();
}
//用于转发信息
public void writeMessage2Client(Socket s,String username,String message) throws IOException {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
// 获取当前时间并格式化为字符串
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = sdf.format(new Date());
message=currentTime+" --- "+username+" :"+message;
bw.write(message);
bw.newLine();
bw.flush();
}
private void talk2All(BufferedReader br,String username) throws IOException {
//提醒所有用户,该用户已上线
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = sdf.format(new Date());
String message=username+"已上线 ---"+currentTime;
//群发
for (Socket s : userList) {
//s依次表示每一个客户端的连接对象
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
bw.write(message);
bw.newLine();
bw.flush();
}
while (true) {
message = br.readLine();
if(message.equals("_socket_close")){
System.out.println(username+"下线!");
//提醒所有用户,该用户已下线
sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
currentTime = sdf.format(new Date());
message=username+"已下线 ---"+currentTime;
//群发
for (Socket s : userList) {
//s依次表示每一个客户端的连接对象
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
bw.write(message);
bw.newLine();
bw.flush();
}
break;
}
System.out.println(username + "发送过来消息:" + message);
//群发
for (Socket s : userList) {
//s依次表示每一个客户端的连接对象
writeMessage2Client(s, username, message);
}
}
}
}
2.2.3 userinfo.txt:(存储用户账号密码的文件,符合Properties的格式)
cmd137=woshicmd137
zhangsan=woshizhangsan
lisi=woshilisi
3.仍存在的问题:
当客户端输入 quit 时,bw.close()会关闭输出流,也会使与之绑定的socket处于半关闭状态,而导致ClientRunnable 中的run抛出 SocketException
类型的异常,提示 “Socket closed”。致使功能上:结束聊天,再次进入登陆/注册时socket已被关闭而不能进入。可以不去bw.close(),但显然这并不安全。
我让我的Linux当服务端。在上面直接java 编译程序时,Server_main.java ServerRunnable.java 这两个服务端的文件由于互相调用,不能单独编译(IDE中不会出现这个问题)。只能将两个类写在一个文件中。