源码已上传至Github↓↓↓
https://github.com/Arielxyx/Myim
一、需求分析
1. 客户端模块
客户端主要为用户提供了账号注册、登录系统、实时刷新所有在线用户、随意选择某一位用户私聊(包括自己)、聊天室群聊、本机存储与各用户的历史聊天记录、在修改用户名的同时更新该用户在列表上的用户名。
主要功能 | 功能概述 | |
账号注册 | 未注册账号的用户可以通过手机注册: 填写手机号、密码、确认密码、用户名、年龄、姓名几个字段后; 注册成功则返会登陆界面;失败则提示失败原因。 | |
用户登录
| 根据手机号和密码登录本系统: 填写手机号、密码; 登录成功则进入用户主页;失败则返回失败信息。 | |
用户主页 | 显示信息 | 在主页最上方可显示用户名和手机号等基本信息。 |
修改密码 | 弹出框提示修改密码。 | |
修改信息 | 显示并可修改基本信息,其中手机号被限制无法修改。 | |
注销账号 | 清除本地登陆缓存记录,重新进入登陆界面后允许登录其他用户。 | |
显示用户 | 实时显示所有与服务器连接成功且在线的用户列表。(用户上线、下线、修改用户名) | |
私聊功能 | 历史记录 | 选定用户即可私聊,自动显示于该用户的本地历史聊天记录。 |
发送消息 | 仅向私聊对象发送消息。 | |
接收消息 | 可接收所有向自己发送的消息,但当前聊天窗口只显示该对象发来的消息。 | |
群聊功能 | 历史记录 | 点击聊天室即可群聊,自动显示该群内所有的聊天信息。 |
发送消息 | 可和在线的所有用户聊天,所有在群里的在线用户都可以看到自己发送的消息,不在群内的用户进群之后也可以看到。 | |
接收消息 | 可接收所有在线用户在群内发送的消息。 |
2. 服务器端模块
服务器端主要用于验证客户端的注册信息是否有效、验证登录信息是否正确、更新用户提交的信息修改内容、存储所有在线用户至列表、转发更新列表信息、转发聊天信息。
主要功能 | 功能概述 |
验证注册信息是否有效 | 服务器端根据注册表单信息查询数据库,验证用户名和手机号是否已经被注册过。 如果信息有效则会将该用户插入数据库,否则向客户端返回无效信息。 |
验证登录信息是否正确 | 服务器端根据手机号和密码查询数据库,验证该用户是否存在、密码是否正确。 如果信息正确则登陆成功,否则向客户端返回无效信息。 |
修改用户存储在数据库中的密码 | 用户可修改密码, 下次再登录的时候就需要输入修改后的密码登录。 |
修改用户存储在数据库中的个人信息 | 用户可修改用户名、性别、年龄(手机号不可以修改), 该用户修改之后服务器不仅修改数据库的内容,还要将新的用户名发送给每个客户端,更新每个客户端的在线用户列表。 |
将在线用户转发给所有用户 | 当有用户登录本系统或有用户退出登录,则将新的在线用户列表传给每个客户端。 |
转发聊天信息 | 和自己私聊时,将消息仅发给传来消息的客户端。 |
和别人私聊时,将消息发给发送方和接收方。 | |
群聊时,将消息发给所有客户端。 |
二、概要设计
在此说明每个部分的算法设计说明(可以是描述算法的流程图),每个程序中使用的存储结构设计说明(如果指定存储结构请写出该存储结构的定义)。
1. 用户信息处理部分框架简要介绍(SpringBoot+OKHttp)
1.1 服务器端 - 基于Mysql数据库使用SpringBoot框架整合Mybatis存储用户信息
1.1.1 设计如下用户表:
属性名 | 含义 | 类型 | 说明 |
id | 主键唯一标识 | int | 每次自增1 |
phone | 手机号 | varchar | 不为空 |
name | 姓名 | varchar | 不为空 |
password | 密码 | varchar | 不为空 |
sex | 性别 | varchar | 不为空 |
age | 年龄 | int | 不为空 |
1.1.2 创建SpringBoot项 - 构建如下目录结构:
注意事项:一定要加上需要的注解,加上之后才可以自动扫描包路径装载并注入对象,这样就无需繁琐的XML配置。Mapper层和Model层属性必须要和数据库一致否则无法映射成功
![](https://img-blog.csdnimg.cn/20200327124810784.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FyaWVsX3g=,size_16,color_FFFFFF,t_70)
层次 | 功能说明 |
Controller层 | 控制层,接收前端的业务请求,在调用业务层处理后返回相应的信息给前端 |
Service层 | 业务层,存放业务逻辑给控制层使用,不直接操作数据库而是调用mapper层的接口 |
Mapper层 | 映射层,对数据库进行持久化操作,通过注解可以更简单的操作数据库 |
Model层 | 实体层,存放实体类,属性要和数据库保持一致 |
1.1.3 注入相关依赖 - springboot开发的起步依赖、mybatis、MySQL的JDBC驱动、generator等注入后,按下图配置服务器和数据库:
注意事项:由于没有购买云服务器,只能使用本机作为服务器,测试的时候在同一局域网下(连接同一个wifi)进行,所以这里的服务器连接地址是WLAN的IPV4地址。之后在测试的时候还需要关闭本机的防火墙,否则连接不上。
![](https://img-blog.csdnimg.cn/20200327125315946.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FyaWVsX3g=,size_16,color_FFFFFF,t_70)
1.1.4 配置generatorConfig.xml - 可以自动生成部分代码
注意事项:配置路径必须按照目录结构进行配置,否则会十分混乱。生成表的配置要按照数据库的表名和实体类名来设置。
![](https://img-blog.csdnimg.cn/20200327125535680.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FyaWVsX3g=,size_16,color_FFFFFF,t_70)
1.2 客户端和服务器端的交互使用OKHttp
1.2.1 安卓导入相关依赖并申请网络权限
![](https://img-blog.csdnimg.cn/20200327125858250.png)
![](https://img-blog.csdnimg.cn/20200327125858137.png)
1.2.2 将使用OKHttp请求的代码封装为一个工具类
在客户端先创建OkHttpClient对象,通过异步Post请求(添加多个键值对),使用Builder模式创建request对象传入formBody。之后在onResponse(…)回调函数中处理服务器传回的请求。
注意事项:这里请求的地址一定要按照服务器注解中的映射地址来配置,否则会报404错误。异步回调函数是在子线程中,不能在子线程中更新UI。回调的参数是Reponse类型,通过response.body().string()转化为字符串类型但这只能操作一次。
![](https://img-blog.csdnimg.cn/20200327130139491.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FyaWVsX3g=,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20200327130140880.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FyaWVsX3g=,size_16,color_FFFFFF,t_70)
1.3 用户信息部分细节说明
注意:其中有的地方不仅涉及到OKHttp的前后后端交互,有两个地方也需要处理Socket管理端,用户登录时要判断是否重复登录;修改信息时需用Socket转发消息刷新列表。
功能 | 说明 |
用户注册 | 分别查询用户名name和手机号phone是否已被注册 |
只有两个都没被注册才能插入数据库,并返回注册成功与否的信息 | |
用户登录 | 首先利用sharedPreference检查本地缓存是否已经存储过用户信息,若查询得到则自动登录无需再输入账号密码 preferences = getSharedPreferences(getPackageName(), MODE_PRIVATE); |
如果是第一次登录,信息正确则要加入缓存中 SharedPreferences.Editor editor = preferences.edit(); //用户信息存入SharedPreferences | |
服务器端检验登录的时候不仅要在数据库中检查账号密码是否正确 | |
若信息正确,还要检查该账号是否已经登录:通过判断客户端集合clientList内是否包含该user即可 User result = userService.selectByPhoneAndPwd( user); } | |
登录成功之后再连接服务器。(后面详细说明) | |
修改密码 | 自定义窗口弹出,无需再调用新的activity |
修改基本信息 | 1. 当提交修改信息后,除了要通过OKHttp修改数据库 2. 修改本地缓存的用户信息 3. 修改当前程序中的临时user的用户信息 注意:通过OKHttp传来的用户姓名格式为:“用户名长度-修改前的用户名-修改后的用户名”,服务器端根据新修改的用户名向所有客户端发送信息要刷新在线用户列表 |
2. 客户端和服务器端使用Socket实时传送消息
2.1 设计一个MessageItem封装类
封装客户端和服务器端所有类型的消息(包括刷新列表类消息和聊天类消息)
属性名 | 含义 | 类型 | 说明 |
id | 主键唯一标识 | int | 每次自增1 |
content | 消息发送内容 | String | 不为空 |
time | 消息发送时间 | String | 不为空 |
sendName | 发送方姓名 | String | 不为空 |
revName | 接收方姓名 | String | 不为空 |
sendId | 发送方ID | int | 不为空 |
revId | 接收方ID | int | 不为空 |
status | 标识消息的状态 | String | 用户上线(online)/下线(offline)/信息更新(onlineupdate)/发给自己(myself)/发给对象others/发给聊天室(聊天室) |
2.2 服务器端Socket编程管理类
属性名 | 类型 | 说明 |
serverSocket | ServerSocket | 用于服务器端监听特定的端口 |
socket | Socket | 套接字:任何一个客户端发来请求都要构建一个socket |
nameList | List<String> | 存储上线用户名字的列表 |
messageList | LinkedList<MessageItem> | 存储向客户端发送消息的列表 |
clientList | Map<Socket,User> | 映射表可以得到每个socket对应的用户user |
2.3 消息传递重点说明
要点 | 说明 | |
流操作 | 读出 | br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); |
写入 | pw = new PrintWriter(socket.getOutputStream()); | |
数据类型转换 | 自定义对象 | messageItem = new Gson().fromJson(receiveMessage, MessageItem.class); |
Json字符串 | String sendMessage = new Gson().toJson(messageItem); | |
客户端 | 发刷新列表消息 | 1.用户上线:登录成功一开始连接服务器 pw.println(new Gson().toJson(user)); |
2.用户下线(自定义了一个栈存储activity;下线时服务器将检测到不正常通信) 用户退出APP:关闭所有的activity 用户注销登录:要先清除本地用户缓存信息再关闭所有的activity | ||
3.用户修改用户名:通过映射@PostMapping("/updateInfo")后,服务器端添加消息到链表中,消息属性为status=onlineupdate;content=新名称;sendName=原名称 | ||
发聊天消息
| 点击聊天对象时先向服务器通过用户名获取用户targetId(为了避免有用户修改用户名之后加载不出来历史聊天记录,所以聊天记录通过id来标识) 1.和自己私聊:status=myself;发送方和接收方都是自己 2.和别人私聊:status=others;接收方id为上述的targetId,name为顶部栏标识 3.群聊:status=聊天室;接收方无需定义 | |
接收消息 | 1.为刷新列表:向OnlineListFragment广播, | |
2.为聊天记录:存储到本地Litepal数据库;向ChatActivity广播 | ||
服务器采用锁机制: 服务器端:当消息链表为空时,发送线程阻塞;当有消息需要传送的地方(更新用户名、聊天),唤醒发送线程。 客户端:没采用锁机制,因为不知道什么时候服务器会转发消息来,需要一直监听是否有消息传来。 | ||
服务器 | 收刷新列表消息 | 根据不同情况处理nameList、clientList、messageItem都需要将处理后的nameList拼接作为content 1.用户上线:位于线程的构造函数内 br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); 2.用户下线:用户退出APP、用户注销登录 发送一个字节的紧急数据,用于判定客户端是否还处于连接状态 |
收聊天消息 | 直接加入messageList即可 | |
发送消息 | 当为刷新列表和群聊时:发送给所有客户端 和自己私聊:只发给自己 和别人私聊:发给对象和自己 |
三、可能遇到的问题
1. SpingBoot框架搭建时有哪些细节要注意?
(1)配置generatorConfig.xml时,tableName为数据库的表名或视图名,domainObjectName为实体类名,名称必须对应否则无法自动生成。
(2)需要注解的地方不能漏掉,否则对象注入失败。
(3)服务器配置便于测试,只能使用WLAN下的IPV4地址(处于同一局域网)。
2. 前后端交互时失败有哪些原因?
(1)本机未关闭防火墙。
(2)未连接同一个wifi。
(3)地址映射不正确,对应服务器端的协议类型、地址、端口号、映射地址检查一下。
3. 服务器端Socket编程整个流程是怎么样的?
答:首先用Serversocket配置服务器的端口号,并调用serverSocket.accept()监听socket(等待客户端来连接),若监听到了则为该客户端开启接收线程和发送线程,否则在该监听语句上阻塞。
4. 前后端使用socket传递消息时如何进行数据的获取和类型转化?
答:由于socket是基于文件流进行操作。读出的信息是字符串类型,便于操作我们可以传递json格式的字符串,将交换的消息封装为一个对象MessageItem,它们之间的转化可以导入Gson库来简化操作。这个消息有可能是刷新列表,也可能是聊天的消息,所以设置一个status表示消息类型。
5. 用户修改信息后各客户端的在线用户列表还是原来的名字?
答:当前客户端修改完名字后需要通知服务器端,服务器端接收到后转发给所有客户端有人改名字了,需要刷新列表了。
6. 为何消息列表出现错乱?
(1)在获取当前聊天记录的时候,标识判断不清晰,要根据发送方接收方以及消息状态判断。
(2)有用户修改名字后,虽然在线用户列表刷新了,但其他用户本地数据库的姓名并未改变,所以后来在MessageItem类中加上了SendId和RevId两个字段,获取历史聊天记录以及刷新聊天记录的时候都通过id来获取,这样名字改了也无所谓。
7. 用户退出时虽然finish()了activity但是并没有退出程序,导致用户列表混乱?
答:如果不使用栈来存储activity,就算finish了也可能会有漏网之鱼,所以当要退出的时候调用封装好的ExitApplication类中的exit函数销毁所有的Activity之后并退出系统。
8. 使用OKHttp编程时无法在回调函数内Toast?
答:异步回调函数是在子线程中,不能在子线程中更新UI。