2.1 网络编程-多用户通信系统(用户登录、拉取在线用户、无异常退出)

news2025/1/18 3:21:37

文章目录

  • 一、多用户通信系统
    • 1.1 介绍
    • 1.2 公共类
      • 1.2.1 封装消息类
      • 1.2.2 用户类
      • 1.2.3 消息类型类
      • 1.2.4 控制台读取内容
  • 二、用户登录
    • 2.1 客户端
      • 2.1.1 菜单界面 QQView
      • 2.1.2 验证用户UserClientService
      • 2.1.3 线程类 ClientConnectServerThread
      • 2.1.4 线程集合类
    • 2.2 服务端
      • 2.2.1 服务端构造器
      • 2.2.2 服务端
      • 2.2.3 线程类 ServerConnectClientThread
      • 2.2.4 线程集合
  • 三、拉取在线用户
    • 3.0 扩展类
    • 3.1 客户端
      • 3.1.1 UserClientService类
      • 3.1.2 ClientConnectServerThread线程类
    • 3.2 服务端
      • 3.2.1 ServerConnectClientThread类
      • 3.2.2 ManagerServerConnectServerThread类
    • 3.3 测试
  • 四、无异常退出系统
    • 4.1 分析
    • 4.2 客户端
      • 4.2.1 UserClientService 退出
    • 4.3 服务端
      • 4.3.1 ServerConnectClientThread 线程类
      • 4.3.2 ManagerServerConnectServerThread 线程集合类

一、多用户通信系统

1.1 介绍

需要技术

  • Java面向对象编程
  • 网络编程
  • 多线程
  • IO流
  • 数据集合

需求分析

  • 用户登录
  • 拉取在线用户列表
  • 无异常退出
  • 私聊
  • 群聊
  • 发文件
  • 服务器推送新闻

当客户端A和服务端建立连接后,两边都会建立一个Socket(也就是一边一个Socket)

当客户端B和服务端建立连接后,两边也都会建立一个Socket,此时服务端有两个socket(一个服务端A的,另一个是服务端B的)

我们在通讯的时候,怎么保证客户端的两个Socket一直被持有(占有)呢

我们启动一个socket,就启动了一个线程,通讯的其实是线程中的Socket

image-20231205170703638

假如客户端B写了一个数据到Socket里面希望群发一个消息,也就是希望获取到服务端中所有线程里的Socket,为此我们可以将服务端的Socket通过一个集合来管理(将来服务端的线程很多)

image-20231205171116026

除此之外,我们的客户端A可能与服务端有多个连接,比如一条连接是发送文本信息的,一条连接是发送文件的,一条信息是视频聊天的…此时服务端与客户端便是多个通道连接,一条通道连接很难把功能一次性实现,此时客户端A也需要一个管理线程的集合

image-20231205171653779

服务端的工作逻辑

  • 当有客户端连接到服务端后,会得到一个Socket
  • 启动一个线程,该线程会持有Socket对象(该Socket是线程的一个属性)
  • 为了将来更好的管理多个线程(以后会涉及到将消息推送给多个客户端),需要使用一个集合来管理

客户端的工作逻辑

  • 和服务端通信时,使用对象方式,可以使用对象流来读写
  • 当客户端连接到服务端后,也会得到socket,我们也会启动一个线程,并且该线程会持有此socket
  • 为了将来更好管理线程,需要使用一个集合来管理线程

1.2 公共类

1.2.1 封装消息类

/**
 * 封装消息
 * 表示客户端和服务端通信时的消息对象
 * 发送消息流程:客户端A -》 服务端 -》 客户端B ,假如服务器瘫痪,聊天便不可以使用
 * (如果客户端A与客户端B在同一个局域网 客户端A -》客户端B)
 */
@Data
public class Message implements Serializable {
    private static final long serialVersionUID = -3567747187962510012L;

    /**
     * 消息类型:发送文件、纯文本、视频聊天....
     */
    private String mesType;

    /*
     *发送者
     */
    private String sender;

    /**
     *接收者
     */
    private String getter;

    /**
     * 消息内容
     */
    private String content;

    /**
     * 发送时间
     */
    private String sendTime;

}

1.2.2 用户类

/**
 * 客户信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = 4300366482842276408L;
    private String userId; //用户id
    private String passwd; //用户密码
}

1.2.3 消息类型类

/**
 * 消息类型
 * 不同行亮的值表示不同的消息类型
 */
