网页聊天室(ChatRoom)项目 保姆级教程

目录

整体演示

准备工作

创建 SpringBoot 项目

配置文件

登录功能

后端

前端

注册功能

后端

前端

获取用户信息功能

后端

前端

获取消息会话列表功能

后端

前端

获取历史消息功能

后端

前端

获取好友列表功能

后端

前端

创建新聊天功能

后端

前端

实时发送消息功能

后端

前端

查询指定用户功能

后端

前端

实时发送好友申请功能

后端

前端

获取好友申请功能

后端

前端

实时接受好友申请功能

后端

前端

实时拒绝好友申请功能

后端

前端

Linux 部署项目

配置文件的修改

数据库的创建

部分源码展示

controller

UserController

FriendController

MessageController

MessageSessionController

service

UserService

FriendService

MessageService

MessageSessionService

mapper

UserMapper

FriendMapper

MessageMapper

MessageSessionMapper

config

WebSocketHandler

WebSocketConfig

component

OnlineUserManager

constant

Constant

model

AddFriend

AddFriendRequest

AddFriendResponse

Friend

Message

MessageRequest

MessageResponse

MessageSession

User

db.sql

完整源码


整体演示

登录页面:

注册页面:

注册账户后,跳转到登录页面,输入正确的账号密码后,进入的主页面:

展示完毕,开始写代码。

准备工作

我的开发环境是 IDEA 2022.1.4 社区版,MySQL 5.7 ,Navicat Premium 15,Postman,Xshell,Visual Studio Code 

创建 SpringBoot 项目

鼠标右键:

再点那个右上角的 m 刷一下。

配置文件

这里把 application.properties 的名字改成 application.yml,我们用 yml 来配置。

把下面这段代码复制进 application.yml 中,然后再改一改。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/chat_room_review?characterEncoding=utf8&useSSL=false
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true  #自动驼峰转换
# 配置打印 MyBatis 执行的 SQL
logging:
  file:
    name: logs/springboot.log

在把前端的模板放进项目中。

准备工作就做完了,可以开始思考每个功能改怎么实现了。

登录功能

首先来设计数据库,登录的话需要一张用户表来存储用户的账号密码信息。

所以用户表有三个字段,分别是 userId,userName,password。

第二是约定接口,后端需要去查询数据库,看看用户名和密码是否匹配。

所以后端需要参数 userName 和 password。

我们可以将整个用户对象返回给前端,这样方便前端展示登录用户的信息。

约定完成,我们就可以来写代码了,先写后端代码。

后端

先在 java 目录下创建个文件 db.sql,我们可以在 dq.sql 中存放我们写的数据库代码。

这样可以方便我们后面将项目部署到服务器,就不用重写数据库代码,而是直接拷贝 db.sql 的代码到数据库中。

首先创建个数据库,用来存放改项目的数据。

然后就是根据上面的分析,创建 user 表,为了方便,我们可以先插入几条数据。

写完之后将代码复制一份,打开 Navicat ,然后将代码粘贴到里面,点运行。

刷新下 mysql ,看看数据库是否存在。

都没什么问题,就可以去写代码了。

首先创建四个包,分别是 controller ,service,mapper,model。

然后在 model 里建个 User 类,该类的属性和 user 表里面的字段一一对应。

加个 @Data ,和构造方法,方便我们开发

然后就可以去写操作数据库的代码了。

先在 mapper 里创建个 UserMapper 接口,用来操作有关 user 表的增删查改等。

加上 @Mapper ,让 Spring 管理 UserMapper

因为是校验账户密码是否正确,所以我们可以写一个根据用户名查找 user 表的方法。

然后我们可以把 service 也一起写了,直接复制方法。

然后在 service 里创建个 UserService 类,来调用 mapper。

加个 @Service 注解方便 Spring 进行管理。

通过 @Autowired 将 UserMapper 注入。

然后写个方法调用 mapper 里的方法即可。

然后再来写 controller,在 controller 里新建个 UserController 类。

加上 @RestController 管理,@RequestMapping 映射路径,再将 UserService 注入。

然后写 login 方法。

先进行参数校验,看看 userName 和 password 是否为空,如果为空,就返回空。

如果都不为空,则查询数据库,根据查询结果看看密码是否一样。

如果一样,就将 user 存进 Session中,然后返回 user。

如果不一样,就返回空。

因为这个 user_session 可能会在多个地方用到,所以我直接打算创建一个 constant 包。

并在包底下创建了个 Constant 类,用来存放常量信息。

然后回 UserController 导一下类即可。

后端写完之后,我们来测试看看有没有问题,启动服务。

好家伙,启动失败了,看看说了啥。

说映射价值不允许啥的,一看,yaml报错,那就是配置文件出错了。

回到 yml 文件一看 ,原来是之前那个自带的 properties 配置忘记删了。

把那行配置删掉后,再次启动服务。

启动成功了。 

打开 postman,输入 url 后,点击右边那个蓝色的 send。

测试结果:

没啥问题,那就来写前端代码。

前端

用 vs code 打开 login.html,然后监视登录按钮的点击情况。

如果登录按钮被点击了,那么就发送 ajax 请求,获取到两个 input 框里面的内容作为参数。

如果参数为空,那当然不能登录,那就弹框提示用户,然后 return。

如果参数不为空,根据上面约定的信息发送 ajax 请求,然后看看后端返回是否为 null。

如果为 null,说明登录失败,弹个框告诉用户。

如果返回 user 对象,说明登录成功,那就直接跳转到聊天主页面,也就是 client.html 。

写出来就是这个样子的:

登录功能写完了,我们先来测试下看下效果。

启动服务,输入 url。

输入正确账号密码:

输入错误密码:

没输入密码:

看起来没什么问题,那接下来就可以写注册功能了。

注册功能

还是先约定接口,因为是直接注册嘛,直接在数据库里插入数据就行。

但是因为之前创建数据库的时候就已经约定 用户名之间不能相同了。

所以代码可能会抛异常,要注意异常的处理。

先写后端代码。

后端

先写 UserMapper 的代码,直接写插入语句即可。

因为返回的是 user 对象,所以还需要获取到 userId。

然后再写 UserService,直接调用下 mapper 里的方法就行。

然后再去写 controller 的方法。

还是先进行参数校验,为空就返回 null。

刚才也说了,代码可能会抛异常,所以我们可以用 try catch 把代码包住。

调用 service ,校验下结果,然后返回即可。

写完后端,可以来测试下看看功能是否正常,启动服务,打开 postman,写 url。

相同用户名:

后台抛异常,没插入到数据库。

创建新账户: 

功能正常,就可以去写前端代码了。

前端

先点开 login.html,监视注册按钮的点击情况,如果注册被点击,那就跳转到 register.html。

在 注册页面完成注册逻辑,逻辑其实跟登录功能差不多。

可以把登录的代码拷贝到注册页面,然后再修改修改。

写完前端后,可以来测试下,启动服务。

点注册可以成功跳转到注册页面:

输入数据库中存在的用户名:

注册账户:

点确定后能跳转到登录页面:

没什么问题,就可以去写下一个功能的代码了。

获取用户信息功能

要获取当前登录用户的名字,可以先约定接口:

后端

我们可以在 session 中获取用户的登录信息,之前登录的时候已经存了 session。

直接在 UserController 类新增一个 getUserInfo 方法,然后从 session 中取 user 信息。

写出来就是这样子的:

然后再去写前端代码。

前端

找到 client.js ,直接定义个函数,然后再调用这个函数。

一进到聊天主页面就触发这个函数,然后发送 ajax 请求。

如果后端返回结果为 null,则说明当前用户尚未登录,那就直接跳转到登录页面。

如果不为空,那就将得到的用户名赋值。

写出来就是这样子的:

写完之后,我们启动服务,来测试下功能。

没登录直接访问聊天主页面:

然后跳转到登录页面,再输入账号密码

点击登录:

可以正确展示,没啥问题就可以去写下一个模块的代码了。

获取消息会话列表功能

从之前的演示上,我们可以看到,消息列表的每一条消息分为两块:

一块是消息会话的名字,一块是消息会话的最后一条消息。 

因为前端只能支持私聊,所以消息会话的名字就是好友的名字。

首先来设计数据库,在上面的分析中,出现了四个对象:用户,会话,消息,好友

可以围绕这四个对象来设计数据库。

首先是用户和会话:

一个用户可以有多个会话,一个会话可以被多个用户所拥有。所以用户和会话是多对多的关系。

然后是用户和消息:

一个用户可以发多条消息,一条消息只能被一个用户所发送。所以用户和消息是一对多的关系。

其次是用户和好友:

一个用户可以有多个好友,一个好友可以被多个用户所拥有。所以用户和好友是多对多的关系,但是好友本身也属于用户。

最后是会话和消息:

一个会话可以有多条消息,一条消息只能存在于一个会话中。所以会话和消息是一对多的关系。

多对多的话得再设计一个关系表来表示他们的关系,一对多的话只用多出一个字段来表示从属关系

所以数据库表可以设计为(用户表已经设计过了):

设计好数据库之后,我们可以约定接口:

然后就可以去写后端代码了。

后端

先写数据库代码,点开 db.sql,按照之前的数据库分析来写 SQL 语句。

写出来就是这样子的:

-- 创建好友表
drop table if exists friend;
create table friend (
    user_id int,
    friend_id int
);

insert into friend values(1, 2);
insert into friend values(2, 1);
insert into friend values(1, 3);
insert into friend values(3, 1);
insert into friend values(1, 4);
insert into friend values(4, 1);



