基于websocket+Springboot+netty实现聊天室

孝. 2022-01-16 13:52:08

基于websocket+Springboot+netty实现聊天室

一.框架概述

websocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

Netty

Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程.

SpringBoot

Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,Spring Boot 整合了所有的框架。

二.项目逻辑

客户端利用websocket与服务端建立长连接,服务端判断客户端发送的消息类型,是注册,私聊还是群聊,根据不同的消息类型去执行相应的操作,如转发信息等.对所有建立连接的客户端将对应的管道加入一个常量onlineUserMap中,若一个用户A向另一个用户B私聊,若B在线,则将消息直接转发到管道中,若B不在线,则将对应的消息存放在缓存中,在每个用户上线时读取缓存中的数据看是否有发送给自己的信息.

 

三.数据库表设计

chat_group表

id:主键

groupId:群组的id

groupName:群组的名字

userId:群组中包含的用户id

groupAvatarUrl:群组的头像

chat_user表

id:主键

userId:用户的id

username:用户名

password:用户密码

avatarUrl:用户头像

chat_friend表

id:主键

userId:用户的id

friendId:用户的朋友的id

主要代码分析

服务端:

初始化

初始化时将webSocketChildChannelHandler设为默认的处理器,在webSocketChildChannelHandler中除了添加编码解码器外再先后添加httpRequestHandler和webSocketServerHandler.httpRequestHandler主要是对发送的http请求进行协议的升级,升级到websocket协议.

    //默认的处理器为webSocketChildChannelHandler
    @Resource(name = "webSocketChildChannelHandler")
    private ChannelHandler childChannelHandler;
​
    @Autowired
    private EventLoopGroup bossGroup;
    @Autowired
    private EventLoopGroup workerGroup;
    @Autowired
    private ServerBootstrap serverBootstrap;
​
    public WebSocketServer() {
​
    }
    @Override
    public void run() {
        try {
            long begin = System.currentTimeMillis();
            //option针对的是boss线程,childoption针对的是worker线程
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)//配置TCP,握手字符串长度设置
                    .option(ChannelOption.TCP_NODELAY, true)//使其尽可能的发送大块数据,提高效率
                    .childOption(ChannelOption.SO_KEEPALIVE, true)//开启心跳机制
                    .childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(592048))//配置固定长度接收缓存区分配器
                    .childHandler(childChannelHandler);
            long end = System.currentTimeMillis();
            logger.info("Netty Websocket服务器启动完成,耗时 " + (end - begin));
            //绑定端口
            serverChannelFuture = serverBootstrap.bind(port).sync();
            //监听关闭事件
            serverChannelFuture.channel().closeFuture().sync();
​
        } catch (Exception e) {
            logger.info(e.getMessage());
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            e.printStackTrace();
        }
    }
    

 

WebSocketServerHandler:主要是两个函数,一个是对用户发送的消息的处理,一个是去缓存中读取用户没有接收的信息并转发给用户.