@Getter
public enum MessageType {
    /**
     * 登录成功
     */
    MESSAGE_LOGIN_SUCCEED("1"),
    /**
     * 登录失败
     */
    MESSAGE_LOGIN_FAIL("2");

    private final String code;


    MessageType(String code) {
        this.code = code;
    }



    public static String find(Integer code) {
        for (MessageType value : MessageType.values()) {
            if (code.toString().equals(value.getCode())) {
                return value.getCode();
            }
        }
        return null;
    }
}

1.2.4 控制台读取内容

public class Utility {

    private static Scanner scanner;

    static {
        scanner = new Scanner(System.in);
    }

    public Utility() {

    }

    public static char readMenuSelection() {
        while (true) {
            String str = readKeyBoard(1, false);
            char c = str.charAt(0);
            if (c == '1' || c == '2' || c == '3' || c == '4' || c == '5') {
                return c;
            }

            System.out.print("选择错误,请重新输入:");
        }
    }

    public static char readChar() {
        String str = readKeyBoard(1, false);
        return str.charAt(0);
    }

    public static char readChar(char defaultValue) {
        String str = readKeyBoard(1, true);
        return str.length() == 0 ? defaultValue : str.charAt(0);
    }

    public static int readInt() {
        while (true) {
            String str = readKeyBoard(2, false);
            try {
                int n = Integer.parseInt(str);
                return n;
            } catch (NumberFormatException var3) {
                System.out.println("数字输入错误,请重新输入:");
            }
        }
    }

    public static int readInt(int defaultValue) {
        while (true) {
            String str = readKeyBoard(2, true);
            if (str.equals("")) {
                return defaultValue;
            }

            try {
                int n = Integer.parseInt(str);
                return n;
            } catch (NumberFormatException var4) {
                System.out.print("数字输入错误,请重新输入:");
            }
        }
    }

    private static String readKeyBoard(int limit, boolean blankReturn) {
        String line = "";

        while (scanner.hasNextLine()) {
            line = scanner.nextLine();
            if (line.length() == 0) {
                if (blankReturn) {
                    return line;
                }
            } else {
                if (line.length() >= 1 && line.length() <= limit) {
                    break;
                }
                System.out.println("输入长度(不大于" + limit + ")错误,请重新输入:");
            }
        }
        return line;
    }
    public static String readString(int limit) {
        return readKeyBoard(limit, false);
    }



    public static char readConfirmSelection(){
        while (true){
            String str=readKeyBoard(1,false).toUpperCase();
            char c=str.charAt(0);
            if(c=='Y'||c=='N'){
                return c;
            }
            System.out.print("选择错误,请重新输入:");
        }
    }

}

二、用户登录

功能说明

暂时不使用数据库(后面使用HashMap模拟数据库,支持多个用户的登录)

人为规定用户名/id=100,密码123456便可以登录,其他用户不能登陆

信息的传递我们都以对象的形式来完成,将客户端和服务端交流的信息封装成对象,这便需要使用对象流

客户端向服务端发送一个User对象,服务器端拿到User对象信息以后进行验证User对象是否合法,然后服务端给客户端回复一个message对象

客户端拿到message对象之后我们可以判断登录成功了还是失败了

2.1 客户端

2.1.1 菜单界面 QQView

/**
 * 菜单界面
 */
public class QQView {

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

    /**
     * 完成用户登录验证和用户注册等功能
     */
    public UserClientService userClientService = new UserClientService();


    public static void main(String[] args) {
        QQView qqView = new QQView();
        qqView.mainMenu();
        System.out.println("退出客户端系统");
    }

