欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 养生 > Java网络编程,多线程,IO流综合项目一一ChatBoxes

Java网络编程,多线程,IO流综合项目一一ChatBoxes

2025/3/9 7:40:27 来源:https://blog.csdn.net/2302_77782043/article/details/146122988  浏览:    关键词:Java网络编程,多线程,IO流综合项目一一ChatBoxes

Java网络编程,多线程,IO流综合小项目一一ChatBoxes

作者:blue

时间:2025.3.7

文章目录

  • Java网络编程,多线程,IO流综合小项目一一ChatBoxes
    • 1.项目介绍
    • 2.项目源码剖析
      • 2.1客户端源码
      • 2.2客户端Sender线程Runnable源码
      • 2.3客户端Receiver线程Runnable源码
      • 2.4服务端源码
      • 2.5服务端Runnable源码
    • 3.项目心得

1.项目介绍

项目目标:实现一个C/S架构,基于TCP协议的控制台版的聊天室,带有注册,登录功能,能实现在局域网内,多个客户端,在一个聊天室中聊天

项目需求

客户端:拥有登录,注册,聊天功能,用户名要唯一,密码第一位必须是字母,后面是纯数字,登录成功后可以直接开始聊天

服务端:对用户,登录和注册的信息进行验证,当登录成功之后,能接收客户端发来的消息,并能向所有已经登录的用户进行转发

2.项目源码剖析

2.1客户端源码

