从零搭建一个在线聊天室

你们跟着我,齐步一块儿走!javascript

文章比较长,还请耐心阅读css

先提供项目源码,欢迎 start 和 forkhtml

github.com/zhouwei713/…前端

总体技术栈

  1. flask 框架
  2. flask_login 的使用
  3. websocket 简单应用
  4. redis 应用
  5. flask_socketio 的使用

搭建权限框架

仍是使用 Flask 来搭建后台应用,使用 flask-login 扩展来处理用户登录鉴权逻辑。 首先定义登录表单java

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), ])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Keep me logged in')
    submit = SubmitField('Log in')
复制代码

一个简单的登录表单,不作过多解释python

接下来定义数据库结构jquery

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))
复制代码

当前,咱们只须要一个 user 用户表,只包含三个字段的简单表。用户密码也只是简单的保存了明文,后面再处理用户密码的 hash 问题。git

下面就能够定义用户登录表单github

from flask_login import LoginManager


login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'login'
app = Flask(__name__)
login_manager.init_app(app)
app.config['SECRET_KEY'] = 'hardtohard'


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user:
            login_user(user)
            return redirect(url_for('chat'))
    return render_template('login.html', form=form)
复制代码

这里定义了,只检查用户名是否存在,若是存在,就执行 login_user() 函数,登录。用户密码的使用,也留到后面再作处理。web

其中 load_user,是回调函数,将获取到的 user 对象存储到浏览器的 session 中,而后在调用 login_user 函数时,就会调用 load_user 来把真正须要登录的用户设置到 session 中。当登录成功后,就会跳转到 chat 函数所对应的页面。

chat 函数比较简单,只是展现一个网页

@app.route('/chat', methods=['GET', 'POST'])
@login_required
def chat():
    return render_template('chat.html')
复制代码

使用 login_required 装饰器,保证该函数只容许登录的用户访问。

增长些初始化函数

@app.route('/adddb', methods=['GET', 'POST'])
def addbd():
    db.create_all()
    return "OK"


@app.route('/deldb', methods=['GET', 'POST'])
def delbd():
    db.drop_all()
    return "OK"


@app.route('/adduser/<user>', methods=['GET', 'POST'])
def adduser(user):
    user = User(username=user, password='admin')
    db.session.add(user)
    db.session.commit()
    return "OK"
复制代码

增长了初始化数据库和新增用户的函数。

构建前端页面

首先处理登录页面,在 login.html 中添加

{% 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, Welcome!</h1>
    </div>
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}
复制代码

使用扩展库 flask_bootstrap 来快速构建页面。

下面重点来看看 chat 页面。 首先来看看主体页面,在 chat.html 中填入代码