-- 创建会话表
drop table if exists message_session;
create table message_session(
    session_id int primary key auto_increment,
    last_time datetime DEFAULT now()
);
insert into message_session values(1, '2000-05-01 00:00:00');
insert into message_session values(2, '2000-06-01 00:00:00');

-- 创建会话和用户的关联表
drop table if exists message_session_user;
create table message_session_user(
    session_id int ,
    user_id int
);

-- 1 号会话里有张三和 mika
insert into message_session_user values(1, 1), (1, 2);
-- 2 号会话里有李四和 mika
insert into message_session_user values(2, 1), (2, 3);


-- 创建消息表
drop table if exists message;
create table message(
    message_id int primary key auto_increment,
    from_id int not null, -- 消息是哪个用户发的
    session_id int not null, -- 消息发给哪个会话
    content varchar(5000), -- 消息的内容
    post_time datetime default now() -- 消息的发送时间
);

-- mika 和 zhangsan 的对话
insert into message values(1,1,1,"星露谷联机?","2024-03-21 17:00:00");
insert into message values(2,2,1,"来来来!","2024-03-21 17:00:03");
insert into message values(3,1,1,"正好 1.6 更新了!!!","2024-03-21 17:00:07");
insert into message values(4,1,1,"我还没开始玩 1.6 呢","2024-03-21 17:00:12");
insert into message values(5,2,1,"行,那接电话,开麦玩","2024-03-21 17:00:16");
insert into message values(6,1,1,"OK","2024-03-21 17:00:20");


-- mika 和 lisi 的对话
insert into message values(7,1,2,"葬送的芙莉莲完结了,快去看!!!","2024-03-23 18:01:00");
insert into message values(8,3,2,"最近在忙,要学习没空","2024-03-23 18:02:00");
insert into message values(9,1,2,"那就吃饭的时候看呗","2024-03-23 18:02:15");
insert into message values(10,3,2,"忙,没空","2024-03-23 18:03:15");

然后将写好的代码粘贴到 navicat 中运行。

刷新一下表,再点开表看看数据有没有。 

数据都在,就可以继续写后端代码了。

再来看看刚刚约定的接口:

要返回会话列表,那就得先有一个会话类。

先在 model 包下面创建一个 MessageSession 类。

因为前端需要消息会话的名字(好友名字) 和 最后一条消息。

所以这个类的属性需要包括 sessionId,friends(好友列表) ,lastMessage。

还需要一个 Friend 类,那也新建个 Friend 类,里面一个包含 friendId,friendName。

有了这两个类,我们就可以很方便的写代码了。

先写 mapper 代码,新建个 MessageSessionMapper 接口。

因为最终要返回的是消息会话列表,所以就需要三个值:

分别是 会话ID,好友列表,最后一条消息。

因为前端什么参数都没传,所以我们得从 session 中取出 user。

user 中有 userId,我们可以先获取 会话ID 列表,然后再根据对应的 会话ID 获取朋友列表,然后再根据 会话ID 获取最后一条消息。

所以我们得写三个 SQL:

第一个:根据 userId,在 message_session_user 查找 sessionId 列表。

第二个:根据 sessionId 和 userId,在 message_session_user 和 user 中查找 Friend 列表。

第三个:根据 sessionId,在 message 中查找最后一条消息。

分析完了,就可以来写 mapper 了,首先在 mapper 包里创建一个 MessageSessionMapper 接口,加上 @Mapper 注解。

然后根据上面的分析来写代码,写几个联合查询就行。

写出来就是这样子的

写完 mapper 之后去写 service。

还是先创建个 MessageSessionService 类,加上注解。

然后注入依赖,再写三个方法,分别调用 mapper 的方法即可。

最后来写 controller,先创建 MessageSessionController 类,加上对应注解,并注入依赖。

然后根据之前的约定,来写方法。

先校验用户是否登录,没登录就直接返回 null。

然后就是调用方法获取 sessionId 列表,然后遍历列表,调用方法构造 MessageSession 对象,最后添加进 会话列表中,然后返回。

写完后端之后,启动服务,来测试一下。

先登录,再进行接口测试。

没啥问题,接下来写前端的代码。

前端

还是找到 client.js ,定义一个方法,然后调用它。

根据约定的接口来发起 ajax 请求,首先进行原列表的清空,防止多次刷新叠加在一起。

然后判断后端返回结果是否有效,无效就直接返回。

如果有效,就构造 html,然后赋值,顺便把 sessionId 也存到 session 中。

然后启动服务,测试看看效果。

先登录:

能正常显示,说明没什么大问题。

获取历史消息功能

还是先约定接口,前端可以把之前存的 sessionId 传给后端,然后后端返回消息列表。

后端

先写 mapper ,新建一个 MessageMapper 接口,加上 @Mapper 注解。

然后写一个根据 sessionId,查询 message 表里的历史消息的方法。

写到这发现还没有 Message 类,那就得去 model 包里新建个 Message 类。

根据前端的展示结果可以知道,类里必须包含 fromId,fromName,sessionId,content 这四个属性,跟 message 表相比,差了个 fromName,所以我们可以照抄 message 表里面的字段,再多添加一个 fromName 属性即可。

然后我们可以继续写 MessageMapper 里的方法了。

因为多了个 fromName 属性,所以得跟 user 表进行联合查询。

但是这里还得注意一个细节,那就是如果历史消息太多,达到 999+。

一口气展示出来的话,就会非常卡,所以这里我们可以展示最近的 100 条消息。

写出来就是这样子的:

写完 mapper 之后,去写 service。

在 service 底下新建一个 MessageService,加上对应的 @Service,再注入 MessageMapper。

然后调用对应的 mapper 方法即可。

写完 service 之后就可以去写 controller 的代码了。

还是先在 controller 底下新建一个 MessageController 类,并加上对应的注解,并注入依赖。

根据上面约定的接口来写方法。

还是先进行参数校验,如果参数不合法就直接返回 null。

然后就是调用 MessageService 获取历史消息,因为我们希望是按时间顺序排列,所以返回前得先逆置一下顺序表。

写完后端后,启动服务来测试一下。

没什么问题,可以来实现前端代码了。

前端

从 clickSession 开始写起。

点击这个会话,就需要将这个会话高亮,然后获取这个会话的历史消息。

就可以分别写两个方法 activeSession 和 getHistoryMessage ,完成对应的功能。

activeSession 比较好实现,就是遍历所有的 li 标签,看看哪一个标签和当前标签一样。

如果一样,就设置被选中状态,如果不一样,就设置为空。

然后来实现 getHistoryMessage 方法。

首先清空右边显示消息的区域,然后还需要重新设置会话名字,直接从左边的会话展示区拿即可。

然后根据之前的约定,发送 ajax 请求,然后进行进行添加消息到页面上即可。

但是这里还要注意,要自动将消息的滚动条拖到最下面才行。

写出来就是这样子。

然后再分别实现 addMessage 和 scrollBottom 方法。

先来实现 addMessage 方法。

就是构造标签,然后判断消息是自己发的还是别人发的,对消息进行调整,最后将消息添加到页面上即可。

然后再来实现 scrollBottom 方法,滚动消息内容的高度与消息展示区域的高度之差即可。

这部分的前端就写完了,可以启动服务测试下看看。

先登录进聊天主页面后,点击会话查看历史消息:

没什么问题就可以进行下个功能的开发了。

获取好友列表功能

还是先约定接口:

约定完了就可以去写后端代码啦。

后端

先写 mapper,创建一个 FriendMapper 接口,加上 @Mapper。

根据 userId,从 friend 表和 user 表中查询对应的好友信息。

写一个联合查询就行,连接条件是 friendId 和 userId。

写出来就是这样子的。

写完 mapper 后,可以来写 service。

在 service 底下创建一个 FriendService,加上注解并注入依赖,然后调用下 mapper 的方法即可。

然后再来写 controller。

在 controller 底下创建一个 FriendController ,并加上注解并注入依赖。

根据之前约定的接口来写方法。

还是先进行登录的校验,如果没登录,那就返回 null。

如果登录了,那就调用 FriendService 的方法,然后返回即可。

后端写完了,就可以启动服务来测试下。

先登录,然后再测试接口。

没啥问题,可以来写前端代码了。

前端

首先得实现标签页的切换。

监视两个按钮的点击情况,如果一个被点击,那么被点的那个就设置成高亮,另一个就取消高亮,并且将对应的列表隐藏,而被点击的那个列表就展示出来。

然后再来写获取好友列表的方法。

直接定义一个方法,发送 ajax 请求,然后清空之前所获取的好友列表(防止叠加),然后构造标签,添加到页面上。

写完前端之后,可以启动服务看看效果。

登录之后,进入主页面,点击好友列表,看看是否高亮。

没啥问题,可以去写下一个功能了。

创建新聊天功能

还是先约定接口:

需要告诉后端,会话里面的用户有谁。

后端

先写 mapper,直接再 MessageSessionMapper 里面写,就是直接创建一个消息会话,然后返回会话ID ,除此之外,还需要在 message_session_user 中插入对应的记录。

所以得写两条 sql 语句。

写完 mapper 之后,就可以去写 service,直接调用 mapper 里面的方法就行。

写完 service 之后就可以写 controller 了,根据之前约定的接口来写方法。

还是先进行参数校验以及登录校验,如果参数不合法或者用户没登录,那就直接返回 null。

