一、项目设计的技术
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)用户登录验证模块
- 用户登录相关模块用
UserClientService
类 --> 编写新类 - 用户端在登陆时,会给服务端发送一个User对象,服务器端收到后会进行验证
- 然后服务端返回给用户端一个Message对象来告知用户端是否合法。
- 如果登录成功,就创建一个和服务器端保持通信的客户端线程 -> 创建一个类
ClientConnectServerThread
- 保持通信–>需要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
- 创建一个
ManageClientConnectServerThread
,使用HashMap管理线程 - 创建线程的地方调用addClientConnectServerThread方法,将线程添加到集合中
- 修改菜单中的检验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);
}