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

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

第一部分可以看这里(链接

整体技术栈

  1. redis 应用
  2. flask_socketio 的使用
  3. websocket 简单应用

应用 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 类型

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

完善 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">
                <li><a href="/">Home</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
                {% if current_user.is_authenticated %}
                <li><a href="{{ url_for('logout') }}">Logout</a></li>
                {% else %}
                <li><a href="{{ url_for('login') }}">Login</a></li>
                {% endif %}
            </ul>
        </div>
    </div>
</div> {% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello, {{ 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 修复

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

原文发布于微信公众号 - 萝卜大杂烩(zhouluoboluandun)

原文发表时间:2019-06-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券