Java笔记(二十七):多线程即时通信系统【上】(需求分析、设计、功能实现--用户登录/拉取在线用户列表/无异常退出系统)

一、项目设计的技术

1 项目框架设计

2 Java面向对象编程

3 网络编程

4 多线程

5 IO流

6 Mysql

二、项目开发流程简介

需求分析→设计阶段→实现阶段→测试阶段→实施阶段→维护阶段。
在这里插入图片描述

三、需求分析

在这里插入图片描述

1、用户登录

在这里插入图片描述

2、拉取在线用户列表

在这里插入图片描述

3、无异常退出(客户端、服务端)

4、私聊

在这里插入图片描述

5、群聊

在这里插入图片描述

6、发文件

在这里插入图片描述

7、服务器推送新闻

在这里插入图片描述

四、通讯系统整体分析

在这里插入图片描述

五、系统代码设计流程

1、功能实现-用户登录

1)业务逻辑示意图

【客户端用户登录示意图】
在这里插入图片描述

【服务端用户登录示意图】
在这里插入图片描述

2)编写用户、消息对象类,并编写消息类型接口

User类

/**
 * 表示一个用户/客户信息
 */
public class User implements Serializable { //序列化:如果想要使用对象流,传输的内容如果有类,这个类必须序列化
    private static final long serialVersionUID = 1L; //增强类的兼容性(后续该类发生变化,原来已经序列化的旧信息是否能反序列化成新的类)

    private String userId; //用户id/用户名
    private String passwd; //用户密码

