【Java】韩顺平Java学习笔记 第22章 多用户通讯系统

项目开发流程

  • 需求分析
  • 设计阶段
  • 实现阶段
  • 测试阶段
  • 实施阶段
  • 维护阶段

需求分析

  • 用户登录
  • 拉取在线用户列表
  • 无异常退出(客户端、服务端)
  • 私聊
  • 群聊
  • 发文件
  • 文件服务器推送新闻

整体分析

  • 多用户:多个客户端
  • 服务端和客户端之间传送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 code AC),就会认为这是不合法的类型代码,因为它期望的是对象的实际数据,于是抛出了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 一遍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值