如果校验通过,那就调用创建消息会话的方法,获取到 sessionId ,然后调用刚刚写的另一个方法把记录存到消息会话用户表里。因为这两条 sql 必须一起成功(原子性)才成功,所以得加个事务的注解,最后再返回 sessionId 即可。

写完之后,启动服务来测试下。

先登录再来测试接口:

没什么问题,就可以来写前端了。

前端

继续来完成刚刚前端写的 clickFriend 方法。

首先得判断下是否以及存在同样的会话了,如果以及存在对话,那就将对应对话设置为高亮并顶置该对话,然后获取该对话的历史消息。如果不存在,那就得创建一个会话,然后将这个会话顶置并设置高亮,最后跳转到消息会话列表。

然后再来写 createSession 方法。

根据之前约定的接口,调用 ajax 请求就行。

写完前端后,启动服务看看效果。

登录之后:

然后点击 zhangsan: 

没什么问题,然后从数据库里面删掉和 wangwu 的会话。

然后刷新下页面。

点击好友列表的 wangwu:

没什么问题,就可以去写下一个功能了。

实时发送消息功能

要实现实时发送消息的话,我们就得用一个新的协议:WebSocket 协议。

WebSocket 是从 HTML5 开始⽀持的⼀种网页端和服务端保持长连接的消息推送机制。

那么什么是消息推送机制呢?

一般主动发起请求的叫做客户端,被动接受请求的叫做服务器。

而我们现在的情况则是需要:A 给服务器发请求,然后服务器给 B 返回响应。

如果让 HTTP 来实现消息推送,那将会非常麻烦,所以我们可以使用 WebSocket 协议。

要使用 WebSocket 协议,我们就得先了解 WebSocket 协议。

可以在浏览器上搜索 websocket rfc 6455

出来的第一个就是,点进去,发现全是英文,这时候不要慌,往下滑能看到目录。

打开有道在线翻译,先将一段目录复制到在线翻译中。

我们先来看报文格式,返回 rfc ,点 5.2 的链接跳转到基础数据帧协议这里。

可以看到有这样的一张表。

往下滑能看到解释,可以把解释复制到有道翻译里面一个个去看,就能知道:

了解玩数据报后,我们来看看 WebSocket 的握手过程。

最初 WebSocket 也是从 HTTP 升级而来的。

101 表示协议切换,这样就完成了 WebSocket 的握手操作了,后续客户端和服务器就会使用 WebSocket 进行操作了,HTTP 与 WebSocket 的关系可以用下面这个例子来表示:

有一天,你喜欢的人突然问你要你的室友的微信,你兴奋的把室友的微信给了你喜欢的人,那么你喜欢的人就和你的室友聊起来了,就没你什么事了。

而在这之中,你的作用就是:工具人。

HTTP 同理,也是工具人,所以 WebSocket 和 HTTP 是并列关系。

学习完 WebSocket 之后,我们就可以写代码了。

还是先约定前后端交互接口,websocket 是使用对象进行传输消息的。

 约定完了,我们就可以来写代码了。

后端

先创建一个 config 包,在里面创建一个 WebSocketHandler 类,用来处理 websocket 发送的请求

加上对应注解,让 Spring 管理这个对象。

让这个类继承 TextWebSocketHandler ,然后重写(快捷键 ctrl + o )里面的四个方法。

然后点 OK。

然后将这个类注册到 Spring 中,并配置对应的路径。

在 config 包下创建一个 WebSocketConfig 类,实现 WebSocketConfigurer 类,然后重写里面的方法。

所以我们得创建一个新的类,用来存放它们的映射关系。

新建一个 component 包,在包底下新建个 OnlineUserManager 类,加上@Component 注解。

我们可以用哈希表来存储映射关系,当用户一上线,那就存映射,下线就删除映射,然后再写个根据 userId 取 session 的方法。

写出来就是这样子的:

// 记录 userId 与 WebSocketSession 的映射
@Slf4j
@Component
public class OnlineUserManager {
    private Map<Integer, WebSocketSession> sessions = new HashMap<>();

    // 用户上线方法
    public void online(Integer userId, WebSocketSession webSocketSession) {
        // 参数校验
        if (userId == null || userId < 1 || webSocketSession == null) return;
        // 先看看用户是否已经在线,如果已经在线,那就啥都不干(防止多开)
        WebSocketSession session1 = sessions.get(userId);
        if (session1 != null) return;

        sessions.put(userId, webSocketSession);
        log.info("用户" + userId + "上线");
    }

    // 用户下线方法
    public void offline(Integer userId, WebSocketSession session) {
        if (userId == null || userId < 1) {
            return;
        }
        WebSocketSession existSession = sessions.get(userId);
        if (session == existSession) {
            // 如果这俩 session 是同一个,才真正进行下线操作,否则啥都不干(从多开角度考虑)
            sessions.remove(userId);
            log.info("[" + userId + "] 用户下线!");
            return;
        }
    }

    // 根据 userId 获取 WebSocketSession
    public WebSocketSession getWebSocketSessionById(Integer userId) {
        if (userId == null || userId < 1) {
            return null;
        }
        return sessions.get(userId);
    }
}

同时,我们也需要回到 WebSocketConfig 类,注入 OnlineUserManager 的依赖,然后实现用户上线和下线的逻辑。

还是先校验用户是否登录,从 session 中获取 user 对象。

校验成功,就存储映射。

用户下线也是同理,先判断是否登录,然后再删除映射。

写完准备工作之后,就可以写处理发送消息的业务代码了。

因为前端传过来的是 JSON 格式的数据,所以我们可以注入 ObjectMapper 这个依赖,将传过来的 JSON 数据转化为对象,所以我们得创建两个类,MessageRequest 和 MessageResponse。

对象的属性可以直接照抄前面的约定。

然后就可以开始重写 handleTextMessage 方法了。

还是先判断用户是否登录,如果校验成功,那就将前端传来的数据转化为对象,然后创建一个方法来处理。

首先定义 transferMessage 方法,最后要发送的也是 JSON 数据,所以我们可以先创建一个 MessageResponse 对象,然后设置里面的属性,然后转成 json 再发送即可。

因为是要发送给这个会话里的所有人(消息发送者也得发一份,方便前端展示),

所以我们可以调用之前写过的根据 userId 和 sessionId 获取好友列表的方法,那就需要注入 MessageSessionService 的依赖了。然后调用方法获取好友列表,并将用户信息也加入进好友列表,然后遍历好友列表,然后根据 friendId 获取 session,然后调用 session.send 发送 json 即可,最后还需要将发送的消息记录给保存到数据库中。

所以我们需要在 MessageMapper 中写一个新增消息的方法。

写完 mapper 之后,去写 service,service直接调用一下刚刚写完的方法即可。

然后将 MessageService 的依赖注入,调用刚刚写好的方法。

    private void transferMessage(User user, MessageRequest messageRequest) throws IOException {
        // 1. 先构造一个 MessageResponse 对象
        MessageResponse messageResponse = new MessageResponse(user.getUserId(), user.getUserName(), messageRequest.getSessionId(), messageRequest.getContent());
        String respJson = objectMapper.writeValueAsString(messageResponse);
        // 2. 根据 sessionId,获取好友列表
        List<Friend> friends = messageSessionService.getFriendsBySessionId(messageResponse.getSessionId(), user.getUserId());
        // 同时给自己也要发一份消息,方便前端展示
        friends.add(new Friend(user.getUserName(), user.getUserId()));
        // 3. 根据 userId 获取 WebSocketSession 对象,进行消息转发
        for (Friend friend : friends) {
            WebSocketSession session = onlineUserManager.getWebSocketSessionById(friend.getFriendId());
            if (session == null) {
                // 用户不在线
                continue;
            }
            session.sendMessage(new TextMessage(respJson));
        }
        // 4. 保存消息记录
        Integer result = messageService.addMessage(new Message(user.getUserId(), user.getUserName(), messageResponse.getSessionId(), messageResponse.getContent()));
        if (result == null || result < 1) {
            log.error("增加消息到数据库失败");
        }
    }

这样一来,后端就写完了,然后就可以去写前端代码了。

前端

可以根据上面的提示来写前端代码。 

可以监视发送按钮按钮的点击情况,判断消息内容是否为空,以及是否选中某个对话,然后获取 sessionId 以及消息内容,构造出 json 对象,然后使用 send 发送请求。

然后后端返回响应,我们就需要使用 websocket,可以根据下面的提示来写前端代码。 

首先创建一个 WebSocket 的对象,被初始化 url 。

然后设置回调函数,在 onmessage 的方法里写接收消息的逻辑。

先进行 type 判断,如果 type 为 message,则进行消息的处理。

然后再来写 handleMessage 函数。

把收到的消息展示在页面上。

