java 多用户即时通信系统的实现 万字详解

news2024/11/26 11:34:49

目录

前言

一、拾枝杂谈

        1.项目开发大体流程 : 

        2.多用户即时通信系统分析 : 

                1° 需求分析

                2° 整体分析

二、用户登录

        1.准备工作 : 

        2.客户端 : 

                1° 菜单界面

                2° 登录验证

                3° 线程创建

                4° 线程管理

        3.服务端 : 

                1° 用户验证

                2° 线程创建

                3° 线程管理

        4.登录测试 : 

三、在线列表

        1.扩充MessageType中的类型 : 

        2.扩充UserClientService类中的方法 : 

        3.扩充客户端线程类中的内容 : 

        4.扩充ControlServerConnectClientThread类中的方法 : 

        5.扩充服务端线程类中的内容 : 

        6.拉取测试 : 

四、退出系统

        1.需要解决的问题 : 

        2.解决办法 : 

                1° 总思路

                2° 客户端

        ​​​​​​​        3° 服务端

五、私聊群聊

        1.私发消息 : 

                1° 思路分析

                2° 代码实现

                3° 运行测试 

        2.群发消息 : 

                1° 客户端 

                2° 服务端 

                3° 运行测试 

六、传输文件

        1.思路分析 : 

        2.客户端 : 

        3.服务端 : 

        4.运行测试 : 

七、最终代码

        1.客户端 : 

                1° Viem

                2° UserClientService

                3° MessageClientService

                4° FileClientService

                5° ClientConnectServiceThread

                6° ControlClientConnectServiceThread

        2.服务端 : 

                1° ChatServer

                2° ServerConnectClientThread

                3° ControlServerConnectClientThread

                4° ChatFrame

        3.公共部分 : 

                1° Message

                2° MessageType

                3° User


前言

        本篇博文适合javaSE基础较为扎实的小伙伴儿们阅读,up会从实现层面和大家分享一个多用户即时通信系统,类似于QQ,微信这种可以实现登录,聊天,发文件,下线等功能的程序。但是声明一点,该多用户即时通信系统不是项目(up之后会专门开新的专栏来出项目),而只是对已学的java知识的联系和应用,可以理解为一个模拟项目,主要涉及到oop集合IO流多线程网络编程等内容。如果你想进一步巩固自己的java基础,这篇博文或许会是很好的选择。感谢阅读!

一、拾枝杂谈

        1.项目开发大体流程 : 

        ①分析阶段 : 需求分析师会从“技术实现”和“行业情况”两方面综合考虑,出一个需求分析报告(通常是白皮书),包含客户的具体要求以及项目最终要实现的功能。需求分析在整个项目开发流程中所占用的时间和资源——往往与项目本身的大小成正比

        ②设计阶段 : 主要是架构师和项目经理揽活儿,有些公司会将二者合并。架构师/项目经理需要负责项目的设计工作(UML类图,流程图,模块设计,数据库,项目架构);并且要完成项目的原型开发——先在虚拟机上跑出一个预览的项目效果(不过多考虑性能),与客户进行对接,签订合约。一切就绪后,架构师/项目经理就会在公司的各个部门物色人选;比方说,当前项目是用java来实现的,架构师/项目经理就会挑选java技术牛逼的🐒。因此,有些时候会出现一个🐒同时在两个甚至多个项目组的情况,这时候这只牛逼的🐒会很忙,但是却痛并快乐着,因为它可以领到double甚至是multiple的工资(🐒们的工资往往由基本工资 + 项目提成构成),设计阶段在整个项目流程中所占用的时间往往比分析阶段短一些,但依然与项目本身的大小成正比

        ③实现阶段 : 不多解释,🐒儿们的主场。🐒儿们要负责把架构师/项目经理给的模块功能进行一一实现,完事儿后在自己run一run,看看自己负责的代码有没有bug。实现阶段在整个项目流程中所占的比重和项目本身成反比,即项目越大,实现阶段反而不如需求阶段和分析阶段重要。但是,实际情况是,小公司小项目的实现阶段往往是占比最大的一个,而且还会出现一边实现一边改需求的情况,即设计阶段和实现阶段缠一块儿了。

        ④测试阶段 : 测试工程师,🐒儿们的天敌;负责把🐒儿们的代码拿来做各种测试,例如黑白盒测试,集成测试,单元测试等;因此,测试工程师与开发工程师往往打成一片,不可开交。在测试阶段,最怕的事情就是——高耦合性的代码出现了bug。

        ⑤实施阶段 : 实施工程师,需要将项目正确地部署到客户的平台,并保证其运行正常,需要有较强的开发能力和环境配置能力,以及较好的身体素质。客户的平台可能部署在不同的省市,甚至国家,因此实施工程师需要东奔西走,把每个平台的服务器,操作系统,环境配置等问题都给搞定。打个比方,小公司的实施工程师——使命召唤;大公司的实施工程师——塞尔达传说。

        ⑥维护阶段 : 解决程序后期出现的bug,解决项目升级相关的问题。大公司——运维工程师;小公司——背锅侠。

        2.多用户即时通信系统分析 : 

                1° 需求分析

        ①用户登录        

        ②在线检测        (拉取在线用户列表)

        ③退出系统        (客户端与服务器端)

        ④私聊群聊        (实现单发和群发消息)

        ⑤传送文件       

                2° 整体分析

        Δ对于服务端——

        服务端上往往提供了不同的服务,因此服务端需要通过ServerSocket来监听不同的端口;

        每当有客户端成功连接到服务端,都会获得一个Socket对象;此时,启动一个线程,并令该线程持有Socket对象,即令每个线程都操纵一个自己的Socket对象,此举可以实现消息的群发;

        可以使用HashMap集合来管理服务端的多个线程

        Δ对于客户端——

       客户端采用对象的形式来与服务端进行通讯,此举可以发送更多的信息;可以使用对象处理流 ObjectInputStream和ObjectOutputStream来进行数据的读取

        当客户端成功根据"IP + 端口"成功连接到服务端后,客户端获得自己的Socket对象;此时,类似地,也启动一个线程,并令该线程持有Socket对象

        同样使用HashMap集合来管理客户端的多个线程。

        Δ如下图所示 : 

        User对象可以验证是否为合法的登录用户Message对象则包含了要传输的信息


二、用户登录

        1.准备工作 : 

                在IDEA中创建一个新项目“ChatServer”,用来模拟通信系统的服务端;并另建一个新项目“ChatClient_0”用来模拟通信系统的一个客户端;如下图所示 : 

                在服务端(ChatServer)项目中,src包下,创建一个mutual包,表示服务端和客户端共有的内容(用户信息和发送的消息)。在mutual包下创建User类,并令其实现Serializable接口;实现Serializable接口后User对象就可以序列化,进行网络传输,就可以被对象处理流操作。User类中定义用户名和用户密码两个属性。User类代码如下 : 

package mutual;

import java.io.Serializable;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @meaning : The shared User between Server and Client
 */
public class User implements Serializable {
    private static final long serialVersionUID = 1L;    //增强兼容性
    private String id;
    private String pwd;

    public User() {}
    public User(String id, String pwd) {
        this.id = id;
        this.pwd = pwd;
    }

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

    public String getPwd() {
        return pwd;
    }
    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}

                在mutual包下创建Message类,表示传输的消息类型,并令其实现Serializable接口Message类中应该包括消息的发送者,消息的接收者,消息的类型等属性,这样服务端解包后才知道这消息是发给谁的,以及消息的具体内容是什么。Message类代码如下

package mutual;