{% extends 'bootstrap/base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Kung Fu Realm{%endblock %}
{% block head %}
<head>
<meta charset="utf-8">
<title>Hi Hi 聊天室</title>
<link rel="shortcut icon" href="{{ url_for('static',filename='chat/images/hihi.jpg')}}">
<link rel="icon" href="{{ url_for('static',filename='chat/images/hihi.jpg')}}" type="image/x-icon">
<link type="text/css" rel="stylesheet" href="/static/chat/css/style.css">
<script type="text/javascript" src="{{ url_for('static', filename='chat/js/jquery.min.js') }}"></script>
</head>
{% endblock %}
{% block content %}
<body>
<div class="chatbox">
  <div class="chat_top fn-clear">
      <div class="uinfo fn-clear"  style="float: left;"><div class="uface"><h1 style="color: #7777">ROOM: 聊天室123哈哈哈</h1></div></div>
    <div class="uinfo fn-clear">
    {% if current_user.is_authenticated %}
      <div class="uface"><img src="{{ url_for('static', filename='chat/images/hi.jpg') }}" width="40" height="40"  alt=""/></div>
    {% else %}
      <div class="uface"><img src="{{ url_for('static', filename='chat/images/hi.jpg')}}" width="40" height="40"  alt=""/></div>
    {% endif %}
      <div class="uname">
        小HI<i class="fontico down"></i>
        <ul class="managerbox">
            {% if current_user.is_authenticated %}
          <li><a href="{{ url_for('login') }}"><i class="fontico lock"></i>退出登录</a></li>
            {% else %}
          <li><a href="{{ url_for('logout') }}"><i class="fontico logout"></i>登陆</a></li>
            {% endif %}
        </ul>
      </div>
    </div>
  </div>
  <div class="chat_message fn-clear">
    <div class="chat_left">
      <div class="message_box" id="message_box">
        <div class="msg_item fn-clear">
          <div class="uface"><img src="{{ url_for('static', filename='chat/images/duck.jpg')}}" width="40" height="40"  alt=""/></div>
          <div class="item_right">
            <div class="msg own"><img src="{{ url_for('static', filename='chat/images/hihi.jpg')}}" width="400" height="400"  alt=""/></div>
            <div class="name_time">小黄鸭 </div>
          </div>
        </div>
          {% if current_user.is_authenticated %}
        <div class="msg_item fn-clear">
          <div class="uface"><img src="{{ url_for('static', filename='chat/images/duck.jpg')}}" width="40" height="40"  alt=""/></div>
          <div class="item_right">
            <div class="msg">Welcome to Hihi Chat Room. 欢迎来到 Hihi 聊天室。 </div>
            <div class="name_time">小黄鸭 </div>
          </div>
        </div>
          {% else %}
          <div class="msg_item fn-clear">
          <div class="uface"><img src="{{ url_for('static', filename='chat/images/duck.jpg')}}" width="40" height="40"  alt=""/></div>
          <div class="item_right">
            <div class="msg">您尚未登录,先和小黄鸭聊聊吧。 </div>
            <div class="name_time">小黄鸭 </div>
          </div>
        </div>
          {% endif %}
      </div>
      <div class="write_box">
      {% if current_user.is_authenticated %}
        <textarea id="message" name="message" class="write_area" placeholder="说点啥吧..."></textarea>
      {% else %}
      <textarea id="message_not" name="message" class="write_area" placeholder="说点啥吧..."></textarea>
      {% endif %}
        <input type="hidden" name="fromname" id="fromname" value="你" />
        <input type="hidden" name="to_uid" id="to_uid" value="0">
        <div class="facebox fn-clear">
          <div class="expression"></div>
          <div class="chat_type" id="chat_type">群聊</div>
            {% if current_user.is_authenticated %}
          <button name="login" class="sub_but" id="sub_but_login">提 交</button>
            {% else %}
          <button name="logout" class="sub_but" id="sub_but">提 交</button>
            {% endif %}
        </div>
      </div>
    </div>
  </div>
</div>
复制代码

总体效果以下,是否是挺少女系的。

enter image description here

当用户在点击“提交”按钮后,调用 JS 函数

/*用户登录的用户点击提交按钮发送消息按钮*/
	$('#sub_but_login').click(function(event){
	    sendMessageLogin(event, fromname, to_uid, to_uname);
	});
复制代码

为了后面便于扩展,将未登陆的用户特别区分开来,后面也许一样容许未登录用户访问该页面,可是只能同机器人小黄鸭聊天

/*用户未登录的用户点击提交按钮发送消息按钮*/
	$('#sub_but').click(function(event){
	    sendMessage(event, fromname, to_uid, to_uname);
	});
复制代码

在来看函数 sendMessageLogin

function sendMessageLogin(event, from_name, to_uid, to_uname){
    var msg = $("#message").val();
	var myDate = new Date();
	var myTime = myDate.toLocaleTimeString();
	var itTime = myDate.toLocaleString();
	//var iTime = myDate.toDateString();
	var htmlData =   '<div class="msg_item fn-clear">'
                   + ' <div class="uface">{% if current_user.is_authenticated %}<img src="{{ url_for('static', filename='chat/images/hi.jpg') }}" width="40" height="40" alt=""/>{% endif %}</div>'
			       + ' <div class="item_right">'
			       + ' <div class="msg own">' + msg + '</div>'
			       + ' <div class="name_time">' + from_name + ' · ' + itTime +'</div>'
			       + ' </div>'
			       + '</div>';
	$("#message_box").append(htmlData);
	$('#message_box').scrollTop($("#message_box")[0].scrollHeight + 20);
	$("#message").val('');
	setTimeout(function(){sendToServer(from_name, msg)}, 1000); //延时调用
}
复制代码

接收几个参数,而后将当前会话消息追加到 HTML 页面中,而且调用真正的后台 API 函数 sendToServer

function sendToServer(name, msg){
    var xmlhttp = new XMLHttpRequest();
	var myDate = new Date();
	//var myTime = myDate.toLocaleTimeString();
    var myTime = myDate.toLocaleString();
    xmlhttp.onreadystatechange=function() {
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
            myObj = xmlhttp.responseText;
            var htmlData2 =   '<div class="msg_item fn-clear">'
                   + ' <div class="uface"><img src="{{ url_for('static', filename='chat/images/duck.jpg')}}" width="40" height="40" alt=""/></div>'
			       + ' <div class="item_right">'
			       + ' <div class="msg">' + myObj + '</div>'
			       + ' <div class="name_time">' + '小黄鸭' + ' · ' + myTime +'</div>'
			       + ' </div>'
			       + '</div>';
            $("#message_box").append(htmlData2);
            $('#message_box').scrollTop($("#message_box")[0].scrollHeight + 20);
        }
    }
    xmlhttp.open("GET", "/api/sendchat/" + msg, true);
	xmlhttp.send();

};
复制代码