function handleMessage(resp) {
    // 把客户端收到的消息, 给展示出来. 
    // 展示到对应的会话预览区域, 以及右侧消息列表中. 

    // 1. 根据响应中的 sessionId 获取到当前会话对应的 li 标签. 
    //    如果 li 标签不存在, 则创建一个新的
    let curSessionLi = findSessionLi(resp.sessionId);
    if (curSessionLi == null) {
        // 就需要创建出一个新的 li 标签, 表示新会话. 
        curSessionLi = document.createElement('li');
        curSessionLi.setAttribute('message-session-id', resp.sessionId);
        // 此处 p 标签内部应该放消息的预览内容. 一会后面统一完成, 这里先置空
        curSessionLi.innerHTML = '<h3>' + resp.fromName + '</h3>'
            + '<p></p>';
        // 给这个 li 标签也加上点击事件的处理
        curSessionLi.onclick = function () {
            clickSession(curSessionLi);
        }
    }
    // 2. 把新的消息, 显示到会话的预览区域 (li 标签里的 p 标签中)
    //    如果消息太长, 就需要进行截断. 
    let p = curSessionLi.querySelector('p');
    p.innerHTML = resp.content;
    if (p.innerHTML.length > 10) {
        p.innerHTML = p.innerHTML.substring(0, 10) + '...';
    }
    // 3. 把收到消息的会话, 给放到会话列表最上面. 
    let sessionListUL = document.querySelector('#session-list');
    sessionListUL.insertBefore(curSessionLi, sessionListUL.children[0]);
    // 4. 如果当前收到消息的会话处于被选中状态, 则把当前的消息给放到右侧消息列表中. 
    //    新增消息的同时, 注意调整滚动条的位置, 保证新消息虽然在底部, 但是能够被用户直接看到. 
    if (curSessionLi.className == 'selected') {
        // 把消息列表添加一个新消息. 
        let messageShowDiv = document.querySelector('.right .message-show');
        addMessage(messageShowDiv, resp);
        scrollBottom(messageShowDiv);
    }
    // 其他操作, 还可以在会话窗口上给个提示 (红色的数字, 有几条消息未读), 还可以播放个提示音.  
    // 这些操作都是纯前端的. 实现也不难, 不是咱们的重点工作. 暂时不做了. 
}

function findSessionLi(targetSessionId) {
    // 获取到所有的会话列表中的 li 标签
    let sessionLis = document.querySelectorAll('#session-list li');
    for (let li of sessionLis) {
        let sessionId = li.getAttribute('message-session-id');
        if (sessionId == targetSessionId) {
            return li;
        }
    }
    // 啥时候会触发这个操作, 就比如如果当前新的用户直接给当前用户发送消息, 此时没存在现成的 li 标签
    return null;
}

写完前端后可以启动服务测试看看效果。

可以实时传送消息,再来看看离线的结果。

wangwu 在 mika 离线的时候给 mika 发消息。

mika 上线也能收到消息。

没什么问题,接下来就是可以实现最后一个添加好友的大模块了。

查询指定用户功能

大概就是图上所展示的样子。

还是先约定接口:

约定完毕,就可以去写后端代码了。

后端

先写 mapper,在 UserMapper 底下新增一个方法。

根据 userName,模糊查询 user 表里的用户即可。

写完 UserMapper 后,就去写 service,直接调用刚刚写的方法就行。

写完 service,就可以去写 controller 了。

先进行参数校验,然后调用 service,最后返回结果就行。

写完后端可以启动服务测试下。

没啥问题,可以去写前端了。

前端

写一个方法,监视搜索按钮的点击情况,获取到搜索框的输入内容,进行参数校验,然后发送 ajax 请求,再将返回的响应展示到页面上,但是这里要注意一点,用户搜索结果不能展示用户自己,毕竟添加好友哪有自己加自己的。

function searchFriends() {
    let sendButton = document.querySelector('.search button');
    sendButton.onclick = function () {
        var targetName = $(".search #userNmae").val();
        if (targetName == null || targetName == '') {
            console.log("targetName == null");
            return;
        }
        console.log("targetName: " + targetName);
        $.ajax({
            type: "get",
            url: "/user/getUsersByName",
            data: {
                userName: targetName
            },
            success: function (result) {
                console.log(result);
                if (result == null) {
                    alert("没有你要查找的这号人物");
                    return;
                }
                // 先清空右侧列表中的已有内容
                var titleDiv = document.querySelector('.right .title');
                titleDiv.innerHTML = '';
                var messageShowDiv = document.querySelector('.right .message-show');
                messageShowDiv.innerHTML = '';
                var selfUsername = document.querySelector('.left .user').innerHTML;
                console.log("selfUsername: " + selfUsername);
                titleDiv.innerHTML = "好友搜索结果";
                var finalHtml = '';
                for (var user of result) {

                    if (user.userName != selfUsername) {
                        // 排除自己
                        finalHtml += '<div class="message">';
                        finalHtml += '<tr>';
                        finalHtml += '<span>' + user.userName + '</span>';
                        finalHtml += '<th> <input id=reason' + user.userId + ' type="text" placeholder="添加理由"></th>';
                        finalHtml += '<td> <button class="btn btn-primary" onclick="addFriend(\'' + user.userId + '\')"> 添加好友 </button> </td>';
                        finalHtml += "</tr>"
                        finalHtml += "</div>"
                    }

                }
                messageShowDiv.innerHTML = finalHtml;
                console.log(messageShowDiv);
            }
        });

    }
}

searchFriends();

前端也写完了,启动服务,测试一下看看效果。

没啥问题,就可以去写下一个功能了。

实时发送好友申请功能

首先设计数据库,得设计一个表,来记录好友申请。

要实时的话,就得用 WebSocket。

约定前后端交互接口,使用对象来传输数据。

约定好对象之后,就可以去写后端代码了。

后端

首先写创建数据库,按照之前设计的来写 sql。

写出来就是这样的,然后在把刚刚写好的代码粘贴到数据库里。

点击运行,然后刷新一下表。

没什么问题就可以继续写代码了

还是先按照约定的接口来新建请求和响应的对象。

我们可以在 model 包底下新建两个类:AddFriendRequest 和 AddFriendResponse。

并加上对应的注解,然后按照刚刚约定来写属性。

写完之后就可以去 WebSocketHandler 里补充 handleTextMessage 的逻辑的。

但是在这里面,还需要注意一个特殊的小问题。

所以我们得在 MessageRequest 里补充新的属性。

然后我们就可以继续写代码了。

我们根据 type 是否为 ‘addFriendRequire’ 来判断是否是发送好友申请的请求,如果是,则将传过来的 json 转化成 AddFriendRequest 对象,新开一个方法来处理。

创建 sendAddFriendRequire ,在这个方法里写要实时发送好友申请的功能。

还是先进行参数的校验,参数校验完毕后,要根据 friendId 获取到对应的 session,然后判断 session 是否为空,为空就说明用户不在线,就直接将好友申请保存在数据库中。如果 session 不为空,那么就构造响应对象,将其转化为 json 格式发送,然后再将好友申请保存到数据库中。

所以根据上面的思路,我们还需要新建一个对象,对象的属性对应数据库中的 add_friend 字段。

在 model 包底下新建个 AddFriend 类,属性照着数据库抄就行。

然后按照上面的思路来写方法,还要注入 FriendService 的依赖。

写完之后,我们去 FriendMapper 新增对应的方法,写一个新增记录的 sql 就行。

写出来就是这样的:

然后写 service,在 FriendService 调用一下刚刚写的方法就行。

写到这里我还发现了一个小小的问题,如果用户之前以及发送了好友申请,现在又重新发送一遍,或者用户和目标好友已经是好友关系了,那还发送好友申请,显然这是不科学的,所以还得新增两个方法:1. 判断用户与目标好友是否是好友关系 2. 查询数据库中是否存在相同的好友申请。

回到 FriendMapper,写根据 userId 和 friendId ,查询 friend 表记录 和 根据 userId 和 friendId,查询 add_friend 表。

然后写 service,调用一下方法即可。

然后返回 sendAddFriendRequire 方法,在参数校验完毕后调用这两个方法。

    private void sendAddFriendRequire(User user, AddFriendRequest req) throws IOException {
        if (user == null || req == null) return;
        // 先看看数据库中是否已经是好友关系了,如果是,那就不用添加了
        Friend friendShip = friendService.isFriendShip(user.getUserId(), req.getFriendId());
        if (friendShip != null && friendShip.getFriendId() > 0) {
            return;
        }
        // 再看看好友申请是否已经在数据库中存在
        AddFriend sameAddFriend = friendService.FindSameAddFriendRequired(user.getUserId(), req.getFriendId());
        if (sameAddFriend != null) return;

        // 1. 先构造一个响应
        AddFriendResponse resp = new AddFriendResponse(req.getAddReason(), req.getFriendId(), user.getUserName(), user.getUserId());
        // 2. 根据 friendId 获取 session
        WebSocketSession session = onlineUserManager.getWebSocketSessionById(req.getFriendId());
        // 构建好友申请对象
        AddFriend addFriend = new AddFriend();
        addFriend.setFromId(user.getUserId());
        addFriend.setTargetId(req.getFriendId());
        addFriend.setAddReason(resp.getAddReason());
        if (session == null) {
            // 用户不在线,则不发送,将好友申请保存到数据库中
            friendService.addFriendRequired(addFriend);
            return;
        }
        // 3. 将响应转化成 json 格式,然后发送
        String respJson = objectMapper.writeValueAsString(resp);
        session.sendMessage(new TextMessage(respJson));
        // 4. 将好友申请保存到数据库中
        friendService.addFriendRequired(addFriend);
    }

写完后端来写前端代码。

前端

直接写 addFriend 函数,根据上面的约定来发送请求。

先获取参数并进行参数判定,然后构造 json 对象,然后发送请求。

然后再来写接收响应的情况。

找到 websocket.onmessage 的回调函数,然后判断响应数据的 type 是否为 ‘addFriendRequire’,如果是,则新增一个方法来处理好友请求的实时展示。

写出来就是这样的:

function handleAddFriendRequire(addRequire) {
    console.log("收到申请好友请求");
    console.log(JSON.stringify(addRequire));
    // 1. 清空之前的会话列表
    let sessionListUL = document.querySelector('#session-list');
    if (addRequire == null) return;
    // 2. 实时发送一条好友申请 针对结果来构造页面
    // 针对 addReason 的长度进行截断处理
    if (addRequire.addReason != null && addRequire.addReason.length > 10) {
        addRequire.addReason = addRequire.addReason.substring(0, 10) + '...';
    }

    let li = document.createElement('li');
    // 把会话 id 保存到 li 标签的自定义属性中. 
    li.innerHTML = '<h3>' + addRequire.fromName + '</h3>'
        + '<p>' + addRequire.addReason + '</p>'
        + '<td> <button class="btn btn-primary" onclick="acceptAddFriendRequire(\'' + addRequire.fromId + '\')"> 接受 </button> </td>'
        + '<td> <button class="btn btn-primary" onclick="rejectAddFriendRequire(\'' + addRequire.fromId + '\')"> 拒绝 </button> </td>';
    sessionListUL.appendChild(li);
    console.log(sessionListUL);

}

前端写完后启动服务测试一下看看效果。

点击添加好友之后确实能实时收到好友申请。

又试了另外一个。

没啥问题,可以去写下一个功能啦。

获取好友申请功能

还是先约定接口:

约定完了就可以去写后端代码啦。

后端

因为前端需要将好友申请展示到页面上,所以我们需要申请者名字以及申请原因。

先写 mapper,在 FriendMapper 底下写一个根据 userId ,查询 add_friend 和 user 的联合查询即可。

因为需要申请者名字,所以得在 AddFriend 类新增一个 fromName 属性。

然后再来写联合查询,

@Select("select af.*, u.user_name as fromName from add_friend af, user u where af.target_id = #{targetId} and u.user_id = af.from_id")
List<AddFriend> getAddRequire(Integer targetId);

然后再写 service,调用一下刚刚写的方法即可。

然后再来写 controller,直接按照刚刚的约定写就行。

还是先进行登录校验,校验通过再来调用方法,最后返回即可。

后端写完了启动服务来测试一下。

先登录,然后再测试接口。

没啥问题,可以去写前端代码了。

前端

因为这个方法是一进入主页面就调用的,所以我们可以把这个方法写的靠前面一点。

按照前面的约定来发送 ajax 请求,然后校验返回的响应参数,最后将参数展示到列表上即可。

// 通过这个函数来达成用户一上线就能看到好友申请的效果(离线用户)
function getAddFriendRequire() {
    console.log("[getAddFriendRequire]");
    $.ajax({
        type: "get",
        url: "friend/getAddRequire",
        success: function (result) {
            if (result != null) {
                // TODO 将获取的好友申请展示到页面上
                let sessionListUL = document.querySelector('#session-list');
                for (let addRequire of result) {
                    // 针对 addReason 的长度进行截断处理
                    if (addRequire.addReason.length > 10) {
                        addRequire.addReason = addRequire.addReason.substring(0, 10) + '...';
                    }

                    let li = document.createElement('li');
                    li.style = 'height: 100px';
                    // 把会话 id 保存到 li 标签的自定义属性中. 
                    li.innerHTML = '<h3>' + addRequire.fromName + '</h3>'
                        + '<p>' + addRequire.addReason + '</p>'
                        + '<td> <button class="btn btn-primary" onclick="acceptAddFriendRequire(\'' + addRequire.fromId + '\')"> 接受 </button> </td>'
                        + '<td> <button class="btn btn-primary" onclick="rejectAddFriendRequire(\'' + addRequire.fromId + '\')"> 拒绝 </button> </td>';
                    sessionListUL.appendChild(li);
                }
                console.log(sessionListUL);
            }
        }
    });

}

getAddFriendRequire();

写完前端之后,就可以启动服务测试下看看啦。

刚刚测试的时候发现了一些问题,就是明明我这个好友申请已经创建好了,但是在页面展示的时候却没展示到页面上,后端的数据也是正常返回的,但是就是在当用户有对话列表的时候,好友申请展示不出来,但是只有好友申请的列表却能正常展示(也就是上面那种情况是正常的)。所以我怀疑是会话列表展示的前端除了问题。

所以我直接把这段代码复制,然后注释掉这段代码,把代码粘贴到我的获取好友列表里。

 

这样,页面就能正常展示了 

没啥问题,就可以去写下一个功能啦。

实时接受好友申请功能

因为是要实现实时接受好友申请功能,所以还是使用 websocket 来完成。

使用 json 来传输数据。

先约定接口:

约定完毕就可以去写后端代码啦。

后端

还是回到 WebSocketHandler,补充 handleTextMessage 的逻辑,首先在 AddFriendRequest 补充新的属性,

然后判断 type 如果是 acceptAddFriendRequire ,就写一个新的方法来处理实时接受好友申请。

然后实现 acceptAddFriendRequire 方法即可。

还是先进行参数校验,然后构建响应,这里我就直接用之前写的 AddFriendResponse,给它补上新的属性。

构造完响应之后,就可以来将好友信息存进数据库,并将之前的好友申请记录删掉了。这个操作是原子性的,所以得加上事务注解。

然后根据 fromId 来获取 session,如果 session 为空,说明申请发送者不在线,直接返回,如果不为空,那么就将刚刚的 java 对象转换成 json 字符串来发送。

然后去 FriendMapper 实现操作即可。

然后去实现 service,调用刚刚实现的方法即可。

写完后端后,就可以去写前端了。

前端

直接实现 acceptAddFriendRequire 方法即可,根据约定构造对象,然后使用 websocket 发送即可。

// 接受好友申请
function acceptAddFriendRequire(fromId) {
    // 记录 friendName
    var selfUsername = document.querySelector('.left .user').innerHTML;
    console.log("[acceptAddFriendRequire] fromId: " + fromId);
    console.log("[acceptAddFriendRequire] websocket...");
    alert("添加成功");
    // 实时告诉对方申请好友成功
    let req = {
        type: 'acceptAddFriendRequire',
        fromId: fromId
    };
    req = JSON.stringify(req);
    console.log("接受好友申请");
    console.log("[websocket] send: " + req);
    // d) 通过 websocket 发送消息
    websocket.send(req);

    location.href = "client.html";
}

然后去实现 websocket.onmessage 的回调函数。

判断 type 是否为 acceptAddFriendRequire,如果是,就写个方法提示 xxx 接受了你的好友申请!即可。

写完前端之后可以启动服务测试下看看效果。

没啥问题,可以去写下一个功能啦。

实时拒绝好友申请功能

这个功能的逻辑和刚刚写的实时接受好友申请逻辑差不多,我们改一下 type ,直接拿过来用

还是先约定接口:

因为是拒绝好友申请,那直接把好友申请记录从数据库里删除即可。

后端

直接抄一下刚刚的代码,然后再改一改。

rejectAddFriendRequire 这个方法也是直接抄刚刚写过的,再改一改即可,把添加好友关系的删了就行。

写完后端,就可以去写前端了。

前端

可以根据约定来发送请求,也可以直接抄刚刚前端写的代码,然后把 type 改一下就行。

写完前端后可以启动服务测试下看看。

好耶!没问题,终于把这个项目给写完了,然后就可以去部署项目啦!!!

Linux 部署项目

配置文件的修改

觉得要手动改配置文件很麻烦,所以就利用 maven 来帮我们打包时打正确的配置文件。

首先将 application.properties 复制两份出来,然后改名字。

改成这种形式的  application-xxxx.yml

将 dev 后缀的配置文件作为开发环境的配置文件,而 prod 为部署时发布的配置文件。

然后就可以将 application.yml 里面的内容全删掉了。

然后点开 pom.xml 文件。

将下面这段文件配置粘贴到 pom 文件中。

    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <!--自定义配置-->
                <profile.name>dev</profile.name>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <!--自定义配置-->
                <profile.name>prod</profile.name>
            </properties>
        </profile>
    </profiles>

然后刷新一下,就是点右上角的 m。

此时再点开 maven,就会发现有个新的选项:

但是还有一步,就是原来的配置文件也要改一下。

在原来的配置文件加上:

spring:
  profiles:
    active: @profile.name@

然后把你的 prod 改一改,包括你的数据库密码等等。

然后就可以打包了。

把包的路径复制粘贴到文件路径中。

这个就是你刚才打的包,然后点开 Xshell,连上你的云服务器。

数据库的创建

打开你的 xshell ,连上云服务器后,输入 :

mysql -uroot -p

输入正确的密码后,进入你的数据库。

把你的 idea 上的 那个 sql 文件的内容全部复制,然后粘贴到云服务器的数据库里。

粘完之后看看对应的表,数据是否完成,有无错漏。

没什么问题就可以输入 exit;  退出数据库。

然后找个地方,把你刚刚打好的包拖动到 Xshell 中。

直接把包拖过去。 

存在相同名字的 jar 包,说明已经传输完毕。

接下来就用命令启动服务即可。

输入 :

nohup  命令  &

输入这个:

会弹出这个。

 然后新开一个窗口,输入 :

ps -aux | grep java

看到上面这张图,也就是存在你的包的名字,那就是部署成功了。

接下来就是查看你云服务器的 ip。

然后用你云服务器的 ip 去访问即可。

最后再测测功能是否正常即可。 

部分源码展示

controller

