多用户即时通讯系统03

4.编码实现02

4.2功能实现-拉取在线用户


4.2.1思路分析

客户端想要知道在线用户列表,就要向服务器发送请求(Message),因为只有服务器端保持着所有与客户端相连接的socket和uid信息。

整个流程大致为:对Message的种类进行扩展,然后客户端向服务器发送一个Message,服务端向客户端返回数据,客户端接收信息(Message),在接收的这个Message信息里面包含了在线的用户列表。

4.2.2代码实现

1.客户端:
1.修改:MessageType接口

拓展了一些Message消息类型

package qqcommon;

/**
* @author 李
* @version 1.0
* 表示消息类型
*/
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";//客户端请求退出 }
2.修改:UserClientService类

在该类中添加onlineFriendList()方法,该方法向服务器发送要求,请求在线用户列表

    //先服务器端请求在线用户列表
public void onlineFriendList() {
//向服务器发送一个Message,类型MESSAGE_GET_ONLINE_FRIEND,要求返回在线用户列表
Message message = new Message();
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
message.setSender(u.getUserId()); //发送给服务器
try {
//从管理线程的集合里面,通过userId,得到这个线程对象
ClientConnectServerThread clientConnectServerThread =
ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
//通过这个线程中获取关联的socket
Socket socket = clientConnectServerThread.getSocket();
//得到当前线程的Socket对应的ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message);//发送一个Message对象向服务器,要求在线用户列表
} catch (IOException e) {
e.printStackTrace();
}
}
3.修改:ClientConnectServerThread类

在该类中的run方法中,增加判断Message的类型,然后做相应的业务处理

@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_RET_ONLINE_FRIEND)
if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {
//取出在线用户列表信息,并展示
//这里假定返回的用户列表是用空格隔开的id名(如:100 200 紫霞仙子 至尊宝 唐僧)
String[] onlineUsers = message.getContent().split(" ");
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.修改QQView类

修改该类中的第54行

使用上述编写的方法,完成拉取在线用户列表的操作

//这里写一个拉取用户在线列表的方法
userClientService.onlineFriendList();


2.服务端

大致思路为:前面我们在服务端的线程的run方法中,使用for循环在不停地请求读取数据,这里我们可以在run方法里面拓展功能,读取客户端发送过来的拉取用户的Message对象,返回在线用户列表

1.修改:MessageType接口

和客户端一样,拓展Message消息类型

package qqcommon;

/**
* @author 李
* @version 1.0
* 表示消息类型
*/
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";//客户端请求退出
}
2.修改:ServerConnectClientThread类

在该类中的run方法添加了客户请求拉取在线用户列表 的业务操作

package qqserver.server;