    /**
     * 显示主菜单
     */
    private void mainMenu() {
        while (loop) {
            System.out.println("***********欢迎登录网络通信系统*************");
            System.out.println("\t\t 1 登录系统");
            System.out.println("\t\t 9 退出系统");
            System.out.print("请输入你的选择:");
            key = Utility.readString(1);

            //根据用户的输入来处理不同的逻辑
            switch (key) {
                case "1":
                    System.out.print("请输入用户号");
                    String userId = Utility.readString(50);
                    System.out.print("请输入密  码");
                    String password = Utility.readString(50);

                    //TODO 到服务端验证用户是否合法
                    if (userClientService.checkUser(userId,password)) {
                        //进入二级菜单
                        System.out.println(String.format("网络通信系统二级菜单(用户%s)", userId));
                        while (loop) {
                            System.out.println(String.format("\n========网络通信系统二级菜单(用户%s)===========", userId));
                            System.out.println("\t\t 1.显示在线用户列表");
                            System.out.println("\t\t 2.群发消息");
                            System.out.println("\t\t 3.私聊消息");
                            System.out.println("\t\t 4.发送文件");
                            System.out.println("\t\t 9.退出系统");

                            System.out.print("请输入你的选择:");
                            key = Utility.readString(1);
                            switch (key) {
                                case "1":
                                    break;
                                case "2":
                                    break;
                                case "3":
                                    break;
                                case "4":
                                    break;
                                case "9":
                                    loop = false;
                                    System.out.println("退出系统");
                                    break;
                            }
                        }
                    }else {
                        System.out.println("登录服务器失败,用户名或密码存在问题");
                    }
                    break;
                case "9":
                    loop = false;
                    System.out.println("退出系统");
            }
        }
    }
}

2.1.2 验证用户UserClientService

/**
 * 完成用户登录验证和用户注册等功能
 */
@Data
public class UserClientService {

    //其他地方也会使用user信息,所以将其作为一个属性
    private User user = new User();

    private Socket socket = null;