sendToServer 函数调用后台 API,并把返回接收到的消息回写到 HTML 页面中。

而目前的后台 API 也比较简单,直接返回用户输入的消息

@app.route('/api/sendchat/<info>', methods=['GET', 'POST'])
@login_required
def send_chat(info):
    return info
复制代码

这样,一个总体的聊天室架子就搭建好了,接下来咱们再接入 redis,来实现聊天功能。

应用 redis

我这里使用 redis 来做为后端数据存储工具。你们若是有本身的 redis 服务器固然是最好了,若是没有的话,推荐下在线的 redis 免费应用 redislabs,你们能够自行体验下,redislabs.com/

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

pool = redis.ConnectionPool(host='redis-12143.c8.us-east-1-3.ec2.cloud.redislabs.com', port=12143,
                            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 中的聊天室就处理完成了,下面再来看看其余的一些辅助功能。

一些辅助功能

1、聊天室列表

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

后台逻辑代码:

@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 %}
复制代码

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

2、退出操做

当用户退出登录时,咱们当前也但愿该用户同时退出聊天室,因此修改 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 中删除当前用户。

3、用户头像

为了聊天室的美观,不一样用户须要拥有不一样的头像,这里仍是使用 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, name=None, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        if name is not None:
            email = name + "@hihichat.com"
        else:
            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)

复制代码

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 = 'ws://' + 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 客户端。

效果图展现

登录页面:

enter image description here

index 页面:

enter image description here

聊天室列表页面:

enter image description here

聊天室页面:

enter image description here

到此为止,其实咱们已经完成了一个简单的聊天室功能。可是呢,该程序还有不少功能须要优化,好比程序代码结构(当前全部后台逻辑代码都在一个文件中),聊天室控制(禁言,踢人等),以及非登录用户的处理,还要比较有意思的聊天机器人等待。


华丽丽的分割线


这里开始,就是一些进阶的功能,完善咱们的聊天室。

调整项目结构

随着咱们项目功能愈来愈多,把全部的逻辑代码都写在一个文件里已经不太合适了,下面就经过 flask 的工厂模式,把项目代码拆分开。

首先来看下拆分后的项目结构:

enter image description here

main 中主要存放后台逻辑代码。 static 中存放 js,css 以及用到的图片等。 templates 中存放 HTML 模板。 models.py 中是数据库模型。 config.py 中是一些公共的配置信息。 manage.py 中是项目的启动信息。

下面咱们分别来看看各个模块对应的代码

具体代码拆分

1. 配置信息

在 config.py 中,填入代码:

import os
import redis


basedir = os.path.abspath(os.path.dirname(__file__))


class Config:
    SECRET_KEY = 'hardtohard'
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'chat.sqlite3')

    @staticmethod
    def init_app(app):
        pass


class DevelopmentConfig(Config):
    pass


class TestingConfig(Config):
    pass


class ProductionConfig(Config):
    pass


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
    }
复制代码

2. 使用工厂函数

在 app/_init_.py 中填入代码:

from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap
from flask_socketio import SocketIO
from config import config


login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'main.login'
db = SQLAlchemy()
bootstrap = Bootstrap()
socketio = SocketIO()


def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)
    socketio.init_app(app)
    login_manager.init_app(app)
    db.init_app(app)
    bootstrap.init_app(app)
    
    # 注册蓝本
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app
复制代码

create_app 函数就是程序的工厂函数,它接受一个配置名的参数。

3. 使用蓝本

蓝本和程序相似,也能够定义路由。不一样的是,在蓝本中定义的路由处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序的一部分。

在 main/_init_.py 中建立蓝本

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, forms
复制代码

经过实例化一个 Blueprint 类对象能够建立蓝本。这个构造函数有两个必须指定的参数: 蓝本的名字和蓝本所在的包或模块。和程序同样,大多数状况下第二个参数使用 Python 的 _name_ 变量便可。

4. 修改 view 视图