import java.io.Serializable;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @message : information that are transmitted
 */
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;    //增强兼容性
    private String sendTime;    //发送时间
    private String sender;      //发送者
    private String receiver;    //接收者
    private String content;     //消息内容
    private String mesType;     //消息类型

    public String getSendTime() {
        return sendTime;
    }

    public void setSendTime(String sendTime) {
        this.sendTime = sendTime;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public String getReceiver() {
        return receiver;
    }

    public void setReceiver(String receiver) {
        this.receiver = receiver;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getMesType() {
        return mesType;
    }

    public void setMesType(String mesType) {
        this.mesType = mesType;
    }
}

                还需要确定Message内容的具体类型,可以定义MessageType接口,在接口中定义不同的常量,以表示不同的消息类型;MessageType接口代码如下 : 

package mutual;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @meaning : Types of message
 */
public interface MessageType {
    //定义常量
    String MESSAGE_LOGIN_SUCCESS = "1";     //表示登录成功
    String MESSAGE_LOGIN_FAIL = "0";        //表示登录失败
}

                最后,将mutual包拷贝一份到客户端,如下图所示 : 

        2.客户端 : 

                1° 菜单界面

                在客户端(ChatClient_0)新建一个包client,用户存放用户相关的类;在client包下,另建一个包menu,用于菜单的界面显示。在menu包下新建一个View类,View类代码如下 : 

package client.menu;

import client.service.UserClientService;
import java.io.IOException;
import java.util.Scanner;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 菜单界面的显示
 * @PS : Run -> Edit Configurations -> Modify options -> allow multiple instances
 */
public class View {
    private boolean loop = true;        //控制是否显示菜单
    private String key = "";            //接收用户的键盘输入
    private static Scanner sc = new Scanner(System.in);
    /*
        将UserClientService对象置为属性,
        该对象用于执行用户登录/注册等操作(该步骤将功能与界面联系起来)
     */
    private UserClientService userClientService = new UserClientService();

    public static void main(String[] args) throws IOException {
        new View().mainMenu();
        System.out.println("客户端退出...");
    }

    private void mainMenu() throws IOException {
        while (loop) {
            System.out.println("===========Welcome to the system of chat:===========");
            System.out.println("\t\t1.登录系统");
            System.out.println("\t\t9.退出系统");


            System.out.print("请输入你的选择:");
            key = sc.nextLine();

            switch (key) {
                case "1" :
                    //登录操作
                    System.out.print("请输入用户名:");
                    String userID = sc.nextLine();
                    System.out.print("请输入密  码:");
                    String password = sc.nextLine();

                    //验证登录的用户是否合法(封装思想)
                    if (userClientService.check(userID, password)) {     //验证成功
                        System.out.println("\n===========Welcome user " + userID + "===========");
                        //向用户显示二级菜单
                        while (loop) {
                            System.out.println("\n===========网络通信系统二级菜单(user:" + userID + ")===========");
                            System.out.println("\t\t1.在线列表:");
                            System.out.println("\t\t2.群发消息:");
                            System.out.println("\t\t3.私发消息:");
                            System.out.println("\t\t4.文件发送:");
                            System.out.println("\t\t9.退出系统:");

                            System.out.print("请输入你的选择:");
                            key = sc.nextLine();

                            switch (key) {
                                case "1" :
                                    System.out.println(1);
                                    break;
                                case "2" :
                                    System.out.println(2);
                                    break;
                                case "3" :
                                    System.out.println(3);
                                    break;
                                case "4" :
                                    System.out.println(4);
                                    break;
                                case "9" :
                                    loop = false;   //在二级菜单中用户也可以直接选择退出系统
                            }
                        }
                    } else {        //验证失败
                        System.out.println("登录失败!请重新尝试!");
                    }

                    break;
                case "9" :
                    sc.close();
                    loop = false;       //将控制while循环的布尔变量设置为false
                    break;
            }

        }
    }
}

                为了实现多用户登录,需要对View类进行配置依次点击Run -> Edit Configurations -> Modify options -> allow multiple instances,允许并行,如下图所示 : 

                2° 登录验证

                View类中有关“用户登录验证”部分的代码,利用封装的思想,将其封装到client.service包下的类UserClientService中,UserClientService类代码如下 : 

package client.service;

import mutual.Message;
import mutual.MessageType;
import mutual.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 登录验证
 */
public class UserClientService {
    /*
        将User对象设置成一个属性,可利用getter和setter修改User对象的引用,便于操作。
        Socket对象同样也可能在其他类中使用,因此也设置为属性。
     */
    private User user = new User();
    private Socket socket;

    public boolean check(String userID, String password) throws IOException {
        //局部变量
        boolean b = false;
        //初始化User对象
        user.setId(userID);
        user.setPwd(password);

        //向服务端发送信息
        try {
            //1.获取Socket对象
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 8888);
            //2.获取与Socket对象相关联的对象处理流(输出流)
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            //3.序列化User对象,写入数据通道(向服务端发送一个User对象,服务端会对这个User对象进行验证)
            oos.writeObject(user);
            //.........

            //4.获取与Socket对象相关联的对象处理流(输入流)
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            //5.读取服务端传输过来的Message对象
            Message message = (Message) ois.readObject();   //类型强转

            if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)) {
                //创建线程对象(目的是为了与服务端保持通讯)
                ClientConnectServiceThread ccst = new ClientConnectServiceThread(socket);
                //启动线程
                ccst.start();
                //将线程放入集合中统一管理
                ControlClientConnectServiceThread.addClientConnectServiceThread(userID, ccst);
                b = true;
            } else {
                //如果没有启动线程,关闭Socket对象。
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } 

        return b;
    }
}

                3° 线程创建

                为了保持通讯,需要让线程持有Socket对象;同时,利用HashMap集合来管理多个线程。UserClientService类中有关线程的部分,同样新建一个类ClientConnectServiceThread,在client.service包下,ClientConnectServiceThread类代码如下 : 

package client.service;

import mutual.Message;

import java.io.ObjectInputStream;
import java.net.Socket;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 客户端用于和服务端进行通讯的线程
 */
public class ClientConnectServiceThread extends Thread{
    //该线程需要持有Socket对象
    private Socket socket;

    public ClientConnectServiceThread(Socket socket) {
        this.socket = socket;
    }

    public Socket getSocket() {
        return socket;
    }

