从头搭建一个在线聊天室(二)

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

转载声明:转载请注明出处,本技术博客是本人原创文章

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 从头搭建一个在线聊天室(二)

微信公众号:**萝卜大杂烩** 关注可了解更多的原创内容。问题或建议,请公众号留言或加本人微信; **如果你觉得文章对你有帮助,欢迎加微信交流**

今天是从头开始做一个在线聊天网站系类的第二部分,完善功能,实现对话。

第一部分可以看这里()

整体技术栈

  • redis 应用
  • flask_socketio 的使用
  • websocket 简单应用
  • flask_socketio 的使用

    应用 redis

    我这里使用 redis 来作为后端数据存储工具。大家如果有自己的 redis 服务器当然是最好了,如果没有的话,推荐下在线的 redis 免费应用 redislabs,大家可以自行体验下,https://redislabs.com/

    下面连接到 redis 服务器并打开连接池

    
    pool = redis.ConnectionPool(host='redis-12143.c8.us-ea.ec2.cloud.redislabs.com', port=17143,
                                decode_responses=True, password='pkAWNdYWfbLLfNOfxTJinm9SO1')
    r = redis.Redis(connection_pool=pool)
    

    redis 中数据结构及用法如下:

  • chat-{ChatRoomName},聊天室及加入的用户,zset 类型
  • msg-{ChatRoomName},每个聊天室对应的消息,zset 类型
  • msg-{ChatRoomName},每个聊天室对应的消息,zset 类型

    当前结构比较简单,暂时只定义了两个域,分别用来存储聊天室和消息。

    完善 chat 视图功能

    在上一部分中,chat 视图函数仅仅是返回了一个 HTML 页面,并没有任何功能逻辑,现在要完善下。最新的代码如下:

    
    @app.route('/chat', methods=['GET', 'POST'])
    @login_required
    def chat():
        rname = request.args.get('rname', "")
        ulist = r.zrange("chat-" + rname, 0, -1)
        messages = r.zrange("msg-" + rname, 0, -1, withscores=True)
        msg_list = []
        for i in messages:
            msg_list.append([json.loads(i[0]), time.strftime("%Y/%m/%d %p%H:%M:%S", time.localtime(i[1]))])
        return render_template('chat.html', rname=rname, user_list=ulist, msg_list=msg_list)
    

    其中 rname 是其他函数传值过来的,我们后面再说。 r.zrange() 函数就是从 redis 中取出对应聊天室的用户列表和历史聊天记录,最后就是把相关的信息返回到模板中。

    创建及加入聊天室

    在 chat 视图中,我们传入了一个 rname 字段,这个字段就是当创建或者加入聊天室时,需要传递过来的。

    创建聊天室

    
    @app.route('/createroom', methods=["GET", 'POST'])
    @login_required
    def create_room():
        rname = request.form.get('chatroomname', '')
        if r.exists("chat-" + rname) is False:
            r.zadd("chat-" + rname, current_user.username, 1)
            return redirect(url_for('chat', rname=rname))
        else:
            return redirect(url_for('chat_room_list'))
    

    判断聊天室名称是否存在,如果不存在,则将当前用户在 redis 中创建并跳转至 chat 函数;否则跳转至聊天室列表页面。

    加入聊天室

    
    @app.route('/joinroom', methods=["GET", 'POST'])
    @login_required
    def join_chat_room():
        rname = request.args.get('rname', '')
        if rname is None:
            return redirect(url_for('chat_room_list'))
        r.zadd("chat-" + rname, current_user.username, time.time())
        return redirect(url_for('chat', rname=rname))
    

    这里是从前端获取到聊天室名称(rname),并将当前用户名加入到对应的聊天室中。

    到这里,redis 中的聊天室就处理完成了,下面再来看看其他的一些辅助功能。

    一些辅助功能

    一、聊天室列表

    既然有加入聊天室的功能,那么就要提供一个列表供用户选择聊天室。

    后台逻辑代码:

    
    @app.route('/roomlist', methods=["GET", 'POST'])
    @login_required
    def chat_room_list():
        roomlist_tmp = r.keys(pattern='chat-*')
        roomlist = []
        for i in roomlist_tmp:
            i_str = str(i, encoding='utf-8')
            istr_list = i_str.split('-', 1)
            roomlist.append(istr_list[1])
        return render_template('chatroomlist.html', roomlist=roomlist)
    

    比较简单,到 redis 中拿到所有以“chat-”开头的 key 值,然后处理成列表返回到前端即可。

    前台页面代码:

    
     extends "bootstrap/base.html" %}
     import "bootstrap/wtf.html" as wtf %}
    
     block title %}Flasky endblock %}
    
     block navbar %}
    div class="navbar navbar-inverse" role="navigation"
        div class="container"
            div class="navbar-header"
                button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"
                    span class="sr-only"Toggle navigation/span
                    span class="icon-bar"/span
                    span class="icon-bar"/span
                    span class="icon-bar"/span
                /button
                a class="navbar-brand" href="/"Flasky/a
            /div
            div class="navbar-collapse collapse"
                ul class="nav navbar-nav"
                    lia href="/"Home/a/li
                /ul
                ul class="nav navbar-nav navbar-right"
                     if current_user.is_authenticated %}
                    lia href=" url_for('logout') }}"Logout/a/li
                     else %}
                    lia href=" url_for('login') }}"Login/a/li
                     endif %}
                /ul
            /div
        /div
    /div  endblock %}
    
     block content %}
    div class="container"
        div class="page-header"
            h1Hello,  current_user.username }}!/h1
        /div
        div class="page-header"
             for i in roomlist %}
            p i }}   a href=" url_for('join_chat_room', rname=i) }}" class="btn btn-default" role="button"Join This Room/a/p
             endfor %}
        /div
    form action=" url_for('create_room') }}" method="POST" class="comment-form"
             div class="form-group comment-form-author"
            label for="chatroomname"Chat Room Name span class="required"*/span/label
            input class="form-control" id="chatroomname" name="chatroomname" type="text" value="" size="30" aria-required='true' /
            /div
            div class="form-group comment-form-comment"
            label for="description"Chat Room Description span class="required"*/span/label
            textarea class="form-control" id="description" name="description" cols="45" rows="6"/textarea
            /div
            button  name="submit" type="submit" id="submit" class="btn btn-primary" value="Submit Comment"Create Room/button
    /form
    /div
     endblock %}
    

    就是循环渲染列表数据,和一个创建聊天室的表单。

    二、退出操作

    当用户退出登陆时,我们当前也希望该用户同时退出聊天室,所以修改 logout 函数如下:

    
    @app.route('/logout')
    @login_required
    def logout():
        rname = request.args.get("rname", "")
        r.zrem("chat-" + rname, current_user.username)
        logout_user()
        return redirect(url_for('login'))
    

    从前端拿到聊天室的名字,并在 redis 的对应 zset 中删除当前用户。

    三、用户头像

    为了聊天室的美观,不同用户需要拥有不同的头像,这里还是使用 gravatar 这个免费的头像服务。
    在 User 模型中添加代码:

    
    class User(UserMixin, db.Model):
        __tablename__ = 'users'
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(64), unique=True, index=True)
        password = db.Column(db.String(64))
        avatar_hash = db.Column(db.String(32))
    
        def gravatar(self, size=100, default='identicon', rating='g'):
            if request.is_secure:
                url = 'https://secure.gravatar.com/avatar'
            else:
                url = 'http://www.gravatar.com/avatar'
            email = self.username + "@hihichat.com"
            myhash = self.avatar_hash or hashlib.md5(email.encode('utf-8')).hexdigest()
            return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=myhash, size=size,
                                                                         default=default, rating=rating)
    
        def new_gravatar(self, name, size=100, default='identicon', rating='g'):
            url = 'http://www.gravatar.com/avatar'
            email = name + "@hihichat.com"
            myhash = hashlib.md5(email.encode('utf-8')).hexdigest()
            return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=myhash, size=size,
                                                                         default=default, rating=rating)
    

    两个 gravatar 函数,一个是给当前用户使用的,另一个用来处理给定名称的头像生成。这里偷懒了,没有合成一个通用的函数,后面再优化吧。关于 gravatar 头像的具体用法,可以直接查看官网。

    消息推送逻辑

    下面就开始编写最主要的消息推送逻辑。
    我采用的技术是 websocket,这样节省了使用 Ajax 轮询带来的额外开销。而且 flask 框架也有很好的 websocket 相关的扩展库供我们使用,即 flask-sokcetio。

    首先安装好 flask_socketio 模块,然后引入并初始化

    
    from flask_socketio import SocketIO, emit
    
    socketio = SocketIO()
    app = Flask(__name__)
    socketio.init_app(app)
    

    编写一个 socket 发送消息的函数

    
    def socket_send(data, user):
        emit("response", {"code": '200', "msg": data, "username": user}, broadcast=True, namespace='/testnamespace')
    
    
    socketio.on_event('request_for_response', socket_send, namespace='/testnamespace')
    

    其中 request_for_response,response 和 testnamespace 都需要和前端代码相对应。request_for_response 是用来接收前端传递到后端的消息,response 是后端传递消息到前端时的标识,而 namespace 则类似于作用域的概念,相互传递的消息都仅仅作用在 testnamespace 这个 namespace 中。

    前端 JavaScript 代码:

    
    //websocket
    var websocket_url = 'http://' + document.domain + ':' + location.port + '/testnamespace';
    var socket = io.connect(websocket_url);
    //发送消息到后端
    socket.emit('request_for_response',{'param':'rname}}'});
    
    
    //监听回复的消息
    socket.on('response',function(data){
        var myDate = new Date();
        var myTime = myDate.toLocaleString();
        var msg = data.msg;
        var username = data.username;
        var currentuser = ' current_user.username }}';
        console.log(currentuser);
        if ( currentuser == username )
        {
        username = '你';
        };
        var hash = md5(username + "@hihichat.com");
        var htmlData2 =
                        'div class="msg_item fn-clear"'
                       + '   div class="uface"img src="http://www.gravatar.com/avatar/' + hash + '?s=40&d=identicon&r=g" width="40" height="40"  alt=""//div'
                       + '   div class="item_right"'
                       + '     div class="msg"' + msg + '/div'
                       + '     div class="name_time"' + username + ' · ' + myTime +'/div'
                       + '   /div'
                       + '/div';
        $("#message_box").append(htmlData2);
        $('#message_box').scrollTop($("#message_box")[0].scrollHeight + 20);
    });
    

    关于更多的 websocket 用法,大家可以自行查找相关资料,这里就不做过多介绍了。

    最后,编写接收聊天内容的 API

    
    @app.route('/api/sendchat/info', methods=['GET', 'POST'])
    @login_required
    def send_chat(info):
        rname = request.form.get("rname", "")
        body = {"username": current_user.username, "msg": info}
        r.zadd("msg-" + rname, json.dumps(body), time.time())
        socket_send(info, current_user.username)
        return info
    

    将接收到的聊天内容插入到对应的 redis 中(msg-*),然后调用 websocket 函数,广播刚刚收到的消息到所有已经连接的 socket 客户端。

    效果图展示

    登陆页面:

    从头搭建一个在线聊天室(二)

    index 页面:

    从头搭建一个在线聊天室(二)

    聊天室列表页面:

    从头搭建一个在线聊天室(二)

    聊天室页面:

    从头搭建一个在线聊天室(二)

    TODO

    聊天室的大体功能已经完成了,但是还有很多不完善的地方,当然,bug 也挺多的,后面再逐步完善。

  • 1. 增加聊天机器人
  • 2. 支持非登陆用户聊天
  • 3. 逻辑优化
  • 4. bug 修复
    1. 支持非登陆用户聊天

    2. bug 修复

    完整代码,会在完善后再提供出来,感谢阅读!

    猜泥稀饭:

    从头系列:

    原文始发于微信公众号(萝卜大杂烩):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    转载声明:转载请注明出处,本技术博客是本人原创文章

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 从头搭建一个在线聊天室(二)


     上一篇
    从头搭建一个在线聊天室(一) 从头搭建一个在线聊天室(一)
    微信公众号:**萝卜大杂烩** 关注可了解更多的原创内容。问题或建议,请公众号留言或加本人微信; **如果你觉得文章对你有帮助,欢迎加微信交流** 今天从头开始做一个在线聊天网站,网上各种各样的聊天工具已经很多了,为啥还要做这么一个
    2021-04-06
    下一篇 
    Python 面试题大全系列(三) Python 面试题大全系列(三)
    微信公众号:**萝卜大杂烩** 关注可了解更多的原创内容。问题或建议,请公众号留言或加本人微信; **如果你觉得文章对你有帮助,欢迎加微信交流** 今天继续分享 Python 相关的面试题,你准备好了嘛! 综合篇(一),网络编程 1
    2021-04-06