对于视图函数,须要导入相关的包,同时因为使用了蓝本,原来用来装饰路由的 app.route 都要修改成 main.route,url_for 函数也须要增长 main 做用域,修改后的部分代码以下:

from flask import render_template, redirect, url_for, request
from flask_login import login_required, login_user, logout_user, current_user
from . import main
from .. import db
from .forms import LoginForm
from ..models import User
from config import config
import time
import json
from ..socket_conn import socket_send

pool = redis.ConnectionPool(host='redis-12143.c8.us-east-1-3.ec2.cloud.redislabs.com', port=12143,
                            decode_responses=True, password='pkAWNdYWfbLLfNOfxTJinm9SO16eSJFx')
r = redis.Redis(connection_pool=pool)



@main.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user:
            login_user(user)
            return redirect(url_for('main.index'))
    return render_template('login.html', form=form)


@main.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('main.chat', rname=rname))
    else:
        return redirect(url_for('main.chat_room_list'))
复制代码

5. 编写 socket 链接函数

在 models.py 的同级目录下建立 socket_conn.py 文件,添加代码以下:

from . import socketio
from flask_socketio import emit


@socketio.on('request_for_response', namespace='/testnamespace')
def socket_send(data, user):
    emit("response", {"code": '200', "msg": data, "username": user}, broadcast=True, namespace='/testnamespace')
复制代码

该函数供视图函数调用,广播 socket 消息。

6. 完成 forms 和 models

将原来的表单代码和数据库模型代码分别拷贝到这两个文件中 forms.py

from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
from flask_wtf import FlaskForm


class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), ])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Keep me logged in')
    submit = SubmitField('Log in')
复制代码

models.py

from . import db
from flask_login import UserMixin
from flask import request
import hashlib


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, name=None, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        if name is not None:
            email = name + "@hihichat.com"
        else:
            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)
复制代码

7. 修改模板

把 HTML 模板里的 url_for() 函数都增长 main.,再放置到 templates 下面便可。

8. 启动脚本

顶级文件夹中的 manage.py 文件用于启动程序。

import os
from app import create_app, socketio


app = create_app(os.getenv('FLASK_CONFIG') or 'default')


if __name__ == '__main__':
    socketio.run(app, debug=True)
复制代码

仍是使用 socketio.run 的方式启动应用。

至此,代码拆分完毕。

功能加强

1. 新增用户

之前咱们都是使用浏览器 URL 直接新增用户的,即函数 adduser,如今咱们作一个简单的页面,来规范这个操做。

定义表单

class CreateUserForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired(), EqualTo('password2',
                                                                            message='Password must match.')])
    password2 = PasswordField('Confirm password', validators=[DataRequired()])
    submit = SubmitField('Create User')

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('Username already in use.')
复制代码

定义了一个函数,来校验用户名是否重复。

修改原来的视图函数 adduser

@main.route('/adduser', methods=['GET', 'POST'])
@login_required
def adduser():
    form = CreateUserForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, password=form.password.data)
        db.session.add(user)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('adduser.html', form=form)
复制代码

还要再修改下 User 模型,由于当前保存的是明文密码,修改为使用 hash 存储。

@property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)
复制代码

分别设置密码的只读权限,以及 hash 计算和验证功能。

接下来编写 HTML 模板

{% 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('main.logout') }}">Logout</a></li>
                {% else %}
                <li><a href="{{ url_for('main.login') }}">Login</a></li>
                {% endif %}
            </ul>
        </div>
    </div>
</div> {% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello, New 一个 User 吧!</h1>
    </div>
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}
复制代码

至此,一个简单的新增用户功能就行了。固然,咱们还能够增长删除用户,重置密码等功能,这些的具体实现,均可以在 GitHub 的代码中看到,就再也不赘述了。

2. 权限控制

咱们其实并不但愿全部人都可以建立聊天室,那么就要作一个简单的控制功能。 首先定义一个 permission 表,用来存储建立聊天室等权限,再定义一个用户和权限的关联关系表

class Permission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    permission_name = db.Column(db.String(64), unique=True, index=True)


class RelUserPermission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer)
    permission_id = db.Column(db.Integer)
复制代码

而后咱们还须要一个增长权限的表,以及一个用户列表页面 在 forms.py 中添加

class EditUserForm(FlaskForm):
    permission = SelectMultipleField('Permission', coerce=int)
    submit = SubmitField('Submit')

    def __init__(self, user, *args, **kwargs):
        super(EditUserForm, self).__init__(*args, **kwargs)
        self.permission.choices = [(per.id, per.permission_name)
                                   for per in Permission.query.order_by(Permission.permission_name).all()]
        self.user = user