UserController

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/login")
    public User login(String userName, String password, HttpServletRequest request) {
        if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
            log.error("[login] 用户名或密码为空");
            return null;
        }
        // 用户登录成功,就存 session
        User user = userService.selectUserByName(userName);
        if (user == null || user.getUserId() < 1) {
            log.error("用户不存在");
            return null;
        }
        if (password.equals(user.getPassword())) {
            user.setPassword("");
            request.getSession(true).setAttribute(Constant.USER_SESSION, user);
            return user;
        }
        return null;
    }


    @RequestMapping("/register")
    public User register(String userName, String password) {
        if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
            log.error("[registry] 用户名或密码为空");
            return null;
        }
        User user = null;
        try {
            user = new User(userName, password);
            Integer result = userService.insertUser(user);
            log.info("register, result: " + result);
            user.setPassword("");
            if (result > 0) return user;
        } catch (Exception e) {
            log.error("e:{}", e);
            return null;
        }
        return null;
    }


    @RequestMapping("/getUserInfo")
    public User getUserInfo(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }
        User user = (User) session.getAttribute(Constant.USER_SESSION);
        if (user == null) return null;
        user.setPassword("");
        return user;
    }

    @RequestMapping("/getUsersByName")
    public List<User> getUsersByName(String userName) {
        if (!StringUtils.hasLength(userName)) {
            return null;
        }
        List<User> users = userService.getUsersByName(userName);
        if (users == null || users.size() == 0) return null;
        return users;
    }
}

FriendController

@Slf4j
@RestController
@RequestMapping("/friend")
public class FriendController {
    @Autowired
    private FriendService friendService;

    @RequestMapping("/getFriendList")
    public List<Friend> getFriendList(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }
        User user = (User) session.getAttribute(Constant.USER_SESSION);
        if (user == null) return null;
        return friendService.getFriendsByUserId(user.getUserId());

    }

    @RequestMapping("/getAddRequire")
    public List<AddFriend> getAddRequire(HttpServletRequest request) {
        // 首先判断是否登录
        HttpSession session = request.getSession(false);
        if (session == null) {
            log.error("用户未登录!");
            return null;
        }
        User user = (User) session.getAttribute(Constant.USER_SESSION);
        if (user == null || user.getUserId() < 1) {
            log.error("用户未登录!");
            return null;
        }
        // 根据 target_id 查找数据库
        List<AddFriend> addRequires = friendService.getAddRequire(user.getUserId());
        return addRequires;
    }

}

MessageController

@RestController
@RequestMapping("/message")
public class MessageController {
    @Autowired
    private MessageService messageService;

    @RequestMapping("/getMessageList")
    public List<Message> getMessageList(Integer sessionId) {
        if (sessionId == null || sessionId < 1) {
            return null;
        }
        List<Message> messages = messageService.getHistoryBySessionId(sessionId);
        Collections.reverse(messages);
        return messages;
    }
}

MessageSessionController

@RestController
@RequestMapping("/messageSession")
public class MessageSessionController {
    @Autowired
    private MessageSessionService messageSessionService;


    @RequestMapping("/getSessionList")
    public List<MessageSession> getSessionList(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }
        User user = (User) session.getAttribute(Constant.USER_SESSION);
        if (user == null) return null;

        List<Integer> sessionIds = messageSessionService.getSessionByUserId(user.getUserId());
        List<MessageSession> ret = new ArrayList<>();

        for (Integer sessionId : sessionIds) {
            MessageSession messageSession = new MessageSession();
            messageSession.setSessionId(sessionId);
            List<Friend> friends = messageSessionService.getFriendsBySessionId(sessionId, user.getUserId());
            messageSession.setFriends(friends);
            String lastMessage = messageSessionService.getLastMessageBySessionId(sessionId);
            // 这里还得判断,如果没有最后一条消息,说明这个会话是新创建的
            if (!StringUtils.hasLength(lastMessage)) {
                messageSession.setLastMessage("");
            } else {
                messageSession.setLastMessage(lastMessage);
            }

            ret.add(messageSession);
        }
        return ret;
    }


    @Transactional
    @RequestMapping("/createSession")
    public Integer addMessageSession(Integer toUserId, @SessionAttribute(Constant.USER_SESSION) User user) {
        if (toUserId == null || toUserId < 1 || user == null || user.getUserId() < 1) return null;
        // 新增会话
        MessageSession messageSession = new MessageSession();
        Integer ret1 = messageSessionService.addMessageSession(messageSession);
        if (ret1 == null || ret1 < 1 || messageSession.getSessionId() == null) return null;
        // 插入会话信息
        Integer ret2 = messageSessionService.addMessageSessionUser(messageSession.getSessionId(), toUserId);
        Integer ret3 = messageSessionService.addMessageSessionUser(messageSession.getSessionId(), user.getUserId());
        if (ret2 + ret3 < 2) return null;

        return messageSession.getSessionId();
    }

}

service

UserService

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public User selectUserByName(String userName) {
        return userMapper.selectUserByName(userName);
    }

    public Integer insertUser(User user) {
        return userMapper.insertUser(user);
    }

    public List<User> getUsersByName(String userName) {
        return userMapper.getUsersByName(userName);
    }
}

FriendService

@Service
public class FriendService {
    @Autowired
    private FriendMapper friendMapper;

    public List<Friend> getFriendsByUserId(Integer userId) {
        return friendMapper.getFriendsByUserId(userId);
    }

    public Integer addFriendRequired(AddFriend addFriend) {
        return friendMapper.addFriendRequired(addFriend);
    }

    public Friend isFriendShip(Integer userId, Integer friendId) {
        return friendMapper.isFriendShip(userId, friendId);
    }

    public AddFriend FindSameAddFriendRequired(Integer fromId, Integer targetId) {
        return friendMapper.FindSameAddFriendRequired(fromId, targetId);
    }

    public List<AddFriend> getAddRequire(Integer targetId) {
        return friendMapper.getAddRequire(targetId);
    }

    public Integer insertFriend(Integer userId, Integer friendId) {
        return friendMapper.insertFriend(userId, friendId);
    }

    public Integer deleteAddFriendRequire(Integer fromId, Integer targetId) {
        return friendMapper.deleteAddFriendRequire(fromId, targetId);
    }
}

MessageService

@Service
public class MessageService {
    @Autowired
    private MessageMapper messageMapper;

    public List<Message> getHistoryBySessionId(Integer sessionId) {
        return messageMapper.getHistoryBySessionId(sessionId);
    }

    public Integer addMessage(Message message) {
        return messageMapper.addMessage(message);
    }

}

MessageSessionService

@Service
public class MessageSessionService {
    @Autowired
    private MessageSessionMapper messageSessionMapper;

    public List<Integer> getSessionByUserId(Integer userId) {
        return messageSessionMapper.getSessionByUserId(userId);
    }

    public List<Friend> getFriendsBySessionId(Integer sessionId, Integer userId) {
        return messageSessionMapper.getFriendsBySessionId(sessionId, userId);
    }

    public String getLastMessageBySessionId(Integer sessionId) {
        return messageSessionMapper.getLastMessageBySessionId(sessionId);
    }

    public Integer addMessageSession(MessageSession messageSession) {
        return messageSessionMapper.addMessageSession(messageSession);
    }

    public Integer addMessageSessionUser(Integer sessionId, Integer userId) {
        return messageSessionMapper.addMessageSessionUser(sessionId, userId);
    }
}

mapper

UserMapper

@Mapper
public interface UserMapper {

    // 根据 userName,查询数据库信息
    @Select("select * from user where user_name = #{userName}")
    User selectUserByName(String userName);

    // 新增 user 用户
    @Options(useGeneratedKeys = true, keyProperty = "userId")
    @Insert("insert into user values(null,#{userName},#{password})")
    Integer insertUser(User user);

    @Select("select * from user where user_name like concat('%',#{userName},'%')")
    List<User> getUsersByName(String userName);

}

FriendMapper

@Mapper
public interface FriendMapper {

    @Select("select f.friend_id, u.user_name as friendName from friend f, user u" +
            " where f.user_id = #{userId} and u.user_id = f.friend_id")
    List<Friend> getFriendsByUserId(Integer userId);

    @Insert("insert into add_friend(from_id,target_id,add_reason) values(#{fromId},#{targetId},#{addReason})")
    Integer addFriendRequired(AddFriend addFriend);

    @Select("select friend_id from friend where user_id = #{userId} and friend_id = #{friendId}")
    Friend isFriendShip(Integer userId, Integer friendId);

    @Select("select * from add_friend where from_id = #{fromId} and target_id = #{targetId}")
    AddFriend FindSameAddFriendRequired(Integer fromId, Integer targetId);

    @Select("select af.*, u.user_name as fromName from add_friend af, user u where af.target_id = #{targetId} and u.user_id = af.from_id")
    List<AddFriend> getAddRequire(Integer targetId);

    @Insert("insert into friend values(#{userId}, #{friendId})")
    Integer insertFriend(Integer userId, Integer friendId);

    @Delete("delete from add_friend where from_id = #{fromId} and target_id = #{targetId}")
    Integer deleteAddFriendRequire(Integer fromId, Integer targetId);
}

MessageMapper

@Mapper
public interface MessageMapper {
    // 根据 sessionId,获取历史消息
    @Select("select m.*, u.user_name as fromName from message m, user u" +
            " where m.session_id = #{sessionId} and u.user_id = m.from_id order by m.post_time desc limit 100")
    List<Message> getHistoryBySessionId(Integer sessionId);

    @Insert("insert into message(from_id,session_id,content) values(#{fromId},#{sessionId},#{content})")
    Integer addMessage(Message message);
}

MessageSessionMapper

@Mapper
public interface MessageSessionMapper {

