多用户即时通讯系统03

4.编码实现02

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

image-20220922182116066

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();
image-20220922180741819
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. 运行服务端:
image-20220922182247168
  1. 分别运行三个客户端,并进行登录,拉取在线用户:

    ? 用户1:

image-20220922182621787 image-20220922183010340


? 用户2:

image-20220922182643944 image-20220922182828114


? 用户3:

image-20220922182701298 image-20220922182911274

此时服务端显示:

image-20220922183325135

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

4.3.1思路分析

image-20220922191529799

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

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

解决方法:

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

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

image-20220922193057065

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();

image-20220922200240425

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.运行服务端:

image-20220922200701524

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

image-20220922200802600 image-20220922201246653

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

image-20220922201404318

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

image-20220922201011864

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

image-20220922201649953

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

image-20220922201736838

标签智能推荐:

WPF 即时通信API文档

即时通信IM即时通信(InstantMessaging,IM)基于Micro底层IM能力开发,仅需植入SDK即可轻松集成聊天、会话、群组、资料管理能力,帮助您实现文字、图片、短语音、短视频等富媒体消息收发,全面满足通信需要。简介针对开发者的不同阶段需求及不同场景,即时通信IM团队提供了一系列解决方案,包括:Android、iOS、Windows、Web的SDK组件、服务端集成WebAPI接口、第三

运行级别

运行级别0、关机1、单用户【找回丢失密码】2、多用户状态没有网络服务3、多用户状态且有网络服务【最常用:无图形、有网络、多用户】4、保留的,未定义5、图形界面【常用】6、系统重启指定默认运行级别init0-6用字符来确定级别multi-user.target//3graphical.target//5返回当前级别systemctlget-default设置默认级别为多用户systemctlset-

零基础入门Socket网络编程到高级进阶与实战精讲

&nbsp;从零开始,从进阶到深入,系统学习Socket编程技术,理论结合实践,掌握Socket核心技术。零基础入门Socket网络编程到高级进阶与实战精讲视频下载。目录:第一章课程介绍及Java语言简介第二章快速入门Socket网络编程第三章快速入门SocketUDP第四章快速入门SocketTCP第五章UDP辅助TCP实现点对点传输案例第六章简易聊天室案例第七章服务器传输优化-NIO第八章数据

技术分析| WebRTC开源服务器商业化过程中遇到的问题及挑战

WebRTC及其发展前景WebRTC,名称源自网页即时通信(WebReal-TimeCommunication)的缩写,是一个支持网页浏览器进行实时语音通话或视频通话的API,旨在建立一个互联网浏览器间的实时通信的平台。它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准。WebRTC官网的介绍如下:WebRTC是一个免费的开源项目,它通过简

操作系统(一)

交互性:实时信息处理系统具有交互性,但人与系统的交互仅限于访问系统中某些特定的专用服务程序。不像分时系统那样能向终端用户提供数据和资源共享等服务。(3)可靠性:分时系统也要求系统可靠,但相比之下,实时系统则要求系统具有高度的可靠性。因为任何差错都可能带来巨大的经济损失,甚至是灾难性后果,所以在实时系统中,往往都采取了多级容错措施保障系统的安全性及数据的安全性。12、操作系统的基本特性(1)并发性(

Linux的运行等级

Linux的运行等级0:关机1:单用户【找回丢失的密码】2:多用户状态,没有网络服务3:多用户状态,有网络服务&nbsp;&nbsp;**常用**4;系统未使用保留给用户5:图形化界面&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**常用**6:系统重启基本语法:init【0/1/2/3/4/5/6】来自b站尚硅谷韩顺平老师的课,这是我的笔记

20211917 2021-2022-2 《网络攻防实践》第七周作业

动程序。Linux的系统调用机制Linux的系统调用通过软中断实现,用户程序为系统调用设置参数,程序执行“系统调用”指令产生中断异常,使得处理器切换到内核态,并跳转到异常处理程序执行系统调用处理函数,处理完之后再返回奋用户态程序。1.3.2Linux安全机制Linux身份认证机制:Linux用户:Root根用户,可以操作系统中任何的文件与命令普通用户,只能操作自己目录中的内容,执行权限受限系统用户

一个程序员的产品路:在线客服系统,开发两年的目标产品

况,我在一个十八线的小县城,算是自用职业?或者算是自己创业?反正就是自己给自己干活。那就介绍下我独自一人开发的产品,GOFLY在线客服系统。产品是使用Golang语言开发的,想要一飞冲天,所以起名GOFLY,足够简单足够有意。主要功能就是实现与网站/APP/公众号访客,进行实时的即时沟通。这里是我新启用的网址&nbsp;www.v1kf.com&nbsp;,旧网址是&nbsp;gofly.sopa

初次安装linux系统

系统级别切换init1(切换级别)0是关机1是单用户2是多用户,不联网3是多用户4是不使用的5是xwindows,也就是有界面的6是重启初次安装更新yum-yupdate升级所有包,改变软件设置和系统设置,系统版本内核都升级yum-yupgrade升级所有包,不改变软件设置和系统设置,系统版本升级,内核不改变yumlistupdates列表更新卸载程序&nbsp;rpm-qa|grepjdk&nb

linux的开机自启原理

linux下的/etc/init.d下的目录是开机启动的脚本地址,相当于win系统下的电脑开机启动位置&nbsp;运行等级:Linux分了7个运行等级,分别用数字0,1,2,3,4,5,6标志,每个运行等级支持的功能不一样0:关机(init0关机)1:单用户模式(一个场景:管理员在配置系统的时候,不希望有人登录)2:无网络连接的多用户命令行模式3:有网络连接的多用户命令行模式4:不可用5:带图形界