import qqcommon.Message;
import qqcommon.MessageType; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket; /**
* @author 李
* @version 1.0
* 该类的一个对象和某个客户端保持通信
*/
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) {
try {
System.out.println("服务端和客户端" + userId + "保持通信,读取数据...");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject(); //后面会使用message,根据message的类型,做相应的业务处理 //业务-:客户请求拉取在线用户列表
if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {
//客户请求拉取在线用户列表
//假定返回的用户列表是用空格隔开的id名(如:100 200 紫霞仙子 至尊宝 唐僧)
System.out.println(message.getSender()+" 要在线用户列表");
String onlineUser = ManageClientThreads.getOnlineUser(); //返回message
//构建一个Message对象(这个Message对象包含了在线用户列表信息),返回给客户端
Message message2 = new Message();
//设置消息类型--返回的在线用户列表类型-客户端会根据返回的消息类型来进行相应的业务处理
message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
message2.setContent(onlineUser);//返回用户消息列表
//服务器发送的消息的接收者Getter 就是服务器接收的信息 的发送者Sender
message2.setGetter(message.getSender()); //返回给客户端
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message2); }else{
System.out.println("其他类型的message,暂时不处理");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3.修改:ManageClientThreads类

在该类中增加了getOnlineUser()方法,用来遍历取出userId,并在上面的ServerConnectClientThread的run方法中使用。

//这里编写方法,可以返回在线用户列表
public static String getOnlineUser() {
//遍历集合,遍历 hashmap 的 key
Iterator<String> iterator = hm.keySet().iterator();//获取hm集合的ketSet集合的迭代器(这里的ketSet就是userId)
String onlineUserList = "";
while (iterator.hasNext()) {//遍历
onlineUserList += iterator.next().toString() + " ";//遍历所有的userId,用空格拼接起来
}
return onlineUserList;
}

运行:

  1. 运行服务端:

  1. 分别运行三个客户端,并进行登录,拉取在线用户:

    ​ 用户1:


​ 用户2:


​ 用户3:

此时服务端显示:

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

4.3.1思路分析

上述代码运行时,在客户端选择退出系统的时候,可以发现程序并没有停止运行,原因是:

退出时,程序将循环标志loop设为false,退出了内层循环,而外层循环因为也用了loop来作为循环条件,外层循环也同样退出。此时在客户端 类QQView中的主线程已经结束,但是在循环过程中,因为与服务端连接而产生的线程并没有结束,整个进程也就没有结束,因此程序仍在运行中。

解决方法:

客户端:在main线程中调用方法,给服务端发送一个退出系统的message对象,然后调用System.exit(0)指令,正常退出。这样整个进程就可以关闭。

服务器端:在服务器这边,接收到一个退出系统的message对象后,把这个客户端对应的线程所持有的socket关闭,然后退出该线程

4.3.2代码实现

1.客户端:
1.修改:UserClientService类

在该类中增加logout()方法

//编写方法,退出客户端,并给服务器端发送一个退出系统的message对象
public void logout(){
Message message = new Message();
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender(u.getUserId());//一定要指定是那个客户端,服务端要根据这个userId移除集合中的线程 //发送message
try {
//从管理线程的集合里面,通过userId,得到这个线程对象
ClientConnectServerThread clientConnectServerThread =
ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
//通过这个线程中获取关联的socket
Socket socket = clientConnectServerThread.getSocket();
//得到当前线程的Socket对应的ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message);
System.out.println(u.getUserId()+"退出系统");
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
}
}
2.修改:QQView类

在该类中的内层循环中,调用logout方法:

//调用方法,给服务器发送一个退出系统的message
userClientService.logout();

2.服务端:
1.修改:ManageClientThreads类

在该类中增加removeServerConnectClientThread()方法

//增加一个方法,从集合中移除某个对象
public static void removeServerConnectClientThread(String userId){
hm.remove(userId);
}
2.修改:ServerConnectClientThread类

在该类的run方法中增加业务二操作:

public void run() {//这里线程处于run的状态,可以发送/接收消息

    while (true) {
try {
System.out.println("服务端和客户端" + userId + "保持通信,读取数据...");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject(); //后面会使用message,根据message的类型,做相应的业务处理 //业务一:客户请求拉取在线用户列表
if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {
//客户请求拉取在线用户列表
//假定返回的用户列表是用空格隔开的id名(如:100 200 紫霞仙子 至尊宝 唐僧)
System.out.println(message.getSender() + " 要在线用户列表");
String onlineUser = ManageClientThreads.getOnlineUser(); //返回message
//构建一个Message对象(这个Message对象包含了在线用户列表信息),返回给客户端
Message message2 = new Message();
//设置消息类型--返回的在线用户列表类型-客户端会根据返回的消息类型来进行相应的业务处理
message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
message2.setContent(onlineUser);//返回用户消息列表
//服务器发送的消息的接收者Getter 就是服务器接收的信息 的发送者Sender
message2.setGetter(message.getSender()); //返回给客户端
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message2); } else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
//业务二:客户请求退出系统
System.out.println(message.getSender() + " 退出");
//将客户端对应的线程从集合中删除
ManageClientThreads.removeServerConnectClientThread(message.getSender());
socket.close();//关闭的是当前的线程持有的socket属性
//退出线程的循环
break;
} else {
System.out.println("其他类型的message,暂时不处理");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行:

1.运行服务端:

2.运行客户端,登录两个用户:


3.查看当前用户列表,可以看到有两个用户:

3.其中一个用户选择退出系统,可以看到用户正确退出,程序结束运行:

4.在另一个用户中查看当前用户列表,可以看到只剩下一个用户,说明服务端已经成功将退出的用户的线程从集合中删除

4.服务端这边显示该用户正确退出:

day03-代码实现02的更多相关文章

  1. 【grunt第二弹】30分钟学会使用grunt打包前端代码(02)

    前言 上一篇博客,我们简单的介绍了grunt的使用,一些基础点没能覆盖,我们今天有必要看看一些基础知识 [grunt第一弹]30分钟学会使用grunt打包前端代码 配置任务/grunt.initCon ...

  2. 活代码LINQ——02

    一.复习基础——属性与实例变量 'Fig. 4.8:GradeBookTest.vb 'Create and manipulate a GradeBook object. Module GradeBo ...

  3. win7 64位 asp+access 数据库连接出错[代码:02],请检查数据库链接文件中的连接字串

    解决办法,启用32位应用程序改为true 打开“Internet 信息服务(IIS)管理器”,在最右边的窗口中点击“应用程序池”,在用到的应用程序池上点击右键,选择“高级设置”

  4. javascript优化--09模式(代码复用)02

    原型继承 ://现代无类继承模式 基本代码: var parent = { name : "Papa" } var child = object(parent); function ...

  5. [linux][c/c++]代码片段02

    gcc `pkg-config --cflags gtk+-3.0` -o example-1 example-1.c `pkg-config --libs gtk+-3.0` #include &l ...

  6. Design Patterns | 02 什么样的代码是好代码

    目录 01 - 什么是好的代码? 02 - 评价代码的标准有哪些 2.1 可维护性(maintainability) 2.2 可读性(readability) 2.3 可扩展性(extensibili ...

  7. ReactNative新手学习之路02第一个RN项目

    开始第一个RN项目(iOS版)我的电影列表0.1版,后面做列表版 打开上一节项目 index.ios.js,android打开index.android.js.我这里使用的是Atom编辑器,你也可以使 ...

  8. Java中静态代码块,代码块,构造方法优先级、区别及代码示例

    在项目中遇到了代码块的知识点,跑了下测试,写下结论 代码优先级:静态代码块 -> 构造代码块 -> 构造方法 多个代码块优先级,按照“先定义的代码先执行,后定义的代码后执行”原则执行 静态 ...

  9. 用代码来细说Csrf漏洞危害以及防御

    开头: 废话不多说,直接进主题. 0x01 CSRF介绍:CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session ...

  10. HOOK大法实现不修改程序代码给程序添加功能

    [文章标题]: HOOK大法实现不修改程序代码给程序添加功能[文章作者]: 0x18c0[软件名称]: Scylla[使用工具]: OD.Stub_PE.ResHacker[版权声明]: 本文原创于0 ...

随机推荐

  1. 30 天精通 Git 版本控管

    https://github.com/doggy8088/Learn-Git-in-30-days

  2. Android - match_parent 和 fill_parent差异

    Android - match_parent 和 fill_parent差异 本文地址: http://blog.csdn.net/caroline_wendy match_parent 和 fill ...

  3. 蓝色的成长记录——追逐DBA(8):为了夺回SP报告,回顾oracle的STATSPACK实验

    ***********************************************声明*************************************************** ...

  4. .NET Core 网络数据采集 -- 使用AngleSharp做html解析

    有这么一本Python的书: <<Python 网络数据采集>> 我准备用.NET Core及第三方库实现里面所有的例子. 这是第一部分, 主要使用的是AngleSharp:  ...

  5. 开源纯C#工控网关+组态软件(二)工控网关的实现

    一.   工控网关是什么 网关是物联网和工控系统的核心组件.网关起的是承上启下的作用.上即上位机,电脑/触屏监控系统.MES这些:下即下位机,包括PLC.传感器.嵌入式芯片等. 不同厂家的下位机,往往 ...

  6. web 压力测试工具

    最近有收到任务,测试新服务器的性能. 花了很长时间做搜索,也整理了一些资料.以下是收集到一些简单易用的分析工具.推荐给大家使用. WebBenchhttp://www.ha97.com/4623.ht ...

  7. ES--04

    第三十一讲! 分布式文档系统 写一致性原理以及相关参数 课程大纲 (1)consistency,one(primary shard),all(all shard),quorum(default) 我们 ...

  8. springboot学习——第二集:整合Mybaits

    1,Mybatis动态插入(insert)数据(使用trim标签):https://blog.csdn.net/h12kjgj/article/details/55003713 2,mybatis 中 ...

  9. 20165236 第六周Java学习总结

    20165236 第六周Java学习总结 一. 第八章内容: 1.String 类: String对象.常量对象:字符串并置: 常用方法: length,equals,startsWith,compa ...

  10. python测试开发django-12.models设置主键primary_key

    前言 django的models新增数据库表时,如果不设置主键,会默认新增一个id为主键,如果我们想自己设置一个字段为主键,需加个参数primary_key=True 默认id主键 新增一张用户表,表 ...