前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从头搭建一个在线聊天室(二)

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

作者头像
周萝卜
发布2019-07-17 17:18:16
1.3K0
发布2019-07-17 17:18:16
举报

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

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

整体技术栈

  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 修复

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 萝卜大杂烩 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 整体技术栈
  • 应用 redis
  • 完善 chat 视图功能
  • 创建及加入聊天室
  • 创建聊天室
  • 加入聊天室
    • 一些辅助功能
    • 一、聊天室列表
    • 二、退出操作
    • 三、用户头像
      • 消息推送逻辑
        • 效果图展示
          • TODO
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档