    // 根据 userId,查找用户会话列表
    @Select("select msu.session_id from message_session_user msu, message_session ms" +
            " where msu.user_id = #{userId} and ms.session_id = msu.session_id order by ms.last_time desc")
    List<Integer> getSessionByUserId(Integer userId);

    // 根据 sessionId,查找朋友列表
    @Select("select" +
            " msu.user_id as friendId," +
            " u.user_name as friendName" +
            " from message_session_user msu, user u" +
            " where msu.session_id = #{sessionId}" +
            " and msu.user_id != #{userId}" +
            " and u.user_id = msu.user_id")
    List<Friend> getFriendsBySessionId(Integer sessionId, Integer userId);

    // 根据 sessionId,查找最后一条消息
    @Select("select content from message where session_id = #{sessionId} order by post_time desc limit 1")
    String getLastMessageBySessionId(Integer sessionId);


    @Options(useGeneratedKeys = true, keyProperty = "sessionId")
    @Insert("insert into message_session values(null, now())")
    Integer addMessageSession(MessageSession messageSession);

    @Insert("insert into message_session_user(session_id, user_id) values(#{sessionId}, #{userId})")
    Integer addMessageSessionUser(Integer sessionId, Integer userId);
}

config

WebSocketHandler

@Slf4j
@Component
public class WebSocketHandler extends TextWebSocketHandler {
    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MessageSessionService messageSessionService;

    @Autowired
    private MessageService messageService;

    @Autowired
    private FriendService friendService;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // WebSocketSession 是在 WebSocket 连接中对应的会话
        // 这个方法会在 WebSocket 成功建立连接之后,自动调用
        // 判断用户是否登录
        Map<String, Object> attributes = session.getAttributes();
        User user = (User) attributes.get(Constant.USER_SESSION);
        if (user == null) return;
        // 用户上线,存映射
        onlineUserManager.online(user.getUserId(), session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info("WebSocketHandler 收到消息!" + message.toString());
        // 判断用户是否登录
        Map<String, Object> attributes = session.getAttributes();
        User user = (User) attributes.get(Constant.USER_SESSION);
        if (user == null) return;

        // TextMessage 就是 收到的消息的值
        // 这个方法会在 WebSocket 接收到消息时,自动调用
        String payload = message.getPayload();
        // 将获取的 payload 转化成 Java 对象
        MessageRequest messageRequest = objectMapper.readValue(payload, MessageRequest.class);
        if (Constant.MESSAGE.equals(messageRequest.getType())) {
            // 处理消息的转发以及保存
            transferMessage(user, messageRequest);
        } else if (Constant.ADD_FRIEND_REQUIRE.equals(messageRequest.getType())) {
            AddFriendRequest req = objectMapper.readValue(message.getPayload(), AddFriendRequest.class);
            // 处理好友申请的发送以及保存
            sendAddFriendRequire(user, req);
        } else if (Constant.ACCEPT_ADD_FRIEND_REQUIRE.equals(messageRequest.getType())) {
            AddFriendRequest req = objectMapper.readValue(message.getPayload(), AddFriendRequest.class);
            req.setType(Constant.ACCEPT_ADD_FRIEND_REQUIRE);
            // 处理接受好友申请
            acceptAddFriendRequire(user, req);
        } else if (Constant.REJECT_ADD_FRIEND_REQUIRE.equals(messageRequest.getType())) {
            AddFriendRequest req = objectMapper.readValue(message.getPayload(), AddFriendRequest.class);
            req.setType(Constant.REJECT_ADD_FRIEND_REQUIRE);
            // 处理拒绝好友申请
            rejectAddFriendRequire(user, req);
        } else {
            log.error("[WebSocketHandler] type 类型错误!" + messageRequest.getType());
        }
    }

    private void rejectAddFriendRequire(User user, AddFriendRequest req) throws IOException {
        if (user == null || req == null) return;
        // 删除好友申请记录
        Integer ret3 = friendService.deleteAddFriendRequire(req.getFromId(), user.getUserId());
        // 1. 先构造一个响应
        AddFriendResponse resp = new AddFriendResponse(req.getType(), req.getFromId(), user.getUserName());
        // 2. 根据 fromId 获取 session
        WebSocketSession session = onlineUserManager.getWebSocketSessionById(resp.getFromId());
        if (session == null) {
            // 用户不在线
            return;
        }
        // 3. 发送 respJson
        String respJson = objectMapper.writeValueAsString(resp);
        session.sendMessage(new TextMessage(respJson));
    }


    @Transactional
    private void acceptAddFriendRequire(User user, AddFriendRequest req) throws IOException {
        if (user == null || req == null) return;
        // 1. 先构造一个响应
        AddFriendResponse resp = new AddFriendResponse(req.getType(), req.getFromId(), user.getUserName());
        // 添加好友关系
        Integer ret1 = friendService.insertFriend(user.getUserId(), req.getFromId());
        Integer ret2 = friendService.insertFriend(req.getFromId(), user.getUserId());
        // 删除好友申请记录
        Integer ret3 = friendService.deleteAddFriendRequire(req.getFromId(), user.getUserId());

        // 2. 根据 fromId 获取 session
        WebSocketSession session = onlineUserManager.getWebSocketSessionById(resp.getFromId());
        if (session == null) {
            // 用户不在线
            return;
        }
        // 3. 发送 respJson
        String respJson = objectMapper.writeValueAsString(resp);
        session.sendMessage(new TextMessage(respJson));
    }


    private void sendAddFriendRequire(User user, AddFriendRequest req) throws IOException {
        if (user == null || req == null) return;
        // 先看看数据库中是否已经是好友关系了,如果是,那就不用添加了
        Friend friendShip = friendService.isFriendShip(user.getUserId(), req.getFriendId());
        if (friendShip != null && friendShip.getFriendId() > 0) {
            return;
        }
        // 再看看好友申请是否已经在数据库中存在
        AddFriend sameAddFriend = friendService.FindSameAddFriendRequired(user.getUserId(), req.getFriendId());
        if (sameAddFriend != null) return;

        // 1. 先构造一个响应
        AddFriendResponse resp = new AddFriendResponse(req.getAddReason(), req.getFriendId(), user.getUserName(), user.getUserId());
        // 2. 根据 friendId 获取 session
        WebSocketSession session = onlineUserManager.getWebSocketSessionById(req.getFriendId());
        // 构建好友申请对象
        AddFriend addFriend = new AddFriend();
        addFriend.setFromId(user.getUserId());
        addFriend.setTargetId(req.getFriendId());
        addFriend.setAddReason(resp.getAddReason());
        if (session == null) {
            // 用户不在线,则不发送,将好友申请保存到数据库中
            friendService.addFriendRequired(addFriend);
            return;
        }
        // 3. 将响应转化成 json 格式,然后发送
        String respJson = objectMapper.writeValueAsString(resp);
        session.sendMessage(new TextMessage(respJson));
        // 4. 将好友申请保存到数据库中
        friendService.addFriendRequired(addFriend);
    }


    private void transferMessage(User user, MessageRequest messageRequest) throws IOException {
        // 1. 先构造一个 MessageResponse 对象
        MessageResponse messageResponse = new MessageResponse(user.getUserId(), user.getUserName(), messageRequest.getSessionId(), messageRequest.getContent());
        String respJson = objectMapper.writeValueAsString(messageResponse);
        // 2. 根据 sessionId,获取好友列表
        List<Friend> friends = messageSessionService.getFriendsBySessionId(messageResponse.getSessionId(), user.getUserId());
        // 同时给自己也要发一份消息,方便前端展示
        friends.add(new Friend(user.getUserName(), user.getUserId()));
        // 3. 根据 userId 获取 WebSocketSession 对象,进行消息转发
        for (Friend friend : friends) {
            WebSocketSession session = onlineUserManager.getWebSocketSessionById(friend.getFriendId());
            if (session == null) {
                // 用户不在线
                continue;
            }
            session.sendMessage(new TextMessage(respJson));
        }
        // 4. 保存消息记录
        Integer result = messageService.addMessage(new Message(user.getUserId(), user.getUserName(), messageResponse.getSessionId(), messageResponse.getContent()));
        if (result == null || result < 1) {
            log.error("增加消息到数据库失败");
        }
    }


    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 这个方法会在 WebSocket 连接异常之后,自动调用
        // 判断用户是否登录
        Map<String, Object> attributes = session.getAttributes();
        User user = (User) attributes.get(Constant.USER_SESSION);
        if (user == null) return;
        // 用户下线,删除映射
        onlineUserManager.offline(user.getUserId(), session);

    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 这个方法会在 WebSocket 连接正常关闭后,自动调用
        // 判断用户是否登录
        Map<String, Object> attributes = session.getAttributes();
        User user = (User) attributes.get(Constant.USER_SESSION);
        if (user == null) return;
        // 用户下线,删除映射
        onlineUserManager.offline(user.getUserId(), session);
    }
}

WebSocketConfig

@Configuration
@EnableWebSocket // 用来启动 WebSocket 的
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private WebSocketHandler webSocketHandler;

    // 配置 WebSocketHandler 的路由, 注册到具体的路径上
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 当浏览器 WebSocket 的请求路径是 /WebSocketMessage 时,就会调用 WebSocketHandler 里面的方法
        registry.addHandler(webSocketHandler, "/WebSocketMessage")
                // 通过注册这个特定的 HttpSession 拦截器,就可以把用户给 Httpsession 中添加的 Attribute 键值对
                // 往我们的 WebSocketSession 里也添加一份
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