    //根据userId和pwd到服务器验证该用户是否合法
    public boolean checkUser(String userId, String pwd) {
        //临时变量b,用户是否合法的标志
        boolean b = false;

        //TODO 创建User对象
        user.setUserId(userId);
        user.setPasswd(pwd);

        try {
            //TODO 连接到服务端,发送User对象
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
            //得到ObjectOutputStream对象流(序列化流,也是字节流中一种)
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(user);
            oos.flush();

            //TODO 读取从服务器回复的Message对象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message msg = (Message) ois.readObject();

            if (MessageType.find(1).equals(msg.getMesType())) {

                //登录成功
                //一旦登录成功,我们需要启动一个线程维护或者持有此socket,保持此线程可以跟我们服务器端一直进行通信
                //不启动线程的话此Socket不好维护。如果我们有数据发送或者接收,我们可以从这个线程里面进行拉取
                //为什么将Socket放入一个线程中管理?
                // 1.如果不创建这个线程的话,一个客户端会有多个socket,socket管理起来就比较麻烦
                // 2.需要socket不断的从数据通道中读写数据,所以也必须做成一个线程
                ClientConnectServerThread ccst = new ClientConnectServerThread(socket);
                //启动客户端的线程
                ccst.start();
                //为了后面客户端的扩展,我们将线程放入到集合中管理
                ManagerClientConnectServerThread.addClientConnectServerThread(userId, ccst);

                b = true;
            } else {
                //登录失败
                //我们是有Socket的,但是没有线程,即登录失败,不能启动和服务器通信的线程
                //关闭socket
                socket.close();
            }

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        return b;
    }
}

2.1.3 线程类 ClientConnectServerThread

@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ClientConnectServerThread extends Thread {
    //该线程需要持有Socket属性
    private Socket socket;


    /**
     *因为Thread需要在后台跟我们的服务器进行通信(保持一个联系),因此我们使用while循环来控制
     */
    @Override
    public void run() {
        while(true){
            //一直读取从服务器端回收的消息
            System.out.println("客户端线程,等待读取从服务端发送的消息....");

            try {
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //如果服务器端没有发送消息过来,这个地方会堵塞,此线程会一直等待
                //这就是一个堵塞式网络编程,效率是相对比较低的
                Message message = (Message)ois.readObject();

            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

2.1.4 线程集合类

/**
 * 管理客户端连接到服务端线程的一个类
 */
public class ManagerClientConnectServerThread {
    //把多个线程放入一个HashMap中进行管理,key是用户id,value是客户端与服务端通信的线程
    private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();

    //将某个线程加入到集合中
    public static void addClientConnectServerThread(String userId, ClientConnectServerThread clientConnectServerThread) {
        hm.put(userId, clientConnectServerThread);
    }

    public static ClientConnectServerThread getClientConnectServerThread(String userId) {
        return hm.get(userId);
    }
}

2.2 服务端

2.2.1 服务端构造器

/**
 * 此类创建一个QQServer对象,启动后台的服务
 */
public class QQFrame {
    public static void main(String[] args) {
        //创建QQServer对象,会启动QQServer构造器
        QQServer qqServer = new QQServer();

    }
}

2.2.2 服务端

/**
 * 这是服务器,在监听9999,等待客户端的连接,并保持通信
 */
@Data
public class QQServer {

    //创建一个集合存放多个用户,如果是此用户登录,便认为是合法的
    //也可以使用ConcurrentHashMap,可以在并发的环境下处理(没有线程安全问题)
    //HashMap是没有处理线程安全的,因此在多线程情况下是不安全的
    private static HashMap<String,User> validUser = new HashMap<>();

    private ServerSocket serverSocket = null;

    /**
     * 进行类加载的时候会执行下面这个代码
     */
    static {
        validUser.put("100",new User("100","123456"));
        validUser.put("200",new User("200","123456"));
        validUser.put("300",new User("300","123456"));
        validUser.put("至尊宝",new User("至尊宝","123456"));
        validUser.put("紫霞仙子",new User("紫霞仙子","123456"));
        validUser.put("菩提老祖",new User("菩提老祖","123456"));
    }

    /**
     * 这是一个循环监听的过程
     * 并不是客户端A发送完信息服务器接收到后此服务器就关闭,而是一直监听,因为还有可能其他客户端发送过来信息
     */
    public QQServer() {
        System.out.println("服务端在9999端口监听....");
        ObjectInputStream ois = null;
        ObjectOutputStream oos = null;
        try {
            this.serverSocket = new ServerSocket(9999);

            //监听是一直进行,当和某个客户端连接后,会继续监听,因此使用while循环
            while (true) {
                //没有客户端连接9999端口时,程序会堵塞,等待连接
                Socket socket = serverSocket.accept();

                ois = new ObjectInputStream(socket.getInputStream());
                //如果服务器端没有发送消息过来,这个地方会堵塞,此线程会一直等待
                //读取客户端发送的User对象
                User user = (User) ois.readObject();

                //创建Message对象,准备恢复客户端
                Message message = new Message();
                oos = new ObjectOutputStream(socket.getOutputStream());
                //验证用户是否合法
                User userValid = validUser.get(user.getUserId());
                if (userValid!=null && userValid.getUserId().equals(user.getUserId()) && userValid.getPasswd().equals(user.getPasswd())) {
                    //合法用户
                    message.setMesType(MessageType.find(1));
                    //给客户端进行回复
//                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(message);
                    oos.flush();

                    //创建一个线程,和客户端保持通信。
                    //该线程需要持有Socket对象
                    ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(user.getUserId(), socket);
                    serverConnectClientThread.start();

                    //把该线程对象放入到一个集合中
                    ManagerServerConnectServerThread.addClientThread(user.getUserId(), serverConnectClientThread);

                } else {
                    //登录失败
                    message.setMesType(MessageType.find(2));
                    oos.writeObject(message);
                    oos.flush();

                    socket.close();
                }
            }


        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
//          如果服务端退出了while循环,说明服务器端不再监听了,因此需要关闭资源
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (ois !=null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (oos !=null){
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

2.2.3 线程类 ServerConnectClientThread

/**
 * 该类对应的对象和某个客户端保持通信
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServerConnectClientThread extends Thread{

    /**
     * 可以区分此socket是和哪个用户进行关联的
     */
    private String userId;//连接到服务端的这个用户id

    private Socket socket;

    /**
     * 线程处于run状态,可以发送或者接收客户端的消息
     */
    @Override
    public void run() {
        //不断的从socket中读数据和写数据
        while(true){
            System.out.println("服务端和客户端保持通信,读取数据.... userId:"+userId);
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(socket.getInputStream());
                //读取数据
                Message message = (Message) ois.readObject();

                //后面会使用Message

            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            //如果服务器端没有发送消息过来,这个地方会堵塞,此线程会一直等待
            //读取客户端发送的User对象

        }
    }
}

2.2.4 线程集合

/**
 * 该类用于管理和客户端通信的线程
 */
@Data
public class ManagerServerConnectServerThread {
    private static HashMap<String,ServerConnectClientThread> hm = new HashMap<>();

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

    /**
     *从集合中获取对应线程对象
     */
    public static ServerConnectClientThread getClientThread(String userId) {
        return hm.get(userId);
    }
}

三、拉取在线用户

可以将所有在线的列表拉下来

如果登录成功的话,客户端会有一个线程,服务端会有一个线程,两个线程都会持有自己的socket。

如果客户端要获得所有在线用户的列表,只能向服务器发送请求索要在线用户列表,因为只有服务器端才知道哪个用户上线了

实现这个功能其实就是客户端向服务端发送一个message对象,服务端会读取到这个message,看我们客户端想要什么东西(message中会封装消息的类型,此次请求的目的是什么),之后服务端会给客户端回复一个Message,并且会包含在线的用户列表

3.0 扩展类

/**
 * 消息类型
 * 不同常量的值表示不同的消息类型
 */
@Getter
public enum MessageType {
    /**
     * 登录成功
     */
    MESSAGE_LOGIN_SUCCEED("1"),
    /**
     * 登录失败
     */
    MESSAGE_LOGIN_FAIL("2"),

    /**
     * 普通信息对象
     */
    MESSAGE_COMM_MES("3"),

    /**
     * 获取在线用户
     * 要求服务器返回在线用户列表
     */
    MESSAGE_GET_ONLINE_FRIEND("4"),

    /**
     * 服务器返回在线用户列表
     */
    MESSAGE_RETTURN_ONLINE_FRIEND("5"),

    /**
     * 客户端请求退出
     */
    MESSAGE_CLIENT_EXIT("6"),

    ;

    private final String code;


    MessageType(String code) {
        this.code = code;
    }


    public static String find(Integer code) {
        for (MessageType value : MessageType.values()) {
            if (code.toString().equals(value.getCode())) {
                return value.getCode();
            }
        }
        return null;
    }
}

3.1 客户端

3.1.1 UserClientService类

向服务端发送消息

/**
 * 完成用户登录验证和用户注册等功能
 */
@Data
public class UserClientService {

    //其他地方也会使用user信息,所以将其作为一个属性
    private User user = new User();

    private Socket socket = null;

  
    /**
     * 向服务器端请求在线用户列表
     */
    public void onlineFriendList(){
        //发送一个message,并且消息的类型是MESSAGE_GET_ONLINE_FRIEND
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND.getCode());
        message.setSender(user.getUserId());
        //发送给服务器
        //得到当前线程的Socket对应的ObjectOutputStream
        //clientConnectServerThread线程一直在运行过程中,监听从服务器传输过来的消息
        ClientConnectServerThread clientConnectServerThread = ManagerClientConnectServerThread.getClientConnectServerThread(user.getUserId());
        try {

            ObjectOutputStream oos = new ObjectOutputStream(clientConnectServerThread.getSocket().getOutputStream());
            oos.writeObject(message);
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

3.1.2 ClientConnectServerThread线程类

处理服务端发送过来的消息

@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ClientConnectServerThread extends Thread {
    //该线程需要持有Socket属性
    private Socket socket;


    /**
     *因为Thread需要在后台跟我们的服务器进行通信(保持一个联系),因此我们使用while循环来控制
     */
    @Override
    public void run() {
        while(true){
            //一直读取从服务器端回收的消息
            System.out.println("客户端线程,等待读取从服务端发送的消息....");

            try {
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //如果服务器端没有发送消息过来,这个地方会堵塞,此线程会一直等待
                //这就是一个堵塞式网络编程,效率是相对比较低的
                Message message = (Message)ois.readObject();

                //判断message的类型,然后做响应的业务处理
                if (message.getMesType().equals(MessageType.MESSAGE_RETTURN_ONLINE_FRIEND.getCode())){
                    //获取在线用户,取出在线列表信息并显示
                    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("其他类型的message,暂时不处理");
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

3.2 服务端

3.2.1 ServerConnectClientThread类

ServerConnectClientThread类会不断的从客户端与服务端的通道中读取数据

/**
 * 该类对应的对象和某个客户端保持通信
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServerConnectClientThread extends Thread{

    /**
     * 可以区分此socket是和哪个用户进行关联的
     */
    private String userId;//连接到服务端的这个用户id

    private Socket socket;

    /**
     * 线程处于run状态,可以发送或者接收客户端的消息
     */
    @Override
    public void run() {
        //不断的从socket中读数据和写数据
        while(true){
            System.out.println("服务端和客户端保持通信,读取数据.... userId:"+userId);
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(socket.getInputStream());
                //读取数据
                Message message = (Message) ois.readObject();

                //根据Message的类型,判断客户端想要执行什么操作
                if (MessageType.MESSAGE_GET_ONLINE_FRIEND.getCode().equals(message.getMesType())){
                    //拉取在线用户(客户端要拉取在线用户列表)
                    Socket socket = ManagerServerConnectServerThread.getClientThread(userId).getSocket();

                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    //构建Message发送给服务端
                    Message returnMessage = new Message();
                    returnMessage.setMesType(MessageType.MESSAGE_RETTURN_ONLINE_FRIEND.getCode());
                    returnMessage.setContent(ManagerServerConnectServerThread.getOnlineUser());
                    //说明要发送给谁
                    returnMessage.setGetter(message.getSender());
                    //返回给客户端
                    oos.writeObject(returnMessage);
                    oos.flush();
                }else {
                    System.out.println("其他类型暂时不处理");
                }

            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            //如果服务器端没有发送消息过来,这个地方会堵塞,此线程会一直等待
            //读取客户端发送的User对象

        }
    }
}

3.2.2 ManagerServerConnectServerThread类

完成获取在线用户功能

/**
 * 该类用于管理和客户端通信的线程
 */
@Data
public class ManagerServerConnectServerThread {
    private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();

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

    /**
     * 从集合中获取对应线程对象
     */
    public static ServerConnectClientThread getClientThread(String userId) {
        return hm.get(userId);
    }

    /**
     * 获取在线用户
     */
    public static String getOnlineUser() {
        //集合遍历,遍历hashMap的key
        Iterator<String> iterator = hm.keySet().iterator();
        String onlineUserList = "";

        while (iterator.hasNext()) {
            onlineUserList += iterator.next().toString() + " ";
        }
        return onlineUserList;
    }

}

3.3 测试

客户端信息

image-20231207230423702

服务端信息

image-20231207230443055

四、无异常退出系统

4.1 分析

为什么要实现无异常退出

正常的情况下应该是下图的情况

客户端相当于一个进程,在进程中会有一个主线程main,在主线程main中又开了另外一个线程和服务端进行通信,此进程并循环的读取服务端发送过来的消息

image-20231207235055939

假如说我们的main线程结束了,但是我们和服务端通信的线程并没有结束,还是在进行等待接收服务器回传过来的消息,因此此线程没有结束,那此进程也不会结束

image-20231207235546663

然后就会出现下面的情况

提示已经退出系统,但是依然还在运行

image-20231207235616321

怎么解决这个问题

我们可以在主线程中调用一个方法,给服务器端发送一个退出系统的消息Message,然后调用System.exit(0)方法正常退出,直接会将整个进程挂掉

给服务器发送一个退出系统的消息Message有什么作用

服务器中会有一个对应的线程不断的读取从客户端发送过来的消息,如果我们发现客户端发送过来的消息是退出的消息,我们将socket关闭并退出线程就可以了

image-20231208000830892

4.2 客户端

4.2.1 UserClientService 退出

/**
 * 编写方法退出客户端,并给服务端发送一个退出系统的Message对象
 */
public void logout(){
    Message message = new Message();
    message.setMesType(MessageType.MESSAGE_CLIENT_EXIT.getCode());
    // 要退出这个用户
    message.setSender(user.getUserId());
    ClientConnectServerThread clientConnectServerThread = ManagerClientConnectServerThread.getClientConnectServerThread(user.getUserId());
    try {

        ObjectOutputStream oos = new ObjectOutputStream(clientConnectServerThread.getSocket().getOutputStream());
        oos.writeObject(message);
        oos.flush();
        
        System.exit(0);
        
    } catch (IOException e) {
        e.printStackTrace();
    }
}

4.3 服务端

4.3.1 ServerConnectClientThread 线程类

退出系统的使用一定要使用一个return或者break

假如说不使用的话while循环会一直进行,也会一直执行 Message message = (Message) ois.readObject();代码,由于客户端已经关闭,这个地方就会抛出大量的IO异常提示XX连接失败或者xxx已经关闭

/**
 * 该类对应的对象和某个客户端保持通信
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServerConnectClientThread extends Thread {

    /**
     * 可以区分此socket是和哪个用户进行关联的
     */
    private String userId;//连接到服务端的这个用户id

    private Socket socket;

    /**
     * 线程处于run状态,可以发送或者接收客户端的消息
     */
    @Override
    public void run() {
        //不断的从socket中读数据和写数据
        while (true) {
            System.out.println("服务端和客户端保持通信,读取数据.... userId:" + userId);
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(socket.getInputStream());
                //读取数据
                Message message = (Message) ois.readObject();

                //根据Message的类型,判断客户端想要执行什么操作
                if (MessageType.MESSAGE_GET_ONLINE_FRIEND.getCode().equals(message.getMesType())) {
                    System.out.println("用户" + userId + "获取在线用户");
                    //拉取在线用户(客户端要拉取在线用户列表)
                    Socket socket = ManagerServerConnectServerThread.getClientThread(userId).getSocket();

                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    //构建Message发送给服务端
                    Message returnMessage = new Message();
                    returnMessage.setMesType(MessageType.MESSAGE_RETTURN_ONLINE_FRIEND.getCode());
                    returnMessage.setContent(ManagerServerConnectServerThread.getOnlineUser());
                    //说明要发送给谁
                    returnMessage.setGetter(message.getSender());
                    //返回给客户端
                    oos.writeObject(returnMessage);
                    oos.flush();
                } else if (MessageType.MESSAGE_CLIENT_EXIT.getCode().equals(message.getMesType())) {
                    //说明客户端想要退出,服务端要将socket关闭并退出线程就可以了
                    //将客户端对应的线程从集合中删除
                    ManagerServerConnectServerThread.remove(userId);
                    //关闭socket
                    socket.close();
                    System.out.println("用户"+userId+"退出系统");
                    //退出循环
                    return;
                } else {
                    System.out.println("其他类型暂时不处理");
                }

            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            //如果服务器端没有发送消息过来,这个地方会堵塞,此线程会一直等待
            //读取客户端发送的User对象

        }
    }
}

4.3.2 ManagerServerConnectServerThread 线程集合类

/**
 * 从集合中删除掉某个线程对象
 */
public static void remove(String userId) {
   hm.remove(userId);
}

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

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

相关文章

C++ - 函数重载和运算符重载

文章目录 1.函数重载2.运算符重载 1.函数重载 函数重载 (Overload)是 C 语言在C语言基础上新增的功能。函数重载能够在程序中使用多个同名的函数。是C多态的特征体现。 通过函数重载来设计一系列的函数&#xff0c;它们完成相同或者相似的功能&#xff0c;但使用不同的参数列表…

el-select的多选multible带全选组件二次封装(vue2,elementUI)

1.需求 Select 选择器 多选需要增加 全选 和 取消全选 功能&#xff0c;前端框架为vue2&#xff0c;UI组件为elementUI。 2. 代码 html部分 <template><el-tooltip effect"dark" :disabled"defaultValue.length < 0" :content"defaul…

Java网络编程,使用UDP实现TCP(一), 基本实现三次握手

简介&#xff1a; 首先我们需要知道TCP传输和UDP传输的区别&#xff0c;UDP相当于只管发送不管对方是否接收到了&#xff0c;而TCP相当于打电话&#xff0c;需要进行3次握手&#xff0c;4次挥手&#xff0c;所以我们就需要在应用层上做一些功能添加&#xff0c;如&#xff1a;…

QT作业1

自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面 头文件代码&#xff1a; #ifndef MYWIDGET_H #define MYWIDGET_H#include <QWidget> #include <QIcon> #include <QLabel> //标签类 #include <QMovie> //动图类 #include <…

Linux操作系统一

一、Linux操作系统通俗认知 假设&#xff0c;我们现在正在做一家外包公司&#xff0c;我们的目标是把这家公司做上市。其中&#xff0c;操作系统就是这家外包公司的老板。我们可以把这家公司的发展阶段分为以下几个阶段&#xff1a; &#xff08;1&#xff09;初创阶段&#x…

查看Linux的Ubuntu的版本

我的Ubuntu版本是 Jammy x86_64&#xff0c;即 Ubuntu 22.04.3 LTS&#xff0c;代号为"Jammy Jellyfish"&#xff0c;架构是 x86_64&#xff08;64位&#xff09;。

微信小程序访问不了阿里云oss图片链接解决办法

以下都有可能导致访问不了oss图片 1.小程序没有加访问白名单 这个需要前端搞,加上白名单,如果是域名加域名白名单,ip的话加ip白名单 2.阿里云设置域名白名单 打开bucket列表,选择对应的bucket 配置这个白名单,配置好以后,开发者工具可以预览了,手机端预览不了,查看自己的路…

【遥感方向EI会议征稿中】第三届遥感与测绘国际学术会议(RSSM 2024)

第三届遥感与测绘国际学术会议&#xff08;RSSM 2024&#xff09; 2024 3rd International Conference on Remote Sensing, Surveying and Mapping 遥感与测绘技术&#xff0c;在全球变化、生态、环境、农、林、气象、人类活动等众多领域发挥了重要作用&#xff0c;受到世界各…

共创共赢|美创科技获江苏移动2023DICT生态合作“产品共创奖”

12月6日&#xff0c;以“5G江山蓝 算网融百业 数智创未来”为主题的中国移动江苏公司2023DICT合作伙伴大会在南京成功举办。来自行业领军企业、科研院所等DICT产业核心力量的百余家单位代表参加本次大会&#xff0c;共话数实融合新趋势&#xff0c;共拓合作发展新空间。 作为生…

1-2算法基础-常用库函数

1.排序 sort(first,last,cmp) first指向要排序范围的第一个元素&#xff0c;从0起 last指向要排序范围的最后一个元素的下一个位置 cmp&#xff08;可选&#xff09;&#xff0c;自定义函数&#xff0c;默认从小到大 评测系统 #include <iostream> #include<algorith…

KUKA机器人坐标点如何赋值?

KUKA机器人坐标点如何赋值? KUKA机器人系统中如何实现将某个点位整体赋值给另一个点位呢? 具体的方法可参考以下内容: 如下图所示,选中某个程序,然后点击下方的打开, 如下图所示,进入程序后,这里有P1和P2两个点位,如果要实现让P2的点位和P1的点位完全相同,除了通过示…

一文详解Java单元测试Junit

文章目录 概述、Junit框架快速入门单元测试概述main方法测试的问题junit单元测试框架优点&#xff1a;使用步骤&#xff1a; 使用案例包结构 Junit框架的常见注解测试 概述、Junit框架快速入门 单元测试概述 就是针对最小的功能单元&#xff08;方法&#xff09;&#xff0c;…

1146-table performance-schema.session_variables don‘t exits打卡navicat连接MySQL报错

navicat连接MySQL时报错&#xff1a; 管理员权限打开cmd 输入下面代码&#xff1a; mysql_upgrade -u root -p --force输入密码 然后就可以正常连接了。 mysql_upgrade检查所有数据库中与mysql服务器当前版本不兼容的所有表。 mysql_upgrade也会升级系统表&#xff0c;以便你…

用23种设计模式打造一个cocos creator的游戏框架----(一)生成器模式

1、模式标准 模式名称&#xff1a;生成器模式 模式分类&#xff1a;创建型 模式意图&#xff1a;将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 结构图&#xff1a; 适用于&#xff1a; 当创建复杂对象的算法应该独立于该对象的…

原生cesium、mars3d、supermap-cesium在vue3+vite中引入

1. 原生cesium 需要下载 yarn add cesiumyarn add vite-plugin-cesium2. mars3d 需要下载 yarn add mars3d mars3d-cesiumyarn add vite-plugin-mars3d3. supermap-cesium 只需要引入官网下载的包&#xff0c;build文件夹下的cesium&#xff0c;以及项目中引入的其他cesiu…

Densely Connected Convolutional Networks(2018.1)

文章目录 Abstract1. Introduction提出问题以前的解决方法我们的方法效果 2. Related Work3. DenseNetsResNets.Dense connectivity.Composite function.Pooling layers.Growth rate.Bottleneck layers.Compression.Implementation Details. 4. Experiments5. DiscussionModel …

C语言之动态内存管理(malloc calloc realloc)

C语言之动态内存管理 文章目录 C语言之动态内存管理1. 为什么要有动态内存管理2. malloc 和 free2.1 malloc2.2 free2.3 例子 3. calloc 和 realloc3.1 calloc3.2 realloc 4. 常见的动态内存错误4.1 对NULL指针的解引⽤操作4.2 对动态开辟空间的越界访问4.3 对⾮动态开辟内存使…

【ARM Trace32(劳特巴赫) 使用介绍 13 -- Trace32 变量篇】

文章目录 Trace32 查看变量值Var.view 查看变量值Var.view 查看数据类型的大小Var.view 根据变量地址查看变量值 Trace32 查看变量值 步骤1 步骤2 步骤3&#xff1a; 步骤4&#xff1a; 查看结构体变量 str_t32 的值 struct t32_str {uint32_t t32_val;uint32_t …

苹果手机ios系统安装了一个免签应用书签webclip描述文件该如何卸载?

随着移动应用的普及&#xff0c;越来越多的用户开始关注到苹果免签的应用。相比于需要通过 App Store 审核和签名的应用&#xff0c;免签应用无需经过苹果的审核过程&#xff0c;可以直接安装和使用。那么&#xff0c;苹果免签应用是如何制作的呢&#xff1f;本文将介绍制作苹果…