复制代码

定义了一个初始化函数,会获取到 Permission 表中的 name,id 等信息

接下来编写视图函数

@main.route('/listuser/', methods=['GET', 'POST'])
@login_required
def listuser():
    user_list = User.query.all()
    return render_template('listuser.html', user_list=user_list)


@main.route('/addper/', methods=['GET', 'POST'])
@login_required
def addper():
    form = CreatePerForm()
    if form.validate_on_submit():
        per = Permission(permission_name=form.permissionname.data)
        db.session.add(per)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('addper.html', form=form)


@main.route('/edituser/<int:id>/', methods=['GET', 'POST'])
@login_required
def edituser(id):
    user = User.query.filter_by(id=id).first()
    form = EditUserForm(user=user)
    if form.validate_on_submit():
        for p in form.permission.data:
            rup = RelUserPermission(user_id=id, permission_id=p)
            db.session.add(rup)
            db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('edituser.html', form=form)
复制代码

三个函数,分别是展现用户列表,增长权限,以及为用户添加权限。

而后再修改下 chat_room_list 函数,使得没有权限的用户不能展现建立聊天室的表单。

@main.route('/roomlist/', methods=["GET", 'POST'])
@login_required
def chat_room_list():
    roomlist_tmp = r.keys(pattern='chat-*')
    roomlist = []
    can_create = False
    create_room_id = Permission.query.filter_by(permission_name='createroom').first().id
    rel_user_id = RelUserPermission.query.filter_by(user_id=current_user.id).first()
    rel_permission = RelUserPermission.query.filter_by(user_id=current_user.id).first()
    if rel_permission and rel_user_id and create_room_id:
        rel_permission_id = rel_permission.permission_id
        if rel_permission_id == create_room_id:
            can_create = True
    for i in roomlist_tmp:
        i_str = str(i)
        istr_list = i_str.split('-', 1)
        roomlist.append(istr_list[1])
    return render_template('chatroomlist.html', roomlist=roomlist, can_create=can_create)
复制代码

这里主要是判断用户是否拥有 createroom 权限,其实还有一种更加简便,可是稍微有些绕的鉴权方式,能够在文末的连接中找到,你们也能够尝试下。

最后处理 HTML 表单

对于聊天室列表页面:

{% if can_create %}
  <form action="{{ url_for('main.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>
  {% endif %}
复制代码

对于用户列表页面:

{% 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('main.logout') }}">Logout</a></li>
                {% else %}
                <li><a href="{{ url_for('main.login') }}">Login</a></li>
                {% endif %}
            </ul>
        </div>
    </div>
</div> {% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello, 这里是全部的用户哦!</h1>
    </div>
    {% for user in user_list %}
    <a href="{{ url_for('main.edityouser', id=user.id) }}" class="btn btn-default" role="button">{{ user.username }}</a>
    {% endfor %}
</div>
{% endblock %}
复制代码

这里为了方便起见,当点击用户时,就会跳转至编辑用户权限的页面。

如今,没有权限的用户,就不能看到建立聊天室的表单喽!

3. 登录优化

当前的登录,只要用户名是正确的,不会验证密码,直接登录成功,如今来处理下密码校验功能。其实也简单,咱们在 User 模型中新增了一个函数 verify_password,只要登录的时候,调用该函数来验证密码便可。

@main.route('/login/', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user)
            return redirect(url_for('main.index'))
    return render_template('login.html', form=form)
复制代码

ok,密码错误的你,已经无法再登录了。

4. 放开非登录也可进入聊天室

1.去掉 chat_room_list,join_chat_room,send_chat 和 chat 视图函数的登录装饰器 @login_required 2.修改 chat_room_list,判断当前用户是否已经登录

@main.route('/roomlist/', methods=["GET", 'POST'])
def chat_room_list():
    roomlist_tmp = r.keys(pattern='chat-*')
    roomlist = []
    can_create = False
    create_room = Permission.query.filter_by(permission_name='createroom').first()
    if current_user.is_authenticated:  # 判断用户是否登录
        rel_user_id = RelUserPermission.query.filter_by(user_id=current_user.id).first()
        rel_permission = RelUserPermission.query.filter_by(user_id=current_user.id).first()
        if rel_permission and rel_user_id and create_room:
            rel_permission_id = rel_permission.permission_id
            create_room_id = create_room.id
            if rel_permission_id == create_room_id:
                can_create = True
    for i in roomlist_tmp:
        i_str = str(i)
        istr_list = i_str.split('-', 1)
        roomlist.append(istr_list[1])
    return render_template('chatroomlist.html', roomlist=roomlist, can_create=can_create)
复制代码

3.导航栏增长 room list 入口

<ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
                <li><a href="{{ url_for('main.chat_room_list') }}">Room List</a></li>
            </ul>
复制代码

4.chat 视图函数增长判断逻辑

@main.route('/chat/', methods=['GET', 'POST'])
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]))])
    if current_user.is_authenticated:
        return render_template('chat.html', rname=rname, user_list=ulist, msg_list=msg_list)
    else:
        email = "youke" + "@hihichat.com"
        hash = hashlib.md5(email.encode('utf-8')).hexdigest()
        gravatar_url = 'http://www.gravatar.com/avatar/' + hash + '?s=40&d=identicon&r=g'
        return render_template('chat.html', rname=rname, user_list=ulist,
                               msg_list=msg_list, g=gravatar_url)