    @Override
    public void run() {
        //∵Thread需要在后台与服务器通信,因此使用while循环
        while (true) {
            try {
                System.out.println("客户端线程,等待读取来自服务器端的消息...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                /*
                    如果服务端没有发送Message对象到数据通道中,线程就会阻塞在这里。
                 */
                Message message = (Message) ois.readObject();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

                4° 线程管理

                UserClientService类中涉及到线程“管理”,将相关代码进行封装,在client.service包下新建一个ControlClientConnectServiceThread类,代码如下 : 

package client.service;

import java.util.HashMap;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 管理客户端的线程
 */
public class ControlClientConnectServiceThread {
    /*
        使用HashMap类来管理多个线程(模拟数据库),key表示用户的id,value表示线程。
     */
    private static HashMap<String, ClientConnectServiceThread> hashMap = new HashMap<>();

    //添加线程的方法
    public static void addClientConnectServiceThread(String userID, ClientConnectServiceThread ccst) {
        hashMap.put(userID, ccst);
    }

    //取出线程的方法
    public static ClientConnectServiceThread getClientConnectServiceThread(String userID) {
        return hashMap.get(userID);
    }
}

        3.服务端 : 

                1° 用户验证

                服务端的代码与客户端类似,都需要创建一个类用于读取数据通道中的数据;还需要一个线程类来持有Socket对象;最后就是一个类来管理服务端的线程。
                在ChatServer包下创建server.service包,在该包下创建ChatServer类,用于接收客户端法来的User和Message对象,并给出回应。ChatServer类代码如下 : 

package server.service;

import mutual.Message;
import mutual.MessageType;
import mutual.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 服务端
 */
public class ChatServer {
    //将ServerSocket设置为属性,写在main函数外
    private ServerSocket serverSocket = null;

    /**
        将合法的用户放入集合中(使用“id + user”的泛型),
        建议使用ConcurrentHashMap集合,线程同步,可在多线程程序下安全使用。
     */
    private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();
    static {    //在静态代码块中初始化validUsers集合对象
        validUsers.put("Cyan", new User("Cyan", "RA9"));
        validUsers.put("Rain", new User("Rain", "flo"));
        validUsers.put("Ice", new User("Ice", "ais"));
        validUsers.put("Five", new User("Five", "55555"));
        validUsers.put("Kyrie", new User("Kyrie", "lrving"));
    }

    public boolean checkUser(String userID, String password) {
        User user = validUsers.get(userID);
        if (user == null) { //如果合法用户集合中不存在当前用户,直接返回false;
            return false;
        }
        if (!(user.getPwd().equals(password))) {    //如果存在该用户,但密码错误,返回false;
            return false;
        }

        return true;
    }
    public ChatServer() {
        //端口也可以写在配置文件中
        try {
            System.out.println("服务端正在8888端口监听...");
            serverSocket = new ServerSocket(8888);

            /*
                监听是不间断的,当服务端和某个客户端建立连接后,服务端会继续监听。
             */
            while (true) {
                //获取Socket类对象(服务端是通过accept方法来获取Socket对象的)
                Socket socket = serverSocket.accept();
                //获取Socket对象关联的输入流与输出流(对象处理流)
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());

                //客户端第一次传过来的是User对象
                User user = (User) ois.readObject();
                //暂时以单用户登录为例(id = Cyan, pwd = RA9)
                //创建一个Message对象,用于回复客户端是否连接成功(Message对象写在if-else语句外)
                Message message = new Message();

                if (checkUser(user.getId(), user.getPwd())) {   //登录成功
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
                    //将包含“登录成功与否”信息的Message对象写入数据通道
                    oos.writeObject(message);
                    //创建一个线程,与客户端保持通讯
                    ServerConnectClientThread scct = new ServerConnectClientThread(socket, user.getId());
                    //启动线程
                    scct.start();
                    //将线程放入集合中
                    ControlServerConnectClientThread.getServerConnectClientThread(user.getId());
                } else {    //登录失败
                    System.out.println("id = " + user.getId() + ",pwd = " + user.getPwd() + " 验证失败!");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    socket.close(); //关闭Socket
                }
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //若退出while循环,说明服务端不再监听,需要关闭ServerSocket对象
            try {
                serverSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

                2° 线程创建

                同样,为了保持通讯,需要让线程持有Socket对象,相关代码封装到service包下的ServerConnectClientThread类中,代码如下 : 

package server.service;

import mutual.Message;

import java.io.ObjectInputStream;
import java.net.Socket;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 服务端的线程,用于和客户端保持通讯
 */
public class ServerConnectClientThread extends Thread {
    private Socket socket;
    private String userID;  //当前连接到服务端的用户的id
    public ServerConnectClientThread(Socket socket, String userID) {
        this.socket = socket;
        this.userID = userID;
    }

    public Socket getSocket() {
        return socket;
    }

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("服务端与客户端" + userID + "保持通讯,读取数据中...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

                Message message = (Message) ois.readObject();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

                3° 线程管理

                涉及到线程管理的代码封装到ControlServerConnectClientThread类中,代码如下 : 

package server.service;

import java.util.HashMap;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 用于管理服务端的线程
 */
public class ControlServerConnectClientThread {
    private static  HashMap<String, ServerConnectClientThread> hashMap = new HashMap<>();

    //添加线程到集合中
    public static void addServerConnectClientThread(String userID, ServerConnectClientThread scct) {
        hashMap.put(userID, scct);
    }

    //根据用户的id获取对应的线程
    public static ServerConnectClientThread getServerConnectClientThread(String userID) {
        return hashMap.get(userID);
    }
}

        4.登录测试 : 

                在服务器端新建一个frame包,在该包下新建一个ChatFrame类,用于启动服务端(客户端在View类中启动)。ChatFrame类代码如下 : 

package frame;

import server.service.ChatServer;

public class ChatFrame {
    public static void main(String[] args) {
        new ChatServer();
    }
}

                同时启动ChatFrame类和View类,效果如下GIF图 : 


三、在线列表

        1.扩充MessageType中的类型 : 

                客户端如果想获取当前多用户通讯系统中的在线成员列表,需要通过Message对象向服务端申请,服务端再通过Message对象的形式,将系统的在线用户列表发送给客户端
                首先我们需要对MessageType中的类型进行扩充,如下图所示 : 

        2.扩充UserClientService类中的方法 : 

                在客户端的UserClientService类中新增一个用于拉取在线用户列表的onlineList方法代码如下 : 

    public void onlineList() {
        //向服务端发送一个Message对象,类型是MESSAGE_GET_ONLINE_FRIENDS
        Message message = new Message();
        message.setSender(user.getId());    //用户登录时已在check方法中设置了id的值,所以可直接用
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIENDS);

        try {
            //得到当前线程持有的Socket对象对应的对象处理流(输出流)
            ObjectOutputStream oos =
                    new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
            oos.writeObject(message);   //向服务端发送“拉取在线用户列表”的请求
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

        3.扩充客户端线程类中的内容 : 

                在客户端的ClientConnectServerThread类中,run方法里面增加对于Message类型判断和处理的逻辑语句代码如下 : 


                /*
                    判断客户端读取到的Message的类型,并做出相应的业务处理。
                 */
                if (message.getMesType().equals(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS)) {
                    //若Message的类型是返回的在线用户列表,取出在线列表并显示,使用空格分隔不同用户的id
                    String[] onlineUsers = message.getContent().split(" ");
                    System.out.println("===========在线用户列表如下:===========");
                    for (int i = 0; i < onlineUsers.length; i++) {
                        System.out.println("用户: " + onlineUsers[i]);
                    }
                } else {
                    System.out.println("...other content");
                }

        4.扩充ControlServerConnectClientThread类中的方法 : 

                拉取在线用户列表的操作要在服务端线程的run方法中进行,同样可以利用oop思想,将相关代码封装起来;考虑每个线程都保存了当前用户的id和对应的Socket对象,于是决定在服务端线程的管理类ControlServerConnectClientThread类中新增一个onlineList方法,用于拉取用户的在线列表,返回一个String类型的字符串给客户端,客户端的线程再对该字符串进行处理。onlineList方法代码如下 : 

    //获取在线用户列表
    public static String getOnlineFriends() {
        /*
            利用hashMap集合中的key是用户id的特点,可以对hashMap对象进行遍历,从而获取用户列表。
         */
        Iterator<String> iterator = hashMap.keySet().iterator();
        String onlineUsers = "";

        while (iterator.hasNext()) {
            onlineUsers += iterator.next() + " ";   //加空格对应客户端的split方法。
        }

        return onlineUsers;
    }

        5.扩充服务端线程类中的内容 : 

                有了拉取在线用户的方法,就可以在服务端的线程类中调用该方法了,如下图所示 : 

        6.拉取测试 : 

                启动ChatFrame类和多个View类,如下GIF图所示 :  


四、退出系统

        1.需要解决的问题 : 

        当用户登录成功后,即客户端与服务端建立连接后,若我们在控制台输入9,整个进程并没有退出,如下图所示 : 

        这是因为主线程退出后,负责联络服务端的子线程还没有退出,还在不停运行,等待服务端发送数据,如下图所示 : 

        2.解决办法 : 

                1° 总思路

                如果我们可以直接令客户端的整个进程关闭,就可以自动退出该进程下的所有线程;可以在客户端的View类下增加一个方法的调用,若用户输入9,就给服务器端发送一个Mesage对象,令服务端退出与当前对象相关联的线程

                服务端的线程类中保存了与当前用户相关联的Socket对象和当前用户的ID,如下所示 : 

                因此,服务端可以根据接收到的Message对象,关闭指定线程及Socket对象。
                然后,在客户端调用System.exit(0)方法退出当前进程

                2° 客户端

                在UserClientService类中的新定义一个方法logout,用于完成对服务端发送“关闭线程”的Message对象的功能logout方法代码如下 : 

    /** logout方法可以退出当前用户 */
    public void logout() {
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(user.getId());    //指定具体要退出的客户端

        //发送Message对象
        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
            oos.writeObject(message);
            
            System.out.println(user.getId() + " 退出系统...");
            System.exit(0);     //0表示正常退出当前“进程”。
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

                同时,在View类中调用该方法,如下图所示 : 

        ​​​​​​​        3° 服务端

                首先,在服务端的线程管理类ControlServerConnectClientThread类中,新定义一个方法用来删除服务端指定的线程,如下所示 : 

                然后在服务端的线程类ServerConnectClientThread中新增一个else if的判断,并在其中调用该方法(可在删除前令线程休眠0.5s,以避免EOF异常),如下图所示 : 

                测试结果如下(成功):


五、私聊群聊

        1.私发消息 : 

                1° 思路分析

                不管是私发还是群发,一般情况下,一个客户端与另一个客户端都是无法直接通讯的,需要经过服务端来转发
                对于客户端它需要将要发送的信息打包成Message对象,然后发给服务端;同时,要接收来自其他用户的(经过服务端转发的)消息
                对于服务端它需要读取某个用户发送给另一个用户的消息,然后根据Message对象中的id信息获取到对应线程,继而获取到该线程持有的Socket,最后通过Socket将信息发送给另一个用户

                2° 代码实现

                客户端 : 

                在client.service包下新建一个MessageClientService类,用于管理消息代码如下 : 

package client.service;

import mutual.Message;
import mutual.MessageType;

import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 提供与消息有关的服务
 */
public class MessageClientService {
    /**
     * @param receiver : 消息的接收者
     * @param content : 消息内容
     * @param sender : 消息的发送者
     */
    public void sendMessageToOne(String receiver, String content, String sender) {
        Message message = new Message();

        message.setMesType(MessageType.MESSAGE_COMMON_MES);     //消息类型
        message.setReceiver(receiver);
        message.setContent(content);
        message.setSender(sender);
        message.setSendTime(new java.util.Date().toString());   //发送时间
        System.out.println(sender + " 对 " + receiver + " 说 \"" + content + "\"");

        try {
            //获取发送消息的用户的输出流对象,并将上面的消息发给服务端
            ObjectOutputStream oos =
                    new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

                在客户端的线程类中,增加一条else if语句,打印出服务端转发来的消息,如下图所示 : 

                最后,在客户端界面的相关部分(View类),调用MessageCilentService类的发送消息的方法,如下图所示 : 

                服务端 : 

                在服务端与“发消息用户”通讯的线程中(ServerConnectClientThread类中),通过Message对象的信息,获取服务端与“要接收消息的用户”通讯的线程,然后通过该线程获取要接受消息的用户的Socket以及其对应的对象处理流;最后将消息写入到该Socket对应的数据通道中,实现消息的转发,如下图所示 : 

                3° 运行测试 

                如下GIF图所示 : 

        2.群发消息 : 

                群发消息,就是在私发消息的基础上,在服务端遍历在线用户列表;然后将自己排序后,把Message对象转发给其他所有的在线用户。这里要对MessageType接口进行扩充,增加一个消息类型,如下图所示 : 

                1° 客户端 

                在MessageClientService类中定义一个群发消息的方法sendMessageToAll,代码如下 : 

    public void sendMessageToAll(String content, String sender) {
        Message message = new Message();

        message.setMesType(MessageType.MESSAGE_COMMON_MES_ALL);     //消息类型
        message.setContent(content);                                //消息内容
        message.setSender(sender);                                  //消息发送者
        message.setSendTime(new java.util.Date().toString());       //消息发送时间
        System.out.println(sender + " 对所有在线的👴们说 \"" + content + "\"");

        try {
            //获取发送消息的用户的输出流对象,并将上面的消息发给服务端
            ClientConnectServiceThread clientConnectServiceThread = ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
            ObjectOutputStream oos = new ObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

                然后在View类中的相应区域调用该方法,如下图所示 : 

                接着,在客户端的线程类中增加一个else if 的判断,用于接收来自服务端转发的群发消息并显示在控制台,如下图所示 : 

                2° 服务端 

                首先在管理线程的类中,定义一个可以返回hashMap的方法,如下图所示 : 

                然后,在服务端的线程类中,新增一个else if 的判断,如果判断Message类型是群发类型,就遍历集合,实现群发。如下图所示 : 

                3° 运行测试 

                如下GIF图 :  


六、传输文件

        1.思路分析 : 

                发送文件与发送消息原理类似,都是以Message对象为载体;只不过发送文件时,Message对象中的内容是一个保存了图片的字节数组了
                对于客户端——
                先把要发送的文件读取到客户端(字节数组);然后把文件对应的字节数组封装到Message对象中;最后将Message对象发送给服务端;当然,客户端还需要接收来自服务端转发过来的Message对象,并将其中的文件内容保存到磁盘
                对于服务端——
                服务端接收到来自某一个用户发来的Message对象后,要进行拆包,获取到具体的接收者,然后实现转发即可

                还需要对MessageType接口进行扩充,如下图所示 : 

                对Message类进行扩充,如下图所示 : 

                提供一些新的属性以及它们对应的getter, setter方法。  

        2.客户端 : 

                在client.service包下新定义一个FileClientService类,用于文件发送功能的实现,FileClientService类代码如下 : 

package client.service;

import mutual.Message;
import mutual.MessageType;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 实现发送文件相关的功能
 */
public class FileClientService {

    /**
     * @param souPath : 数据源文件路径
     * @param desPath : 目的地文件路径
     * @param sender : 发送者(ID)
     * @param receiver : 接收者(ID)
     */
    public void setFileToOne(String souPath, String desPath, String sender, String receiver) {
        Message message = new Message();

        message.setMesType(MessageType.MESSAGE_FILE_TRANSMISSION);
        message.setSouPath(souPath);
        message.setDesPath(desPath);
        message.setSender(sender);
        message.setReceiver(receiver);

        //1.读取文件
        /*
            利用File类的length方法(获取当前文件的大小,以字节计算),
            可以得知要创建的字节数组的大小;
            因为length方法的返回值是long类型,所以此处需要类型强转。
         */
        byte[] file = new byte[(int)new File(souPath).length()];

        //创建一个输入流
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(souPath);
            fileInputStream.read(file);     //将file文件读取到字节数组中。

            //将文件包装到Message对象
            message.setFile(file);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //关闭输入流
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        //2.提示信息
        System.out.println("\n" + sender + " 给 " + receiver + " 发送 " + souPath +
                "到对方电脑的目录" + desPath + "下...");

        //3.发送文件
        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());

            oos.writeObject(message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

                然后,还要在客户端的线程类中扩展一个else if 的判断语句,若接收到的Message对象为服务端转发来的文件消息,就将其读取并保存到本地磁盘中。如下图所示 : 

                最后在View类中创建FileClientService对象,如下图所示 : 

                然后再对应的部分调用该对象的方法,如下图所示: 

        3.服务端 : 

                服务端还是老样子,在服务端的线程中增加一个else if 的Message类型判断,如果判断为发送文件的Message对象,就和私发消息一样给转发一下就OK了。如下图所示 : 
                截图没截到的部分,就是之前的老样子——先通过线程管理类的得到线程的方法,根据接收者的id获取对应的线程;然后在根据获得的线程,获取其持有的Socket对象,最后再获取与该Socket对象相关联的输出流

        4.运行测试 : 

                如下GIF图演示 : 


七、最终代码

        1.客户端 : 

                1° Viem

package client.menu;

import client.service.FileClientService;
import client.service.MessageClientService;
import client.service.UserClientService;

import java.io.IOException;
import java.util.Scanner;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 菜单界面的显示
 * @PS : Run -> Edit Configurations -> Modify options -> allow multiple instances
 */
public class View {
    private boolean loop = true;        //控制是否显示菜单
    private String key = "";            //接收用户的键盘输入
    private static Scanner sc = new Scanner(System.in); //静态扫描仪

    /*
        将userClientService对象置为属性,
        该对象用于执行用户登录/注册等操作(该步骤将功能与界面联系起来)
     */
    private UserClientService userClientService = new UserClientService();

    /*
        将messageClientService对象置为属性,
        该对象用于消息的管理(该步骤将功能与界面联系起来)
    */
    private MessageClientService messageClientService = new MessageClientService();

    /*
        将fileClientService对象置为属性,
        该对象用于文件的发送(该步骤将功能与界面联系起来)
    */
    private FileClientService fileClientService = new FileClientService();

    public static void main(String[] args) throws IOException {
        new View().mainMenu();
        System.out.println("客户端退出...");
        sc.close();
    }

    private void mainMenu() throws IOException {
        while (loop) {
            System.out.println("===========Welcome to the system of chat:===========");
            System.out.println("\t\t1.登录系统");
            System.out.println("\t\t9.退出系统");


            System.out.print("请输入你的选择:");
            key = sc.nextLine();

            switch (key) {
                case "1" :
                    //登录操作
                    System.out.print("请输入用户名:");
                    String userID = sc.nextLine();
                    System.out.print("请输入密  码:");
                    String password = sc.nextLine();

                    //验证登录的用户是否合法(封装思想)
                    if (userClientService.check(userID, password)) {     //验证成功
                        System.out.println("\n===========Welcome user " + userID + "===========");
                        //向用户显示二级菜单
                        while (loop) {
                            System.out.println("\n===========网络通信系统二级菜单(user:" + userID + ")===========");
                            System.out.println("\t\t1.在线列表:");
                            System.out.println("\t\t2.群发消息:");
                            System.out.println("\t\t3.私发消息:");
                            System.out.println("\t\t4.文件发送:");
                            System.out.println("\t\t9.退出系统:");

                            System.out.print("请输入你的选择:");
                            key = sc.nextLine();

                            switch (key) {
                                case "1" :
                                    userClientService.onlineList();
                                    break;
                                case "2" :
                                    System.out.println("请输入你要对大家说的话:");
                                    String announcement = sc.nextLine();
                                    //调用群发消息的方法
                                    messageClientService.sendMessageToAll(announcement, userID);
                                    break;
                                case "3" :
                                    System.out.print("请输入你想聊天的对象(在线),receiver = ");
                                    String receiver = sc.nextLine();
                                    System.out.print("请输入你要说的话: ");
                                    String content = sc.nextLine();
                                    //调用私发消息的方法
                                    messageClientService.sendMessageToOne(receiver, content, userID);
                                    break;
                                case "4" :
                                    System.out.print("请输入你想发送文件的对象(在线),receiver = ");
                                    String fileReceiver = sc.nextLine();
                                    System.out.print("请输入数据源文件的路径, souPath = ");
                                    String souPath = sc.nextLine();
                                    System.out.print("请输入目的地文件的路径, desPath = ");
                                    String desPath = sc.nextLine();
                                    fileClientService.setFileToOne(souPath, desPath, userID, fileReceiver);
                                    break;
                                case "9" :
                                    userClientService.logout();
                                    loop = false;   //在二级菜单中用户也可以直接选择退出系统
                                    break;
                            }
                        }
                    } else {        //验证失败
                        System.out.println("登录失败!请重新尝试!");
                    }

                    break;
                case "9" :
                    loop = false;       //将控制while循环的布尔变量设置为false
                    break;
            }
        }
    }
}

                2° UserClientService

package client.service;

import mutual.Message;
import mutual.MessageType;
import mutual.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 登录验证
 */
public class UserClientService {
    /*
        将User对象设置成一个属性,可利用getter和setter修改User对象的引用,便于操作。
        Socket对象同样也可能在其他类中使用,因此也设置为属性。
     */
    private User user = new User();
    private Socket socket;

    /** check方法可以向服务端发起用户登录的验证 */
    public boolean check(String userID, String password) throws IOException {
        //局部变量
        boolean b = false;
        //初始化User对象
        user.setId(userID);
        user.setPwd(password);

        //向服务端发送信息
        try {
            //1.获取Socket对象
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 8888);
            //2.获取与Socket对象相关联的对象处理流(输出流)
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            //3.序列化User对象,写入数据通道(向服务端发送一个User对象,服务端会对这个User对象进行验证)
            oos.writeObject(user);
            //.........

            //4.获取与Socket对象相关联的对象处理流(输入流)
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            //5.读取服务端传输过来的Message对象
            Message message = (Message) ois.readObject();   //类型强转

            if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)) {
                //创建线程对象(目的是为了与服务端保持通讯)
                ClientConnectServiceThread ccst = new ClientConnectServiceThread(socket);
                //启动线程
                ccst.start();
                //将线程放入集合中统一管理
                ControlClientConnectServiceThread.addClientConnectServiceThread(userID, ccst);
                b = true;
            } else {
                //如果没有启动线程,关闭Socket对象。
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return b;
    }

    /** onlineList方法可以向服务端请求拉取在线列表 */
    public void onlineList() {
        //向服务端发送一个Message对象,类型是MESSAGE_GET_ONLINE_FRIENDS
        Message message = new Message();
        message.setSender(user.getId());    //用户登录时已在check方法中设置了id的值,所以可直接用
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIENDS);

        try {
            //得到当前线程持有的Socket对象对应的对象处理流(输出流)
            ObjectOutputStream oos =
                    new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
            oos.writeObject(message);   //向服务端发送“拉取在线用户列表”的请求
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /** logout方法可以退出当前用户 */
    public void logout() {
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(user.getId());    //指定具体要退出的客户端

        //发送Message对象
        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
            oos.writeObject(message);

            ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).setLoop(false);
            System.out.println(user.getId() + " 退出系统...");
            System.exit(0);     //0表示正常退出当前“进程”。
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

                3° MessageClientService

package client.service;

import mutual.Message;
import mutual.MessageType;

import java.io.ObjectOutputStream;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 提供与消息有关的服务
 */
public class MessageClientService {
    /**
     * @param receiver : 消息的接收者
     * @param content : 消息内容
     * @param sender : 消息的发送者
     */
    public void sendMessageToOne(String receiver, String content, String sender) {
        Message message = new Message();

        message.setMesType(MessageType.MESSAGE_COMMON_MES);     //消息类型
        message.setReceiver(receiver);                          //消息接收者
        message.setContent(content);                            //消息内容
        message.setSender(sender);                              //消息发送者
        message.setSendTime(new java.util.Date().toString());   //发送时间
        System.out.println(sender + " 对 " + receiver + " 说 \"" + content + "\"");

        try {
            //获取发送消息的用户的输出流对象,并将上面的消息发给服务端
            ClientConnectServiceThread clientConnectServiceThread = ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
            ObjectOutputStream oos = new ObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @param content : 群发消息的内容
     * @param sender : 群发消息的发送者
     */
    public void sendMessageToAll(String content, String sender) {
        Message message = new Message();

        message.setMesType(MessageType.MESSAGE_COMMON_MES_ALL);     //消息类型
        message.setContent(content);                                //消息内容
        message.setSender(sender);                                  //消息发送者
        message.setSendTime(new java.util.Date().toString());       //消息发送时间
        System.out.println(sender + " 对所有在线的👴们说 \"" + content + "\"");

        try {
            //获取发送消息的用户的输出流对象,并将上面的消息发给服务端
            ClientConnectServiceThread clientConnectServiceThread = ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
            ObjectOutputStream oos = new ObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

                4° FileClientService

package client.service;

import mutual.Message;
import mutual.MessageType;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 实现发送文件相关的功能
 */
public class FileClientService {

    /**
     * @param souPath : 数据源文件路径
     * @param desPath : 目的地文件路径
     * @param sender : 发送者(ID)
     * @param receiver : 接收者(ID)
     */
    public void setFileToOne(String souPath, String desPath, String sender, String receiver) {
        Message message = new Message();

        message.setMesType(MessageType.MESSAGE_FILE_TRANSMISSION);
        message.setSouPath(souPath);
        message.setDesPath(desPath);
        message.setSender(sender);
        message.setReceiver(receiver);

        //1.读取文件
        /*
            利用File类的length方法(获取当前文件的大小,以字节计算),
            可以得知要创建的字节数组的大小;
            因为length方法的返回值是long类型,所以此处需要类型强转。
         */
        byte[] file = new byte[(int)new File(souPath).length()];

        //创建一个输入流
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(souPath);
            fileInputStream.read(file);     //将file文件读取到字节数组中。

            //将文件包装到Message对象
            message.setFile(file);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //关闭输入流
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        //2.提示信息
        System.out.println("\n" + sender + " 给 " + receiver + " 发送 " + souPath +
                " 到对方电脑的目录 " + desPath + " 下...");

        //3.发送文件
        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());

            oos.writeObject(message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

                5° ClientConnectServiceThread

package client.service;

import mutual.Message;
import mutual.MessageType;

import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 客户端用于和服务端进行通讯的线程
 */
public class ClientConnectServiceThread extends Thread {
    //该线程需要持有Socket对象
    private Socket socket;
    private boolean loop = true;

    public ClientConnectServiceThread(Socket socket) {
        this.socket = socket;
    }

    public Socket getSocket() {
        return socket;
    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }

    @Override
    public void run() {
        //∵Thread需要在后台与服务器通信,因此使用while循环
        while (loop) {
            try {
                System.out.println("客户端线程,等待读取来自服务器端的消息...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                /*
                    如果服务端没有发送Message对象到数据通道中,线程就会阻塞在这里。
                 */
                Message message = (Message) ois.readObject();

                /**
                 判断客户端读取到的Message的类型,并做出相应的业务处理。
                 */
                if (message.getMesType().equals(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS)) {
                    //若Message的类型是返回的在线用户列表,取出在线列表并显示,使用空格分隔不同用户的id
                    String[] onlineUsers = message.getContent().split(" ");
                    System.out.println("\n===========在线用户列表如下:===========");
                    for (int i = 0; i < onlineUsers.length; i++) {
                        System.out.println("用户: " + onlineUsers[i]);
                    }
                } else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES_ALL)) {
                    System.out.println("\n" + message.getSender() + " 对所有在线的👴说 \"" +
                            message.getContent() + "\"");
                } else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES)) {
                    System.out.println("\n" + message.getSender() + " 对 " +
                            message.getReceiver() + " 说 \"" + message.getContent() + "\"");
                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_TRANSMISSION)) {
                    System.out.println("\n" + message.getSender() + " 给 " + message.getReceiver() + " 发送 " + message.getSouPath() +
                            " 到对方电脑的目录 " + message.getDesPath() + "下...");
                    FileOutputStream fileOutputStream = new FileOutputStream(message.getDesPath());
                    fileOutputStream.write(message.getFile());
                    fileOutputStream.close();
                    System.out.println("\n保存文件成功!");
                } else {
                    System.out.println("...other content");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

                6° ControlClientConnectServiceThread

package client.service;

import java.util.HashMap;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 管理客户端的线程
 */
public class ControlClientConnectServiceThread {
    /*
        使用HashMap类来管理多个线程(模拟数据库),key表示用户的id,value表示线程。
     */
    private static HashMap<String, ClientConnectServiceThread> hashMap = new HashMap<>();

    //添加线程的方法
    public static void addClientConnectServiceThread(String userID, ClientConnectServiceThread ccst) {
        hashMap.put(userID, ccst);
    }

    //取出线程的方法
    public static ClientConnectServiceThread getClientConnectServiceThread(String userID) {
        return hashMap.get(userID);
    }
}

        2.服务端 : 

                1° ChatServer

package server.service;

import mutual.Message;
import mutual.MessageType;
import mutual.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 服务端
 */
public class ChatServer {
    //将ServerSocket设置为属性,写在main函数外
    private ServerSocket serverSocket = null;

    /**
        将合法的用户放入集合中(使用“id + user”的泛型),
        建议使用ConcurrentHashMap集合,线程同步,可在多线程程序下安全使用。
     */
    private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();
    static {    //在静态代码块中初始化validUsers集合对象
        validUsers.put("Cyan", new User("Cyan", "RA9"));
        validUsers.put("Rain", new User("Rain", "flo"));
        validUsers.put("Ice", new User("Ice", "ais"));
        validUsers.put("Five", new User("Five", "55555"));
        validUsers.put("Kyrie", new User("Kyrie", "lrving"));
    }

    public boolean checkUser(String userID, String password) {
        User user = validUsers.get(userID);
        if (user == null) { //如果合法用户集合中不存在当前用户,直接返回false;
            return false;
        }
        if (!(user.getPwd().equals(password))) {    //如果存在该用户,但密码错误,返回false;
            return false;
        }

        return true;
    }
    public ChatServer() {
        //端口也可以写在配置文件中
        try {
            System.out.println("服务端正在8888端口监听...");
            serverSocket = new ServerSocket(8888);

            /*
                监听是不间断的,当服务端和某个客户端建立连接后,服务端会继续监听。
             */
            while (true) {
                //获取Socket类对象(服务端是通过accept方法来获取Socket对象的)
                Socket socket = serverSocket.accept();
                //获取Socket对象关联的输入流与输出流(对象处理流)
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());

                //客户端第一次传过来的是User对象
                User user = (User) ois.readObject();
                //暂时以单用户登录为例(id = Cyan, pwd = RA9)
                //创建一个Message对象,用于回复客户端是否连接成功(Message对象写在if-else语句外)
                Message message = new Message();

                if (checkUser(user.getId(), user.getPwd())) {   //登录成功
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
                    //将包含“登录成功与否”信息的Message对象写入数据通道
                    oos.writeObject(message);
                    //创建一个线程,与客户端保持通讯
                    ServerConnectClientThread scct = new ServerConnectClientThread(socket, user.getId());
                    //启动线程
                    scct.start();
                    //将线程放入集合中
                    ControlServerConnectClientThread.addServerConnectClientThread(user.getId(), scct);
                } else {    //登录失败
                    System.out.println("id = " + user.getId() + ",pwd = " + user.getPwd() + " 验证失败!");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    socket.close(); //关闭Socket
                }
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //若退出while循环,说明服务端不再监听,需要关闭ServerSocket对象
            try {
                serverSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

                2° ServerConnectClientThread

package server.service;

import mutual.Message;
import mutual.MessageType;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 服务端的线程,用于和客户端保持通讯
 */
public class ServerConnectClientThread extends Thread {
    private Socket socket;
    private String userID;  //当前连接到服务端的用户的id

    public ServerConnectClientThread(Socket socket, String userID) {
        this.socket = socket;
        this.userID = userID;
    }

    public Socket getSocket() {
        return socket;
    }

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("服务端与客户端" + userID + "保持通讯,读取数据中...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

                Message message = (Message) ois.readObject();

                /**
                 判断服务端读取到的Message的类型,并做出相应的业务处理。
                 */
                if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIENDS)) {
                    System.out.println(message.getSender() + " 请求拉取在线用户列表。");
                    String onlineUsers = ControlServerConnectClientThread.getOnlineFriends();

                    //构建Message对象,将获取到的在线用户列表的信息发送给客户端
                    Message message2 = new Message();
                    message2.setMesType(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS);
                    message2.setContent(onlineUsers);
                    message2.setReceiver(message.getSender());  //发送者 ——> 接收者
                    /*
                        对象处理流写在相应业务里面,各是各的,各用各的,不易冲突。
                     */
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(message2);
                } else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
                    //删除负责与当前用户通信的线程
                    System.out.println(message.getSender() + " 退出...");
                    Thread.sleep(500);
                    ControlServerConnectClientThread.removeServerConnectClientThread(userID);
                    //关闭Socket(若忽略此步骤,客户端无异常退出,服务端仍然依次。
                    socket.close();
                    //退出while循环
                    break;
                } else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES_ALL)) {
                    //遍历管理线程的集合
                    HashMap<String, ServerConnectClientThread> hashMap = ControlServerConnectClientThread.getHashMap();

                    Iterator<String> iterator = hashMap.keySet().iterator();
                    while (iterator.hasNext()) {
                        String onlUser = iterator.next();
                        //排序自己
                        if (!onlUser.equals(message.getSender())) {
                            ObjectOutputStream oos =
                                    new ObjectOutputStream(hashMap.get(onlUser).getSocket().getOutputStream());
                            oos.writeObject(message);
                        }
                    }
                } else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES)) {
                    /*
                        根据message对象中的receiver信息,获取对应的线程;
                        进而获取线程持有的Socket,以及与该Socket相关联的对象处理流,
                        利用对象处理流将信息发送给另一个用户
                     */
                    ObjectOutputStream oos =
                            new ObjectOutputStream(ControlServerConnectClientThread.getServerConnectClientThread(message.getReceiver()).getSocket().getOutputStream());
                    oos.writeObject(message);   //转发(注意:要使用正确的输出流)
                    /*
                        拓展 : 如果用户不在线,可以将消息保存到数据库,实现离线留言/离线发文件。
                     */
                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_TRANSMISSION)) {
                    ObjectOutputStream oos =
                            new ObjectOutputStream(ControlServerConnectClientThread.getServerConnectClientThread(message.getReceiver()).getSocket().getOutputStream());
                    oos.writeObject(message);
                } else {
                    System.out.println("...other content");
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

                3° ControlServerConnectClientThread

package server.service;

import java.util.HashMap;
import java.util.Iterator;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @function : 用于管理服务端的线程
 */
public class ControlServerConnectClientThread {
    private static HashMap<String, ServerConnectClientThread> hashMap = new HashMap<>();

    public static HashMap<String, ServerConnectClientThread> getHashMap() {
        return hashMap;
    }

    //添加线程到集合中
    public static void addServerConnectClientThread(String userID, ServerConnectClientThread scct) {
        hashMap.put(userID, scct);
    }

    //根据用户的id获取对应的线程
    public static ServerConnectClientThread getServerConnectClientThread(String userID) {
        return hashMap.get(userID);
    }

    //获取在线用户列表
    public static String getOnlineFriends() {
        /*
            利用hashMap集合中的key是用户id的特点,可以对hashMap对象进行遍历,从而获取用户列表。
         */
        Iterator<String> iterator = hashMap.keySet().iterator();
        String onlineUsers = "";

        while (iterator.hasNext()) {
            onlineUsers += iterator.next() + " ";   //加空格对应客户端的split方法。
        }

        return onlineUsers;
    }

    //删除指定线程
    public static void removeServerConnectClientThread(String userID) {
        hashMap.remove(userID);
    }
}

                4° ChatFrame

package frame;

import server.service.ChatServer;

public class ChatFrame {
    public static void main(String[] args) {
        new ChatServer();
    }
}

        3.公共部分 : 

                1° Message

package mutual;

import java.io.Serializable;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @message : information that are transmitted
 */
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;    //增强兼容性
    private String sendTime;    //发送时间
    private String sender;      //发送者
    private String receiver;    //接收者
    private String content;     //消息内容
    private String mesType;     //消息类型
    //与文件相关的属性
    private byte[] file;        //文件
    private int fileLen;        //文件大小
    private String souPath;     //数据源文件路径
    private String desPath;     //目的地文件路径

    public byte[] getFile() {
        return file;
    }

    public void setFile(byte[] file) {
        this.file = file;
    }

    public int getFileLen() {
        return fileLen;
    }

    public void setFileLen(int fileLen) {
        this.fileLen = fileLen;
    }

    public String getSouPath() {
        return souPath;
    }

    public void setSouPath(String souPath) {
        this.souPath = souPath;
    }

    public String getDesPath() {
        return desPath;
    }

    public void setDesPath(String desPath) {
        this.desPath = desPath;
    }

    public String getSendTime() {
        return sendTime;
    }

    public void setSendTime(String sendTime) {
        this.sendTime = sendTime;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public String getReceiver() {
        return receiver;
    }

    public void setReceiver(String receiver) {
        this.receiver = receiver;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getMesType() {
        return mesType;
    }

    public void setMesType(String mesType) {
        this.mesType = mesType;
    }
}

                2° MessageType

package mutual;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 * @meaning : Types of message
 */
public interface MessageType {
    //定义常量
    String MESSAGE_LOGIN_SUCCESS = "1";     //表示登录成功
    String MESSAGE_LOGIN_FAIL = "0";        //表示登录失败
    String MESSAGE_COMMON_MES = "2";                //表示普通信息包
    String MESSAGE_COMMON_MES_ALL = "6";            //表示群发的信息包
    String MESSAGE_GET_ONLINE_FRIENDS = "3";        //表示请求拉取在线用户的列表
    String MESSAGE_RETURN_ONLINE_FRIENDS = "4";     //表示返回在线用户的列表
    String MESSAGE_CLIENT_EXIT = "5";               //表示客户端请求退出系统
    String MESSAGE_FILE_TRANSMISSION = "8";         //表示文件传输
}

                3° User

                User类并无改动,准备工作中的User类,即时最终的User类。

        System.out.println("END-------------------------------------------------------------------------------");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/480108.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

探索深度学习世界:掌握PyTorch,成为AI领域的行家

探索深度学习世界&#xff1a;掌握PyTorch&#xff0c;成为AI领域的行家 PyTorch的背景介绍PyTorch的基本概念与特点PyTorch的基本应用张量和自动求导神经网络搭建训练和测试模型 模型的保存和加载模型保存&#xff1a;模型加载&#xff1a;模型使用&#xff1a; PyTorch与其他…

【KVM虚拟化】· 命令行KVM安装linux

目录 &#x1f341;基础本环境配置 &#x1f341;添加lvm卷 &#x1f341;qemu-img创建磁盘文件 &#x1f342;创建raw格式 &#x1f342;创建虚拟机 &#x1f342;转换格式为qcow2 &#x1f341;virt-install命令参数 &#x1f341;案例操作 &#x1f990;博客主页&#xff1a…

【C++】 小项目---宠物小屋的分析设计与开发实现

目录 需求 分析设计 动物类 笼子类 房子类 人类 小贴士 整体设计图 开发实现 动物类 笼子类 房子类 人类 小贴士 控制台主函数 需求 动物猫&#xff08;CCat&#xff09;、狗&#xff08;CDog&#xff09;、蛇&#xff08;CSnake&#xff09;&#xff0c;包含名字&…

对折纸张厚度超过珠峰

对折 0.1 毫米的纸张&#xff0c;循环对折&#xff0c;超过珠峰高度输出对折次数。 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&#xff0c;不仅仅是基础那么简单…… 地址&a…

【计算机网络】学习笔记:第六章 应用层【王道考研】

基于本人观看学习b站王道计算机网络课程所做的笔记&#xff0c;不作任何获利 仅进行交流分享 特此鸣谢王道考研 若有侵权请联系&#xff0c;立删 如果本篇笔记帮助到了你&#xff0c;还请点赞 关注 支持一下 ♡>&#x16966;<)!! 主页专栏有更多&#xff0c;如有疑问欢迎…

每天一道算法练习题--Day17 第一章 --算法专题 --- ----------布隆过滤器

场景 假设你现在要处理这样一个问题&#xff0c;你有一个网站并且拥有很多访客&#xff0c;每当有用户访问时&#xff0c;你想知道这个 ip 是不是第一次访问你的网站。 hashtable 可以么 一个显而易见的答案是将所有的 IP 用 hashtable 存起来&#xff0c;每次访问都去 hash…

ARM架构基本理论(1)

ARM架构基本理论 一、ARM的简介 ARM&#xff08;Advanced RISC Machine&#xff09;是一种基于RISC&#xff08;Reduced Instruction Set Computing&#xff09;架构的计算机处理器架构&#xff0c;由ARM Holdings&#xff08;ARM公司&#xff09;开发和授权给其他公司生产和…

【C++开发】基于QT+sqlite3的医疗管理系统

文章目录 前言数据库设计主要实现的功能病人列表页面病人信息页信息录入页面信息修改页面 & 信息查看页面 总结 前言 本次做的这个项目是医疗管理系统&#xff0c;是根据需求所定制的&#xff0c;因此只面向个人本地的使用。 本项目是本人在完全0基础的情况下边学边…

14-2-进程间通信-FIFO

一、命名管道FIFO 1.作用范围 对于命名管道FIFO&#xff0c;它可以在不相关的进程间也能相互通信。 2.命名管道可左右用于不相关进程的原因 因为命令管道&#xff0c;提前创建了一个类型为管道的设备文件&#xff0c;在进程里只要使用这个设备文件&#xff0c;就可以相互通信…

14-1-进程间通信-pipe

一.无名管道pipe 特点&#xff1a; (1)半双工&#xff08;在同一时刻&#xff0c;数据传输的方向只能是接收或发送&#xff09;&#xff1b; 例如&#xff1a;对于主机和从机而言&#xff0c;在某一时刻&#xff0c;只能是主机发送&#xff08;从机接收&#xff09;&#xff0…

CentOS+nginx手动搭建WordPress

文章目录 前提条件php安装安装 EPEL 源及源管理工具&#xff1a;安装 REMI 源&#xff1a;安装 PHP7.3 及扩展&#xff1a;设置开机自动启动其他php命令 wordpress 安装下载WordPress将下载的WordPress移动至网站根目录修改WordPress配置文件配置nginx 创建完成后根据域名访问 …

分析linux中动态库so文件的常用方法

前言 在linux系统中&#xff0c;我们经常会遇到各种各样的动态库文件&#xff0c;常见的是.so后缀&#xff0c;那么我们应该如何分析这些文件的用途和作用呢&#xff1f;毕竟我们不能一知半解的“搞事情”。 正文 查看文件属性 首先&#xff0c;我们从整体上了解一下该文件的基…

maven-gpg-plugin gpg禁用交互式输入密码 免密码输入 设置默认密码 关闭pinentry-qt输入 passphrase

一、问题描述 在使用maven-gpg-plugin打包jar时,默认情况下&#xff0c;每次都会弹出对话框要你输入密码&#xff1a; 这就有点烦&#xff0c;有啥办法可以设置默认方法没&#xff1f;网上找了一圈&#xff0c;通过搜索关键词“passphrase”&#xff0c;找到了一些教程&#x…

无损以太网与网络拥塞管理(PFC、ECN)

无损以太网 无损以太网&#xff08;Lossless Ethernet&#xff09;是一种专门用于数据中心网络的网络技术&#xff0c;旨在提供低延迟、高吞吐量和可靠性的传输服务。它是在传统以太网的基础上进行了扩展&#xff0c;引入了新的拥塞管理机制&#xff0c;以避免数据包丢失和网络…

探索【Stable-Diffusion WEBUI】的插件:ControlNet 1.1

文章目录 &#xff08;零&#xff09;前言&#xff08;二&#xff09;ControlNet&#xff08;v1.1&#xff09;&#xff08;2.1&#xff09;模型&#xff08;2.2&#xff09;新版界面&#xff08;2.3&#xff09;预处理器 &#xff08;三&#xff09;偷懒 &#xff08;零&#…

机器学习与深度学习——通过决策树算法分类鸢尾花数据集iris求出错误率画出决策树并进行可视化

什么是决策树&#xff1f; 决策树是一种常用的机器学习算法&#xff0c;它可以对数据集进行分类或回归分析。决策树的结构类似于一棵树&#xff0c;由节点和边组成。每个节点代表一个特征或属性&#xff0c;每个边代表一个判断或决策。从根节点开始&#xff0c;根据特征的不同…

list的使用介绍---C++

一、list简介 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个元素和后一个元素。…

计算机网络第一章(谢希仁第8版学习)

作者&#xff1a;爱塔居 专栏&#xff1a;计算机网络 作者简介&#xff1a;大三学生&#xff0c;希望和大家一起加油 文章目录 目录 文章目录 一、网络、互连网、互联网&#xff08;因特网&#xff09;的概念 二、因特网的组成 三、交换方式 3.1 电路交换 3.2 分组交换 3.3 电路…

详解c++---list介绍

目录标题 list介绍list定义list遍历list数据插入push_backpush_frontinsert list删除pop_backpop_fronterase list排序list去重list合并list转移list其他函数emptysizefrontbackassignswapresizeclear list排序效率问题 list介绍 list是可以在常数范围内在任意位置进行插入和删…

【Java笔试强训 22】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、选择题 二、编程题 &#x1f525;小易的升…