IM聊天学习资源

发布于:2025-02-10 ⋅ 阅读:(36) ⋅ 点赞:(0)

参考链接

zzhua/netty-chat-web - 包括前后端

vue.js实现带表情评论功能前后端实现(仿B站评论)
vue.js实现带表情评论仿bilibili(滚动加载效果)

vue.js支持表情输入
vue.js表情文本输入框组件

个人说说vue组件

JS操作文本域获取光标/指定位置插入

分布式ID生成之雪花算法(SnowFlake)


netty-demo-crazy - 疯狂架构师netty

IM即时通讯系统[SpringBoot+Netty]——梳理(总)代码 im-system 在gitee

构建IM即使通讯Web页面 - B站视频,仅前端代码,代码 chat-demo 在gitee

基于vue3实现一个简单的输入框效果
vue3通过组合键实现换行操作的示例详解

easychat - B站视频,前后端,代码在gitee

subtlechat-mini - 前后端,有mini版和完整版

lyf-im - 前后端项目

box-im - 盒子im,很棒

MallChat - 前后端项目,代码很棒

木杉/ 视频通话 netty webrtc websocket springboot uniapp

木杉/ /mushan-im

ProgHub/chat_room

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端AQChatServeraqchat-mobileAQChat文档中心,

考拉开源/im-uniappim-platform 后台代码

ws-chat - 前后端代码

yan代码B站视频)

im-whale-shark代码B站视频

轻语im

使用

前端使用: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);


    }


}

网站公告

今日签到

点亮在社区的每一天
去签到