文章目录
参考链接
zzhua/netty-chat-web - 包括前后端
vue.js实现带表情评论功能前后端实现(仿B站评论)
vue.js实现带表情评论仿bilibili(滚动加载效果)
netty-demo-crazy - 疯狂架构师netty
IM即时通讯系统[SpringBoot+Netty]——梳理(总), 代码 im-system 在gitee
构建IM即使通讯Web页面 - B站视频,仅前端代码,代码 chat-demo 在gitee
- 禹神:一小时快速上手Electron,前端Electron开发教程,笔记。一篇文章入门Electron
- Electron集成Vite + Vue 开发IM即使通讯
- easychat - gitee代码
基于vue3实现一个简单的输入框效果
vue3通过组合键实现换行操作的示例详解
subtlechat-mini - 前后端,有mini版和完整版
lyf-im - 前后端项目
box-im - 盒子im,很棒
MallChat - 前后端项目,代码很棒
木杉/ 视频通话 netty webrtc websocket springboot uniapp
yymao/chatroom - 仅前端,im界面好看
H260788/PureChat - im界面好看,前端难度大
netty-chatroom - netty实现,仅后端代码
liurq_netty_barrage - netty实现的1个简单的弹幕效果
yorick_socket一套基于Netty的轻量级Socket和WebSocket框架,可用于搭建聊天服务器和游戏同步服务器
【聊天系统】从零开始自己做一个"wechat" - uniapp 和 springboot
linyu-mini-web&linyu-mini-server gitee 前后端代码
aq-chat web端,AQChatServer,aqchat-mobile,AQChat文档中心,
考拉开源/im-uniapp,im-platform 后台代码
ws-chat - 前后端代码
使用
前端使用:vue.js + vuex + iconfont + element-ui
后端使用:springboot + mybatisplus + redis + netty + websocket + spring security
可能有不少问题,反正先按照自己思路一点一点写,再参考下别人是怎么搞的再优化
前端界面
先写下大概的前端界面,界面出来了,才有继续写下去的动力
简单效果
消息窗口平滑滚动至底部
<div class="panel-main-body" ref="panelMainBodyContainerRef">
<!-- 对应会话 的消息列表 -->
<div class="msg-item-list" ref="msgItemListContainerRef">
<div :class="['msg-item', familyChatMsg.senderId != currUserId ? 'other' : 'owner']"
v-for="(familyChatMsg, idx) in familyChatMsgList"
:key="idx">
<div class="avatar-wrapper ">
<img :src="familyChatMsg.avatar" class="avatar fit-img" alt="">
</div>
<div class="msg">
<div class="msg-header">
{{ familyChatMsg.nickName }}
</div>
<div class="msg-content" v-html="familyChatMsg.content"></div>
</div>
</div>
</div>
</div>
<script>
export default {
methods: {
/* 滚动至底部,不过调用此方法的时机应当在familyChatMsgList更新之后, 因此需要监听它 */
scrollToEnd() {
const panelMainBodyContainerRef = this.$refs['panelMainBodyContainerRef']
const msgItemListContainerRef = this.$refs['msgItemListContainerRef']
// console.log(msgItemListContainerRef.scrollTop);
// console.log(panelMainBodyContainerRef.scrollHeight);
msgItemListContainerRef.scrollTop = msgItemListContainerRef.scrollHeight
console.log('滚动至底部~');
},
}
}
</script>
<style>
.msg-item-list {
/* 平滑滚动 */
scroll-behavior: smooth;
}
</style>
vue使用watch监听vuex中的变量变化
computed: {
...mapGetters('familyChatStore', ['familyChatMsgList']),
},
watch: {
// 监听store中的数据 - 是通过监听getters完成的
familyChatMsgList:{
handler(newVal, oldVal) {
// console.log('---------------------');
// console.log(newVal.length, oldVal.length);
this.$nextTick(()=>{
this.scrollToEnd()
})
}
}
},
websocket握手认证
客户端在登录完成后,可以请求后端的接口获取1个chatKey(这个chatKey只有在用户登录后,携带token访问时才能得到),得到此chatKey后,连接websocket客户端时,把这个chatKey作为请求参数拼接到ws://xxxx.xx.xx:9091/ws?chatKey=xxx,这样在握手的时候,就可以拿到这个请求参数。但是,我不想在握手完成事件时再去拿这个chatKey(虽然这样做,也没什么问题,但感觉逻辑不是很好,都已经握手完成了,再来断掉ws连接有点不好),因此,设置1个ChatKeyCheckHandler,它继承自SimpleInboundHandlerAdapter,处理的泛型是FullHttpRequest,并且把这个处理器放在WebSocketServerProtocolHandler的前面,这样,在处理握手请求时,就可以拿到请求参数了,而握手完成之后,由于后面的消息是websocket协议帧数据,它不会FullHttpRequest类型的,因此不会经过这个处理器,这样感觉比较好~
ChatKeyCheckHandler
@Slf4j
@ChannelHandler.Sharable
@Component
public class ChatKeyCheckHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
public ChatKeyCheckHandler() {
super(false);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
log.info("http请求-chatKeyCheckHandler处理");
FullHttpRequest request = ((FullHttpRequest) msg);
String uri = request.uri();
log.info("请求uri: {}");
log.info("请求header: {}", Arrays.toString(request.headers().names().toArray()));
List<String> chatKeys = UriComponentsBuilder.fromUriString(uri)
.build()
.getQueryParams()
.get(Constants.CHAT_KEY);
if (CollectionUtils.isEmpty(chatKeys)) {
log.error("欲建立websocket连接,但未携带chatKey,直接略过");
// 还得写个响应回去,并且关闭HTTP连接
HttpRequest req = msg;
FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK,
Unpooled.wrappedBuffer("NOT ALLOWD WITHOUT CHAT_KEY".getBytes()));
response.headers()
.set(CONTENT_TYPE, TEXT_PLAIN)
.setInt(CONTENT_LENGTH, response.content().readableBytes());
// Tell the client we're going to close the connection.
response.headers().set(CONNECTION, CLOSE);
ChannelFuture f = ctx.writeAndFlush(response);
f.addListener(ChannelFutureListener.CLOSE);
return;
}
String chatKey = chatKeys.iterator().next();
ctx.channel().attr(WsContext.CHAT_KEY_ATTR).set(chatKey);
log.info("建立websocket连接的握手请求, 携带了chatKey: {}", chatKey);
// 在此处校验chatKey是否合理, 如果不合理, 则不允许建立websocket链接(不会进行后面的握手处理)
ctx.fireChannelRead(request);
}
}
NettyChatServer
@Slf4j
@Component
public class NettyChatServer implements SmartLifecycle {
@Autowired
private NettyProperties nettyProps;
@Autowired
private NettyChatInitializer nettyChatInitializer;
private volatile boolean running = false;
private ServerChannel serverChannel;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
public void start() {
log.info("starting netty server~");
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup(2);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(nettyChatInitializer);
// 这里会异步调用
ChannelFuture channelFuture = serverBootstrap.bind(nettyProps.getPort());
channelFuture.addListener(future -> log.info("netty started, listening: {}", nettyProps.getPort()));
// 保存对ServerSocketChannel的引用
serverChannel = (NioServerSocketChannel) channelFuture.channel();
channelFuture.channel().closeFuture().addListener(future -> log.info("netty stopped!"));
running = true;
}
@Override
public void stop() {
log.info("stop netty server");
try {
serverChannel.close();
} catch (Exception e) {
log.error("关闭ServerSocketChannel失败");
}
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
}
NettyChatInitializer
@Component
public class NettyChatInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
private NettyProperties nettyProperties;
@Autowired
private DispatcherMsgHandler dispatcherMsgHandler;
@Autowired
private HandShakeHandler handShakeHandler;
@Autowired
private ChatKeyCheckHandler chatKeyCheckHandler;
@Override
protected void initChannel(SocketChannel ch) throws Exception {
DefaultEventLoopGroup eventExecutors = new DefaultEventLoopGroup(2);
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("http-decoder", new HttpRequestDecoder());
pipeline.addLast("http-encoder", new HttpResponseEncoder());
pipeline.addLast("aggregator", new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new ChunkedWriteHandler());
WebSocketServerProtocolConfig wsServerConfig = WebSocketServerProtocolConfig
.newBuilder()
.websocketPath(nettyProperties.getWsPath())
.checkStartsWith(true)
.maxFramePayloadLength(Integer.MAX_VALUE)
.build();
pipeline.addLast("chatKeyHandler", chatKeyCheckHandler);
pipeline.addLast("websocketHandler", new WebSocketServerProtocolHandler(wsServerConfig));
pipeline.addLast("handShakeHandler", handShakeHandler);
pipeline.addLast("heartBeanCheckHandler", new HeatBeatCheckHandler());
pipeline.addLast(eventExecutors, "dispatcherMsgHandler", dispatcherMsgHandler);
}
}