文章目录
视频参考: 韩顺平老师
1、登录
1.1 准备工作
- 过程分析,这个图是很关键的
- 服务端和客户端是通过
socket
进行通信的。为了方便管理,每个socket
使用一个线程
进行管理。 - 通信的消息是
Message
以及User
对象,因为要在网络上进行传输,所以两个对象必须进行序列化
。同时,客户端和服务端,要有一个共同的common
包,用来存放用于通信的类、以及消息类型等等。 - 补充:
- 关于这个
common
包,不管是客户端还是服务端,两个包的包名
一定要一样
,否则在反序列化
的时候会失败
。 - 在进行序列化的时候要实现
Serializable
接口,同时以防万一还是加一段private static final long serialVersionUID = 1L;
。 - 结构:
1.2、客户端
- 所需要做的事是红线部分。
- 编写一个
UserClientService
类去对用户的登陆或者注册等操作进行处理。调用他的checkUser(userId, pwd)
方法进行验证用户是否合法。 - 可简单理解为上图的客户端,因为需要发送自己用户的信息以及要使用
socket
进行通信,所以,将User
和Socket
作为自己的两个成员属性。(暂时未涉及到管理线程的集合)。 - 此时,将自己的信息封装好,然后连接到服务端,因为要发送给服务器,所以创建一个
ObjectOutputStream
对象,发送user
对象,同时,服务端也会回传给客户端一个message
对象,回传消息类型。如果消息类型为登陆成功
,意味着登陆成功,那么在客户端就要使用一个线程
去管理这个socket
。 - 根据图分析,该线程会持有一个
socket
对象,所以在设计的时候,将socket
设置为自己的成员变量。该线程永远读取从服务器回传的消息,并且根据消息类型做响应的判断处理
。 - 同时,要
保持
和服务端通信
,使用while
循环,这意味着,可以持续的收发消息。 UserClientService
已经做了发送自己信息的功能,该线程是用来接收
服务器回传消息的。也就是需要一个ObjectInputStream
读取,从服务端发来的一定是消息的某个类型,所以读到的必定是一个Message
类型的对象。
1.2.1、多线程管理
- 当写好了管理socket的线程之后,就需要将该线程
启动
起来,这样就可以持续的读取。 - 场景假设:如果此时又需要聊天、又需要发邮件,那么这是两个线程做的事,所以使用一个
集合进行线程管理
。 - 使用
HashMap
去管理线程,key=>id
,value=>管理socket的线程
。将线程放入至集合中,同时为了方便,也写一个通过用户id得到线程的方法
1.3、服务端
- 此时做的是红线部分。
- 服务器端在
9999
端口进行监听,那么就new ServerSocket(9999);
。 - 服务端不能只连接一个客户端就断开连接,要进行持续的监听。所以使用
while(true)
使得监听一直发生。 - 一旦退出了while循环,那么就意味着服务器停止了监听,那么就需要
关闭ServerSocket
- 读取客户端发送的
User
对象,进行验证。- 一旦验证通过,那么就设置一个消息,类型为
登陆成功
,并且回传给客户端。 - 没有通过验证,就登陆失败,要将
socket
关闭
- 一旦验证通过,那么就设置一个消息,类型为
1.3.1、多线程管理
- 同样,服务端也是一个线程管理一个socket,那么创建一个
ServerConnectClientThread
,用来管理服务端到客户端的线程。 - 在该线程中,已知的有socket的成员属性,但是此时是在服务端,一旦客户端多了之后,无法区分某个线程属于哪一个客户端的,所以还需要
新增
一个成员属性-用户的id
。 - 同样,线程要一直运行,不断的通过那根管道接发消息。
- 客户端发消息的时候,同样会告知发送消息的类型,所以,在该线程读取从
客户端
读取的是一个Message
类型的消息。 - 当
不同客户端
连接服务器的时候,会产生很多线程,因此需要对这些线程进行管理。 - 同样,使用一个
HashMap
进行管理,key=>id
,value=>管理socket的线程
,写两个方法:1、将线程添加到集合中;2、根据用户Id,返回ServerConnectClientThread
线程。
1.4、客户端和服务端的连接验证
- 本例并未使用数据库进行用户存储,使用的
concurrentHashMap
进行存储用户信息,并在静态代码块中进行了初始化validUsers
。 - 进行是进行的多用户的,所以在客户端启用的时候,一个小点:
2、拉取在线用户
- 只有服务器端才知道有哪些用户,所以某客户像服务器发送一个消息,消息类型就是
获得在线用户列表
。
- 在对消息类型进行扩充之后,服务器的消息类型要和客户端的消息类型一样才可以。
2.1、客户端
- 客户端在
UserClientService
中编写一个获取在线用户列表
的方法。 - 因为在客户端是使用集合去管理集合了,所以
先
通过集合或者获取到用户对应的线程对象,然后
再通过这个线程对象获取关联的socket
。获取了socket
之后就可以发送消息。 - 在用户发送过后,会得到来自服务端回传的
message
对象。记得在之前,每个socket
是由一个线程
管理的,线程在不断的运行,读取从服务器发送回来的信息。所以可以根据服务端回传的消息类型判断是否是获取在线用户列表
,如果是,那么就进行显示。
2.2、服务端
- 在管理服务端的线程里面,有停留的
message
,这个就是客户端发给服务端的消息类型
,可以通过这个消息类型,判断客户端是需要做什么。 - 同时,服务端的所有的线程是通过一个集合进行统一管理的,所以在管理线程的类中增添一个获取所有在线用户的方法。
- 获取了之后,要构建一个
Message
对象,返回给客户端,这样客户端就可以通过返回消息类型进行确认。
3、无异常退出
- 即:在客户端给服务器端发送一个
message
对象,指明了消息类型为退出
,客户端直接调用System.exit(0)
退出即可,给服务端发送的时候,需要指定客户端的Id
。同时服务端也需要退出与该客户端关联的线程。
3.1、客户端
- 客户端在
UserClientService
中新增一个退出的方法,发送消息的时候,要指定消息类型为退出
,以及自己的Id
,发送给服务端。发送完成之后,直接结束进程,即:直接使用System.exit(0)
。
3.2、服务端
- 服务端在管理线程的类中,会对消息类型进行判断,一旦得到的消息是退出,那么要将与该用户关联的线程从线程集合中删除,然后关闭
socket
流,最后退出线程
4、私聊
4.1、客户端
- 使用一个
MessageClientService
类专门处理发送消息的,在里面写一个私发消息
的方法,需要指定发送消息类型、内容、发送者、接受者、时间,然后在管理线程集合中通过发送者Id找到这个线程,然后得到socket
,进而得到输出流,将消息发出给服务端。 - 在客户端线程这里,因为是循环的读取,所以得到服务端回传的消息类型之后,把从服务器转发的消息显示即可。
4.2、服务端
- 服务端收到来自客户端的
message
消息,首先需要找到接收者的线程对应的socket
,然后通过输出流写出即可。
注:这里可能稍微会有疑虑,为什么不会服务端与发送消息的客户端对应socket转发给getter的线程,然后再又getter的socket写出?
答:其实这里,服务端已经收到消息了,服务端只需要确定要发出去的客户端是谁,通过message的getter就可以知道,消息是在整个服务器都是可见的,服务器只需要找到接收者的对应的socket即可。
5、群发
5.1、客户端
- 同样在
MessageClientService
中新增一个群发消息的方法,在该方法中,同样需要指定消息类型、发送者Id、内容以及时间,然后通过发送者Id在客户端管理线程得到socket
对应的输出流,然后发送给服务端。 - 在管理线程中,通过得到消息类型,将发送的消息打印即可。
5.2、服务端
- 在服务端管理线程中,根据消息类型,判断得到是群发类型,然后从服务端管理线程集合中得到现在集合中所有的线程,但是需要除去发送者的对应的线程,然后通过线程对应的
socket
得到输出流,然后将发送过来的消息进行转发转发。
6、发文件
客户端和服务端要同时进行扩展和文件相关的成员。因为将文件读取和写出都需要byte[]
、文件长度
、以及目标地点
和原文件地点
。
6.1、客户端
- 客户端分
接收方
和发送方
,发送方
首先要把文件进行读取,需要指明自己的文件位置
,接收方接收文件的位置
,发送方的Id
以及接收方的Id
。文件读取使用一个byte[]
进行读取,写入到内存中,将消息的,然后关闭流。随后通过发送方的Id去管理客户端线程集合中去取对应的线程,然后就可以得到socket
,这样通过socket
写出文件。 - 接收方在管理线程的地方进行接收,因为线程一直在运行,不断读取从服务端发送过来的东西。
- 取出
message
的文件字节数组,通过文件输出流
(内存=>磁盘)写出到磁盘。最后记得要关闭流,不然不能将文件写入。
6.2、服务端
- 服务端在管理与客户端通信的线程在一直读取从客户端发送过来的消息,根据消息类型进行判断。但是要转发给接收方,所以,要在管理线程集合中,通过接收方的Id得到对应的线程的
socket
,然后将对象进行转发。
7、新闻推送
新闻推送可以理解为服务端的群发消息。
7.1、客户端
- 因为是新闻推送,所以客户端只需要
被动
的接收就可以了,那么在管理客户端的线程里面,根据服务端的消息类型判断,然后将消息显示在客户端即可。
7.2、服务端
- 因为是新闻推送,是服务端主动发起的,那么
单独
开一个线程
进行新闻推送,构建一个消息,指明发送者为服务器、消息类型、内容以及时间。此时需要将构建的消息发给所有的客户端,那么就通过管理服务器和客户端的线程集合得到所有的客户Id,然后通过他们的Id可以得到各自对应的线程,进而得到socket
,利用socket
将信息写出。 - 同时为了可以多次推送新闻,使用
while
。 - 如果想要退出,那么给一个退出位(例如:
exit
),一旦退出,那么就break就好。
8、离线留言和发文件
8.1、客户端
- 客户端还是和之前一样正常的发送消息和发送文件。
8.2、服务端
- 服务端需要建立一个
map
取存放离线的消息,key=>getterId,value=>ArrayList<Message>
。在QQServer
中写两个方法,一个是服务器暂存离线消息
,一个是服务器发送离线消息
。 - 因为线程在一直读,所以在线程管理的这个类中,对于发送消息的时候,
先判断
,在管理线程集合中有没有接收者的线程
,如果没有
,就将消息进行暂存
,如果有,还是和之前一样,进行消息正常转发。 - 同时,离线用户上线了,在
QQServer
进行验证登陆通过以及加入集合进行线程管理之后,将离线消息进行发送
。 - 发送邮件也是一个道理。
9、总结
- 项目虽然很小,但是知识点绝对杠杠的,涉及到网络编程、线程、集合使用以及JavaSE基础知识。对于
应届生
来说,掌握底层知识
远远重要于框架
学习。