    public User(String userId, String passwd) {
        this.userId = userId;
        this.passwd = passwd;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

Message类


public class Message implements Serializable {
    private static final long serialVersionUID = 1l;
    private String sender; //发送者
    private String getter; //接收者
    private String content; //消息内容
    private String sendTime; //发送时间
    private String mesType; //消息类型【可以在接口定义消息类型】



    public Message(String sender, String getter, String content, String sendTime) {
        this.sender = sender;
        this.getter = getter;
        this.content = content;
        this.sendTime = sendTime;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getSender() {
        return sender;
    }

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

    public String getGetter() {
        return getter;
    }

    public void setGetter(String getter) {
        this.getter = getter;
    }

    public String getContent() {
        return content;
    }

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

    public String getSendTime() {
        return sendTime;
    }

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

    public String getMesType() {
        return mesType;
    }

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

MessageType接口

/**
 * 表示消息类型
 */
public interface MessageType {
    //1. 在接口中定义一些常量
    //2. 不同的常量的值,表示不同的消息类型
    String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功
    String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败
}

3)编写用户显示界面

Utility工具类

public class Utility {
    //静态属性。。。
    private static Scanner scanner = new Scanner(System.in);


    /**
     * 功能:读取键盘输入的一个菜单选项,值:1——5的范围
     * @return 1——5
     */
    public static char readMenuSelection() {
        char c;
        for (; ; ) {
            String str = readKeyBoard(1, false);//包含一个字符的字符串
            c = str.charAt(0);//将字符串转换成字符char类型
            if (c != '1' && c != '2' &&
                    c != '3' && c != '4' && c != '5') {
                System.out.print("选择错误,请重新输入:");
            } else break;
        }
        return c;
    }

    /**
     * 功能:读取键盘输入的一个字符
     * @return 一个字符
     */
    public static char readChar() {
        String str = readKeyBoard(1, false);//就是一个字符
        return str.charAt(0);
    }
    /**
     * 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符
     * @param defaultValue 指定的默认值
     * @return 默认值或输入的字符
     */

    public static char readChar(char defaultValue) {
        String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符
        return (str.length() == 0) ? defaultValue : str.charAt(0);
    }

    /**
     * 功能:读取键盘输入的整型,长度小于10位
     * @return 整数
     */
    public static int readInt() {
        int n;
        for (; ; ) {
            String str = readKeyBoard(10, false);//一个整数,长度<=10位
            try {
                n = Integer.parseInt(str);//将字符串转换成整数
                break;
            } catch (NumberFormatException e) {
                System.out.print("数字输入错误,请重新输入:");
            }
        }
        return n;
    }
    /**
     * 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数
     * @param defaultValue 指定的默认值
     * @return 整数或默认值
     */
    public static int readInt(int defaultValue) {
        int n;
        for (; ; ) {
            String str = readKeyBoard(10, true);
            if (str.equals("")) {
                return defaultValue;
            }

            //异常处理...
            try {
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
                System.out.print("数字输入错误,请重新输入:");
            }
        }
        return n;
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串
     * @param limit 限制的长度
     * @return 指定长度的字符串
     */

    public static String readString(int limit) {
        return readKeyBoard(limit, false);
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串
     * @param limit 限制的长度
     * @param defaultValue 指定的默认值
     * @return 指定长度的字符串
     */

    public static String readString(int limit, String defaultValue) {
        String str = readKeyBoard(limit, true);
        return str.equals("")? defaultValue : str;
    }


    /**
     * 功能:读取键盘输入的确认选项,Y或N
     * 将小的功能,封装到一个方法中.
     * @return Y或N
     */
    public static char readConfirmSelection() {
        System.out.println("请输入你的选择(Y/N): 请小心选择");
        char c;
        for (; ; ) {//无限循环
            //在这里,将接受到字符,转成了大写字母
            //y => Y n=>N
            String str = readKeyBoard(1, false).toUpperCase();
            c = str.charAt(0);
            if (c == 'Y' || c == 'N') {
                break;
            } else {
                System.out.print("选择错误,请重新输入:");
            }
        }
        return c;
    }

    /**
     * 功能: 读取一个字符串
     * @param limit 读取的长度
     * @param blankReturn 如果为true ,表示 可以读空字符串。
     * 					  如果为false表示 不能读空字符串。
     *
     *	如果输入为空,或者输入大于limit的长度,就会提示重新输入。
     * @return
     */
    private static String readKeyBoard(int limit, boolean blankReturn) {

        //定义了字符串
        String line = "";

        //scanner.hasNextLine() 判断有没有下一行
        while (scanner.hasNextLine()) {
            line = scanner.nextLine();//读取这一行

            //如果line.length=0, 即用户没有输入任何内容,直接回车
            if (line.length() == 0) {
                if (blankReturn) return line;//如果blankReturn=true,可以返回空串
                else continue; //如果blankReturn=false,不接受空串,必须输入内容
            }

            //如果用户输入的内容大于了 limit,就提示重写输入
            //如果用户如的内容 >0 <= limit ,我就接受
            if (line.length() < 1 || line.length() > limit) {
                System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:");
                continue;
            }
            break;
        }

        return line;
    }
}

QQview菜单显示类

import com.rxli.utils.Utility;

/**
 *用户界面,一级二级菜单
 */
public class QQView {

    private boolean loop = true;//控制是否显示菜单
    private String key = ""; //接受用户的键盘输入

    public static void main(String[] args) {
        new QQView().mainMenu();
        System.out.println("客户端退出系统...");
    }
    //显示主菜单
    private void mainMenu(){
        //进入一级菜单
        while (loop){
            System.out.println("=========欢迎登录网络通信系统=========");
            System.out.println("\t\t1 登录系统");
            System.out.println("\t\t9 退出系统");

            System.out.print("请输入你的选择:");
            key = Utility.readString(1); //接收字符串
            //根据用户的输入,处理不同的逻辑
            switch (key){
                case "1":
                    System.out.print("请输入用户号:");
                    String userId = Utility.readString(50);
                    System.out.print("请输入密  码:");
                    String pwd = Utility.readString(50);

                    //这里需要到服务端验证该用户是否合法(即是否存在该用户)
                    //...这里是校验用户是否合法的代码...

                    if(true){//假设合法
                        System.out.println("=========欢迎用户 "+userId+" 登录成功=========");

                        //进入二级菜单
                        while (loop){
                            System.out.println("=========网络通信系统二级菜单(用户 "+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 = Utility.readString(1);
                            switch (key){
                                case "1":
                                    System.out.println("显示在线用户列表");
                                    break;
                                case "2":
                                    System.out.println("群发消息");
                                    break;
                                case "3":
                                    System.out.println("私聊消息");
                                    break;
                                case "4":
                                    System.out.println("发送文件");
                                    break;
                                case "9":
                                    loop = false;
                                    //System.out.println("退出系统");
                                    break;
                            }
                        }
                    } else {//用户名或密码出错,校验失败-->登录服务器失败
                        System.out.println("========登陆失败========");//这里会继续第一层循环
                    }
                    break;
                case "9":
                    loop = false;
                    //System.out.println("退出系统");
                    break;

            }
        }
    }
}

4)用户登录验证模块

  1. 用户登录相关模块用 UserClientService 类 --> 编写新类
  2. 用户端在登陆时,会给服务端发送一个User对象,服务器端收到后会进行验证
  3. 然后服务端返回给用户端一个Message对象来告知用户端是否合法。
  4. 如果登录成功,就创建一个和服务器端保持通信的客户端线程 -> 创建一个类 ClientConnectServerThread
  5. 保持通信–>需要socket属性,且该类在run方法中不断监测服务端是否发来信息

User类添加一个无参构造器

public User(){}

UserClientService类

public class UserClientService {
    private User u = new User(); //因为我们可能在其他地方要使用user信息,方便调用,因此做成一个成员属性
    private Socket socket;       //因为我们可能在其他地方要使用socket信息,方便调用,因此做成一个成员属性

    /**
     * 根据 userId 和 pwd 到服务器验证该用户是否合法
     * @param userId
     * @param pwd
     * @return
     */
    public boolean checkUser(String userId, String pwd) {
        //创建User对象
        u.setUserId(userId);
        u.setPasswd(pwd);

        //连接到服务端,发送u对象
        boolean b = false;
        try {
            socket = new Socket(InetAddress.getByName("192.168.220.129"), 9999);

            //创建输出对象流,发送user对象
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(u);

            //创建输入对象流,接收服务端返回的message对象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message ms = (Message) ois.readObject();

            if(ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) { //登录成功
                b = true;

                //创建一个和服务器端保持通信的客户端线程 -> 创建一个类 ClientConnectServerThread
                ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);
                //启动客户端的线程
                clientConnectServerThread.start();
                //这里为了后面客户端的扩展,这里将线程放入集合中


            } else { //登录失败

            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return b;
    }
}

ClientConnectServerThread类

public class ClientConnectServerThread extends Thread{
    //该线程需要持有Socket(网络连接的插头)
    private Socket socket;

    //构造器->设置socket
    public ClientConnectServerThread(Socket socket) {
        this.socket = 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 ms = (Message) ois.readObject();

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

5)单个用户多线程管理

一个用户不同的服务也可以用不同的socket

  1. 创建一个ManageClientConnectServerThread ,使用HashMap管理线程
  2. 创建线程的地方调用addClientConnectServerThread方法,将线程添加到集合中
  3. 修改菜单中的检验UserClientService的checkUser方法
/**
 * 该类管理客户端连接到服务端的线程的类(单个用户多线程管理)
 */
public class ManageClientConnectServerThread {
    //我们把多个线程放入到一个 HashMap 集合中,key 就是 用户id,value 就是一线程
    private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();
    //将某个线程加入到集合
    public static void addClientConnectServerThread(String id, ClientConnectServerThread clientConnectServerThread){
        hm.put(id,clientConnectServerThread);
    }
    //通过userId,可以得到对应的线程
    public static ClientConnectServerThread getClientConnectServerThread(String id) {
        return hm.get(id);
    }
}

修改UserClientService

if(ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) { //登录成功

	//创建一个和服务器端保持通信的客户端线程 -> 创建一个类 ClientConnectServerThread
	ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);
	//启动客户端的线程
	clientConnectServerThread.start();
	//这里为了后面客户端的扩展,这里将线程放入集合中
	ManageClientConnectServerThread.addClientConnectServerThread(userId,clientConnectServerThread);
	
	b = true; //让逻辑代码执行完再执行跳出循环的代码

} else { //登录失败
   //如果登录失败,就不能启动和服务器通信的线程,但是socket开着,必须关闭socket
   socket.close();
}

修改QQView

//创建对象,设为属性
private UserClientService userClientService = new UserClientService(); //用于校验用户名和密码

//这里需要到服务端验证该用户是否合法(即是否存在该用户)
//编写一个类 UserClientService【用户登录服务:验证注册等】
if(userClientService.checkUser(userId,pwd)){//调用checkUser方法检验
System.out.println("=========欢迎用户 "+userId+" 登录成功=========");

6)服务端用户登录实现

在这里插入图片描述

QQserver

/**
 * 这是服务端,在监听9999,等待客户端的连接,并保持通信
 */
public class QQServer {
    private ServerSocket ss = null;
    //这里模拟数据库,存放合法用户集合,如果这些用户登录,就认为校验通过
    //这里也可以使用ConcurrentHashMap,可以处理并发的集合,没有线程安全问题
    //由于本项目没有涉及到修改用户信息,所以不用考虑线程安全问题
    private static HashMap<String , User> validUsers = new HashMap<>();

    static { //在静态代码块,初始化validUsers

        validUsers.put("100",new User("100","123456"));
        validUsers.put("200",new User("200","123456"));
        validUsers.put("300",new User("300","123456"));
        validUsers.put("至尊宝",new User("至尊宝","123456"));
        validUsers.put("紫霞仙子",new User("紫霞仙子","123456"));
        validUsers.put("菩提老祖",new User("菩提老祖","123456"));

    }

    public boolean checkUser(String userId, String passwd) {
        User user = validUsers.get(userId);
        if(user == null) { //说明userId没有存在在validUsers的key中
            return false;
        }
        if(!user.getPasswd().equals(passwd)) { //userId正确但是密码错误
            return false;
        }
        return true;
    }

    public QQServer() {
        //注意,端口可以写在配置文件
        try {
            ss = new ServerSocket(9999);
            System.out.println("服务端在9999端口监听...");

            while(true) { //当和某个客户端连接后,会继续监听,所以while
                Socket socket = ss.accept();
                //得到socket关联的对象输入流
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                User u = (User) ois.readObject(); //读取客户端发送的User对象
                //得到socket关联的对象输出流
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                //创建一个Message对象,准备回复客户端
                Message message = new Message();
                //验证
                if(checkUser(u.getUserId(),u.getPasswd())) { //登录成功
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                    //创建一个线程,和客户端保持通信,该线程需要持有socket对象
                    ServerConnectClientThread serverConnectClientThread =
                            new ServerConnectClientThread(socket, u.getUserId());
                    //启动线程
                    serverConnectClientThread.start();
                    //把该线程对象,放入到一个集合中,进行管理...
                    ManageServerConnectClientThread.addServerConnectClientThread(u.getUserId(),
                            serverConnectClientThread);

                } else { //登录失败
                    System.out.println("用户id=" + u.getUserId() + " pwd=" + u.getPasswd() + " 验证失败");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    //登录失败要关闭socket,不然这个socket没有意义
                    socket.close();
                }
                oos.writeObject(message);//无论验证是否通过,都要把message发给客户端
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //如果服务端推出了while,说明服务器端不再监听,因此需要关闭ServerSocket
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

ServerConnectClientThread类

/**
 * 该类的一个对象和某个客户端保持通信
 */
public class ServerConnectClientThread extends Thread{
    private Socket socket;
    private String userId; //连接到服务端的用户id

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

    @Override
    public void run() { //服务端线程处于run状态,可以发送/接收消息

        while(true) { //不断地连接(保持连接)
            System.out.println("服务端和客户端" + userId + "保持通信,读取数据...");
            try {
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject(); //接收客户端传来的Message对象
                //后面会在这里使用Message

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

ManageServerConnectClientThread类

/**
 * 管理服务端和客户端通信的线程
 */
public class ManageServerConnectClientThread {
    private static HashMap<String , ServerConnectClientThread> hm = new HashMap<>();

    //添加线程对象到hm集合
    public static void addServerConnectClientThread(String userId, ServerConnectClientThread scct) {
        hm.put(userId , scct);
    }

    //根据userId返回一个Ser
    public static ServerConnectClientThread getServerConnectClientThread(String userId) {
        return hm.get(userId);
    }
}

7)测试

让客户端能够同时运行两个及以上的进程(模拟多个客户端)
点击客户端启动类的右上角edit Configurations
在这里插入图片描述
点击Modify options
在这里插入图片描述
将Allow multiple instances勾选上
在这里插入图片描述

这样,每一次点运行按钮,都会开启一个新的客户端进程(只是运行窗口是并在一起的)
在这里插入图片描述

2、功能实现 - 拉取在线用户列表

规定在线用户列表形式:
在这里插入图片描述

1)业务逻辑示意图

【用户端业务逻辑示意图】
在这里插入图片描述

【服务端用户逻辑示意图】
在这里插入图片描述
【⭐】代码中的一个细节
在这里插入图片描述

2)MessageType拓展

服务端和客户端都要修改

/**
 * 表示消息类型
 */
public interface MessageType {
    //在接口中定义一些常量
    //不同的常量的值,表示不同的消息类型
    String MESSAGE_LOGIN_SUCCEED = "1";//表示登陆成功
    String MESSAGE_LOGIN_FAIL = "2";//表示登陆失败
    String MESSAGE_COMM_MES = "3"; //普通信息包
    String MESSAGE_GET_ONLINE_FRIEND = "4"; //(客户端)请求返回在线用户列表
    String MESSAGE_RET_ONLINE_FRIEND = "5"; //(服务端)返回在线用户列表
    String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出
}

3)客户端代码修改

QQview类修改

while (loop){
     //进入二级菜单,并循环显示
     //进入二级菜单之前休眠100ms,用于等待上一次客户端请求接收服务端传回来的数据
     //等上一次需要的数据显示出来,再显示下一次的二级菜单
     try {
         Thread.sleep(100);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     System.out.println("=========网络通信系统二级菜单(用户 "+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 = Utility.readString(1);
     switch (key){
         case "1":
             //在这里获得用户列表
             userClientService.onlineFriendList();
             break;

UserClientService类增加方法

 /**
   * 向服务器端请求在线用户列表
   */
  public void onlineFriendList() {
      //发送一个Message,类型MESSAGE_GET_ONLINE_FRIEND
      Message message = new Message();
      message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
      message.setSender(u.getUserId());

      //发送请求给服务器
      //应该得到当前线程socket对应的输出流
      try {
          //[1] socket直接从属性拿
          //因为调用onlineFriendList之前必须调用checkUser,而checkUser中对socket进行了赋值
          ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
          oos.writeObject(message);
      } catch (IOException e) {
          e.printStackTrace();
      }
   }

ClientConnectServerThread类修改run

    @Override
    public void run() {
        //因为Thread需要在后台和服务器一直保持连接(不断通信),因此我们while循环
        while(true) {

            try {
                System.out.println("【客户端线程】" + "等待从服务器端发过来的消息...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //如果管道中,服务器没有发送message对象,线程就会阻塞在这里
                Message message = (Message) ois.readObject();
                //这里处理接收到message后的代码
                //注意判断这个message类型,然后做相应的业务处理
                if(message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {
                    //取出在线列表信息,并显示
                    String[] onlineUsers = message.getContent().split(" "); //需要按照规定的返回信息格式划分
                    //在线用户列表形式:100 200 紫霞仙子 至尊宝
                    System.out.println("\n======当前在线用户列表=======");
                    for (int i = 0; i < onlineUsers.length; i++) {
                        System.out.println("用户: " + onlineUsers[i]);
                    }
                } else {
                    System.out.println("是其他类型的message,暂时不处理...");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

4)服务端代码修改

ServerConnectClientThread类修改run方法

    @Override
    public void run() { //服务端线程处于run状态,可以发送/接收消息

        while(true) { //不断地连接(保持连接)
            System.out.println("服务端和客户端" + userId + "保持通信,读取数据...");
            try {
                //获取输入流
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject(); //接收客户端传来的Message对象
                //判断客户申请类型
                String type = message.getMesType();

                //Type1.客户申请在线用户列表
                if(type.equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {
                    System.out.println(message.getSender() + "用户申请在线用户列表");
                    //调用方法获取
                    String onlineUser = ManageServerConnectClientThread.getOnlineUser();
                    //返回message
                    //构建一个message对象,返回给客户端
                    Message message2 = new Message();
                    message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
                    message2.setContent(onlineUser);
                    message2.setGetter(message.getSender()); //客户由发送端在此时变成接收端
                    //获取输出流,发送message2对象
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(message2);
                } else {
                    System.out.println("用户请求其他信息,待会儿处理...");
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

ManageServerConnectClientThread类增加方法

    //这里编写方法,可以返回在线用户列表
    public static String getOnlineUser() {
        //集合遍历,遍历HashMap的key
        Iterator<String> iterator = hm.keySet().iterator();
        String allOnlineUsers = "";
        while(iterator.hasNext()) {
            allOnlineUsers += iterator.next() + " "; //按照规定格式编写message内容
        }
        return allOnlineUsers;
    }

3、功能实现 - 无异常退出系统

1)问题

客户端情况
在这里插入图片描述
可以看到,当main线程退出时,和服务端通信的线程并没有结束(一直在run),所以客户端的整个进程就不会结束
进程不结束,程序也就没办法正常退出

2)解决方法

在这里插入图片描述
具体来说

客户端发送一个退出系统的message对象 —> 服务端接收到该对象 —> 把该客户端对应的线程关闭(1. 将对应线程从集合中移除 2. 关闭socket 3. 退出run中的while循环 )

客户端调用System.exit(0) —> 客户端进程退出(线程自然也就关了)

【⭐】服务端只是关闭了一个和客户端连接线程,其他线程(其他和另外没有退出进程的客户端 连接的线程)还是连着的 —> 服务端进程并没有关闭

【⭐】先将线程从集合中移除再关闭socket,因为有可能客户端还没有退出,服务端此时就关闭socket连接,那么客户端就会报错。把线程从集合中移除放在前面,给了客户端退出缓冲时间

3)客户端代码修改

QQview类,修改二级菜单

case "9":
    //调用方法,无异常退出程序
    userClientService.logout();
    loop = false;
    break;

UserClientService类新增logout方法

    /**
     * 给服务器发送一个退出程序message对象,自身再无异常退出程序
     */
    public void logout() {
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(u.getUserId()); //一定要指定是哪个客户端要退出程序,服务端才能正确关闭对应的线程
        try {
            //[1]通过属性拿到socket
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message);
            System.out.println(u.getUserId() + "用户退出系统...");
            //客户端自身退出进程
            System.exit(0);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

4)服务端代码修改

ServerConnectClientThread修改run方法

else if(type.equals(MessageType.MESSAGE_CLIENT_EXIT)) { //Type2:客户退出程序
    System.out.println("【服务端】检测到用户" + message.getSender() + "想要退出程序");
    //1. 线程从集合中移除(调用方法)
    ManageServerConnectClientThread.removeServerConnectClientThread(message.getSender());
    //2. 关闭对应服务端socket
    socket.close();
    //3. 结束run的while循环 --> 线程关闭
    break;
}

ManageServerConnectClientThread类增加remove…方法

//从集合中移除某个线程
public static void removeServerConnectClientThread(String userId) {
	hm.remove(userId);
}

5)测试

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值