简易QQ项目

本文详细介绍了使用Java实现一个简单的即时通讯系统的过程,涵盖了客户端和服务端的登录验证、多线程管理、用户在线状态获取、私聊、群发消息、文件传输、新闻推送等功能。讲解了如何通过Socket进行通信,以及如何通过线程池管理多个客户端连接。此外,还涉及了消息序列化、并发控制和离线消息处理等关键技术。
摘要由CSDN通过智能技术生成


视频参考: 韩顺平老师

1、登录

1.1 准备工作

  1. 过程分析,这个图是很关键的
    在这里插入图片描述
  • 服务端和客户端是通过socket进行通信的。为了方便管理,每个socket使用一个线程进行管理。
  • 通信的消息是Message以及User对象,因为要在网络上进行传输,所以两个对象必须进行序列化。同时,客户端和服务端,要有一个共同的common包,用来存放用于通信的类、以及消息类型等等。
  • 补充:
  1. 关于这个common包,不管是客户端还是服务端,两个包的包名一定要一样,否则在反序列化的时候会失败
  2. 在进行序列化的时候要实现Serializable接口,同时以防万一还是加一段private static final long serialVersionUID = 1L;
  3. 结构:
    在这里插入图片描述
    在这里插入图片描述

1.2、客户端

  • 所需要做的事是红线部分。在这里插入图片描述
  1. 编写一个UserClientService类去对用户的登陆或者注册等操作进行处理。调用他的checkUser(userId, pwd)方法进行验证用户是否合法。
  2. 可简单理解为上图的客户端,因为需要发送自己用户的信息以及要使用socket进行通信,所以,将UserSocket作为自己的两个成员属性。(暂时未涉及到管理线程的集合)。
  3. 此时,将自己的信息封装好,然后连接到服务端,因为要发送给服务器,所以创建一个ObjectOutputStream对象,发送user对象,同时,服务端也会回传给客户端一个message对象,回传消息类型。如果消息类型为登陆成功,意味着登陆成功,那么在客户端就要使用一个线程去管理这个socket
  4. 根据图分析,该线程会持有一个socket对象,所以在设计的时候,将socket设置为自己的成员变量。该线程永远读取从服务器回传的消息,并且根据消息类型做响应的判断处理
  5. 同时,要保持和服务端通信,使用while循环,这意味着,可以持续的收发消息。
  6. UserClientService已经做了发送自己信息的功能,该线程是用来接收服务器回传消息的。也就是需要一个ObjectInputStream读取,从服务端发来的一定是消息的某个类型,所以读到的必定是一个Message类型的对象。

1.2.1、多线程管理

  1. 当写好了管理socket的线程之后,就需要将该线程启动起来,这样就可以持续的读取。
  2. 场景假设:如果此时又需要聊天、又需要发邮件,那么这是两个线程做的事,所以使用一个集合进行线程管理
  3. 使用HashMap去管理线程,key=>idvalue=>管理socket的线程。将线程放入至集合中,同时为了方便,也写一个通过用户id得到线程的方法

1.3、服务端

  • 此时做的是红线部分。
    在这里插入图片描述
  1. 服务器端在9999端口进行监听,那么就new ServerSocket(9999);
  2. 服务端不能只连接一个客户端就断开连接,要进行持续的监听。所以使用while(true)使得监听一直发生。
  3. 一旦退出了while循环,那么就意味着服务器停止了监听,那么就需要关闭ServerSocket
  4. 读取客户端发送的User对象,进行验证。
    1. 一旦验证通过,那么就设置一个消息,类型为登陆成功,并且回传给客户端。
    2. 没有通过验证,就登陆失败,要将socket关闭

1.3.1、多线程管理

  1. 同样,服务端也是一个线程管理一个socket,那么创建一个ServerConnectClientThread,用来管理服务端到客户端的线程。
  2. 在该线程中,已知的有socket的成员属性,但是此时是在服务端,一旦客户端多了之后,无法区分某个线程属于哪一个客户端的,所以还需要新增一个成员属性-用户的id
  3. 同样,线程要一直运行,不断的通过那根管道接发消息。
  4. 客户端发消息的时候,同样会告知发送消息的类型,所以,在该线程读取从客户端读取的是一个Message类型的消息。
  5. 不同客户端连接服务器的时候,会产生很多线程,因此需要对这些线程进行管理。
  6. 同样,使用一个HashMap进行管理,key=>idvalue=>管理socket的线程,写两个方法:1、将线程添加到集合中;2、根据用户Id,返回ServerConnectClientThread线程。

1.4、客户端和服务端的连接验证

  1. 本例并未使用数据库进行用户存储,使用的concurrentHashMap进行存储用户信息,并在静态代码块中进行了初始化validUsers
  2. 进行是进行的多用户的,所以在客户端启用的时候,一个小点:在这里插入图片描述
    在这里插入图片描述

2、拉取在线用户

  1. 只有服务器端才知道有哪些用户,所以某客户像服务器发送一个消息,消息类型就是获得在线用户列表
    在这里插入图片描述
  2. 在对消息类型进行扩充之后,服务器的消息类型要和客户端的消息类型一样才可以。

2.1、客户端

  1. 客户端在UserClientService中编写一个获取在线用户列表的方法。
  2. 因为在客户端是使用集合去管理集合了,所以通过集合或者获取到用户对应的线程对象,然后再通过这个线程对象获取关联的socket。获取了socket之后就可以发送消息。
  3. 在用户发送过后,会得到来自服务端回传的message对象。记得在之前,每个socket是由一个线程管理的,线程在不断的运行,读取从服务器发送回来的信息。所以可以根据服务端回传的消息类型判断是否是获取在线用户列表,如果是,那么就进行显示。