复制代码

5.修改 send_chat 视图

@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
    if current_user.is_authenticated:
        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
    else:
        return info
复制代码

当前对于未登录的用户(游客),直接回复游客发送的消息。

清理过时消息

因为咱们须要定时清理 redis 中保存的聊天记录,那么就须要一个定时任务。flask 有一个完善的插件 flask-apscheduler,可是简单实验了下,限制仍是挺多的,因此,我这里选择本身实现一个简单的定时器功能。 建立一个 tasks.py 文件 首先定义定时器类

from threading import Timer


class Scheduler(object):
    def __init__(self, sleep_time, func, mytime=None):
        self.sleep_time = sleep_time
        self.func = func
        self._t = None
        self.mytime = mytime

    def start(self):
        if self._t is None:
            self._t = Timer(self.sleep_time, self._run)
            self._t.start()
        else:
            raise Exception("this timer is already running")

    def _run(self):
        if self.mytime is not None:
            self.func(self.mytime)
        else:
            self.func()
        self._t = Timer(self.sleep_time, self._run)
        self._t.start()

    def stop(self):
        if self._t is not None:
            self._t.cancel()
            self._t = None

    @staticmethod
    def init_app(app):
        pass
复制代码

使用线程中的 Timer 来调用真正的函数,经过 sleep time 的方式达到定时调用的效果。

而后编写须要定时调用的函数,即清理数据的函数。

def keep_msg(mytime=None):
    if mytime is not None:
        expare_time = mytime
    else:
        expare_time = 604800
    msg_list = r.keys("msg-*")
    for msg in msg_list:
        _ = r.zrange(msg, 0, 0)
        for i in _:
            score = r.zscore(msg, i)
            if time.time() - score > expare_time:
                r.zrem(msg, i)
复制代码

比较简单,判断 redis 中的 score 是否处于过时时间,是,则删除。

接下来注册函数到咱们的 flask 应用当中。 在 _init_.py 中填入以下代码:

from .tasks import Scheduler, keep_msg


sch = Scheduler(86400, keep_msg)  # 每间隔一天执行


def create_app(config_name):
    ...
    sch.init_app(app)
    ...
    return app
复制代码

最后还要注意的是,因为咱们前面是使用 socketio 来启动的应用,由于 socketio 是异步 io,而咱们的 scheduler 是阻塞运行的,因此须要在 socketio 中建立子线程来启动。 修改 manage.py 以下:

import os
from app import create_app, socketio, sch


app = create_app(os.getenv('FLASK_CONFIG') or 'default')


if __name__ == '__main__':
    my = sch.start
    socketio.start_background_task(target=my)  # 启动一个子线程
    socketio.run(app, debug=True)
复制代码

这样,一个简单的定时任务就作好了。

禁言功能

正所谓“林子大了,什么鸟都有”,当聊天室人数不少的时候,常常会出现一些不和谐的话语和人,那么禁言功能就颇有必要了。

首先在 views 中建立一个新的函数

@main.route('/chat/block/roomuser/', methods=['GET', 'POST'])
@login_required
def block_roomuser():
    rname = request.args.get('rname', "")
    new_b_user = request.args.get('b_user', "")
    b_time = request.args.get('b_time', "")
    if b_time is "":
        r.set('b_user-' + new_b_user, new_b_user, ex=None)
    else:
        r.set('b_user-' + new_b_user, new_b_user, ex=b_time)
    return redirect(url_for('main.room_user_list', rname=rname))
复制代码

从前端获取到对应的聊天室名字、须要禁言的用户和禁言时间,而后根据禁言时间,把用户添加到 redis 中。