package com.bluening.Client;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/** 客户端程序* 功能:1.登录*      2.注册*      3.聊天* */
public class Client {public static void main(String[] args) throws IOException, InterruptedException {//创建Socket对象,与指定服务端连接Socket socket = new Socket("127.0.0.1", 10086);//没有连接上的话,程序会报错,所以以下代码只有当连接成功才会执行System.out.println("与服务端连接成功");//主界面Scanner sc = new Scanner(System.in);//Scanner对象//字符缓冲输入流,用于接收服务端发回来的数据,利用转换流将socket的InputStream包装BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));//字符缓冲输出流,用于向服务端发送数据,利用转换流将socket的OutputStream包装BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));while (true) {System.out.println("=======控制台版聊天室=======");System.out.println("1.登录");System.out.println("2.注册");System.out.println("请输入你所需要的功能:");String choice = sc.nextLine();//输入所选择的功能if ("1".equals(choice)) {//登录模块while (true) {System.out.println("请输入用户名:");String username = sc.nextLine();System.out.println("请输入密码:");String password = sc.nextLine();bw.write("username=" + username + "&" + "password=" + password + "&" + "login");//向服务器发送信息,login表示这是一个登录请求bw.newLine();bw.flush(); // 刷新缓冲区确保数据发送//接收服务端发来的信息String line = br.readLine();if (line.equals("1")) {System.out.println("登录成功");break;} else if (line.equals("2")) {System.out.println("账户或密码错误,请重新登录");}}} else if ("2".equals(choice)) {//注册模块System.out.println("请输入用户名:");String username = sc.nextLine();String password = null;while (true) {System.out.println("请输入密码(密码只需要以字母开头,后面为纯数字):");password = sc.nextLine();//可以直接在客户端检查密码是否符合条件if (checkPassword(password)) break;else System.out.println("密码不符合条件");}bw.write("username=" + username + "&" + "password=" + password + "&" + "register");//向服务器发送信息,register表示这是一个注册请求bw.newLine();bw.flush(); // 刷新缓冲区确保数据发送//接收服务端发来的信息String line = br.readLine();if (line.equals("1")) {System.out.println("注册成功");} else if (line.equals("2")) {System.out.println("用户名重复");}continue; //注册完应该执行continue逻辑} else continue;//此处表示登录成功后开始聊天//为了使用户收发信息的操作能够同时进行,采用多线程编程来解决问题//创建一条发送信息的线程Thread sender = new Thread(new ClientSendMessageRunnable(bw));sender.start();//创建一条接收信息的线程Thread Receiver = new Thread(new ClientReceiveMessageRunnable(br));Receiver.start();//此时只让线程运行就好,可以跳出主循环break;}//释放资源//socket.close();}private static boolean checkPassword(String password) {for (int i = 0; i < password.length(); i++) {char x = password.charAt(i);if (i == 0) {if (!((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z'))) return false;//不是以字母开头} else {if (x < '0' || x > '9') return false;//不是以纯数字作为后续}}return true;}
}

此处我对客户端源码进行大概剖析,在源代码中我的注释比较完备了,故而在此处我只挑选花费我思考时间较多的部分进行解释。

1.针对客户端其登录和注册的逻辑并不难,但这里有第一个坑点,登录与注册同样是给服务端发送username和password的信息,如何让服务端区分你是在登录还是在注册呢?

答:这里我所采用的方法是,在发送的信息字段上附带上状态信息,如果是登录那在发送的信息后面就加上"&" + “login"字段,同理如果是注册,则加上”&" + “register”,这样就方便服务端识别用户当前的行为了。

2.另外,为了保证Client与Server进行实时交互,我们在每次使用BufferedWriter bw的write方法发送信息后,我们都使用了一个flush方法,这是什么意思呢?

答:因为缓冲区是内存中的一块区域,用于临时存储数据。当进行数据写入操作时,数据不会立即被写入到目标设备(如文件、网络套接字等),而是先被存储在缓冲区中。当缓冲区满了或者满足某些条件时,才会将缓冲区中的数据一次性写入到目标设备。

flush() 方法的作用是强制将缓冲区中暂存的数据立即写入到目标设备中,无论缓冲区是否已满。具体步骤如下

检查缓冲区状态一一>写入数据到目标设备一一>清空缓冲区

3.客户端的难点,在登录成功后如何实现同时可以发送信息给服务器并可以接收服务器传来的消息?

答:我采用了多线程编程的方式来解决这个问题。针对每一个Client在其登录成功后,均有两个线程,对于Sender线程,我利用了构造方法传给了他bw,对于Receiver方法,我传给了他br。因为每个客户端是独立运行的,所以每个客户端的IO流是独立的,不会出现混乱。

对于Sender和Receiver,他们分别拥有当前Socket对象的bw和br,并不相互纠缠。

另外值得注意的是IO流的生命周期和线程的生命周期都是相互独立的。

2.2客户端Sender线程Runnable源码

package com.bluening.Client;import java.io.BufferedWriter;
import java.util.Scanner;//为了使用户收发信息的操作能够同时进行,采用多线程编程来解决问题
public class ClientSendMessageRunnable implements Runnable{//利用构造方法来传递,针对Socket的输出流对象//输入对象BufferedWriter bw;public ClientSendMessageRunnable(BufferedWriter bw) {this.bw = bw;}@Overridepublic void run() {Scanner sc = new Scanner(System.in);try {while(true){System.out.println("请输入你要发送的信息:");if (sc.hasNextLine()) {String message = sc.nextLine();// 检查输入是否为空,如果为空则继续等待有效输入if (!message.isEmpty()) {bw.write(message);bw.newLine();bw.flush();}}}} catch (Exception e) {throw new RuntimeException(e);}}
}

2.3客户端Receiver线程Runnable源码

package com.bluening.Client;import java.io.BufferedReader;
import java.io.IOException;public class ClientReceiveMessageRunnable implements Runnable{BufferedReader br;public ClientReceiveMessageRunnable(BufferedReader br) {this.br = br;}@Overridepublic void run() {try {while(true){String line = br.readLine();if(line!=null) System.out.println(line);}} catch (IOException e) {throw new RuntimeException(e);}}
}

2.4服务端源码

package com.bluening.Server;import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*
*服务端程序
* */
public class Server {public static void main(String[] args) throws IOException {//1.读取本地文件中所有正确用户的信息ArrayList<String> userInfo = getUserInfo("Username_Password.txt");//2.创建ServerSocket对象,注意端口与客户端指定的端口保持一致ServerSocket serverSocket = new ServerSocket(10086);//3.服务端会被多个客户端连接,当用户数量过大时,单纯的循环效率低下//我们利用多线程来进行优化,但频繁地创建,销毁线程,对系统的开销比较大,我们可以利用线程池来进行优化//定义线程池ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,//核心线程的数量16,//最大线程数,不能小于0,最大数量>=核心线程数量60,//空闲线程最大存活时间TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue<>(5),//任务队列Executors.defaultThreadFactory(),//创建线程工厂new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略);//由于涉及到服务端,向多个客户端转发信息,故而可以每个监听到的socket对象,利用集合来存储//由于用户分为登录和未登录两个状态,只有登录状态的用户才能收到服务器发送的聊天信息//所以我们应该用一个双列集合来存储信息//键为Socket,值为其登录状态码 1为登录,0为未登录HashMap<Socket,Integer> AllUserSocketMap = new HashMap<>();while(true){//监听客户端Socket socket = serverSocket.accept();//将socket对象加入到集合中//由于AllUserSocketMap对象有可能在线程中被修改,所以其为一个共享数据//在此利用同步代码块保证线程安全synchronized (Server.class){AllUserSocketMap.put(socket,0);}//监听到一个客户端则开启一条线程poolExecutor.submit(new ServerRunnable(socket,userInfo,AllUserSocketMap));}}//1.读取本地文件中所有正确用户的信息private static ArrayList<String> getUserInfo(String file) throws IOException {ArrayList<String> userInfo = new ArrayList<>();//字符缓冲输入流,可以用其中的readLine方法读取整行数据BufferedReader bw = new BufferedReader(new FileReader(file));//循环获取String line;while((line=bw.readLine())!=null){userInfo.add(line);}//释放资源bw.close();return userInfo;}
}

2.5服务端Runnable源码

package com.bluening.Server;import com.sun.source.tree.WhileLoopTree;import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*
* 执行线程的runnable类
* 功能:1.对登录用户进行校验
*      2.完成用户注册
*      3.用户聊天时将聊天信息转发给所有用户
* */
public class ServerRunnable implements Runnable{//显然要完成登录与注册功能需要,该条线程对应的Socket对象,用户信息的集合//完成聊天功能则需要所有客户端对应的socket对象集合//我们可以通过构造方法的方式,来从Server类中获取这些内容Socket socket;//本线程对应的socketString User; //若用户登录成功要记录其usernamestatic String Message; //群发的消息应该是共享的/** 值得注意的是,在此处userInfo,AllUserSocketMap是会发生变化的的数据* 每条线程都有可能对其进行增加,这说明userInfo,AllUserSocketMap是一个共享数据* 我们应该利用同步代码块,来保证线程安全* *///锁对象static final Object LOCK = new Object();//存放每个socket对象,对应的BufferedWriterstatic HashMap<Socket,BufferedWriter> AllSocketBufferedWriter = new HashMap<>();ArrayList<String> userInfo;//所有用户信息HashMap<Socket,Integer> AllUserSocketMap;//所有客户端对应的socket,与登录状态码//构造方法public ServerRunnable(Socket socket, ArrayList<String> userInfo, HashMap<Socket,Integer> AllUserSocketMaps) {this.socket = socket;this.userInfo = userInfo;this.AllUserSocketMap = AllUserSocketMaps;}@Overridepublic void run() {//服务端应该先判断当前的Client用户是否登录,如果登录已经登录,那么就只要群发信息即可//如果没有登录,则需要判断用户当前的行为是注册还是登录Integer status = AllUserSocketMap.get(socket);//获取状态码//字符输入流,准备读取客户端所发送来的信息BufferedReader br = null;try {br = new BufferedReader(new InputStreamReader(socket.getInputStream()));} catch (IOException e) {throw new RuntimeException(e);}//字符输出流,准备向客户端反馈信息BufferedWriter bw = null;try {bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));} catch (IOException e) {throw new RuntimeException(e);}//维护AllSocketBufferedWritersynchronized (LOCK) {AllSocketBufferedWriter.put(socket,bw);}//用于将数据写回文件BufferedWriter file_writer = null;try {//应该打开续写开关,不能清空文件file_writer  = new BufferedWriter(new FileWriter("Username_Password.txt",true));} catch (IOException e) {throw new RuntimeException(e);}while(status==0){//表明用户未登录try {String line = br.readLine();String[] arr = line.split("&");if("login".equals(arr[2])){ //用户正在登录,应当对其账户密码进行验证String username = arr[0].split("=")[1];String password = arr[1].split("=")[1];String loginInfo = username+"="+password;//直接拼接成文件中信息的形式if(checkLogin(loginInfo)){//登录成功//修改当前socket状态码synchronized (LOCK) { //修改操作,应当保证共享数据安全AllUserSocketMap.put(socket,1);}status = AllUserSocketMap.get(socket);//向用户发送登录成功的信息bw.write("1"); //1表示登录成功bw.newLine();bw.flush(); // 刷新缓冲区确保数据发送//登录成功了,要记录当前socket对象的用户名User = username;}else { //登录失败bw.write("2"); //2表示登录失败bw.newLine();bw.flush();}}else if("register".equals(arr[2])){  //用户正在注册,应当对其账户密码进行验证String username = arr[0].split("=")[1];String password = arr[1].split("=")[1];if(usernameContains(username)){//用户名重复bw.write("2"); //2表示注册失败bw.newLine();bw.flush();}else {//注册成功,应该把数据写回文件,并且在集合中添加,这都是对共享数据的操作synchronized (LOCK) { //修改操作,应当保证共享数据安全file_writer.write(username+"="+password);// 使用系统默认的换行符file_writer.write(System.lineSeparator());file_writer.flush();userInfo.add(username+"="+password);}bw.write("1"); //1表示注册成功bw.newLine();bw.flush();}}} catch (IOException e) {throw new RuntimeException(e);}}//用户登录成功了,下面部分是群发消息的逻辑while (true) {try {Message = br.readLine();//发送逻辑Set<Map.Entry<Socket,Integer>> AllUserSocketentrySet = AllUserSocketMap.entrySet();for (Map.Entry<Socket, Integer> socketIntegerEntry : AllUserSocketentrySet) {if(socketIntegerEntry.getValue()==1){//如果已经登录//获取到对应socket对应的流BufferedWriter SocketBw = AllSocketBufferedWriter.get(socketIntegerEntry.getKey());synchronized (LOCK) {try {SocketBw.write(User+":"+Message);SocketBw.newLine();SocketBw.flush();} catch (IOException e) {throw new RuntimeException(e);}}}}} catch (IOException e) {throw new RuntimeException(e);}}}//给予注册模块使用,查看用户名是否已经存在private boolean usernameContains(String username) {for (String string : userInfo) {String name = string.split("=")[0];if(name.equals(username)) return true;}return false;}//给予登录模块使用,检查账户密码是否完全匹配private boolean checkLogin(String loginInfo) {for (String string : userInfo) {if(string.equals(loginInfo)) return true;}return false;}
}

服务端的基本逻辑是这样的

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

②当有客户端来连接时,就开启一条线程

③线程中判断当前用户是登录还是注册操作

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

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

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

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

1.首先如何判断用户是否登录了?

答:我的做法是这样的,我利用一个双列集合HashMap<Socket,Integer> AllUserSocketMaps来记录每个Socket对象的状态信息,0是未登录,1是登录

2.在编写服务端代码时,我遇到最严重的问题就是共享数据的混乱,但是我通过梳理思路,一步步解决了bug,在此我想记录我梳理的过程:

答:共享数据UserInfo和AllUserSocketMap是通过构造方法传过来的。

User变量是为了记录每个线程对应的用户名,方便群发消息的时候,知道是谁发的,所以User针对每个线程应该独立

Message则是需要被群发的消息,所以每个线程都需要,而且需要同一个,所以这显然是一个共享数据,我设置其为static,并且在对其修改值做了锁操作,保证线程安全。

此外我应该格外关注ServerRunnable中的IO流,这很关键,最简单的file_writer它是面向username_password.txt的输出流,于各个线程中独立存在,它应该没有什么问题。

其次就是两个针对Socket的流,br和bw,这两个流针对每个线程也是独立的,他们都是针对于自己线程的Socket对象

3.如何做到消息群发?

答:想做到消息群发,就要获取每个线程Socket对象对应的BufferedWriter对象,我是这样设计的,我创建了一个static的(各线程之间共享,所以这是一个共享数据) HashMap,键为Socket,值为对应的BufferedWriter,在群发消息时,我通过遍历AllSocketMap,如果状态码为1,则获取其BufferedWriter对消息进行发送。值得注意的是,这样每个线程的BufferedWriter就不仅是只会在本线程中用到了,故我要对BufferedWriter操作时,应当做同步处理。

3.项目心得

项目编码耗时大约10h左右,主要在线程中的排错比较耗费时间,常有思绪打结的情况,不过我通过梳理思路,也是逐一解决了。另外我在编程时写了必要的注释,进行了分模块的编程,使我修改起来更加方便,我还使用了git来进行版本管理,方便代码混乱时做版本穿梭,回溯版本。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词