2.2、服务端

  1. 在管理服务端的线程里面,有停留的message,这个就是客户端发给服务端的消息类型,可以通过这个消息类型,判断客户端是需要做什么。
  2. 同时,服务端的所有的线程是通过一个集合进行统一管理的,所以在管理线程的类中增添一个获取所有在线用户的方法。
  3. 获取了之后,要构建一个Message对象,返回给客户端,这样客户端就可以通过返回消息类型进行确认。

3、无异常退出

在这里插入图片描述

  • 即:在客户端给服务器端发送一个message对象,指明了消息类型为退出,客户端直接调用System.exit(0)退出即可,给服务端发送的时候,需要指定客户端的Id。同时服务端也需要退出与该客户端关联的线程。

3.1、客户端

  1. 客户端在UserClientService中新增一个退出的方法,发送消息的时候,要指定消息类型为退出,以及自己的Id,发送给服务端。发送完成之后,直接结束进程,即:直接使用System.exit(0)

3.2、服务端

  1. 服务端在管理线程的类中,会对消息类型进行判断,一旦得到的消息是退出,那么要将与该用户关联的线程从线程集合中删除,然后关闭socket流,最后退出线程

4、私聊

在这里插入图片描述

4.1、客户端

  1. 使用一个MessageClientService类专门处理发送消息的,在里面写一个私发消息的方法,需要指定发送消息类型、内容、发送者、接受者、时间,然后在管理线程集合中通过发送者Id找到这个线程,然后得到socket,进而得到输出流,将消息发出给服务端。
  2. 在客户端线程这里,因为是循环的读取,所以得到服务端回传的消息类型之后,把从服务器转发的消息显示即可。

4.2、服务端

  1. 服务端收到来自客户端的message消息,首先需要找到接收者的线程对应的socket,然后通过输出流写出即可。

注:这里可能稍微会有疑虑,为什么不会服务端与发送消息的客户端对应socket转发给getter的线程,然后再又getter的socket写出?
答:其实这里,服务端已经收到消息了,服务端只需要确定要发出去的客户端是谁,通过message的getter就可以知道,消息是在整个服务器都是可见的,服务器只需要找到接收者的对应的socket即可。

5、群发

5.1、客户端

  1. 同样在MessageClientService中新增一个群发消息的方法,在该方法中,同样需要指定消息类型、发送者Id、内容以及时间,然后通过发送者Id在客户端管理线程得到socket对应的输出流,然后发送给服务端。
  2. 在管理线程中,通过得到消息类型,将发送的消息打印即可。

5.2、服务端

  1. 在服务端管理线程中,根据消息类型,判断得到是群发类型,然后从服务端管理线程集合中得到现在集合中所有的线程,但是需要除去发送者的对应的线程,然后通过线程对应的socket得到输出流,然后将发送过来的消息进行转发转发。

6、发文件

在这里插入图片描述
客户端和服务端要同时进行扩展和文件相关的成员。因为将文件读取和写出都需要byte[]文件长度、以及目标地点原文件地点

6.1、客户端

  1. 客户端分接收方发送方发送方首先要把文件进行读取,需要指明自己的文件位置接收方接收文件的位置发送方的Id以及接收方的Id。文件读取使用一个byte[]进行读取,写入到内存中,将消息的,然后关闭流。随后通过发送方的Id去管理客户端线程集合中去取对应的线程,然后就可以得到socket,这样通过socket写出文件。
  2. 接收方在管理线程的地方进行接收,因为线程一直在运行,不断读取从服务端发送过来的东西。
  3. 取出message的文件字节数组,通过文件输出流(内存=>磁盘)写出到磁盘。最后记得要关闭流,不然不能将文件写入。

6.2、服务端

  1. 服务端在管理与客户端通信的线程在一直读取从客户端发送过来的消息,根据消息类型进行判断。但是要转发给接收方,所以,要在管理线程集合中,通过接收方的Id得到对应的线程的socket,然后将对象进行转发。

7、新闻推送

在这里插入图片描述
新闻推送可以理解为服务端的群发消息。

7.1、客户端

  1. 因为是新闻推送,所以客户端只需要被动的接收就可以了,那么在管理客户端的线程里面,根据服务端的消息类型判断,然后将消息显示在客户端即可。

7.2、服务端

  1. 因为是新闻推送,是服务端主动发起的,那么单独开一个线程进行新闻推送,构建一个消息,指明发送者为服务器、消息类型、内容以及时间。此时需要将构建的消息发给所有的客户端,那么就通过管理服务器和客户端的线程集合得到所有的客户Id,然后通过他们的Id可以得到各自对应的线程,进而得到socket,利用socket将信息写出。
  2. 同时为了可以多次推送新闻,使用while
  3. 如果想要退出,那么给一个退出位(例如:exit),一旦退出,那么就break就好。

8、离线留言和发文件

在这里插入图片描述

8.1、客户端

  1. 客户端还是和之前一样正常的发送消息和发送文件。

8.2、服务端

  1. 服务端需要建立一个map取存放离线的消息,key=>getterId,value=>ArrayList<Message>。在QQServer中写两个方法,一个是服务器暂存离线消息,一个是服务器发送离线消息
  2. 因为线程在一直读,所以在线程管理的这个类中,对于发送消息的时候,先判断,在管理线程集合中有没有接收者的线程,如果没有,就将消息进行暂存,如果有,还是和之前一样,进行消息正常转发。
  3. 同时,离线用户上线了,在QQServer进行验证登陆通过以及加入集合进行线程管理之后,将离线消息进行发送
  4. 发送邮件也是一个道理。

9、总结

  • 项目虽然很小,但是知识点绝对杠杠的,涉及到网络编程、线程、集合使用以及JavaSE基础知识。对于应届生来说,掌握底层知识远远重要于框架学习。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值