关于简单网页聊天室开发的记录(基于flask flask_socketio)
由于专业原因,平时课程没有设计网络开发,因此趁着假期学习写网络开发相关知识,先从项目入手,写的磕磕碰碰还有一些功能没有实现一些bug亟待改进。
一、工具
使用语言:python、js、html
借助工具包:flask_socketio、flask、eventlet
二、背景:
首先熟悉些专业术语:
Python路由:Web 服务器如何根据用户请求的 URL 找到对应的处理逻辑。
路由:路由的任务就是为每个数据包选择一条最优路径(比如距离最短、速度最快、最稳定的路线),确保数据高效、准确地到达。
URL:类似网址、它的核心作用是告诉浏览器或其他网络工具:如何找到并访问某个具体的资源。通常包含协议(http|https...)、域名或者主机名、端口、查询参数、片段(只在客户端生效)。
三、项目规定:
本项目是个多房间聊天室遵循以下规则:
(1)多房间聊天室有多个房间
(2)用户作为user只能在一个房间(前端控制但是后端允许一个user多房间)
(3)用户作为creator可以在多个房间
(5)只有creator才有权利关闭房间
(6)user才能发送接受信息
(7)创建房间后user不会自动加入user需要手动加入
(8)一个房间只能有一个creator
(9)由于user只能在一个房间内因此离开房间只能离开当前房间
(10)用户一登录就会进入大厅,大厅不允许关闭不允许离开,离开大厅只能通过返回登录
聊天室的前后端交互事件:
1、房间事件
create_room->room_created
close_room->room_closed
room_list->room_list
room_members->room->members(前端暂未实现处理)
2、用户事件定义
join_room->user_joined
leave_room->user_left
room_members->room_members
client_message->client_message
message_query->message_got
3、系统事件
error
connect/disconnect
(ps:左边是后端处理消息右边是前端处理消息)
后端维护容器:
rooms:字典容器,存储房间以及user和creator,格式rooms[room]={'user':[],'creator':}
message:字典容器,存储每个房间消息,格式message[room]={[{'username':,'message':,'message_id':},.......]}(这里的message_id直接使用时间戳)
user_session:字典容器,保存username和sid的映射关系,格式user_session[sid]=username
username_to_sids:字典容器,保存sid和username的映射关系,usrname_to_sids[username]=sid
message_id_index:字典容器,保存message_id和message的index之间的关系,方便快速查找定位消息
前端维护容器:
messageFlag={
hasMore:,
newRoom:,
Next:,(本例还未使用)
Prev:,
}四个bool型变量状态机用于处理加载消息方式
currentRoom:当前房间
next_message_id:当前message_id
isLoading:模拟加载锁后面会讲
localMessage:本地消息存储后面会讲
四、工程中的问题以及解决方法:
1、前端怎么实现消息加载:
由于用户在进入聊天室时可能前面已经有大量的消息但是一次性传输这么多消息到客户端信息量太大,可能会导致网络堵塞而用户也可能不需要那么多消息,因此采用分页加载逻辑(感觉网络上参考比较少!!!!!!!!啊啊啊),规则如下:
(1)用户一进入房间就加载最新一页消息到localMessage
(2)用户滚轮向上滚动到顶部就加载之前一页消息到localMessage
(3)进入房间后每由用户发送消息直接保存到localMessage,滚轮滚动到底部时直接渲染
(4)每次渲染都是渲染整个localMessage内容,滚轮位置依据情况而定(滚轮位置接下来会讲,这样将渲染和滚轮位置拆分开理解的方法感觉思路会比较清晰)
关于分页的方法,最先打算直接用原始的按编号分页的方法但是这样会有诸多问题:
按照代码逻辑最新的消息会被插到列表尾部而当要加载最新消息时要找到尾部所以我们规定从尾部开始往前一页为第0页(现实中最后页可能不完整因为消息数不是页数的整数倍,这里直接将剩余的消息直接放最后页)
| x | x-1 | x-2 |.....| 0 |
但是这个列表在浏览时不是静态的,随时都有可能有新消息进来,因此这是一个动态变化的列表,那么要怎么保存之前的浏览位置以及根据要求更新消息呢?
比如之前的列表变成:
| x+1 | x | x-1 |.....| 1 | 0 |
那之前的第0页变成了第一页如果按前端保存的页数继续读就会出错,因此需要有什么东西标记下消息位置,由于列表长度一直在变化因此我们就不能使用下标啊什么的,应该让消息本身就有方便查找的特征,即messsage_id,这里是模拟了游标分页的方法来实现消息加载,为什么是模拟呢,因为根据查到的资料可知,游标分页在数据库处理时用的比较多因为数据库为了平衡存储效率、查询性能和灵活性所以内存是不连续的会用类似链表的方式组织起来,因此如果用常用的LIMIT+offset的方法查找不是像c语言数组一样直接定位的,而是要从头开始在扫描知道找到为止,所以在offset很大时非常慢,而游标分页将message_id和索引直间直接建立映射,申请消息时只要携带下一页或者之前一页开头或者末尾的message_id就可以实现快速定位(本例偷懒直接传递的是当前页的message_id因为实际是顺序存储的不影响使用[汗]),这在特定应用场景下就可以提升查找速度,但游标分页不支持随机跳页,但是这对于聊天室这样的应用场景而言并不影响使用。
后端响应前端获取消息获取消息代码
def get_message(room,start_id,sid,type):
#新建房间
if type=='newRoom':
#起始和结束索引
end=len(message[room])
start=end-PAGE_COUNT
#向前
elif type=='Next':
#异常处理
index=search_message_by_id(msg_id=start_id,room=room)
if index==-1:
emit('error',{'err_msg':'消息id不存在'},to=sid)
return []
start=index+PAGE_COUNT
end=start+PAGE_COUNT
elif type=='Prev':
#异常处理
index=search_message_by_id(msg_id=start_id,room=room)
if index==-1:
emit('error',{'err_msg':'消息id不存在'},to=sid)
return []
end=index
start=end-PAGE_COUNT
else:
return []
#异常控制
if start<0:
start=0
if end<0:
return {'message':[],'next_id':None}
#如果是空列表
if start==end:
return {'message':[],'next_id':None}
#如果超出范围
if start>=len(message[room]):
return {'message':[],'next_id':None}
#向后
return {'message':message[room][start:end],'next_id':message[room][start]['message_id']}
#查找消息索引
def search_message_by_id(msg_id,room):
# 检查房间是否在索引表中
if room not in message_id_index:
return -1
# 直接从索引表获取索引(O(1) 操作)
return message_id_index[room].get(msg_id, -1)
2、前端怎么对滚轮位置进行控制
为了方便用户阅读,需要在渲染同时保持滚轮位置,这里的规则规定如下:
(1)当用户在底部时有新消息来直接渲染并滚动到底部
(2)当用户在浏览时有新消息来时保留当前页面位置只提示有新消息
(3)如果用户自己发送消息则滚动到底部显示
滚轮需要知道几个基本概念:
(1)scrollTop:元素内容向上滚动的像素距离(容器顶部到内容顶部的隐藏距离)。
(2)scrollHeight:元素内容的总高度(包括可见区域和隐藏区域,不包含滚动条宽度)。
(3)clientHeight:元素的可见高度(包含内边距 padding,不包含边框、滚动条和外边距)。
具体解释差不多就下面这张图了(原谅我拙劣的画技好吧[哭]):

