微信公众号:**萝卜大杂烩** 关注可了解更多的原创内容。问题或建议,请公众号留言或加本人微信; **如果你觉得文章对你有帮助,欢迎加微信交流**
今天是从头开始做一个在线聊天网站系类的第二部分,完善功能,实现对话。
第一部分可以看这里()
整体技术栈
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 中数据结构及用法如下:
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 也挺多的,后面再逐步完善。
支持非登陆用户聊天
bug 修复
完整代码,会在完善后再提供出来,感谢阅读!
猜泥稀饭:
从头系列:
原文始发于微信公众号(萝卜大杂烩):