1.后端
- netty服务端
@Component("NettyChatServer")
public class NettyChatServer {
//主线程池:处理连接请求
private static NioEventLoopGroup boss = new NioEventLoopGroup(2);
//工作线程池:接收主线程发过来的任务,完成实际的工作
private static NioEventLoopGroup worker = new NioEventLoopGroup(6);
//创建一个服务器端的启动对象
ServerBootstrap serverBootstrap=null;
@Autowired
//自定义handler、处理客户端发送过来的消息进行转发等逻辑
MyTextWebSocketFrameHandler myTextWebSocketFrameHandler = new MyTextWebSocketFrameHandler();
public void run() {
serverBootstrap= new ServerBootstrap().group(boss, worker)
.channel(NioServerSocketChannel.class)
//连接的最大线程数
.option(ChannelOption.SO_BACKLOG, 128)
//长连接,心跳机制
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
//因为基于http协议,使用http的编码和解码器
nioSocketChannel.pipeline().addLast(new HttpServerCodec());
//是以块方式写,添加ChunkedWriteHandler处理器
nioSocketChannel.pipeline().addLast(new ChunkedWriteHandler());
/**
* 说明
* 1. http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
* 2. 这就就是为什么,当浏览器发送大量数据时,就会发出多次http请求
*/
nioSocketChannel.pipeline().addLast(new HttpObjectAggregator(8192));
/**
* 说明
* 1. 对应websocket ,它的数据是以帧(frame,基于TCP)形式传递
* 2. 可以看到WebSocketFrame下面有六个子类
* 3. 浏览器请求时 ws://localhost:8888/wechat 表示请求的uri
* 4. WebSocketServerProtocolHandler 核心功能是将 http协议升级为 ws协议 , 保持长连接
* 5. 是通过一个 状态码 101
*/
nioSocketChannel.pipeline().addLast(new WebSocketServerProtocolHandler("/wechat"));
//自定义handler、处理客户端发送过来的消息进行转发等逻辑
nioSocketChannel.pipeline().addLast(myTextWebSocketFrameHandler);
}
});
//server监听接口
try {
ChannelFuture channelfuture = serverBootstrap.bind(8888).sync();
// 添加注册监听,监控关心的事件,当异步结束后就会回调监听逻辑
channelfuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("监听端口8888成功");
}else{
System.out.println("监听端口8888失败");
}
}
});
//关闭通道和关闭连接池(不是真正关闭,只是设置为关闭状态)
channelfuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//EventLoop停止接收任务、任务结束完毕停掉线程池
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
- 自定义handler,处理业务逻辑
@Component
@ChannelHandler.Sharable
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
//记录客户端和channel的绑定
private static Map<Integer, Channel> channelMap=new ConcurrentHashMap<Integer, Channel>();
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
//将发过来的内容进行解析成 自定义的Message
Message message = JSON.parseObject(textWebSocketFrame.text(), Message.class);
//绑定对应用户和channel
if (!channelMap.containsKey(message.getFromUid())){
channelMap.put(message.getFromUid(),channelHandlerContext.channel());
}else{
channelMap.replace(message.getFromUid(),channelHandlerContext.channel());
}
//发送给对应的客户端对应的channel
if(channelMap.containsKey(message.getToUid())){
//因为连接成功会发送一次注册消息(注册消息message.getToUid()== message.getFromUid())
if(message.getToUid()!= message.getFromUid()){
//不能重用之前的textWebSocketFrame
channelMap.get(message.getToUid()).writeAndFlush(new TextWebSocketFrame(textWebSocketFrame.text()));
}
}else{
//该用户暂未在线,先将消息存进数据库(这里没实现)
System.out.println("该用户暂未在线,先将消息存进数据库");
}
//计数-1(计数法来控制回收内存)
channelHandlerContext.fireChannelRead(textWebSocketFrame.retain());
}
}
- netty整合到springboot
@SpringBootApplication
public class OnlinechatApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(OnlinechatApplication.class, args);
NettyChatServer nettyChatServer = (NettyChatServer)context.getBean("NettyChatServer");
nettyChatServer.run();
}
}
2.前端
- 和weocket的demo区别发送的ws协议uri不同,
ws://localhost:8888/wechat
- 还有就是,websocket建立连接之后就先发送一次绑定消息到服务器端(将用户和channel的关系对应起来)
<template>
<div class="bg">
<el-container class="wechat">
<el-aside width="35%" style="border-right: 1px solid #fff">
<!-- 自己 -->
<div class="item">
<el-avatar
:size="46"
:src="user.avatarUrl"
style="float: left; margin-left: 2px"
></el-avatar>
<div class="name">
{{ user.nickname
}}<el-tag style="margin-left: 5px" type="success">本人</el-tag>
</div>
</div>
<!-- 在线用户 -->
<div
class="item"
v-for="(item1, index) in userlist"
:key="item1.uid"
@click="selectUser(index)"
>
<!-- 新数消息 -->
<el-badge
:value="new_message_num[index]"
:max="99"
:hidden="!new_message_num[index] > 0"
style="float: left; margin-left: 2px"
>
<el-avatar :size="46" :src="item1.avatarUrl"></el-avatar>
</el-badge>
<div class="name">{{ item1.nickname }}</div>
</div>
</el-aside>
<el-main>
<el-container class="wechat_right">
<!-- 右边顶部 -->
<el-header class="header">{{
anotherUser != null && anotherUser.uid > 0
? anotherUser.nickname
: "未选择聊天对象"
}}</el-header>
<!-- 聊天内容 -->
<el-main class="showChat">
<div v-for="item2 in messageList[index]" :key="item2.msg">
<!-- 对方发的 -->
<div class="leftBox" v-if="item2.FromUid == anotherUser.uid">
<span style="font-size: 4px">{{ item2.time }}</span
>{{ item2.msg }}
</div>
<div class="myBr" v-if="item2.FromUid == anotherUser.uid"></div>
<!-- 自己发的 -->
<div class="rightBox" v-if="item2.FromUid == user.uid">
<span style="font-size: 4px">{{ item2.time }}</span
>{{ item2.msg }}
</div>
<div class="myBr" v-if="item2.FromUid == user.uid"></div>
</div>
</el-main>
<!-- 输入框 -->
<el-main class="inputValue">
<textarea v-model="inputValue" id="chat" cols="26" rows="5">
</textarea>
<!-- 发送按钮 -->
<el-button
v-if="
anotherUser != null && anotherUser.uid > 0 && inputValue != ''
"
type="success"
size="mini"
round
id="send"
@click="senMessage"
>发送</el-button
>
</el-main>
</el-container>
</el-main>
</el-container>
</div>
</template>
<script>
export default {
data() {
return {
//自己
user: {},
//要私信的人
anotherUser: {},
//在线的用户
userlist: [],
//要私信的人在userlist的索引位置
index: 0,
//消息队列集合 [本人和第一个人之间的消息集合、本人和第二个人之间的消息集合、...]
messageList: [],
//新消息个数集合
new_message_num: [],
//将要发送的内容
inputValue: "",
//websocket
websocket: null,
};
},
methods: {
//获取自己被分配的信息
getYourInfo(uid) {
let params = new URLSearchParams();
this.$axios
.post("/user/getYourInfo/" + uid, params)
.then((res) => {
this.user = res.data.data;
if (res.data.code == 200) {
//获取在线用户
this.getUserList();
}
})
.catch((err) => {
console.error(err);
});
},
//获取在线用户
getUserList() {
let params = new URLSearchParams();
this.$axios
.post("/user/getUserList", params)
.then((res) => {
this.userlist = res.data.data.filter(
//去掉自己
(user) => user.uid !== this.user.uid
);
//填充消息数据 messagelist:[[]、[]...] 并且将新消息队列置为0
for (let i = 0; i < this.userlist.length; i++) {
this.messageList.push([]);
this.new_message_num.push(0);
}
//将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑
this.init(this.user.uid);
})
.catch((err) => {
console.error(err);
});
},
//选择聊天对象
selectUser(index) {
this.anotherUser = this.userlist[index];
this.index = index;
//将新消息置为0
this.new_message_num[index] = 0;
},
//将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑
init(uid) {
var self = this;
if (typeof WebSocket == "undefined") {
console.log("您的浏览器不支持WebSocket");
return;
}
//清除之前的记录
if (this.websocket != null) {
this.websocket.close();
this.websocket = null;
}
//-----------------------连接服务器-----------------------
let socketUrl = "ws://localhost:8888/wechat";
//开启WebSocket 连接
this.websocket = new WebSocket(socketUrl);
//指定连接成功后的回调函数
this.websocket.onopen = function () {
console.log("websocket已打开");
//发送一次注册消息(使后端先绑定channel和用户的关系,以至于找到对应的channel转发消息)
let message = {
FromUid: uid,
ToUid: uid,
msg: uid + "的绑定消息",
time: new Date().toLocaleTimeString(),
};
self.websocket.send(JSON.stringify(message));
};
//指定连接失败后的回调函数
this.websocket.onerror = function () {
console.log("websocket发生了错误");
};
//指定当从服务器接受到信息时的回调函数
this.websocket.onmessage = function (msg) {
//消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"} => message对象
let data = JSON.parse(msg.data);
//添加到对应的消息集合中
let index = data.FromUid > uid ? data.FromUid - 2 : data.FromUid - 1;
self.messageList[index].push(data);
//新消息数+1
self.new_message_num[index]++;
};
//指定连接关闭后的回调函数
this.websocket.onclose = function () {
console.log("websocket已关闭");
};
},
//发送信息
senMessage() {
//消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"}
let message = {
FromUid: this.user.uid,
ToUid: this.anotherUser.uid,
msg: this.inputValue,
time: new Date().toLocaleTimeString(),
};
//将消息插进消息队列,显示在前端
this.messageList[this.index].push(message);
//将消息发送至服务器端再转发到对应的用户
this.websocket.send(JSON.stringify(message));
//清空一下输入框内容
this.inputValue = "";
},
},
created() {
let uid = this.$route.query.uid;
if (uid != undefined) {
//获取被分配的用户信息
this.getYourInfo(uid);
}
},
};
</script>
<style>
/*改变滚动条 */
::-webkit-scrollbar {
width: 3px;
border-radius: 4px;
}
::-webkit-scrollbar-track {
background-color: inherit;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background-color: #c3c9cd;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.bg {
background: url("https://s1.ax1x.com/2022/06/12/Xgr9u6.jpg") no-repeat top;
background-size: cover;
background-attachment: fixed;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.wechat {
width: 60%;
height: 88%;
margin: 3% auto;
border-radius: 20px;
background-color: rgba(245, 237, 237, 0.3);
}
/*聊天框左侧 */
.item {
position: relative;
width: 94%;
height: 50px;
margin-bottom: 3%;
border-bottom: 1px solid #fff;
}
.item .name {
line-height: 50px;
float: left;
margin-left: 10px;
}
/*聊天框右侧 */
.wechat_right {
position: relative;
width: 100%;
height: 100%;
}
.header {
text-align: left;
height: 50px !important;
}
.showChat {
width: 100%;
height: 65%;
}
.inputValue {
position: relative;
margin: 0;
padding: 0;
width: 100%;
height: 50%;
}
.inputValue #chat {
font-size: 18px;
width: 96%;
height: 94%;
border-radius: 20px;
resize: none;
background-color: rgba(245, 237, 237, 0.3);
}
#send {
position: absolute;
bottom: 12%;
right: 6%;
}
/*展示区 */
.leftBox {
float: left;
max-width: 60%;
padding: 8px;
position: relative;
font-size: 18px;
border-radius: 12px;
background-color: rgba(40, 208, 250, 0.76);
}
.rightBox {
float: right;
max-width: 60%;
padding: 8px;
font-size: 18px;
border-radius: 12px;
position: relative;
background-color: rgba(101, 240, 21, 0.945);
}
.myBr {
float: left;
width: 100%;
height: 20px;
}
.leftBox > span {
left: 3px;
width: 120px;
position: absolute;
top: -16px;
}
.rightBox > span {
width: 120px;
position: absolute;
right: 3px;
top: -16px;
}
</style>
源码
源代码