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

简介: **项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。**项目亮点**:- **C/S架构**:客户端与服务端通过TCP连接通信。- **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。- **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。- **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。

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

作者:blue

时间:2025.3.7

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;
    }
}
AI 代码解读

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

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;
    }

    @Override
    public 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);
        }
    }
}
AI 代码解读

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;
    }

    @Override
    public void run() {
   
        try {
   
            while(true){
   
                String line = br.readLine();
                if(line!=null) System.out.println(line);
            }
        } catch (IOException e) {
   
            throw new RuntimeException(e);
        }
    }
}
AI 代码解读

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;
    }
}
AI 代码解读

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;//本线程对应的socket

    String User; //若用户登录成功要记录其username

    static String Message; //群发的消息应该是共享的

    /*
     * 值得注意的是,在此处userInfo,AllUserSocketMap是会发生变化的的数据
     * 每条线程都有可能对其进行增加,这说明userInfo,AllUserSocketMap是一个共享数据
     * 我们应该利用同步代码块,来保证线程安全
     * */

    //锁对象
    static final Object LOCK = new Object();

    //存放每个socket对象,对应的BufferedWriter
    static 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;
    }

    @Override
    public 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);
        }

        //维护AllSocketBufferedWriter
        synchronized (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;
    }
}
AI 代码解读

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

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

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

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

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

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

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

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

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

答:我的做法是这样的,我利用一个双列集合HashMap 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来进行版本管理,方便代码混乱时做版本穿梭,回溯版本。

仓库地址:https://gitee.com/zhang-tenglan/chat-boxes.git

目录
打赏
0
21
23
0
174
分享
相关文章
基于Reactor模式的高性能网络库之线程池组件设计篇
EventLoopThreadPool 是 Reactor 模式中实现“一个主线程 + 多个工作线程”的关键组件,用于高效管理多个 EventLoop 并在多核 CPU 上分担高并发 I/O 压力。通过封装 Thread 类和 EventLoopThread,实现线程创建、管理和事件循环的调度,形成线程池结构。每个 EventLoopThread 管理一个子线程与对应的 EventLoop(subloop),主线程(base loop)通过负载均衡算法将任务派发至各 subloop,从而提升系统性能与并发处理能力。
51 3
02理解网络IO:实现服务与客户端通信
网络IO指客户端与服务端通过网络进行数据收发的过程,常见于微信、QQ等应用。本文详解如何用C语言实现一个支持多客户端连接的TCP服务端,涉及socket编程、线程处理及通信流程,并分析“一消息一线程”模式的优缺点。
116 0
掌握并发模型:深度揭露网络IO复用并发模型的原理。
总结,网络 I/O 复用并发模型通过实现非阻塞 I/O、引入 I/O 复用技术如 select、poll 和 epoll,以及采用 Reactor 模式等技巧,为多任务并发提供了有效的解决方案。这样的模型有效提高了系统资源利用率,以及保证了并发任务的高效执行。在现实中,这种模型在许多网络应用程序和分布式系统中都取得了很好的应用成果。
106 35
Python 高级编程与实战:深入理解网络编程与异步IO
在前几篇文章中,我们探讨了 Python 的基础语法、面向对象编程、函数式编程、元编程、性能优化、调试技巧、数据科学、机器学习、Web 开发和 API 设计。本文将深入探讨 Python 在网络编程和异步IO中的应用,并通过实战项目帮助你掌握这些技术。
JAVA网络IO之NIO/BIO
本文介绍了Java网络编程的基础与历史演进,重点阐述了IO和Socket的概念。Java的IO分为设备和接口两部分,通过流、字节、字符等方式实现与外部的交互。
187 0
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
515 12
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
162 2
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问