WebSocketFrame是WebSocket服务在建立的时候,在通道中处理的数据类型.

    /**
     * 描述:读取完连接的消息后,对消息进行处理。
     * 这里主要是处理WebSocket请求
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame webSocketFrame) throws Exception {
        //关闭请求
        if (webSocketFrame instanceof CloseWebSocketFrame) {
            //
            WebSocketServerHandshaker handshaker =
                    Constant.webSocketHandshakerMap.get(ctx.channel().id().asLongText());
            if (handshaker == null) {
                sendErrorMessage(ctx, "不存在的客户端连接!");
            } else {
                handshaker.close(ctx.channel(), ((CloseWebSocketFrame) webSocketFrame).retain());
            }
            return;
        }
        //ping请求
        if (webSocketFrame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain()));
            return;
        }
        //只支持文本格式,不支持二进制消息
        if (!(webSocketFrame instanceof TextWebSocketFrame)) {
            sendErrorMessage(ctx, "仅支持文本格式,不支持二进制消息");
            return;
        }
        //服务端收到新消息
        String request = ((TextWebSocketFrame) webSocketFrame).text();
        LOGGER.info("服务端收到了信息消息: " + request);
        JSONObject param = null;
        try {
            param = JSONObject.parseObject(request);
        } catch (Exception e) {
            sendErrorMessage(ctx, "JSON字符串转换出错!");
            e.printStackTrace();
        }
        if (param == null) {
            sendErrorMessage(ctx, "参数为空!");
            return;
        }
        String type = (String) param.get("type");
        switch (type) {
            case "REGISTER":
                chatService.register(param, ctx);
                break;
            case "SINGLE_SENDING":
                chatService.singleSend(param, ctx);
                break;
            case "GROUP_SENDING":
                chatService.groupSend(param, ctx);
                break;
            default:
                chatService.typeError(ctx);
                break;
        }
        readRedisCache();
    } 
    public void readRedisCache() {
        if (chatRedisUtils.hgetCacheChatMessage(Constant.USERS_ID) != null) {
            Map<Object, Object> chatMap = chatRedisUtils.hgetCacheChatMessage(Constant.USERS_ID);
            String msg;
            for (Map.Entry<Object, Object> entry : chatMap.entrySet()) {
                String item = entry.getKey() + "";
                StringBuilder key = new StringBuilder();
                for (int i = 0; i < item.length(); i++) {
                    if (item.charAt(i) != '-') {
                       key.append(item.charAt(i));
                    } else {
                       break;
                    }
               }
                msg = entry.getValue() + "";
                String responseJson = new ResponseJson().success()
                            .setData("fromUserId", key)
                            .setData("content", msg)
                            .setData("type", ChatType.SINGLE_SENDING)
                            .toString();
                sendMessage(Constant.USERS_ID, responseJson);
​
            }
            chatRedisUtils.deleteCacheChatMessage(Constant.USERS_ID);
        }
    }

    

消息转发

私聊:根据客户端发送的消息获取消息发送者id,接受者id以及消息的内容,去onlineUserMap中获得接受者的管道,如果管道不存在,即说明接受者不在线,那么将消息暂存在缓存中,在接受者下次登录时再进行转发.其中对redis的操作使用了工具类chatRedisUtils.主要是基本redisUtil操作的抽取,在此不在赘述.

    @Override
    public void singleSend(JSONObject param, ChannelHandlerContext ctx) {
        String fromUserId = (String) param.get("fromUserId");
        String toUserId = (String) param.get("toUserId");
        String content = (String) param.get("content");
        ChannelHandlerContext toUserCtx = Constant.onlineUserMap.get(toUserId);
        if (toUserCtx == null) {
            System.out.println("toUserCtx:" + toUserCtx);
            String responJson = new ResponseJson()
                    .error(MessageFormat.format("userId为 {0} 的用户没有登录!", toUserId))
                    .toString();
            String key = toUserId;
            String item = chatRedisUtils.createChatNumber(Integer.parseInt(fromUserId));
            chatRedisUtils.saveCacheChatMessage(key, item, content);
            sendMessage(ctx, responJson);
        } else {
            String responseJson = new ResponseJson().success()
                    .setData("fromUserId", fromUserId)
                    .setData("content", content)
                    .setData("type", ChatType.SINGLE_SENDING)
                    .toString();
            sendMessage(toUserCtx, responseJson);
        }
    }

群聊:群聊实现思路与私聊差不多,只是少了一个接受者管道是否存在的判断环节,根据toGroupId去数据库中取得对应的群组信息,对群组中的每一个用户(管道)都执行转发消息操作.

    
@Override
    public void groupSend(JSONObject param, ChannelHandlerContext ctx) {
        String fromUserId = (String) param.get("fromUserId");
        String toGroupId = (String) param.get("toGroupId");
        String content = (String) param.get("content");
        GroupInfo groupInfo = groupInfoDao.getByGroupId(toGroupId);
        if (groupInfo == null) {
            String responseJson = new ResponseJson().error("该群id不存在").toString();
            sendMessage(ctx, responseJson);
        } else {
            String responseJson = new ResponseJson().success()
                    .setData("fromUserId", fromUserId)
                    .setData("content", content)
                    .setData("toGroupId", toGroupId)
                    .setData("type", ChatType.GROUP_SENDING)
                    .toString();
            groupInfo.getMembers().stream()
                    .forEach(member -> {
                        ChannelHandlerContext toCtx = Constant.onlineUserMap.get(member.getUserId());
                        if (toCtx != null && !member.getUserId().equals(fromUserId)) {
                            sendMessage(toCtx, responseJson);
                        }
                    });
        }
    }
    private void sendMessage(ChannelHandlerContext ctx, String responseJson) {
        ctx.channel().writeAndFlush(new TextWebSocketFrame(responseJson));
    }

 

客户端:

建立websocket连接

利用原生websocket建立连接,socket.onmessage根据服务端回传的消息判断消息的类型并执行对应的操作.socket.onopen和socket.onclose分别是刚建立连接和结束连接时执行的操作.刚建立连接后,客户端会执行register函数向服务端注册连接管道.

socket = new WebSocket("ws://localhost:3333");
        socket.onmessage = function (event) {
            var json = JSON.parse(event.data);
            if (json.status == 200) {
                var type = json.data.type;
                console.log("收到一条新信息,类型为:" + type);
                switch (type) {
                    case "REGISTER":
                        ws.registerReceive();
                        break;
                    case "SINGLE_SENDING":
                        ws.singleReceive(json.data);
                        break;
                    case "GROUP_SENDING":
                        ws.groupReceive(json.data);
                        break;
                    default:
                        console.log("不正确的类型!");
                }
                
socket.onopen = setTimeout(function (event) {
   console.log("WebSocket已成功连接!");
   ws.register();
}, 1000)
​
socket.onclose = function (event) {
   console.log("WebSocket已关闭...");
};
                
ws.register: function () {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            var data = {
                "userId": userId,
                "type": "REGISTER"
            };
            socket.send(JSON.stringify(data));
        } else {
            alert("Websocket连接没有开启!");
        }
    }

发送消息

发送消息主要分为私聊singleSend与群聊groupSend

   singleSend: function (fromUserId, toUserId, content) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            var data = {
                "fromUserId": fromUserId,
                "toUserId": toUserId,
                "content": content,
                "type": "SINGLE_SENDING"
            };
            socket.send(JSON.stringify(data));
        } else {
            alert("Websocket连接没有开启!");
        }
    },
​
    groupSend: function (fromUserId, toGroupId, content) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            var data = {
                "fromUserId": fromUserId,
                "toGroupId": toGroupId,
                "content": content,
                "type": "GROUP_SENDING"
            };
            socket.send(JSON.stringify(data));
        } else {
            alert("Websocket连接没有开启!");
        }
    }

接受消息

主要是接受到数据后前端页面显示的处理,如对消息框及好友列表的处理,通过将消息拼接为原生html加入当前页面中显示给用户.

    singleReceive: function (data) {
        // 获取、构造参数
        console.log("data", data);
        var fromUserId = data.fromUserId;
        var content = data.content;
        var fromAvatarUrl;
        var $receiveLi;
        $('.conLeft').find('span.hidden-userId').each(function () {
            if (this.innerHTML == fromUserId) {
                fromAvatarUrl = $(this).parent(".liRight")
                    .siblings(".liLeft").children('img').attr("src");
                $receiveLi = $(this).parent(".liRight").parent("li");
            }
        })
        var answer = '';
        answer += '<li>' +
            '<div class="answers">' + content + '</div>' +
            '<div class="answerHead"><img src="' + fromAvatarUrl + '"/></div>' +
            '</li>';
​
        // 消息框处理
        processMsgBox.receiveSingleMsg(answer, fromUserId);
        // 好友列表处理
        processFriendList.receiving(content, $receiveLi);
    },
​
    groupReceive: function (data) {
        // 获取、构造参数
        console.log(data);
        var fromUserId = data.fromUserId;
        var content = data.content;
        var toGroupId = data.toGroupId;
        var fromAvatarUrl;
        var $receiveLi;
        $('.conLeft').find('span.hidden-userId').each(function () {
            if (this.innerHTML == fromUserId) {
                fromAvatarUrl = $(this).parent(".liRight")
                    .siblings(".liLeft").children('img').attr("src");
                /* $receiveLi = $(this).parent(".liRight").parent("li"); */
            }
        })
        $('.conLeft').find('span.hidden-groupId').each(function () {
            if (this.innerHTML == toGroupId) {
                $receiveLi = $(this).parent(".liRight").parent("li");
            }
        })
        var answer = '';
        answer += '<li>' +
            '<div class="answers">' + content + '</div>' +
            '<div class="answerHead"><img src="' + fromAvatarUrl + '"/></div>' +
            '</li>';
​
        // 消息框处理
        processMsgBox.receiveGroupMsg(answer, toGroupId);
        // 好友列表处理
        processFriendList.receiving(content, $receiveLi);
    }

项目演示

登录

私聊

 

 

 群聊

 作者:NP447

 

...全文
1911 2 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
2 条回复
切换为时间正序
请发表友善的回复…
发表回复
SC-鄧 2022-12-24
  • 打赏
  • 举报
回复

有源码嘛,急需

Wjy0208 2022-05-31
  • 打赏
  • 举报
回复

有源码吗

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