从规则可知我们需要判断什么时候在底部什么时候在顶部,代码如下:
底部顶部判断代码
function isAtBottom(tolerance = 10) {
const { scrollTop, clientHeight, scrollHeight } = messageContainer;
return (scrollTop + clientHeight) >= (scrollHeight - tolerance);
}
function isAtTop(tolerance=5) {
// 滚动距离小于等于5px视为已到顶部(兼容浏览器像素误差)
return messageContainer.scrollTop <= tolerance;
}
滚轮滚动到底部
function scroll_to_bottom() {
messageContainer.scrollTop=messageContainer.scrollHeight-messageContainer.clientHeight;
}
消息渲染
//前端渲染消息
function renderMessage() {
// 记录渲染前的关键状态
const scrollTopBefore = messageContainer.scrollTop; // 滚动条位置
const clientHeight = messageContainer.clientHeight; // 可视区域高度
const scrollHeightBefore = messageContainer.scrollHeight; // 内容总高度
//渲染消息
messageList.innerHTML = '';
const fragment = document.createDocumentFragment();
localMessage.forEach((msg) => {
fragment.appendChild(create_message_element(msg.username, msg.message, msg.message_id));
});
messageList.appendChild(fragment);
}
//消息显示处理
function handleMessageRender() {
//不在底部以及消息够多提醒有条新消息
if(!isAtBottom()&&!isContentNotEnough()){
//不是更新新消息展示提示
if(!(isAtTop()&&isScroll())){
showNewMessageHint();
}
//更新新消息渲染
//else{
renderMessage();
//}
}
else{
renderMessage();
scroll_to_bottom();
}
}
申请时加锁
function loadEarlierMessage() {
// 加载锁:如果正在加载则直接返回
if (isLoading) return;
// 标记为加载中
isLoading = true;
// 还有消息显示加载提示(有更多消息且向前请求)
if(messageFlag.hasMore&&messageFlag.Prev){
showLoader();
}
// 设置加载历史消息标志
messageFlag.Prev = true;
// 发送消息加载请求
apply_new_message();
}
加载完成后解锁
socket.on('message_got',function (data) {
const{room,message}=data;
//状态机更新
if(room==currentRoom){
//没有更多消息了
if(message.message.length<PAGE_COUNT){
messageFlag.hasMore=false;
}
//是空的话不刷新
if(message.message.length===0){
//加载完成后渲染
isLoading=false;
return false;
}
//是新房间
if(messageFlag.newRoom===true){
//更新标志
messageFlag.newRoom=false;
MessageCover(message);
}
//冗余设计实际本项目不会使用到
else if(messageFlag.Next===true){
//更新标志
messageFlag.Next=false;
MessageInsertTail(message);
}
else if(messageFlag.Prev===true){
//更新标志
messageFlag.Prev=false;
MessageInsertHead(message);
}
}
//加载完成后渲染
isLoading=false;
//关闭提示窗口
// 6. 显示/隐藏加载提示
hideLoader();
//渲染消息
//renderMessage();
handleMessageRender();
});
五、代码架构组织
1、后端:
比较简单就是一堆响应前端消息并发送消息的函数,实际上本例基本是前端发送后后端才会响应。
2、前端:
connect/disconeect:处理基本的登录,存储用户名等等(有缺陷,不能正常运行,但是登录界面可以正常运行)
消息事件:client_message:接受消息存入localMessage,如果是自己消息直接滚动到底部
message_got:主要状态机根据状态变量方式更新localMessage并直接渲染(Prev:是在hasMore为真时加载之前消息,NewRoom:是新房间直接加载最新的一页消息)
apply_new_message():根据用户情况出发发送message_query
render_message():渲染消息(清空列表直接显示,简单粗暴)
handleMesssageRender():根据滚轮状况选择要不要滚动
房间事件根据规则跟新currentRoom即可。
六、效果

(ps:界面是ai帮忙美化的[汗])
七、完整代码
等我学下Git怎么上传[哭]
/------分割线:好了现在会了------/
项目地址
https://gitee.com/aa0101001/sos.git

浙公网安备 33010602011771号