再来看看禁言功能的入口函数

@main.route('/chat/roomuser/list', methods=['GET', 'POST'])
@login_required
def room_user_list():
    rname = request.args.get('rname', "")
    ulist = r.zrange("chat-" + rname, 0, -1)
    b_user = r.keys('b_user-*')
    b_user_list = []
    for b in b_user:
        b_user_list.append(r.get(b))
    return render_template('roomuser_list.html', ulist=ulist, rname=rname, b_user=b_user_list)
复制代码

从 redis 对应的有序集合中取出正处于禁言状态的用户,把这些用户传递到模板供渲染使用。

对应的 roomuser_list.html 代码为:

<div class="container">
    <div class="page-header">
        <h1>Hello, 这里是聊天室 {{ rname }} 全部的用户哦!</h1>
    </div>
    {% for user in ulist %}
    <p>
        <button class="btn btn-primary">{{ user }}</button>
        {% if user in b_user %}
        <span class="label label-default">禁言中。。。</span>
        {% endif %}
    </p>
    <p>
        <div class="btn-group">
            <button type="button" class="btn btn-warning">禁言</button>
            <button type="button" class="btn btn-warning dropdown-toggle dropdown-toggle-split" data-toggle="dropdown">
                <span class="caret"></span>
            </button>
            <div class="dropdown-menu">
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=300)}}">5 Mins</a></li>
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=600)}}">10 Mins</a></li>
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=18000)}}">5 Hours</a></li>
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user)}}">永久禁言</a></li>
            </div>
        </div>
        <a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=1)}}" class="btn btn-info" role="button">解禁</a>
        <a href="{{ url_for('main.kick_roomuser', rname=rname, del_user=user) }}" class="btn btn-danger" role="button">踢出</a>
    </p>
    {% endfor %}
</div>
复制代码

方便起见,直接使用 bootstrap 框架渲染页面。同时这里取了个巧,在“解禁”的时候,只是传入 b_time 为1,这样1秒以后,用户就自动从 redis 中过时了,也就成功解禁了。

最后,再来处理聊天室的消息,禁言的用户,固然不能再发消息啦。

在 chat 函数中,添加代码:

@main.route('/chat/', methods=['GET', 'POST'])
def chat():
    ...
    b_user = r.keys('b_user-*')
    b_user_list = []
    for b in b_user:
        b_user_list.append(r.get(b))
    ...
    if current_user.is_authenticated:
        return render_template('chat.html', rname=rname, user_list=ulist, msg_list=msg_list,
                               b_user_list=b_user_list)
复制代码

把处于禁言的用户取出,传递给模板。

在 send_chat 函数中添加代码:

@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
    ...
    b_user = r.exists('b_user-%s' % current_user.username)
    if b_user:
        data = json.dumps({'code': 201, 'msg': 'Your are under block now!'})
        return data
    ...
复制代码

若是用户处于禁言状态,直接返回 json 消息。

修改 chat.html 中的 javascript 函数 sendToServer,增长代码以下:

var jsondata = JSON.parse(myObj);
               if ( jsondata.code == 201 || jsondata.code == 403) {
                   var htmlData3 =   '<div class="msg_item fn-clear">'
                   + ' <div class="uface"><img src="{{ url_for('static', filename='chat/images/duck.jpg')}}" width="40" height="40" alt=""/></div>'
			       + ' <div class="item_right">'
			       + ' <div class="msg">' + "自动回复: " + jsondata.msg + '</div>'
			       + ' <div class="name_time">' + '小黄鸭' + ' · ' + myTime +'</div>'
			       + ' </div>'
			       + '</div>';
			   $("#message_box").append(htmlData3);
               $('#message_box').scrollTop($("#message_box")[0].scrollHeight + 20);
               }
复制代码

判断返回的 json 中 code 值若是是 201 或 403,则由小黄鸭自动回复消息。

最后的效果以下:

enter image description here

踢人功能

若是在聊天室中,这我的真的让人忍无可忍,那么踢人就是最好的办法了。 其实实现思想和逻辑都和禁言相相似,这里直接给出部分代码

新增函数 kick_roomuser

@main.route('/chat/kick/roomuser/', methods=['GET', 'POST'])
@login_required
def kick_roomuser():
    rname = request.args.get("rname", "")
    del_user = request.args.get("del_user", "")
    r.zrem("chat-" + rname, del_user)
    return redirect(url_for('main.room_user_list', rname=rname))
复制代码

修改 send_chat 函数

