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

文章目录

  • 一、多用户通信系统
    • 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; //用户idprivate 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的,但是没有线程,即登录失败,不能启动和服务器通信的线程//关闭socketsocket.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循环来控制*/@Overridepublic 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;//连接到服务端的这个用户idprivate Socket socket;/*** 线程处于run状态,可以发送或者接收客户端的消息*/@Overridepublic 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_FRIENDMessage 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循环来控制*/@Overridepublic 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;//连接到服务端的这个用户idprivate Socket socket;/*** 线程处于run状态,可以发送或者接收客户端的消息*/@Overridepublic 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的keyIterator<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;//连接到服务端的这个用户idprivate Socket socket;/*** 线程处于run状态,可以发送或者接收客户端的消息*/@Overridepublic 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);//关闭socketsocket.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.mzph.cn/news/206167.shtml

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

相关文章

【面试经典150 | 二叉树】从前序与中序遍历序列构造二叉树

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;递归 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等内容…

Jest与typescript单元测试

文章目录 前言安装配置.vscode/launch.jsonjest.config.tsts.config.ts 测试例子 前言 简单记录一下vscode里跑Jest单元测试。 安装 yarn add -D ts-jest ts-node types/jest jest 配置 .vscode/launch.json {"version": "0.2.0","configurations…

通过异步序列化提高图表性能 Diagramming for WPF

通过异步序列化提高图表性能 2023 年 12 月 6 日 MindFusion.Diagramming for WPF 4.0.0 添加了异步加载和保存文件的功能&#xff0c;从而提高了响应能力。 MindFusion.Diagramming for WPF 提供了一个全面的工具集&#xff0c;用于创建各种图表&#xff0c;包括组织结构图、图…

【Docker二】docker网络模式、网络通信、数据管理

目录 一、docker网络模式&#xff1a; 1、概述 2、docker网络实现原理&#xff1a; 3、docker的网络模式&#xff1a; 3.1、bridge模式&#xff1a; 3.2、host模式&#xff1a; 3.3、container模式&#xff1a; 3.4、none模式&#xff1a; 3.5、自定义网络模式&#xf…

shell命令学习(1)——(待完善)

explainshell.com shell统计当前文件夹下的文件个数、目录个数Linux之shell常用命令&#xff08;三&#xff09; sort&#xff08;排序&#xff09;、uniq&#xff08;处理重复字符&#xff09; linux中shell将换行输入到文件中 shell脚本&#xff0c;将多行内容写入文件中 f…

使用Python实现轮盘赌选择法Roulette Wheel Selection Method in Python

一、引言 最近在手写遗传算法&#xff0c;想尝试解决一些优化问题。然而&#xff0c;在编码的过程中&#xff0c;自己发现了很多都不懂的问题。比如&#xff0c;交叉的操作&#xff0c;有单点交叉、两点交叉和多点交叉&#xff0c;具体选哪一种会更好呢&#xff1f;未知。还有交…

读者和写者问题

它可以解决的问题&#xff1a; 可以支持多个读者访问&#xff0c;通过count计数 来实现多个读者访问的时候是互斥的&#xff0c;不会出现不符合进程同步的问题&#xff1a;设置mutex互斥锁&#xff0c;保证count或count--和if Pv(mutex)是一气呵成的 读写公平&#xff0c;通过…

C#大型LIS检验信息系统项目源码

LIS系统&#xff0c;一套医院检验科信息系统。它是以数据库为核心&#xff0c;将实验仪器与电脑连接成网&#xff0c;基础功能包括病人样本登录、实验数据存取、报告审核、打印分发等。除基础功能外&#xff0c;实验数据统计分析、质量控制管理、人员权限管理、试剂出入库等功能…

【深度学习】迁移学习中的领域转移及迁移学习的分类

领域转移 根据分布移位发生的具体部分&#xff0c;域移位可分为三种类型&#xff0c;包括协变量移位、先验移位和概念移位 协变量移位: 在协变量移位的情况下&#xff0c;源域和目标域的边际分布是不同的&#xff0c;即ps(x)∕ pt(x)&#xff0c;而给定x的y的后验分布在域之间…

SAP UI5 walkthrough step2 Bootstrap

我的理解&#xff0c;这就是一个引导指令 1.我们右键打开命令行--执行 ui5 use OpenUI5 2.执行命令&#xff1a;ui5 add sap.ui.core sap.m themelib_sap_horizon 执行完之后&#xff0c;会更新 yaml 文件 3.修改index.html <!DOCTYPE html> <html> <head&…

AR + 通信,虚实结合让工作协同从线上到「现场」

在数字经济无所不在的当下&#xff0c;千行百业都与数智化办公接轨并因其实现转型升级。关注【融云 RongCloud】&#xff0c;了解协同办公平台更多干货。 升级的背后&#xff0c;是利用技术把工作用更自然的方式连接起来&#xff0c;让整个工作流协同更顺、体验更好。 而其中…

小白备战蓝桥杯:Java常用API

目录 一、什么是API 二、API帮助文档的使用 三、String String中的成员方法都不会修改原字符串 String是啥&#xff1f; String常见构造方法 equals&#xff1a;字符串比较&#xff08;区分大小写&#xff09;​编辑 equalsIgnoreCase&#xff1a;字符串比较&#xff0…

【接口自动化测试入门】从0到1落地接口自动化测试!

前段时间写了一系列自动化测试相关的文章&#xff0c;当然更多的是方法和解决问题的思路角度去阐述我的一些观点。这篇文章来聊聊新手如何从零到一落地实践接口自动化测试。 为什么要做接口测试 测试理念的演变 早些时候&#xff0c;软件研发交付流程大多遵循V型或W型的瀑布…

Java se之类和对象

目录 类的定义格式如何去自定义this的引用如何初始化对象构造方法的定义和使用 类的定义格式 class ClassName{ //属性(成员变量) //行为(成员方法) } 1>变量与方法 1.成员变量:普通成员变量 静态成员变量 2.成员方法:普通成员方法 静态成员方法 其中的静态变量与方法,在后…

文件同步及实现简单监控

1. 软件简介 rsync rsync 是一款开源的、快速的、多功能的、可实现全量及增量的本地或远程 数据同步备份的优秀工具。在同步备份数据时&#xff0c;默认情况下&#xff0c;Rsync 通过其 独特的“quick check”算法&#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;以便你…

学习spring、springmvc、mybatis、ssm所有可能用到的依赖总结,父工程pom文件依赖,<packaging>pom</packaging>

1、父工程pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/PO…

idea报错——Access denied for user ‘root‘@‘localhost‘ (using password: YES)

项目场景&#xff1a; 使用idea启动SpringBoot项目报错&#xff0c;可以根据提示看到是数据库的原因&#xff0c;显示使用了密码&#xff0c;具体报错信息如下&#xff1a; 解决方案&#xff1a; 第一步&#xff1a;先去配置文件里面查看连接MySQL的url是否正确&#xff0c;如果…

微前端 -- wujie 预加载和原理 无界传参

目录 wujie 预加载和原理 原理解析 MessageChannel 无界传参 1.全局变量 2.Props 3.event bus wujie 预加载和原理 提前把无界实例创建好 runPreload 赋值给sandbox.preload 预先加载好 startApp 判断是否有preload 需要从wujie的实例导出preloadApp,参数跟startApp 一…

frp内网穿透部署,轻松实现内网服务对外访问

FRP&#xff08;Fast Reverse Proxy&#xff09;是一种轻量级、高性能的反向代理工具&#xff0c;利用反向代理技术将公网请求转发至内网服务器上&#xff0c;并将内网服务器的响应再次转发至公网请求者。在实现内网穿透时&#xff0c;FRP能够将公网与内网之间的隔离突破&#…