文章目录
- 项目开发流程
- 需求分析
- 整体分析
- 用户登录
- 注意
- 拉取在线用户列表
- 无异常退出
- 私聊功能
- 注意
- 发送文件
- 服务端推送新闻
- 接收离线消息和文件
项目开发流程
- 需求分析
- 设计阶段
- 实现阶段
- 测试阶段
- 实施阶段
- 维护阶段
需求分析
- 用户登录
- 拉取在线用户列表
- 无异常退出(客户端、服务端)
- 私聊
- 群聊
- 发文件
- 文件服务器推送新闻
整体分析
- 多用户:多个客户端
- 服务端和客户端之间传送Message对象来进行通信(提高效率),可以使用对象流来读取
- socket在线程里进行管理
- 群发消息时需要获取所有socket——服务端需要用一个集合来管理线程(socket)
- 一个客户端和服务端可能出现多个连接通道、多个线程(如同时传文件和聊天),故客户端也要用一个集合来管理线程,可以用hashmap管理
- 注:User的id要统一!不能一下用String 一下用int!不然会非常麻烦!同时如果用int,id就不能为中文,故不推荐!
用户登录
- 用 hashMap 模拟数据库,则可以实现多个用户登录
- 共有的类:User(用户信息)、Message(传递的消息)
- 共有的接口:MessageType(表示消息类型的接口)
- 注意共有的类需要通过对象流进行传输,传输则需要序列化
- 登录界面(一级菜单、二级菜单)
- 创建socket,用于向服务端发送需要验证的User和接收服务端发来的登录确认信息Message
- 如果登录成功,需要创建一个线程,传入socket,让这个线程不停地在端口进行监听,监听服务端发送来的信息(线程有run方法可以实现不停工作)
- 用HashMap存储线程,方便实现多个用户同时通信
- 验证完登录,如果登录失败,则发送User和接收Message的socket已经没有用,需要进行关闭
- ConcurrentHashMap 替换 HashMap ,可以处理并发的集合,没有线程安全问题
注意
- 建立链接后,不能随便关闭 ObjectOutputStream 和 ObiectInputStream ,这样会关闭建立好的socket,从而报错
- while循环外暂时不能写 serverSocket.close(),Java编译器不允许
拉取在线用户列表
- 客户端发送Message对象向服务端要在线用户列表信息
- 先对Message种类进行拓展,两边同时拓展
- Message 得到当前线程的socket 对应的 ObjectOutputStream
- 规定在线用户列表形式:
100 200 紫霞仙子
- 在run方法中读取message信息并根据类型进行不同的处理操作
- 在服务端的管理线程的HashMap类中写一个返回在线用户列表的方法(遍历HashMap)
无异常退出
- 客户端选择退出系统选项后,只是退出了main方法,并没有退出创建的客户端线程,故进程还没有退出
- 客户端解决方法:在主线程调用一个方法,给服务器端发送一个退出系统的message;调用System.exit(0)正常退出
- 服务端解决方法:接收到一个退出的Message,将线程持有的socket关闭,退出该线程的run方法,相当于结束了该线程
私聊功能
- 将聊天内容包装成Message对象,发送给服务端
- 服务端读取,获取到对应线程的socket,然后将这个Message发给对应的用户
- 在客户端线程里写一个接收方法,读取发送过来的信息
注意
-
oos = new ObjectOutputStream(ServerThreadHashMap. get(message.getSender()).getSocket().getOutputStream()); message1.setContent("向用户 " + message.getGetter() + " 发送数据成功!"); oos.writeObject(message1);//这里当时没有写这行,重新刷新oos,结果导致invalid type code: AC报错
在Java中,当你尝试使用
ObjectOutputStream
对同一个OutputStream
多次进行序列化操作,尤其是在没有正确关闭前一个ObjectOutputStream
的情况下,再次创建一个新的ObjectOutputStream
实例并尝试写入对象,很可能会导致java.io.StreamCorruptedException: invalid type code: AC
异常。这是因为每当创建一个
ObjectOutputStream
实例时,它会在底层输出流的开始处写入一个特殊的序列化头(称为Stream Header),这个头包含了一些用于验证序列化数据格式的魔数(magic number)。对于Java,默认的魔数值是0xAC ED
,用于标记这是一个Java序列化的数据流。当你在一个已经写入过
ObjectOutputStream
数据的OutputStream
上再次创建一个新的ObjectOutputStream
实例,没有先关闭之前的流,那么新写入的序列化头(即0xAC ED
)就会紧接着前一次写入的数据之后,而不会被当作新的序列化流的开始。当在另一端使用ObjectInputStream
尝试读取这些数据时,它在解析完第一个对象后遇到额外的魔数值0xAC
(即type codeAC
),就会认为这是不合法的类型代码,因为它期望的是对象的实际数据,于是抛出了StreamCorruptedException: invalid type code: AC
异常。为了避免这个问题,确保每次完成序列化操作后正确地关闭
ObjectOutputStream
,或者如果需要在同一OutputStream
上进行多次序列化,考虑仅创建一个ObjectOutputStream
实例并重复使用它,而不是反复创建新的实例。此外,在某些场景下,如果必须创建多个ObjectOutputStream
实例,确保在创建新实例之前彻底刷新和关闭前一个实例,并考虑到这可能需要更复杂的逻辑来管理输出流的状态。
发送文件
- 将文件读取到客户端的字节数组
- 将字节数组封装到message对象
- 将message对象发给服务端
- 服务端进行转发
服务端推送新闻
- 本质其实就是群发消息
- 在服务器启动一条独立县城,专门负责发送推送新闻
接收离线消息和文件
- 创建一个HashMap存放所有的离线消息和文件
- HashMap 的 key 是接收消息的用户名,value 是 message,即消息内容
- 每当一个用户上线后,检查这个用户名对应的 HashMap,如果有离线消息,给其传递所有的离线消息
- 注意以下错误:
注意每次 new 的时候都会调用writeStreamHeader()方法写入4个字节的StreamHeader(看源码),标记这个是对象处理流,可能导致的错误:如果在接收方每次都是new一个新的ObjectInputStream 来接收,而发送方只 new 过一个 ObjectOutputStream,之后每次都是用new过的 ObjectOutputStream对象来发送东西,这样就会导致之后并没有添加头部。而接受方每次都是新的 new,则每次接收都要检测头部,当发送方没有提供头部时,接收方就把内容当做了头部,导致出错。解决方法之一是每次使用 ObjectOutputStream 时都重新 new 一遍