@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
    ...
    if current_user.is_authenticated:
        rname = request.form.get("rname", "")
        ulist = r.zrange("chat-" + rname, 0, -1)
        if current_user.username in ulist:
            body = {"username": current_user.username, "msg": info}
            r.zadd("msg-" + rname, json.dumps(body), time.time())
            socket_send(info, current_user.username)
            data = json.dumps({'code': 200, 'msg': info})
            return data
        else:
            data = json.dumps({'code': 403, 'msg': 'You are not in this room'})
            return data
    else:
        data = json.dumps({'code': 202, 'msg': info})
        return data
复制代码

最后效果以下

enter image description here

对接聊天机器人

当前,若是用户没有登录,是没法和其余人聊天的。那么一个友好的聊天机器人就很是有必要了。咱们可使用免费的图灵聊天机器人,固然也能够本身训练一个。之前我也写过一篇关于如何训练聊天机器人,感兴趣的小伙伴儿能够到个人公众号里查看(萝卜大杂烩)。

在这里也直接复用之前部署的 API 了,只须要增长几行代码便可 修改 send_chat 函数

@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
    ...
    else:
        base_url = 'http://luobodazahui.top:8889/api/chat/'
        chat_text = requests.get(base_url + info).text
        return chat_text
复制代码

在函数中调用聊天机器人的 API 地址,将返回的内容传递给前端便可。

最终的效果以下:

enter image description here


最后的最后,咱们再来看看如何部署上线呢,毕竟没有部署到公网的 web 服务,都是啥啥啥

首先,你得有一个公网服务器,云主机之类的。 而后就是一顿折腾,各类安装了。个人云主机是 CentOS 7.5,下面的一切操做都默认是这个操做系统了。

在 CentOS 7.5 上安装 docker

curl -fsSL https://get.docker.com 
复制代码

安装 redis

docker pull redis
复制代码

启动 redis

docker run --name=myredis -p 6379:6379 -v /home/redis-data:/data  -d redis redis-server --appendonly yes
复制代码

在本机链接 redis,测试一下

docker exec -it 491f1051715a redis-cli
复制代码

安装 python3 源码安装 安装编译软件

yum -y groupinstall "Development tools"
yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
yum install libffi-devel -y
复制代码

下载源码包并解压

wget https://www.python.org/ftp/python/3.7.1/Python-3.7.1.tar.xz
tar Jxvf Python-3.7.1.tar.xz
复制代码

编译安装

cd Python-3.7.1
./configure --prefix=/usr/local/python3
make && make install
复制代码

建立软链接

ln -s /usr/local/python3/bin/python3 /usr/local/bin/python3
ln -s /usr/local/python3/bin/pip3 /usr/local/bin/pip3
复制代码

验证

python3 -V
pip3 -V
复制代码

安装 chatterbot

因为使用 pip 直接安装 chatterbot,一直报错,因此我这里采用 conda 来安装。

首先到 conda 官网下载好安装脚本,而后一键安装便可。

sh Miniconda3-latest-Linux-x86_64.sh
复制代码

以后就可使用 conda 的 pip 命令来安装 chatterbot 了。

pip install --upgrade chatterbot
pip install chatterbot_corpus
复制代码

安装 gevent 和 gunicorn

pip install gevent
pip install gunicorn
复制代码

以上,全部的安装准备工做基本完成了!

下面就很简单了呀

首先编写 gunicorn 启动脚本

debug = True
loglevel = 'debug'
bind = '0.0.0.0:5000'
logfile = '/home/mychat/online_chat/log/debug.log'
workers = 1
worker_class = 'eventlet' 
reload = True
复制代码

使用 gunicorn 启动 flask_socketio,貌似还不能很好的启动多进程,这部分,留待之后再研究。

而后再写一个程序启动脚本

/root/miniconda3/bin/gunicorn -D -c /home/mychat/online_chat/gunicorn manage:app
复制代码

最后,运行 run.sh 脚本,而后使用 ps -ef|grep python 来查看是否有进程存在

如上图所示,说明咱们的程序已经启动成功了,如今让咱们来访问 http://www.{hostip}:5000,不出意外的话,已经能够正常访问了。

也许有小伙伴会问,怎么没有用 Nginx 啊,这个本文就不详写了,后面咱们再一块儿看看如何使用 Nginx 代理,绑定域名,使用 https 等。

最后,再给出一个示例网站吧,http://106.13.2.228/

小破云主机,你们轻拍!

最后,再安利下公众号,“萝卜大杂烩”,欢迎关注!

enter image description here

全文完!

相关文章
相关标签/搜索