component

OnlineUserManager

// 记录 userId 与 WebSocketSession 的映射
@Slf4j
@Component
public class OnlineUserManager {
    private Map<Integer, WebSocketSession> sessions = new HashMap<>();

    // 用户上线方法
    public void online(Integer userId, WebSocketSession webSocketSession) {
        // 参数校验
        if (userId == null || userId < 1 || webSocketSession == null) return;
        // 先看看用户是否已经在线,如果已经在线,那就啥都不干(防止多开)
        WebSocketSession session1 = sessions.get(userId);
        if (session1 != null) return;

        sessions.put(userId, webSocketSession);
        log.info("用户" + userId + "上线");
    }

    // 用户下线方法
    public void offline(Integer userId, WebSocketSession session) {
        if (userId == null || userId < 1) {
            return;
        }
        WebSocketSession existSession = sessions.get(userId);
        if (session == existSession) {
            // 如果这俩 session 是同一个,才真正进行下线操作,否则啥都不干(从多开角度考虑)
            sessions.remove(userId);
            log.info("[" + userId + "] 用户下线!");
            return;
        }
    }

    // 根据 userId 获取 WebSocketSession
    public WebSocketSession getWebSocketSessionById(Integer userId) {
        if (userId == null || userId < 1) {
            return null;
        }
        return sessions.get(userId);
    }
}

constant

Constant

public class Constant {
    public static final String USER_SESSION = "user_session";
    // 这个是 websocket 用来处理 实时发送消息 的请求
    public static final String MESSAGE = "message";
    // 这个是 websocket 用来处理 实时发送好友申请 的请求
    public static final String ADD_FRIEND_REQUIRE = "addFriendRequire";
    // 这个是 websocket 用来处理 实时接受好友申请 的请求
    public static final String ACCEPT_ADD_FRIEND_REQUIRE = "acceptAddFriendRequire";
    // 这个是 websocket 用来处理 实时拒绝好友申请 的请求
    public static final String REJECT_ADD_FRIEND_REQUIRE = "rejectAddFriendRequire";
}

model

AddFriend

@Data
public class AddFriend {
    private Integer id;
    private Integer fromId;
    private Integer targetId;
    private String addReason;
    private Date createTime;

    private String fromName;
}

AddFriendRequest

@Data
@NoArgsConstructor
public class AddFriendRequest {
    private String type = Constant.ADD_FRIEND_REQUIRE;
    private String addReason;
    private Integer friendId;

    // 用来处理接受/拒绝好友申请
    private Integer fromId;
}

AddFriendResponse

@Data
@NoArgsConstructor
public class AddFriendResponse {
    private String type = Constant.ADD_FRIEND_REQUIRE;
    private String addReason;
    private Integer friendId;
    private String fromName;
    private Integer fromId;

    public AddFriendResponse(String addReason, Integer friendId, String fromName, Integer fromId) {
        this.addReason = addReason;
        this.friendId = friendId;
        this.fromName = fromName;
        this.fromId = fromId;
    }

    // 用来处理接受/拒绝好友申请
    private String friendName;

    public AddFriendResponse(String type, Integer fromId, String friendName) {
        this.type = type;
        this.fromId = fromId;
        this.friendName = friendName;
    }
}

Friend

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Friend {
    private String friendName;
    private Integer friendId;
}

Message

@Data
@NoArgsConstructor
public class Message {
    private Integer messageId;
    private Integer fromId;
    private String fromName;
    private Integer sessionId;
    private String content;
    private Date postTime;

    public Message(Integer fromId, String fromName, Integer sessionId, String content) {
        this.fromId = fromId;
        this.fromName = fromName;
        this.sessionId = sessionId;
        this.content = content;
    }
}

MessageRequest

@Data
public class MessageRequest {
    private String type = "message";
    private Integer sessionId;
    private String content;

    // 用来处理 添加好友请求 的属性
    private String addReason;
    private Integer friendId;

    // 用来处理 接受好友请求
    private Integer fromId;
    private String friendName;

}

MessageResponse

@Data
@NoArgsConstructor
public class MessageResponse {
    // 可以想想前端页面需要展示什么
    private String type = "message";
    private Integer fromId;
    private String fromName;
    private Integer sessionId;
    private String content;

    public MessageResponse(Integer fromId, String fromName, Integer sessionId, String content) {
        this.fromId = fromId;
        this.fromName = fromName;
        this.sessionId = sessionId;
        this.content = content;
    }
}

MessageSession

@Data
public class MessageSession {
    private Integer sessionId;
    private List<Friend> friends;
    private String lastMessage;
}

User

@Data
@NoArgsConstructor
public class User {
    private Integer userId;
    private String userName;
    private String password;

    public User(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }
}

db.sql

-- 创建用户表
create database if not exists chat_room_review charset utf8;

use chat_room_review;

drop table if exists user;
create table user(
    user_id int primary key auto_increment,
    user_name varchar(20) unique,
    password varchar(50)
);

insert into user(user_name,password) values("mika","123");
insert into user(user_name,password) values("zhangsan","123");
insert into user(user_name,password) values("lisi","123");
insert into user(user_name,password) values("wangwu","123");


-- 创建好友表
drop table if exists friend;
create table friend (
    user_id int,
    friend_id int
);

insert into friend values(1, 2);
insert into friend values(2, 1);
insert into friend values(1, 3);
insert into friend values(3, 1);
insert into friend values(1, 4);
insert into friend values(4, 1);



-- 创建会话表
drop table if exists message_session;
create table message_session(
    session_id int primary key auto_increment,
    last_time datetime DEFAULT now()
);
insert into message_session values(1, '2000-05-01 00:00:00');
insert into message_session values(2, '2000-06-01 00:00:00');

-- 创建会话和用户的关联表
drop table if exists message_session_user;
create table message_session_user(
    session_id int ,
    user_id int
);

-- 1 号会话里有张三和 mika
insert into message_session_user values(1, 1), (1, 2);
-- 2 号会话里有李四和 mika
insert into message_session_user values(2, 1), (2, 3);


-- 创建消息表
drop table if exists message;
create table message(
    message_id int primary key auto_increment,
    from_id int not null, -- 消息是哪个用户发的
    session_id int not null, -- 消息发给哪个会话
    content varchar(5000), -- 消息的内容
    post_time datetime default now() -- 消息的发送时间
);

-- mika 和 zhangsan 的对话
insert into message values(1,1,1,"星露谷联机?","2024-03-21 17:00:00");
insert into message values(2,2,1,"来来来!","2024-03-21 17:00:03");
insert into message values(3,1,1,"正好 1.6 更新了!!!","2024-03-21 17:00:07");
insert into message values(4,1,1,"我还没开始玩 1.6 呢","2024-03-21 17:00:12");
insert into message values(5,2,1,"行,那接电话,开麦玩","2024-03-21 17:00:16");
insert into message values(6,1,1,"OK","2024-03-21 17:00:20");


-- mika 和 lisi 的对话
insert into message values(7,1,2,"葬送的芙莉莲完结了,快去看!!!","2024-03-23 18:01:00");
insert into message values(8,3,2,"最近在忙,要学习没空","2024-03-23 18:02:00");
insert into message values(9,1,2,"那就吃饭的时候看呗","2024-03-23 18:02:15");
insert into message values(10,3,2,"忙,没空","2024-03-23 18:03:15");


-- add_friend 表用来存放申请好友记录
drop table if exists add_friend;
create table add_friend(
    id int primary key auto_increment,
    from_id int , -- 谁主动申请好友
    target_id int , -- 好友关系发给谁
    add_reason varchar(30), -- 申请理由
    create_time datetime default now()
);

完整源码

链接1:Gitee源码

链接2:Github源码

  • 38
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
Linux网络聊天项目是一个基于Linux操作系统的聊天应用程序。通过使用C语言编写服务器和客户端代码,可以实现多个用户之间的即时通信。该项目使用MySQL数据库来存储聊天的相关信息,通过与数据库的交互来实现用户注册、登录、发送消息等功能。 要运行该项目,您需要先编译服务器端代码和客户端代码。编译服务器端的命令是: gcc server.c mysql.c -lmysqlclient -lpthread -o s 。 在编译完成后,您还需要在MySQL数据库中创建一个名为"chatroom"的数据库,并在其中创建一个名为"infor"的数据表。要进行此操作,您需要使用MySQL客户端工具,并执行相应的SQL语句。具体的数据库和数据表的创建可以在您的项目代码中找到。 网络聊天项目通常包括以下几个主要功能: 1. 用户注册和登录:用户可以注册一个账号,并使用该账号登录到聊天。 2. 消息发送和接收:用户可以向其他在线用户发送消息,并接收其他用户发送的消息。 3. 在线用户列表:显示当前在线的用户列表,以便用户选择与之进行聊天。 4. 聊天记录保存:将用户之间的聊天记录进行保存,以便后续查看。 在项目的代码中,您可以找到一些特定的函数和方法,例如print_all_data、search、Sconnect等。这些函数和方法用于与MySQL数据库进行交互,执行数据的插入、查询、删除等操作。 在项目的实现过程中,您可能还会使用到多线程编程技术,以实现多个用户的并发连接和通信。 总的来说,Linux网络聊天项目是一个基于Linux操作系统的聊天应用程序,通过使用C语言编写服务器和客户端代码,并结合MySQL数据库来实现用户注册、登录、发送消息等功能。您可以根据具体的需求和代码实现来进一步了解和定制该项目

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值