恋恋风辰的个人博客


  • Home

  • Archives

  • Categories

  • Tags

  • Search

python基础之循环控制

Posted on 2026-04-05 | In python , AI

Python中循环的介绍

什么是循环

现实生活中,也有很多循环的应用场景:

(1)老师批改作业:拿起作业本→查看答案→批改打分→放回作业本,重复以上过程,直到所有学生的作业都批改完了

(2)收银员收款:接过顾客商品→扫码计价→收取钱款→打印小票,重复以上过程,直到所有排队顾客都完成付款。

(3)环卫工人扫地:拿起扫帚→清扫路面→收集垃圾→移动到下一段路面,重复以上过程,直到负责区域清扫完毕。

(4)学生晨读:打开课本→朗读段落→标记重点→继续朗读,重复以上过程,直到晨读时间结束。

以上场景都有一个共同的特点:有条件地重复地做一件事,每一次做的事情不同但类似。

程序是为了解决实际问题的,实际问题中存在着重复动作,那么程序中也应该有相应的描述,这就是循环。

循环的作用

循环的作用是什么?

答:让代码高效的重复执行,避免重复编写大量相同或相似的代码,提升编程效率。

循环的种类

在Python中,循环一共分为两大类:while循环与for循环

思考问题:while循环和for循环如何选择呢?

①:对于循环次数固定的(已知)情况下,建议使用for循环, 例如: 打印1~10的数字

②:对于循环次数未知(不确定)的情况下,建议使用while循环, 例如: 猜数字游戏

while循环语法及其应用

while循环的基本语法

1
2
3
4
5
6
7
8
9
# Step1: 定义一个计数器(初始化一个计数器)
i = 0或1
# Step2: 编写while循环结构
while 循环条件(判断 计数器 是否达到了目标位置):
循环体1
循环体2
...
# Step3: 在循环内部更新计数器
i = i + 1 或 i += 1

普及小知识:在计算机程序中,计数器大部分都是从0开始的,少数场景会从1开始,根据需求灵活选择。

总结:while循环三步走

① 初始化计数器

② 编写循环条件(判断计数器是否达到了目标位置)

③ 在循环内部更新计数器

while循环入门案例:使用while循环,循环输出5遍“Python真有趣”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 演示 while循环
"""
while 判断条件:
# 当条件满足 需要执行的内容

说明:
while: 实现循环操作的关键字
条件判断: 是否进入循环的依据
当条件成立, 就执行循环里面的内容, 条件一直成立 就一直循环执行内部的代码 直到条件不成立
"""

# 需求: 请循环 打印 5次 Python真有趣
# 1- 定义计数器
num = 1

# 2- 编写循环操作
while num <= 5:
# 2.1 执行循环的主体代码
print("Python真有趣")

# 2.2 更新计数器 +1
# num = num + 1
num += 1

while循环流程图:

image-20260405165806644

while循环案例

案例1:使用while循环求1..50的和

分析:定义一个while循环,让其可以帮助我们计算 1 + 2 + 3 + 4 + 5 … + 50,结果:1275

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 需求: 求 1~50的和
# 思路: 1- 需要对 1~50进行遍历(循环) 2- 每循环一个 就要对其进行累加计算(在while循环外部定义一个初试累加变量)

# 如何进行 1 ~ 50 循环呢
# 第一步: 定义计数器
i = 1

# 第二步: 编写循环操作
# 2.1 定义一个累加求和的变量
sum_total = 0

while i <= 50:
# 2.2 循环的代码块
sum_total = sum_total + i
# 第三步: 更新计数器
i += 1

print(sum_total)

案例2:求1~50之间,所有奇数的和

什么是奇数:所谓的奇数,就是不能被2整除的数字,数学中可以使用2n+1来表示奇数。(Python代码 => if 数值 % 2 != 0 代表它是一个奇数)

知识点:在while循环中,我们还可以结合if进行判断,筛选出符合条件的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 需求2: 求1~50之间,所有奇数的和  不能被2 整除的 就是奇数(余数不为0)
# 思路: 1- 需要对 1~50进行遍历(循环) 2- 每循环一个 先判断是否为奇数, 如果是对其进行累加计算(在while循环外部定义一个初试累加变量)

# 1- 定义计数器
i = 1

# 2- 编写循环操作
# 2.2.1 先定义一个累加的初始变量
sum_odd = 0
while i <= 50:
# 循环的主体要执行的代码块
# 2.1 判断是否为奇数
if i % 2 != 0:
# 2.2 如果是奇数, 进行累加
# 2.2.2 执行累加
sum_odd += i

# 3- 更新计数器
i += 1

print(sum_odd)

循环中的两大关键词

两大关键词

在Python循环中,经常会遇到两个常见的关键词:break 与 continue

break:代表终止整个循环结构,一旦执行,直接退出循环,不再执行后续循环体内容。

continue:代表中止当前本次循环,跳过本次循环剩余内容,继续执行下一次循环。

  • 举个栗子
1
2
3
4
5
举例:老师检查6本作业,检查完第一本,检查第二本…,这里"检查作业"的动作是不是重复执行?

场景一:如果检查过程中,发现第三本作业空白未写,无需再检查后续作业,直接停止检查,这里就是break控制循环流程,即终止此循环。

场景二:如果检查过程中,发现第三本作业有少量错误,标注错误后跳过这本,继续检查第四本作业,这里就是continue控制循环流程,即退出当前一次循环继而执行下一次循环代码。

6.1.1 break关键字

场景一:如果检查过程中,发现第三本作业空白未写,无需再检查后续作业,直接停止检查,这里就是break控制循环流程,即终止此循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 演示 break:  实现中断循环 直接退出循环操作
# 需求: 遍历从 1 ~ 50 , 要求 一旦值能够被15整除(除 15 余数为 0) 立即退出循环
# 1- 定义计数器
i = 1

# 2- 编写循环
while i <= 50:
# 循环的主体内容
# 如果值能够被15整除, 退出循环
if i % 15 == 0:
break

print(i)

# 3- 更新计数器
i += 1

# 退出循环后, 执行后续的代码
print("循环已终止,退出程序")

6.1.2 continue关键字

场景二:如果检查过程中,发现第三本作业有少量错误,标注错误后跳过这本,继续检查第四本作业,这里就是continue控制循环流程,即退出当前一次循环继而执行下一次循环代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 演示 continue:  跳过本次循环, 进入下一次循环

# 需求: 遍历从 1 ~ 50 , 要求 如果遍历的值能够被 6 整除 将该值直接跳过不打印
# 1- 定义计数器
i = 1

# 2- 编写循环
while i <= 50:
# 循环的主体内容
# 如果能够被6整除, 将该值跳过 不打印
if i % 6 == 0:
# 更新计数器: 必须在continue前更新,否则会出现死循环
i += 1
continue

print(i)

# 3- 更新计数器
i += 1

# 退出循环后, 执行后续的代码
print("循环执行完毕")

如果在使用continue的时候,不手工更新计数器会有什么后果呢?

答:会出现死循环(计数器一直停留在满足continue条件的数值上,永远无法满足循环结束条件),建议使用Debug调试工具观看执行过程。

死循环概念

在编程中一个靠自身控制无法终止的程序称为“死循环”,死循环会一直占用计算机资源,开发中应尽量避免。

在Python中,我们也可以使用while True来模拟死循环:

1
2
3
# 注意 如果条件永久成立, 那么这个循环也被成为 死循环
while True:
print("这是一个死循环,按Ctrl+C可强制终止")

while循环案例:猜数字

案例需求

编写一个Python程序,系统提前设定一个1到20之间的固定整数(如12),让用户反复输入数字猜测,每次输入后提示“猜大了”“猜小了”或“猜对了”,直到用户猜对为止,猜对后提示游戏结束,不使用任何异常相关知识。

实现思路

① 编写死循环,满足“猜对”条件后终止循环。

② 提前设定一个固定整数(如12)作为答案,无需随机生成(避开未学的random模块)。

③ 让用户输入数字,通过if判断输入数字与答案的大小关系,给出对应提示,猜对则终止循环。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 演示 猜数字游戏(无random、无异常,适合未学相关知识的学生)
# 1- 提前设定固定答案(1~20之间),无需随机生成
answer = 12
# 2- 让用户反复猜测,写死循环方式
print("欢迎来到猜数字游戏!")
print("规则:系统已设定1~20之间的一个整数,猜对即可结束游戏,每次猜测会提示猜大或猜小~")
while True:
# 2.1 让用户输入猜测的数字(直接转为整数,提示用户输入整数)
user_guess = int(input("请输入你猜测的1~20之间的整数:"))
# 2.2 判断猜测结果,给出对应提示
if user_guess == answer:
print("恭喜你,猜对了!游戏结束~")
# 猜对后终止循环
break
elif user_guess > answer:
print("猜大了,再试试小一点的数字!")
else:
print("猜小了,再试试大一点的数字!")

扩展: 要求增加限制次数(3次), 并且要提示用户还剩余几次猜测机会: 仅做了解即可, 可以尝试写一写(对理解 while 和 if 很有作用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 演示 猜数字游戏(限制次数版,无random、无异常)
# 1- 提前设定固定答案(1~20之间),无需随机生成
answer = 12
# 2- 定义猜测次数(限制3次)
guess_count = 1
# 3- 写死循环,控制猜测次数
print("欢迎来到猜数字游戏!")
print("规则:系统已设定1~20之间的一个整数,你有3次猜测机会,猜对即可获胜~")
while True:
# 3.1 让用户输入猜测的数字,提示剩余次数
user_guess = int(input(f"请输入你猜测的1~20之间的整数(剩余{3 - guess_count + 1}次机会):"))
# 3.2 判断猜测结果,给出对应提示
if user_guess == answer:
print(f"恭喜你,猜对了!你在第{guess_count}次就猜对了,游戏结束~")
break
elif user_guess > answer:
print("猜大了,再试试小一点的数字!")
else:
print("猜小了,再试试大一点的数字!")
# 3.3 判断次数是否用完
if guess_count >= 3:
print(f"猜测次数已用完!正确答案是{answer},游戏结束~")
break
# 计数器+1
guess_count += 1

for循环基本语法及其应用

for循环基本语法

for循环结构主要用于(序列 => 字符串、列表、元组、集合以及字典)类型数据的遍历(循环)操作,无需手动定义和更新计数器,使用起来更简洁。

for循环主要用于序列类型数据的循环操作(遍历操作)

另外当循环次数已知的情况,建议使用for循环(比while循环更简洁)。

1
2
3
for 临时变量 in 序列:
重复执行的代码1
重复执行的代码2

案例:使用for循环遍历字符串”python”

1
2
3
4
5
6
7
8
9
10
11
12
# 演示: for基本使用
"""
语法格式:
for 临时变量 in 序列:
循环执行的代码块

注意:
临时变量的值 其实 就是 序列中每一个元素,循环次数等于序列的长度
"""
# 需求: 遍历一个字符串 : python
for char in "python":
print(char)

使用Debug调试以上代码可知:for循环功能非常强大,可以自动判断序列的长度,长度为多少,则for循环就循环多少次。每次循环时,系统会自动将序列中的每个元素赋值给临时变量,赋值完成后,for循环内部会自动更新计数器,向后移动一位,继续循环,直至元素全部循环结束。

range方法(函数)

在Python3中 range() 函数返回的是一个可迭代对象(类型是对象)(由于我们还未学习面向对象,为了方便大家理解,你可以简单的将其理解为一个序列结构)

主要作用:用于生成一段连续的整数序列,方便配合for循环使用。

基本语法:

1
2
3
4
5
6
range(stop)
range(start, stop, step)

start: 计数从 start 开始。默认是从 0 开始。例如range(4)等价于range(0, 4);
stop: 计数到 stop 结束,但不包括 stop。例如:range(0,4) 是 [0, 1, 2, 3] 没有 4
step:步长,默认为1。例如:range(0,4) 等价于 range(0, 4, 1)

案例:for循环与range方法,使用for循环,循环4次

1
2
3
4
5
6
7
# 演示 range函数
# 作用: 帮我们生成一个连续的数字, 例如 0~3
# 语法: range(起始值,结束值,步长)
# 起始值: 默认为 0 结束值: 必填的 在生成序列的时候, 不含结束值 步长: 默认为1 表示 数字与数字之间的间隔
# 需求: 生成 4以内的序列, 并且将每一个值打印出来
for num in range(0,4):
print(num)

for循环案例

案例: 使用for循环,求1 ~ 50之间所有奇数的和

1
2
3
4
5
6
# 需求: 求 1~50 之间 所有的奇数之和
sum_odd = 0
for num in range(1,51):
if num % 2 != 0:
sum_odd += num
print(sum_odd)

综合案例:使用for循环实现用户名+密码认证-作业

案例:用for循环实现用户登录

① 输入用户名和密码

② 判断用户名和密码是否正确(username=’student’,password=’python123’)

③ 登录仅有2次机会,超过2次会提示“登录失败,次数已用完”

分析:用户登陆情况有3种:

① 用户名错误(此时便无需判断密码是否正确) – 登陆失败

② 用户名正确 密码错误 –登陆失败

③ 用户名正确 密码正确 –登陆成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 用户登录案例(for循环版)

# 1- 构建循环, 并且循环只能有 2次
for i in range(0,2):
# 2: 让用户输入 用户名 和 密码
user_name = input("请输入用户名:")
user_pwd = input("请输入密码:")

# 3: 判断 用户名和密码是否正确
if user_name == 'student':
if user_pwd == 'python123':
print("恭喜您, 登录成功!")
# 登录成功后, 直接退出循环
break
else:
print(f"您输入的密码不正确, 您还剩余{1-i}次机会")
else:
print(f"您输入的用户名不正确, 您还剩余{1-i}次机会")
else:
# 循环正常结束(未break),说明次数用完
print("登录失败,次数已用完!")

for循环中的else结构

为什么需要在for循环中添加else结构

循环可以和else配合使用,else下方缩进的代码指的是当循环正常结束之后要执行的代码(即没有被break终止,完整执行完所有循环次数)。

强调:’正常结束’,非正常结束(如遇到break、程序异常),其else中的代码是不会执行的。

for循环结构中的else结构

基本语法:

1
2
3
4
for 临时变量 in 序列:
循环体
else:
当for循环正常结束后,执行的代码

break关键字对for…else结构的影响

1
2
3
4
5
6
7
8
str1 = 'python'
for i in str1:
if i == 'h':
print('遇h强制结束循环')
break
print(i)
else:
print('循环正常结束之后执行的代码')

运行结果:只会打印p、y、t,然后打印“遇h强制结束循环”,else中的代码不会执行(因为循环被break终止,属于非正常结束)。

continue关键字对for…else结构的影响

1
2
3
4
5
6
7
8
str1 = 'python'
for i in str1:
if i == 'h':
print('遇h不打印')
continue
print(i)
else:
print('循环正常结束之后执行的代码')

运行结果:打印p、y、t、遇h不打印、o、n,最后打印“循环正常结束之后执行的代码”(continue只是跳过本次循环,循环整体正常结束)。

else结构整体演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 演示: for循环结合else情况
"""
语法:
for 临时变量 in 序列:
循环体
else:
当循环正常结束后, 需要执行的代码

如果for循环中,
通过break退出的循环, 那么就不会执行else语句的内容
通过continue跳过当前循环, 对于else 不会有影响的
在循环过程中程序出现异常(程序异常结束), else也不会执行
"""
# 需求: 循环输出从 1~6, 当循环正常结束后, 打印 循环完成
for i in range(1,7):
# 当循环到3的时候, 强制退出循环(注释后可测试正常情况)
# if i == 3:
# break

# 当循环到3的时候, 跳过当前循环 进入下一次循环
# if i == 3:
# continue
# 异常情况(注释后可测试正常情况)
# 1/0
print(i)
else:
print("循环结束了.....")

循环要求

  • 1- 能够分别使用 for和while 实现 1~50 循环操作
  • 2- 能够分别使用 for 和 while 完成 求 奇数之和的案例

双层循环

for循环里可以嵌套for循环以及while循环,while循环中也可以嵌套for循环和while循环

for循环嵌套for循环

1
2
3
4
for 临时变量1 in 序列1:
for 临时变量2 in 序列2:
内层循环体

for循环嵌套while循环

1
2
3
4
for 临时变量1 in 序列1:
index = 初始值
while index的判断条件:
内层循环体

while循环嵌套while循环

1
2
3
4
5
index = 初始值
while index的判断条件:
index2 = 初始值
while index2的判断条件:
内层循环体

while循环嵌套for循环

1
2
3
4
index = 初始值
while index的判断条件:
for 临时变量 in 序列:
内层循环体

双层循环经常用来控制双层遍历的情况,比如打印九九乘法表

1
2
>双层循环,如果内层循环使用了break,break只能跳出
>它所在的循环,也就是内层循环

免费可打印九九乘法表 - Multiplication Chart

image-20260406214559454

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
'''
演示双层循环实现九九乘法表
'''

'''
双层循环,如果内层循环使用了break,break只能跳出
它所在的循环,也就是内层循环
'''

'''
需求:
实现九九乘法表
思路:
1. 变量row控制行号,row从1到9结束,[1,9]
2. 变量col控制列号,col从1开始回到当前行号结束,[1,row]
3. 用外层循环控制行号的变化,内层循环控制列号的变化
for row in range(1,10):
for col in range(1,row+1):
pass
'''
for row in range(1,10):
for col in range(1,row+1):
# print(f'当前行号{row},当前列号{col}')
print(f'{col} * {row} = {row * col}',end='\t')
# print()默认输出换行符
print()

print('#'*100)
'''
要求九九乘法表最多只输出到第3列
思路:
col为4的时候结束内层循环即可,外层循环继续
'''
for row in range(1,10):
for col in range(1,row+1):
# print(f'当前行号{row},当前列号{col}')
print(f'{col} * {row} = {row * col}',end='\t')
# 当col为4的时候结束内层循环,跳转到外层循环
if col == 4:
# break只能跳出它所在的循环(离他最近的循环)
break
# print()默认输出换行符
print()
print('#'*100)

源码连接:

https://gitee.com/secondtonone1/python_-ai

变量和逻辑控制

Posted on 2026-03-27 | In python , AI

Python 变量和逻辑控制

课程目标

  1. 了解 Python 语言的特点与应用场景,完成 Python 环境搭建

  2. 熟练使用 PyCharm 开发工具,掌握基本操作流程

  3. 精通 Python 基础语法(注释、变量、输入输出、数据类型转换、运算符、条件判断)

  4. 能够运用基础语法完成简单的业务逻辑实现

Python 语言初识与环境搭建

为什么选择学习 Python

(1)行业趋势

Python 连续多年稳居 TIOBE 编程语言排行榜前三,在 PYPL 指数中更是长期占据榜首位置,成为全球最受欢迎的编程语言之一。无论是后端开发、数据分析、人工智能,还是自动化运维、爬虫开发,Python 都有广泛应用。

image-20260327162617562

(2)易学易用

Python 仅包含 33 个关键字、7 种核心数据类型,语法规则简洁直观,接近自然语言。同样的功能实现,Python 代码量仅为 Java 的 1/5、C++ 的 1/10,极大降低了入门门槛。

(3)生态丰富

Python 拥有庞大的标准库和第三方库生态:

  • 数据处理:NumPy、Pandas

  • 可视化:Matplotlib、Seaborn

  • 机器学习:Scikit-learn、TensorFlow

  • Web 开发:Django、Flask

  • 自动化:Selenium、PyAutoGUI

Python 的发展历程

1989 年,荷兰程序员吉多・范・罗苏姆(Guido van Rossum,昵称 “龟叔”)为打发圣诞节假期,开始开发 Python 语言;1991 年,第一个 Python 解释器正式发布。Python 的命名源于龟叔喜爱的电视剧《Monty Python’s Flying Circus》(蒙蒂・蟒蛇的飞行马戏团),也体现了 Python 轻松有趣的设计理念。

Python’s creator shares his thoughts on other language | Guido van Rossum |  AIOC DEV'S NEWS | AIOC

人生苦短我用Python——模拟鼠标点击和键盘输入的操作 - 知乎

Life is short, use Python.

Python 的优缺点分析

优点

  • 简洁性:代码可读性极高,专注解决问题而非语言本身

  • 开源免费:完全开源,可自由修改、分发,社区活跃

  • 跨平台:可在 Windows、Linux、MacOS 等主流系统无缝运行

  • 全能型:覆盖全栈开发、数据分析、人工智能等多个领域

  • 胶水语言:可轻松与 C/C++、Java、Go 等语言混合开发

缺点

  • 执行效率较低(解释型语言特性),但可通过 C 扩展、JIT 编译(PyPy)优化

  • 多线程受 GIL 限制,高并发场景需结合多进程或异步编程

Python 解释器详解

Python 是高级编程语言,无法直接被计算机执行,需通过解释器将代码转换为机器语言(01 二进制)。常见解释器类型:

image-20260327163308453

  • CPython:官方默认版本,C 语言开发,应用最广泛

  • IPython:交互式解释器,增强了 CPython 的交互体验

  • PyPy:JIT 编译型解释器,执行效率远超 CPython

  • Jython/IronPython:分别运行在 Java/.NET 平台的解释器

Python 版本选择

生产环境建议选择稳定版(如 3.8/3.9/3.10/3.11),避免最新版本的兼容性问题。官方下载地址:https://www.python.org/downloads/

环境安装(Anaconda)

(1)Anaconda/Miniconda/Conda 的区别

  • Conda:底层包管理和环境管理工具

  • Anaconda:”全家桶”,包含 Conda+Python+1500+数据科学库+图形化工具(体积约 500MB-1GB)

  • Miniconda:迷你版,仅包含 Conda+Python+核心依赖(体积小,适合定制化环境)

(2)安装步骤

1 下载对应系统版本的 Anaconda(官网:https://www.anaconda.com/)开始安装

1740668600059

1740668629646

2 选择为所有人安装

1740668667912

3 运行安装包,建议安装到非 C 盘目录

1740668753327

4 勾选 “Add Anaconda to my PATH environment variable”(可选,方便命令行调用)

1740668982175

1740670257930

1740670281760

1740670324881

5 等待安装完成,验证:

image-20260327164145237

1
2
conda -V  # 查看conda版本
python -V # 查看Python版本

配置环境变量

为了可以在cmd中使用Python, 建议去系统环境变量中配置

image-20260327182317074

image-20260327182327576

image-20260327182338800

开发工具 PyCharm 使用指南

PyCharm 的优势

作为 Python 专用 IDE,PyCharm 提供:

  • 项目管理与代码组织
  • 智能代码提示与补全
  • 语法高亮与错误检测
  • 断点调试与性能分析
  • 内置版本控制(Git/SVN)
  • 丰富的插件生态

image-20260327164250190

PyCharm 版本选择

现在收费版(专业版)和免费版统一下载,免费版可以供个人使用,如果想升级收费版,需要购买激活码

PyCharm 安装与配置

(1)下载安装

官网地址:https://www.jetbrains.com/pycharm/download/

安装步骤:

  1. 下载对应系统版本的社区版

    image-20260327164503965

    双击exe安装

    image-20210306104505660

  2. 选择非 C 盘安装路径

    image-20210306105046370

  3. 勾选 “Create desktop shortcut” 和 “.py file association”

image-20210306105223088

选择试用

image-20260317101823647

(2)核心操作

① 创建项目

  1. 打开 PyCharm,点击 “New Project”

  2. 设置项目名称与路径(非 C 盘)

  3. 选择 Python 解释器(Anaconda 或系统 Python)

    image-20260327165647384

  4. 点击 “Create” 完成创建

ps:

如果找不到解释器,则打开目录输入解释器路径即可

image-20260327170016782

② 新建文件与编写代码

  1. 右键项目根目录 → New → Python File

  2. 输入文件名(如 hello_world)

    image-20260327170302636

  3. 编写入门代码:

1
2
# 第一个Python程序
print("Hello Python!")

③ 运行代码

  • 方式 1:右键代码编辑区 → Run ‘文件名’

    image-20260327170438953

  • 方式 2:点击代码右上角的运行按钮

    ​ image-20260327170403470

  • 方式 3:快捷键 Shift+F10

④ 个性化设置

  1. 主题设置:File → Settings → Appearance & Behavior → Appearance

    image-20260327170523501

  2. 字体设置:File → Settings → Editor → Font(建议字号 14-16,字体 Consolas / 等线)

    image-20260327170604814

  3. 解释器切换:File → Settings → Project: 项目名 → Python Interpreter

    image-20260327170707409

⑤ 常用快捷键

快捷键 功能
Ctrl+S 保存代码
Ctrl+/ 快速注释 / 取消注释
Ctrl+D 复制当前行
Ctrl+Z 撤销操作
Ctrl+Y 恢复撤销
Ctrl+Alt+L 格式化代码
Shift+F10 运行代码
F9 断点调试

Python 基础语法核心知识点

注释规范

注释是代码的说明文档,解释器不会执行注释内容,核心作用是提高代码可读性。

(1)单行注释

以#开头,注释单行内容:

1
2
# 这是单行注释(推荐写在代码上方)
print("Hello World") # 这是行尾注释(建议保留2个空格)

(2)多行注释

使用三引号("""或''')包裹多行内容:

1
2
3
4
5
6
7
8
9
10
11
"""
这是多行注释
适用于:
1. 函数/类的功能说明
2. 大段代码的逻辑解释
3. 临时注释多行代码
"""
'''
单引号三引号同样支持多行注释
'''
print("多行注释演示")

变量的定义与使用

(1)变量的本质

变量是存储数据的临时容器,程序运行过程中值可动态修改。

(2)定义语法

1
2
3
4
# 变量名 = 变量值(等号两侧保留1个空格)
age = 18 # 整数类型
name = "张三" # 字符串类型
score = 98.5 # 浮点数类型

(3)命名规则

强制规则

  1. 仅能包含字母、数字、下划线(_)

  2. 不能以数字开头

  3. 严格区分大小写(age 和 Age 是不同变量)

  4. 不能使用 Python 关键字(如 if、for、while)

推荐规范

  1. 见名知义(如 user_name 而非 a1)

  2. 多个单词命名:

    • 小驼峰:userName(第二个单词首字母大写)

    • 下划线:user_name(推荐,Python 官方风格)

    • 大驼峰:UserName(类名专用)

(4)变量的数据类型

Python 是动态类型语言,变量类型由赋值的内容决定:

类型 说明 示例
整数(int) 无小数的数字 age = 20
浮点数(float) 带小数的数字 price = 19.9
字符串(str) 文本内容(单 / 双引号包裹) name = “Python”
布尔值(bool) 逻辑值(True/False) is_ok = True
列表(list) 有序可变集合 nums = [1,2,3]
元组(tuple) 有序不可变集合 info = (“张三”, 20)
字典(dict) 键值对集合 student = {“name”:”李四”, “age”:19}
集合 (set) 无序, 不重复序列 num_set = {1,3,5}

(5)类型检测

1
2
3
4
5
6
7
# 方法1:type()函数
age = 18
print(type(age)) # <class 'int'>

# 方法2:isinstance()函数(推荐)
print(isinstance(age, int)) # True
print(isinstance(age, str)) # False

输入与输出

(1)格式化输出

方式 1:f-string(Python3.6+,推荐)

1
2
3
4
5
6
7
8
9
name = "李四"
age = 20
score = 95.5
# 基础用法
print(f"姓名:{name},年龄:{age},成绩:{score}")
# 格式控制(保留2位小数)
print(f"成绩:{score:.2f}")
# 数字补零(6位宽度)
print(f"学号:{1:06d}") # 输出:000001

方式 2:format() 方法

1
2
3
4
5
print("姓名:{},年龄:{}".format(name, age))
# 指定位置
print("年龄:{1},姓名:{0}".format(name, age))
# 格式控制
print("成绩:{:.2f}".format(score))

方式 3:% 占位符(兼容旧版本)

1
print("姓名:%s,年龄:%d,成绩:%.2f" % (name, age, score))

(2)转义字符

转义符 功能
\n 换行
\t 制表符(4 个空格)
\ 反斜杠本身
" 双引号
' 单引号

示例:

1
2
3
4
print("姓名:{}\t年龄:{}\n成绩:{:.2f}".format(name, age, score))
# 自定义print结束符(默认\n)
print("Hello", end=" ")
print("Python") # 输出:Hello Python

(3)输入函数 input()

获取用户从键盘输入的内容,所有输入均为字符串类型:

1
2
3
4
5
6
7
8
# 基础用法
name = input("请输入您的姓名:")
age = input("请输入您的年龄:")
print(f"您输入的姓名:{name},年龄:{age}")

# 带提示的输入
phone = input("请输入您的手机号:")
print(f"手机号:{phone},类型:{type(phone)}")

数据存储本质

image-20260328205252142

数据类型转换

当需要对输入的字符串进行数值运算时,需进行类型转换:

函数 功能 示例
int(x) 转换为整数 int(“100”) → 100
float(x) 转换为浮点数 float(“9.9”) → 9.9
str(x) 转换为字符串 str(18) → “18”
eval(x) 执行字符串表达式 eval(“1+2”) → 3

示例:超市收银系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 输入商品信息
product_name = input("请输入商品名称:")
price_str = input("请输入商品单价:")
count_str = input("请输入购买数量:")

# 类型转换
price = float(price_str)
count = int(count_str)

# 计算总价
total = price * count

# 输出结果
print(f"您购买了{product_name},单价:{price:.2f}元,数量:{count}件,总价:{total:.2f}元")

运算符

(1)算术运算符

运算符 描述 示例
+ 加法 10 + 20 → 30
- 减法 20 - 10 → 10
* 乘法 10 * 20 → 200
/ 除法(返回浮点数) 20 / 10 → 2.0
// 整除(向下取整) 21 // 10 → 2
% 取模(余数) 21 % 10 → 1
** 幂运算 2 ** 3 → 8

示例:

1
2
3
4
5
6
7
8
9
a = 10
b = 3
print(f"{a} + {b} = {a + b}") # 13
print(f"{a} - {b} = {a - b}") # 7
print(f"{a} * {b} = {a * b}") # 30
print(f"{a} / {b} = {a / b}") # 3.3333333333333335
print(f"{a} // {b} = {a // b}") # 3
print(f"{a} % {b} = {a % b}") # 1
print(f"{a} ** {b} = {a ** b}") # 1000

(2)赋值运算符

运算符 描述 示例
= 基础赋值 x = 10
+= 加后赋值 x += 5 → x = x + 5
-= 减后赋值 x -= 5 → x = x - 5
*= 乘后赋值 x *= 5 → x = x * 5
/= 除后赋值 x /= 5 → x = x / 5
//= 整除后赋值 x //= 5 → x = x // 5
%= 取模后赋值 x %= 5 → x = x % 5
**= 幂后赋值 x **= 5 → x = x ** 5

(3)比较运算符(返回布尔值)

运算符 描述 示例
== 等于 10 == 10 → True
!= 不等于 10 != 20 → True
> 大于 20 > 10 → True
< 小于 10 < 20 → True
>= 大于等于 20 >= 10 → True
<= 小于等于 10 <= 20 → True

(4)逻辑运算符

运算符 描述 示例
and 逻辑与(都真才真) (10>5) and (20>10) → True
or 逻辑或(有真就真) (10>5) or (20<10) → True
not 逻辑非(取反) not (10>5) → False

条件判断(if 语句)

(1)核心作用

根据条件表达式的布尔结果(True/False),执行不同的代码块,实现程序的分支逻辑,是程序实现 “判断” 能力的核心语法。

(2)基本语法规则

  1. Python 中用 缩进(4 个空格 / 制表符) 划分代码块,而非大括号,缩进必须统一

  2. 条件表达式后必须加冒号 :

  3. 条件表达式可结合比较运算符、逻辑运算符组合使用

(3)四种基本用法

① 单分支(if)

适用场景:满足条件则执行某段代码,不满足则跳过

1
2
3
4
5
6
7
8
# 语法
if 条件表达式:
满足条件时执行的代码块(缩进)

# 示例:判断是否成年
age = 19
if age >= 18:
print("你已成年,可独立出行")

② 双分支(if-else)

适用场景:二选一,满足条件执行 A 代码块,不满足执行 B 代码块

1
2
3
4
5
6
7
8
9
10
11
12
# 语法
if 条件表达式:
满足条件执行的代码块
else:
不满足条件执行的代码块

# 示例:判断是否成年
age = int(input("请输入你的年龄:"))
if age >= 18:
print("你已成年,可观看此电影")
else:
print("你未成年,需由监护人陪同")

③ 多分支(if-elif-else)

适用场景:多个条件依次判断,满足某个条件则执行对应代码块,后续条件不再判断;所有条件都不满足则执行 else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 语法
if 条件表达式1:
满足条件1执行的代码块
elif 条件表达式2:
满足条件2执行的代码块
elif 条件表达式3:
满足条件3执行的代码块
...
else:
所有条件都不满足时执行的代码块

# 示例:成绩评级
score = float(input("请输入你的考试成绩:"))
if score >= 90:
print("成绩评级:优秀")
elif score >= 80:
print("成绩评级:良好")
elif score >= 70:
print("成绩评级:中等")
elif score >= 60:
print("成绩评级:及格")
else:
print("成绩评级:不及格,需要补考")

④ 嵌套分支(if 内部嵌套 if/if-else)

适用场景:主条件满足后,需要再判断细分条件

1
2
3
4
5
6
7
8
9
10
11
# 示例:判断是否能参加驾照考试
age = int(input("请输入你的年龄:"))
if age >= 18:
print("你满足年龄要求,可报名驾照考试")
height = float(input("请输入你的身高(cm):"))
if height >= 150:
print("身高符合要求,可正常报名")
else:
print("身高未达到要求,无法报名")
else:
print("年龄未达标,无法报名驾照考试")

(4)条件表达式的高级用法

  1. 结合逻辑运算符实现复杂条件
1
2
3
4
5
6
7
# 示例:判断是否为工作日的工作时间
hour = int(input("请输入当前小时数(0-23):"))
weekday = input("请输入是否为工作日(是/否):")
if weekday == "是" and 9 <= hour <= 18:
print("当前是工作时间,请专注工作")
else:
print("当前是非工作时间,可休息")
  1. 三元表达式(简化单 / 双分支)
    **语法**:`结果1 if 条件表达式 else 结果2`(满足条件返回结果 1,否则返回结果 2)
    
1
2
3
4
5
6
7
8
9
10
# 示例:简化判断成年与否
age = 20
result = "成年" if age >= 18 else "未成年"
print(f"你是:{result}")

# 等价于原双分支代码
if age >= 18:
result = "成年"
else:
result = "未成年"

(5)常见易错点

  1. 忘记写**冒号 **:条件表达式和 else/elif 后必须加冒号,否则语法报错

  2. 缩进不统一:同一代码块的缩进必须一致(要么全 4 个空格,要么全制表符)

  3. 把**赋值号 =当成等于号 ==**:条件判断中判断相等必须用==,=是赋值,会导致逻辑错误

1
2
3
4
5
6
7
# 错误写法
if age = 18: # 赋值操作,非判断,直接报错
print("成年")

# 正确写法
if age == 18:
print("刚成年")
  1. 多分支中条件顺序错误:多分支是依次判断,范围大的条件不能写在前面
1
2
3
4
5
6
7
8
9
10
11
12
# 错误写法:score>=60写在前面,会导致80+的成绩也判断为"及格"
score = 85
if score >= 60:
print("及格")
elif score >= 80:
print("良好")

# 正确写法:从范围小的条件开始判断
if score >= 80:
print("良好")
elif score >= 60:
print("及格")

综合实战案例

案例 1:简易成绩管理系统(含条件判断)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"""
简易成绩管理系统
功能:
1. 输入学生姓名和三门科目成绩
2. 计算总分和平均分
3. 根据平均分进行成绩评级
4. 格式化输出所有信息
"""

# 输入学生信息
student_name = input("请输入学生姓名:")
chinese_str = input("请输入语文成绩:")
math_str = input("请输入数学成绩:")
english_str = input("请输入英语成绩:")

# 类型转换(处理输入的字符串,转为浮点数)
chinese = float(chinese_str)
math = float(math_str)
english = float(english_str)

# 计算总分和平均分
total_score = chinese + math + english
avg_score = total_score / 3

# 条件判断:根据平均分评级
if avg_score >= 90:
grade = "优秀"
elif avg_score >= 80:
grade = "良好"
elif avg_score >= 70:
grade = "中等"
elif avg_score >= 60:
grade = "及格"
else:
grade = "不及格"

# 格式化输出结果
print("=" * 40)
print(f"学生姓名:{student_name}")
print(f"语文成绩:{chinese:.1f}分")
print(f"数学成绩:{math:.1f}分")
print(f"英语成绩:{english:.1f}分")
print(f"总成绩:{total_score:.1f}分")
print(f"平均成绩:{avg_score:.1f}分")
print(f"成绩评级:{grade}")
print("=" * 40)

案例 2:班级活动费用分摊系统(运算符 + 输入输出 + 条件判断)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"""
班级活动费用分摊系统
功能:
1. 输入参与人数和活动总花费
2. 额外收取10%组织管理费,计算最终总费用
3. 判断人数是否合法(至少1人)
4. 计算每人需要分摊的金额并输出
"""

# 输入并转换数据类型
total_people = int(input("请输入参与活动人数:"))
total_cost = float(input("请输入活动总花费(元):"))

# 条件判断:人数是否合法
if total_people < 1:
print("人数输入错误,至少1人!")
else:
# 计算含组织管理费的总费用和人均费用
manage_fee = total_cost * 0.1 # 10%组织管理费
final_total = total_cost + manage_fee
per_person = final_total / total_people

# 格式化输出
print("=" * 30)
print(f"原始活动花费:{total_cost:.2f}元")
print(f"10%组织管理费:{manage_fee:.2f}元")
print(f"最终总费用:{final_total:.2f}元")
print(f"参与人数:{total_people}人")
print(f"每人需分摊:{per_person:.2f}元")
print("=" * 30)

学习建议

  1. 多敲代码:基础语法(尤其是 if 判断)需通过反复练习巩固,建议每天编写 1-2 个带判断逻辑的小案例

  2. 注重缩进:养成统一缩进的习惯,这是 Python 的核心语法规范,也是新手最易出错的点

  3. 调试思维:遇到条件判断逻辑错误时,用print()打印条件表达式的结果,排查判断逻辑问题

  4. 简化代码:简单的双分支逻辑尝试用三元表达式实现,提升代码简洁性

  5. 场景练习:结合生活场景(如成绩评级、会员判断、缴费计算)设计带判断逻辑的程序,学以致用

课后练习

  1. 编写程序:输入一个整数,判断其是奇数还是偶数(提示:用取模运算符%,偶数 %2=0)

  2. 编写程序:输入一个人的身高和体重,计算 BMI 指数并判断体型(偏瘦 / 正常 / 超重 / 肥胖)

    • BMI 公式:BMI = 体重 (kg) / (身高 (m))²

    • 标准:<18.5 偏瘦,18.5-23.9 正常,24-27.9 超重,≥28 肥胖

  3. 编写程序:模拟超市会员打折,非会员无折扣,会员消费满 200 减 50,不满 200 打 9 折(输入会员状态和消费金额,输出实际支付金额)

AI大模型课程大纲

Posted on 2026-03-24 | In python , AI

简介

这是Python 人工智能 + NLP 全体系学习目录,100% 对标 Boss 直聘 AI 工程师 / NLP 工程师 / 大模型应用工程师 企业招聘标准,无废话、全求职向,可直接当作学习大纲,补充企业高频加分知识点,覆盖2026年最新求职需求。

面向人群

  1. 小白选手(大学生,跨行业产品经理,项目经理,家庭主妇),想要入行AI开发
  2. 有编程经验的程序员,转行或者拓宽AI技术栈
  3. 已经从事了部分AI开发的人群,扩充技术栈
  4. 具备传统工程化的C++程序员, Rust程序员,集成AI框架

第一阶段:Python 编程与工程基础(求职门槛)

  1. Python 核心语法

    • 变量、数据类型、条件、循环、函数

    • 面向对象、异常处理、文件 IO

    • 虚拟环境、包管理、模块导入

  2. 数据结构与算法(面试必考)

    • 列表、栈、队列、字典、哈希

    • 排序、查找、复杂度分析

  3. 必备工程工具

    • Git & GitHub

    • Linux 常用命令

    • Jupyter、VS Code

    • Docker 基础

第二阶段:AI 数学与数据科学基础(AI 底座)

  1. 数学核心(够用版)

    • 线性代数:矩阵、向量、维度

    • 概率统计:分布、期望、方差、相关性

    • 微积分:梯度、导数、优化基础

  2. 数据处理三剑客

    • NumPy 数值计算

    • Pandas 数据清洗、分析

    • Matplotlib/Seaborn 可视化

  3. 机器学习核心

- 数据集划分、过拟合、正则化

- 分类、回归、聚类、降维

- 经典算法:线性/逻辑回归、决策树、随机森林、XGBoost

- 模型评估:准确率、召回率、F1、AUC、交叉验证
  1. 数据工程基础(补充)

    • 数据清洗、去重、质量过滤

    • 数据脱敏、隐私保护(差分隐私等)

第三阶段:深度学习基础(AI 核心)

  1. 神经网络原理

    • 感知机、激活函数、损失函数、优化器

    • 前向传播、反向传播

  2. 深度学习框架

    • PyTorch(企业主流)

    • TensorFlow / Keras

  3. 经典网络结构

    • CNN 图像基础(为多模态铺垫)

    • RNN/LSTM/GRU 序列模型

    • Transformer 全解(Attention 机制)

第四阶段:NLP 自然语言处理(核心专项)

  1. NLP 基础与传统方法

    • 文本预处理:分词、去停用词、文本清洗

    • 文本表示:One-hot、TF-IDF、Word2Vec、GloVe

    • 文本分类、情感分析、关键词提取、文本相似度

  2. 深度学习 NLP

    • LSTM/GRU 文本建模

    • Transformer 与 Attention

    • BERT 系列模型原理与实战

    • 任务:NER、意图识别、关系抽取、文本摘要

  3. NLP 工具库

    • Jieba、SnowNLP、NLTK

    • HuggingFace Transformers、Datasets

  4. 多模态基础(补充,2026 NLP岗默认要求)

- 多模态基础:图文对齐、CLIP、BLIP

- 音视频转文本(Whisper、ASR)+ TTS

第五阶段:大模型 AIGC 与企业级 NLP(2026 最值钱)

  1. 大语言模型 LLM 基础

  2. 提示词工程 Prompt Engineering

  3. RAG 检索增强生成(企业 90% 项目)

- 文档切分、向量化

- 向量数据库:FAISS、Chroma、Milvus

- 知识库问答、文档解析

- 知识库工程进阶(补充):混合检索(稀疏检索BM25+稠密检索)、重排模型(Reranker)训练与使用、复杂文档(PDF/表格/长文档)处理、RAG评估(召回率、精确率、MRR、HitRate)
  1. 大模型微调

    • LoRA/QLoRA 轻量化微调

    • 指令微调、对话微调

    • 指令数据构造(补充,Self-Instruct、Evol-Instruct)、数据蒸馏、synthetic data生成

  2. 大模型部署

    • FastAPI 接口封装

    • 流式输出、本地部署

    • vLLM 推理加速

    • 模型推理与性能优化(补充,高薪必备):模型量化(INT4/INT8/AWQ/GPTQ)、推理加速(vLLM、TGI、TensorRT-LLM)、显存优化、分布式推理、高并发、缓存策略

  3. 大模型安全与对齐(补充,大厂必问)

    • 幻觉检测与抑制

    • 安全对齐、RLHF/RLAIF/DPO

    • 红队测试、prompt注入防御

    • 内容合规、敏感词过滤、偏见控制

  4. Agent 智能体(补充,2026最火方向)

    • Agent框架:LangChain、LlamaIndex、AutoGPT

    • 工具调用、Function Calling

    • 规划、反思、记忆机制

    • 多Agent协作、工作流

  5. 多模态进阶(补充)

    • 多模态RAG(文档OCR+表格+图片理解)

    • 多模态对话、图文生成

第六阶段:AI 工程化与上线(企业刚需能力)

  1. 模型封装:Flask / FastAPI

  2. 容器化:Docker

  3. 服务部署:Linux、Nginx、后台运行

  4. 日志、配置、接口文档、压力测试

  5. MLOps / LLMOps(补充,大厂标配)

    • 模型版本管理、实验跟踪(MLflow、Weights & Biases)

    • 监控:延迟、吞吐量、幻觉率、用户满意度

    • 模型持续迭代、冷启动、A/B测试

    • 成本监控与资源调度

第七阶段:求职实战项目(可直接写进 Boss 简历)

  1. 电商评论情感分析系统

  2. 基于 BERT 的简历信息抽取(NER)

  3. 智能客服意图识别与多轮对话

  4. 企业知识库 RAG 问答系统(融入混合检索、重排优化)

  5. 本地大模型 API 服务部署(融入量化、推理加速)

  6. 文本摘要 + 内容生成助手

  7. 多模态图文问答系统(补充,贴合2026趋势)

  8. Agent 智能任务执行系统(补充,提升简历竞争力)

  9. 行业垂直项目(补充,简历差异化关键,任选1-2个)

    • 金融:研报分析、风控对话、合规审查系统

    • 法律:法条检索、合同审查、法律问答工具

    • 医疗:医学NLP、病历结构化系统

    • 教育:题库生成、智能批改工具

    • 政企:公文处理、内部知识库、政务问答系统

第八阶段:面试冲刺(企业标准)

  1. Python 面试题

  2. 机器学习 & 深度学习面试题

  3. NLP 原理 & 项目面试题

  4. 大模型 RAG 实战问答

  5. 手撕代码、项目讲解、简历优化

  6. 补充高频面试点(贴合补充知识点)

    • 大模型安全与对齐面试题(幻觉、RLHF、prompt注入防御)

    • 模型推理优化、部署面试题(量化、vLLM、并发处理)

    • Agent、多模态相关面试题

    • MLOps/LLMOps 核心考点

    • 行业垂直领域项目讲解技巧

补充说明:本大纲已整合2026年企业AI/NLP岗位高频考点和加分方向,既保留原有的求职核心链路,又补充了大厂必问、高薪必备的知识点,可直接作为学习计划、简历项目参考,也可根据自身求职方向(如大模型应用、NLP工程、多模态)侧重学习对应模块。

Python大模型开发(01) 行业前景分析

Posted on 2026-03-18 | In python , AI

IT方向排行

等级排名 热词 IT方向
1 级 夯 Python人工智能开发,具身智能, , cuda编程,量化交易,ros机器人开发
2 级 顶级 QT(图像处理),yolo视频,嵌入式AI
3 级 人上人 C++大厂后端, 游戏开发
4 级 NPC QT, 大数据,QT(音视频),Java+AI后端, C++后端传统开发,C#, 测试
5 级 不推荐 网络安全,Java传统开发,前端,安卓,ios, 鸿蒙,运维

前端行情

特点

  1. 更新迭代快
  2. 学习成本不高
  3. 市场人才基数很大

不推荐学

劣势

  1. 前端招聘对应届生非常不友好,因为目前前端人多,机会少。主要机会停留在3~5年经验招聘
  2. 前端仅仅会Vue, React, Angular这些不够了,还要扩充前端技术栈,fluter,andriod,ios,鸿蒙
  3. 尽量把后端,go,rust,java学一个,增加一下竞争力,目前小厂前端都是一个人干的同时也做后端工作。
  4. 92应届可以学前端应聘大厂,非92应届不推荐

大数据行情

技术栈

  1. python基础+进阶(并发,网络框架,数据分析库)
  2. spark , hadoop, flink这些大数据必备开发工具
  3. MySQL, mongodb,redis常见数据服务,MPP架构数据
  4. 某一个领域的行业知识
  5. 数据清洗,ETL转换工具,可视化
  6. 数据仓库搭建和架构能力

现状

应届生找工作难,目前岗位有需求,但是面向与3年到5年居多

企业不太喜欢培训新人

大数据本质上是一个吃行业经验的学科

5到10年工作,降薪可能性大,企业也考虑性价比,目前3到5年需求居多。

小企业做不了大数据,大企业招聘居多,但是普通本科很难进入

C# 行业分析

应用方向

  1. 桌面上位机开发
  2. 游戏unity客户端开发

主要做客户端开发

优势

机会折中,薪资折中,中庸

劣势

方向局限性客户端上位机开发,游戏开发。

出差多

网络安全行情

工作内容

从运维学科分支出来,主要做安全维护方面。

网络攻防,web渗透,网络管理,防火墙路由器安全系统架设

技术栈

  1. web渗透攻防技术
  2. 数据库安全
  3. linux操作系统,kali渗透等,漏洞检测
  4. 黑客攻防技术

薪酬

应届生起薪7K左右,有一定竞争压力,机会不多

短板

起薪低

核心技术编码接触的不多

考一些认证,有一些难度,主要取决于持证机构

优势

学历要求不高,专科可做

上手难度不难,平滑上手。分支要自己选择。

软件测试行情

分类

  1. 白盒测试
  2. 黑盒测试
  3. 测试开发
  4. AI自动化测试+开发

要求

  1. 多种语言, Java,JS,Go, Rust,C++
  2. 懂Linux环境,windows环境,懂数据库,关系型,非关系型
  3. 常见测试手段,web测试,回归测试,白盒测试脚本等
  4. 压力测试,工程化集成测试环境要懂

劣势

  1. 薪资低
  2. 长期做简单测试,没有太多增值空间
  3. 要懂得多,但是广而不精

挽救

  1. 朝AI测试发展,要了解Pytorch,Tensorflow主流AI框架,集成测试
  2. 了解AI机器人测试原理,视觉测试,决策树测试等

C++ 就业方向

学习资料

文档: https://www.yuque.com/lianlianfengchen-cvvh2

B站视频资料: https://space.bilibili.com/271469206

QT工程师

上限不高,5年左右就可以精通

C++级别语法精通很难,熟练即可

医疗,军工, 桌面软件

本科,专科(外包,军工), 硕士干这个有点低

但是薪资可观,可和Java横向对比

image-20260306212346463

image-20260306212406768

进阶

QT+音视频

QT+ ffmpeg, webrtc流媒体, SDK封装等,或者做客户端+服务器全栈适合硕士

熟悉常用流媒体协议:RTP、RTCP、RTMP、RTSP等;

做音视频服务+AI

1.c/c++基础扎实,熟悉linux/unix系统;

  1. 了解面向对象编程,了解常用设计模式
  2. 了解多线程、多进程开发,SOCKET网络程序开发
  3. 了解音视频传输,TCP 协议,媒体处理相关算法优先。
  4. 了解webrtc,有webrtc开发经验优先。

进阶2 图像处理

医疗 + AI + QT+ C++ 图像处理(算法)

游戏开发

技术栈:

前端: 游戏引擎开发经验, cocos-creator, cosos-2d

Unity3d, Unreal5

后端

网络编程 Linux epoll/slect

陈硕muduo网络网库(Linux)

boost::asio libevent 学一遍

啃一遍源码Trinity-core源码(早期MMORPG服务器源码)

数据,内存模型,并发编程

量化交易(天花板)

python + C++

天花板 : 92学历+硕士+算法+分布式系统(redis连接池,grpc连接池+asio网络库)+高并发+协程C++20

交易算法和策略

需要金融知识

高频交易,分布式通信

cuda编程

利用cuda提升并行计算,本质上C++并行计算(需要92或者重本)

工作方向

  1. 底层算子开发开发,用cuda + C++ 实现/优化深度学习核心算子, pytorch,tensorflow的cuda扩展
  2. 模型推理优化,cuda并行并行优化,算子融合,做模型推理加速
  3. 分布式训练加速,基于NCCL实现GPU多节点分布式训练加速(BAT, Deepseek)
  4. 音视频编码加速,图像处理,医疗影像,工业视觉(缺陷检测)GPU加速算法

技术栈

  1. C++核心语法C++ 11/14/17

  2. cuda核心语法(核函数__global__,device,host),线程层级(thread, block,grid),结束线程(wrap)优化

    内存模型(全局内存,共享内存,常量内存,纹理内存)

    流技术stream, 时间(event),异步执行,多CPU并行

  3. GPU架构,理解SM(流多处理器),CUDA核心, 缓存层级,内存带宽瓶颈

  4. 并行算法:掌握并行归约(Reduction)、并行扫描(Scan)、矩阵分块(Tiling)、稀疏矩阵优化;

    CUDA 高级特性:

    • 统一内存(Unified Memory)、CUDA Graph(任务图)、Cooperative Groups(协作组);
    • CUDA 动态并行(Dynamic Parallelism)、异构编程接口(HIP,跨 NVIDIA/AMD);

    高性能库:熟练使用 NVIDIA 官方加速库(核心是掌握调用 + 理解底层原理):

    • 线性代数:cuBLAS(基础矩阵运算)、cuSOLVER(线性方程组求解);
    • 深度学习:cuDNN(深度学习原算子)、TensorRT(推理加速);
    • 其他:cuFFT(傅里叶变换)、cuSPARSE(稀疏矩阵)、Thrust(CUDA 版 STL);

    编译优化:nvcc 编译选项优化(如-O3/-arch=sm_80)、PTX 汇编(底层优化)、算子融合(Kernel Fusion)。

就业方向 补充技术栈
AI / 深度学习 Python(PyTorch/TensorFlow)、ONNX、TensorRT、NCCL、算子量化(INT8/FP16)
高性能计算(HPC) MPI(分布式并行)、OpenMP、Linux 系统编程、超算集群调度(Slurm)
音视频 / 图形图像 FFmpeg、OpenGL/Vulkan、OpenCV(GPU 版)、YUV/RGB 色彩空间处理
芯片 / 底层软件 Linux 驱动开发、汇编语言(x86/ARM)、PCIe 协议、GPU 硬件架构

传统开发

系统开发

银行系统,证券登录系统 大部分是外包

本科

大厂后端(天花板)

卡92

技术栈:

网络编程(epoll,poll,select内核之类的)

并发编程(多线程+无锁编程)

数据库操作

分布式系统

现状

岗位少

技术栈:

中间件,从零造轮子或者维护大厂轮子

推荐系统: C++性能好

搜索引擎+ AI

C++和嵌入式AI

技术栈

  1. C语言/C++语言基础
  2. stm32,esp32单片机编程
  3. 数电模电,spi,i2c协议
  4. linux系统+linux网络编程
  5. armlinux 系统,根文件系统,uboot,内核原理,驱动源码,子系统I2C子系统,SPI子系统,gpio子系统
  6. QT+PyQt
  7. 图像识别,基础识别算法,yolo智能实现库会调用
  8. 视频流拉取和抽帧,分析,结合模型部署到arm板子上,做视觉分析

无人车AI,图像识别,智能路径规划

优势

行业经验赋能,软硬件都掌握,抗拒35岁危机

具身智能(C++ + AI+ 智能硬件)

**技术栈 **

  1. 传感器编程和调试
  2. C/C++编程,要读懂linux或者ros操作系统内核,内核驱动(字符设备驱动,块设备驱动)
  3. 操作系统调度,LRU调度原理,系统内存管理,消息队列
  4. 图像和视频识别,仿真(AI+图像识别算法)
  5. 深度学习(CNN,RNN神经网络)
  6. 数电模电基础,单片机编程stm32,spi,i2c协议等
  7. arm+linux系统编程
  8. 机器人系统 ros系统, 机器人应用开发

要求

  1. 92学历,或者算法竞赛获奖

Java行情

目前分为传统Java(Java Web, 传统微服务) 以及Java智能应用开发(coze,dify,大模型调用原理等)

传统开发薪资

image-20260225192609000

Java微服务+AI大模型应用开发

image-20260225192524944

技术栈

1 spring cloud, Spring 生态 + 微服务 + AI 工程化

2 中间件elestic search, redis, rocketmq

3 数据处理(JDBC/MyBatis/Spark Java)

4 coze, dify,等智能体落地经验

6 基于T ensorFlow/PyTorch Java API,熟练用 Java 对接主流深度学习框架,实现模型推理、训练集成,AI 应用的核心能力

7 AI框架集成+工程化落地,从会AI到AI项目落地。

Python工作方向

Python + AI

就业方向

  1. 提示词工程师(本科)
  2. Agent开发(本科)
  3. RAG工程师(本科)
  4. 深度学习+图像处理 RNN CNN(硕士/算法工程师获奖/92学历)
  5. 大模型部署和调优(本科)
  6. 大模型应用开发(Java+ Go + Python+Rust)(本科)
  7. 算法工程师(数学基础好+92+硕士),行业有医疗,智能制造等

技术栈

  1. Python基础,Python进阶(装饰器,并发线程,协程,进程,网络编程,异步同步)
  2. Agent开发,大模型原理,coze, dify应用,工作流搭建,agent机器人落地应用落地,ragflow
  3. 数据分析,numpy, pandas,统计基础
  4. 机器学习原理,高等数学,分类,线性回归,损失函数,聚类
  5. 深度学习,CNN,RNN神经网络,使用pytorch框架搭建深度学习项目
  6. NLP 自然语言处理,语义解析,分词技术,向量化技术,Transformer框架,RNN模型讲解,以及AI建模
  7. 大模型微调,Lora, 全量微调
  8. Rag解决大模型幻觉问题,提出的知识库,构建知识库,向量化,索引技术

优势

本科可做,未来长期的一个发展方向,工作流,智能体,普通人也要求会

python上手简单,专注精力学习AI知识,效果好,就业可观

劣势

  1. 如果学习不精通,那么会存在一些面试短板
  2. python语言优势不大,所以需要提升自己AI知识和能力。

Python + 大数据

这两年市场有些饱和(大数据主要招聘经验段在3-5年)

  1. 大数据开发(难度有一些四星)

  2. 大数据分析(简单)

  3. 爬虫工程师

因为大数据都要给模型提供参考和调优

大数据开发是AI的前身

AI课程里包含部分大数据分析的逻辑

Pandas, 数分思想

webrtc信令服务器实现

Posted on 2026-02-11 | In C++聊天项目

WebRTC 1v1 视频通话技术方案(Node.js + WebSocket)


一、整体架构设计

1.1 系统架构图

1
2
3
4
5
6
浏览器A  ─────┐
│ (SDP / ICE 交换)
浏览器B ─────┼──── WebSocket ─── 信令服务器 (Node.js)
│
│
媒体流 (P2P / TURN 中继)

1.2 组件职责

组件 职责
前端浏览器 建立 RTCPeerConnection,采集音视频
信令服务器 交换 offer / answer / ice
TURN 服务器 NAT 穿透失败时中继媒体流
STUN 获取公网候选地址

二、信令服务器技术选型

技术 原因
Express 快速搭建 HTTP 服务
ws 轻量级 WebSocket 实现
原生 Map 管理房间

信令服务器不参与媒体传输,只负责:

  • 房间管理
  • 消息转发

三、核心代码结构解析

3.0通信流程

image-20260213085510995


3.1 静态资源加载

1
app.use(express.static(path.join(__dirname, "..", "web")));

作用

  • 将 web 目录暴露为 HTTP 静态站点
  • 访问 http://localhost:3000 即加载 index.html

3.2 WebSocket 服务器创建

1
const wss = new WebSocketServer({ server });

说明:

  • 复用 HTTP 服务器
  • WebSocket 与 HTTP 共享 3000 端口

四、房间模型设计

1
const rooms = new Map();

结构:

1
Map<roomId, Set<ws>>

示例:

1
2
3
rooms = {
"1001" => Set(wsA, wsB)
}

设计思想:

  • 每个房间最多 2 人
  • 每个 ws 保存自己所属 roomId

五、信令流程设计

5.1 加入房间流程

客户端发送:

1
2
3
4
{
"type": "join",
"roomId": "1001"
}

服务器逻辑:

  1. 检查 roomId
  2. 从旧房间移除
  3. 如果人数 >= 2 返回 full
  4. 加入房间
  5. 返回 joined
  6. 如果人数 == 2,发送 ready

5.2 ready 阶段

当房间人数 = 2 时:

1
send(peer, { type: "ready", isInitiator });

设计:

  • 后加入者作为 initiator
  • initiator 负责创建 offer

5.3 WebRTC 协商流程

时序图

1
2
3
4
5
6
7
8
Initiator                Receiver
| |
| ---- offer ----------> |
| |
| <--- answer -----------|
| |
| <--- ICE --------------|
| ---- ICE ------------> |

5.4 信令转发逻辑

1
2
3
if (type === "offer" || type === "answer" || type === "ice") {
relayToOthers(roomId, ws, msg);
}

服务器不解析 SDP,不参与媒体。

它只是“转发器”。


六、核心函数解析


6.1 send()

1
2
3
4
function send(ws, data) {
if (ws.readyState === ws.OPEN)
ws.send(JSON.stringify(data));
}

说明:

  • 统一发送 JSON
  • 防止向已关闭连接发送数据

6.2 relayToOthers()

1
function relayToOthers(roomId, sender, data)

功能:

  • 向房间内除自己外的人广播
  • 实现一对一信令交换

6.3 removeFromRoom()

负责:

  • 用户离开
  • 通知房间剩余成员
  • 清理空房间

防止内存泄漏。


七、连接生命周期管理

1
2
ws.on("close", () => removeFromRoom(ws));
ws.on("error", () => removeFromRoom(ws));

说明:

  • 防止异常断开导致房间脏数据
  • 保证 rooms 数据一致性

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import express from "express";
import http from "http";
import { WebSocketServer } from "ws";

import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
// 把 web 目录设置成:server 的上一级 + /web
app.use(express.static(path.join(__dirname, "..", "web")));

const server = http.createServer(app);
const wss = new WebSocketServer({ server });

/**
* rooms: Map<roomId, Set<ws>>
*/
const rooms = new Map();

/** 给某个 ws 发送 JSON */
function send(ws, data) {
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
}

/** 房间内给除了自己之外的其他人转发 */
function relayToOthers(roomId, sender, data) {
const set = rooms.get(roomId);
if (!set) return;
for (const peer of set) {
if (peer !== sender) send(peer, data);
}
}

function removeFromRoom(ws) {
const roomId = ws.roomId;
if (!roomId) return;

const set = rooms.get(roomId);
if (!set) return;

set.delete(ws);
// 通知剩余的人:对方离开
for (const peer of set) {
send(peer, { type: "peer-left" });
}

if (set.size === 0) rooms.delete(roomId);
ws.roomId = null;
}

wss.on("connection", (ws) => {
ws.roomId = null;

ws.on("message", (buf) => {
let msg;
try {
msg = JSON.parse(buf.toString());
} catch {
return;
}

const { type } = msg;

if (type === "join") {
const roomId = String(msg.roomId || "").trim();
if (!roomId) return send(ws, { type: "error", message: "roomId is required" });

// 如果 ws 已在别的房间,先移除
removeFromRoom(ws);

const set = rooms.get(roomId) || new Set();
if (set.size >= 2) {
return send(ws, { type: "full", roomId });
}

set.add(ws);
rooms.set(roomId, set);
ws.roomId = roomId;

send(ws, { type: "joined", roomId, peers: set.size - 1 });

// 房间凑齐 2 人,通知双方开始协商
if (set.size === 2) {
// 约定:后加入的人做 initiator(也可以反过来)
for (const peer of set) {
const isInitiator = peer === ws;
send(peer, { type: "ready", isInitiator });
}
}
return;
}

// 后面的消息都必须在房间里
const roomId = ws.roomId;
if (!roomId) return send(ws, { type: "error", message: "join a room first" });

// 透传 WebRTC 协商消息
if (type === "offer" || type === "answer" || type === "ice") {
relayToOthers(roomId, ws, msg);
return;
}

if (type === "leave") {
removeFromRoom(ws);
send(ws, { type: "left" });
return;
}
});

ws.on("close", () => removeFromRoom(ws));
ws.on("error", () => removeFromRoom(ws));
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, "0.0.0.0", () => {
console.log(`Signaling+Web server running: http://localhost:${PORT}`);
});

八、为什么信令服务器不处理媒体?

因为:

WebRTC 是 P2P 协议。

媒体路径:

1
浏览器A  ←→  浏览器B

不是:

1
浏览器A  → 服务器 → 浏览器B

除非使用 SFU。


九、当前版本限制

项目 当前实现
房间人数 最多 2 人
认证 无
房间权限 无
重连机制 无
多人视频 不支持

十、如何扩展为多人房间(技术方向)

当前结构:

1
Map<roomId, Set<ws>>

升级方案:

  1. 为每个 ws 分配唯一 peerId
  2. 信令改为定向发送
  3. 前端维护:
1
Map<peerId, RTCPeerConnection>

每加入一个人:

  • 为其创建一个新的 PeerConnection
  • 动态创建 video 元素

这叫:

Mesh 架构


十一、生产环境建议

1️⃣ 使用 HTTPS + WSS

WebRTC 在公网通常必须 HTTPS。

2️⃣ TURN 使用动态签名

不要写死:

1
user=webrtc:password

应改为:

1
2
use-auth-secret
static-auth-secret=xxx

防止带宽被盗用。

3️⃣ 加入房间认证

目前任何人知道房间号即可进入。

应加入:

  • token
  • 用户身份

webrtc的coturn服务搭建

Posted on 2026-02-11 | In C++聊天项目

前情回顾

前面我们已经将聊天的基本功能完成了,接下来来安装coturn服务,生成webrtc视频通信

coturn服务集成了stun和turn服务器


三个常用的概念

STUN = 告诉你自己是谁
TURN = 帮你转发数据
coturn = STUN/TURN 的服务器实现软件


image-20260212125500580

为什么需要 STUN / TURN?

因为现在大部分设备都在 NAT(内网)后面:

1
手机/电脑 → 路由器(NAT) → 公网

内网 IP(192.168.x.x)在公网是看不到的。

WebRTC / 视频通话 / P2P 通信 就必须解决:

👉 “我怎么让对方知道我的公网地址?”


STUN 是什么?

全称:

Session Traversal Utilities for NAT

作用:

👉 告诉客户端:你的公网 IP 和端口是多少

工作流程:

1
2
客户端 → STUN服务器
STUN服务器 → 告诉你:你在公网看到的地址是 1.2.3.4:5678

然后客户端把这个地址发给对方。


✅ 优点

  • 轻量
  • 免费
  • 不转发数据

❌ 缺点

  • 只能在 NAT 允许打洞的情况下成功
  • 对称 NAT 基本失败

TURN 是什么?

全称:

Traversal Using Relays around NAT

作用:

👉 当打洞失败时,帮你转发数据

工作模式:

1
A → TURN服务器 → B

TURN 服务器相当于一个“中转站”。


✅ 优点

  • 几乎 100% 成功
  • 解决所有 NAT 问题

❌ 缺点

  • 服务器要承担带宽
  • 成本高

coturn 是什么?

👉 coturn 是一个 开源的 STUN + TURN 服务器软件

官网:
https://github.com/coturn/coturn

你部署 coturn 之后:

  • 可以当 STUN 服务器
  • 也可以当 TURN 服务器

三者关系图

1
2
3
4
5
6
7
8
9
10
          WebRTC 通信
│
┌───────────┴───────────┐
│ │
先尝试 STUN 如果失败 → TURN
│ │
直接P2P通信 通过服务器转发
│
coturn
(实现STUN和TURN的程序)

安装coturn

因为docker被限制,自己尝试安装了docker拉取coturn服务还是超时,所以果断选择本机安装。

本机安装有两种方式,一种源码方式,一种集成服务安装。

官方推荐集成服务安装,源码安装是为了制作自己的webrtc服务,修改源码才用到,这里我们不用。

1
2
3
4
5
6
7
8
9
# 更新软件包
sudo apt update

# 安装 Coturn
sudo apt install coturn -y

# 查看版本
turnserver --help | head -1

修改配置文件

先备份原来的配置文件

1
sudo cp /etc/turnserver.conf /etc/turnserver.conf.bak

再修改配置文件

1
sudo vim /etc/turnserver.conf

改为如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 监听端口
listening-ip = 0.0.0.0
listening-port=3478
tls-listening-port=5349

# 外部 IP
external-ip=81.68.86.146
#relay-ip=81.68.86.146

# 启用指纹
fingerprint

# 使用长期凭证机制
lt-cred-mech

# 用户认证
user=webrtc:Kx9mP2qL8rY5tZ

# Realm
realm=turn.example.com

# 日志
log-file=/var/log/turnserver/turnserver.log
verbose

# 端口范围
min-port=49152
max-port=65535

# 拒绝私有 IP
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255

创建日志目录

1
2
3
4
5
# 创建日志目录
sudo mkdir -p /var/log/turnserver

# 设置权限
sudo chown turnserver:turnserver /var/log/turnserver

验证服务

1
2
3
4
5
6
7
8
# 查看端口监听
sudo netstat -tuln | grep 3478

# 查看日志
sudo tail -f /var/log/turnserver/turnserver.log

# 或者
sudo journalctl -u coturn -f

启用并启动服务

1
2
3
4
5
6
7
8
9
# 启用 Coturn 服务
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn

# 启动服务
sudo systemctl enable coturn
sudo systemctl start coturn

# 查看状态
sudo systemctl status coturn

如果出错,可以手动编辑设置启动 Coturn 服务

1
2
# 编辑配置文件
sudo vim /etc/default/coturn

找到这一行:

1
#TURNSERVER_ENABLED=1

**去掉注释符号 #**,改为:

1
TURNSERVER_ENABLED=1

保存并且退出

常用管理命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 启动服务
sudo systemctl start coturn

# 停止服务
sudo systemctl stop coturn

# 重启服务
sudo systemctl restart coturn

# 查看状态
sudo systemctl status coturn

# 查看日志
sudo journalctl -u coturn -n 100 -f

# 查看配置文件
sudo cat /etc/turnserver.conf

云服务器开启端口

必须开放的端口:

  • 3478 (UDP + TCP) - STUN/TURN 主端口
  • 49152-65535 (UDP) - TURN 数据中继端口范围

可选端口(如果使用 TLS):

  • 5349 (UDP + TCP) - TURN over TLS

腾讯云安全组配置步骤

1. 登录腾讯云控制台

访问:https://console.cloud.tencent.com/cvm/instance

2. 配置安全组

1
控制台 → 云服务器 → 实例列表 → 找到你的服务器 → 安全组 → 修改规则

3. 添加入站规则

类型 端口范围 协议 来源 策略
自定义 3478 UDP 0.0.0.0/0 允许
自定义 3478 TCP 0.0.0.0/0 允许
自定义 49152-65535 UDP 0.0.0.0/0 允许
自定义 5349 UDP 0.0.0.0/0 允许
自定义 5349 TCP 0.0.0.0/0 允许

image-20260211135234856

快速验证端口是否开放

在你的服务器上执行:

1
2
3
4
5
6
# 查看 Coturn 是否监听端口
sudo netstat -tuln | grep -E '3478|5349'

# 应该看到类似输出:
# udp 0 0 0.0.0.0:3478 0.0.0.0:*
# tcp 0 0 0.0.0.0:3478 0.0.0.0:*

从外部测试(在你的本地电脑上):

1
2
3
4
5
# 测试 UDP 端口(需要安装 nc)
nc -u -v 81.68.86.146 3478

# 测试 TCP 端口
nc -v 81.68.86.146 3478

测试 TURN 服务

访问测试页面:https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

填入:

  • TURN URI: turn:81.68.86.146:3478
  • Username: webrtc
  • Password: Kx9mP2qL8rY5tZ

点击 Gather candidates,如果看到 relay 类型的候选者,说明成功!

先添加,然后点击add server

image-20260211132631986

然后点击下方

image-20260211133328948


如果需要修改配置

1
2
3
4
5
6
7
8
# 编辑配置文件
sudo vim /etc/turnserver.conf

# 保存后重启服务
sudo systemctl restart coturn

# 查看日志确认
sudo journalctl -u coturn -f

用户登录加载聊天资源

Posted on 2026-02-09 | In C++聊天项目

前情回顾

前面我们实现了聊天资源的下载和上传,以及异步通知断点下载等功能。

今天主要实现登录后聊天信息的加载,之前把文本信息的加载实现了,现在需要实现用户登录后图片信息的加载。

这个功能做完,聊天的基本功能就都收尾了,有意思的是好像一个圆圈,我们又回到了最初的原点,从登录逻辑出发,补充聊天资源加载

登录流程回顾

image-20260209150133552

点击登录按钮后,通过HttpMgr发送登录请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void LoginDialog::on_login_btn_clicked()
{
qDebug()<<"login btn clicked";
if(checkUserValid() == false){
return;
}

if(checkPwdValid() == false){
return ;
}

enableBtn(false);
auto email = ui->email_edit->text();
auto pwd = ui->pass_edit->text();
//发送http请求登录
QJsonObject json_obj;
json_obj["email"] = email;
json_obj["passwd"] = xorString(pwd);
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/user_login"),
json_obj, ReqId::ID_LOGIN_USER,Modules::LOGINMOD);
}

HttpMgr内部封装了PostHttpReq接口,这是异步http请求,会提前构造好一个reply,以及注册一个回调函数,将来GateServer将登录信息返回后会出发这个回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void HttpMgr::PostHttpReq(QUrl url, QJsonObject json, ReqId req_id, Modules mod)
{
//创建一个HTTP POST请求,并设置请求头和请求体
QByteArray data = QJsonDocument(json).toJson();
//通过url构造请求
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::ContentLengthHeader, QByteArray::number(data.length()));
//发送请求,并处理响应, 获取自己的智能指针,构造伪闭包并增加智能指针引用计数
auto self = shared_from_this();
QNetworkReply * reply = _manager.post(request, data);
//设置信号和槽等待发送完成
QObject::connect(reply, &QNetworkReply::finished, [reply, self, req_id, mod](){
//处理错误的情况
if(reply->error() != QNetworkReply::NoError){
qDebug() << reply->errorString();
//发送信号通知完成
emit self->sig_http_finish(req_id, "", ErrorCodes::ERR_NETWORK, mod);
reply->deleteLater();
return;
}

//无错误则读回请求
QString res = reply->readAll();

//发送信号通知完成
emit self->sig_http_finish(req_id, res, ErrorCodes::SUCCESS,mod);
reply->deleteLater();
return;
});
}

我们查看下GateServer的处理流程

1
2
3
4
5
6
7
8
bool LogicSystem::HandlePost(std::string path, std::shared_ptr<HttpConnection> con) {
if (_post_handlers.find(path) == _post_handlers.end()) {
return false;
}

_post_handlers[path](con);
return true;
}

服务器会根据客户端传递的url进行分析,然后去_post_handlers中根据path查找并调用回调函数

回调函数在LogicSystem中提前注册到_post_handlers中

1
2
3
void LogicSystem::RegPost(std::string url, HttpHandler handler) {
_post_handlers.insert(make_pair(url, handler));
}

在构造函数中调用RegPost注册消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//用户登录逻辑
RegPost("/user_login", [](std::shared_ptr<HttpConnection> connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "receive body is " << body_str << std::endl;
connection->_response.set(http::field::content_type, "text/json");
Json::Value root;
Json::Reader reader;
Json::Value src_root;
bool parse_success = reader.parse(body_str, src_root);
if (!parse_success) {
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

auto email = src_root["email"].asString();
auto pwd = src_root["passwd"].asString();
UserInfo userInfo;
//查询数据库判断用户名和密码是否匹配
bool pwd_valid = MysqlMgr::GetInstance()->CheckPwd(email, pwd, userInfo);
if (!pwd_valid) {
std::cout << " user pwd not match" << std::endl;
root["error"] = ErrorCodes::PasswdInvalid;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//查询StatusServer找到合适的连接
auto reply = StatusGrpcClient::GetInstance()->GetChatServer(userInfo.uid);
if (reply.error()) {
std::cout << " grpc get chat server failed, error is " << reply.error()<< std::endl;
root["error"] = ErrorCodes::RPCFailed;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

std::cout << "succeed to load userinfo uid is " << userInfo.uid << std::endl;
root["error"] = 0;
root["email"] = email;
root["uid"] = userInfo.uid;
root["token"] = reply.token();
root["chathost"] = reply.host();
root["chatport"] = reply.port();
auto& gCfgMgr = ConfigMgr::Inst();
std::string res_port = gCfgMgr["ResServer"]["Port"];
std::string res_host = gCfgMgr["ResServer"]["Host"];
root["reshost"] = res_host;
root["resport"] = res_port;

std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

所以GateServer会触发上面的lambda表达式处理

在lambda表达式中调用GRPC连接池,向StatusServer发送请求,获取可用的聊天服务器地址,将聊天服务器地址返回给GateServer,GateServer再将地址返回给客户端, rpc封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GetChatServerRsp StatusGrpcClient::GetChatServer(int uid)
{
ClientContext context;
GetChatServerRsp reply;
GetChatServerReq request;
request.set_uid(uid);
auto stub = pool_->getConnection();
Status status = stub->GetChatServer(&context, request, &reply);
Defer defer([&stub, this]() {
pool_->returnConnection(std::move(stub));
});
if (status.ok()) {
return reply;
}
else {
reply.set_error(ErrorCodes::RPCFailed);
return reply;
}
}

客户端收到GateServer回复后,客户端会根据消息ID为ReqId::ID_LOGIN_USER发送sig_http_finish信号通知

该信号连接了

1
2
3
4
5
HttpMgr::HttpMgr()
{
//连接http请求和完成信号,信号槽机制保证队列消费
connect(this, &HttpMgr::sig_http_finish, this, &HttpMgr::slot_http_finish);
}

分别对应三个请求的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void HttpMgr::slot_http_finish(ReqId id, QString res, ErrorCodes err, Modules mod)
{
if(mod == Modules::REGISTERMOD){
//发送信号通知指定模块http响应结束
emit sig_reg_mod_finish(id, res, err);
}

if(mod == Modules::RESETMOD){
//发送信号通知指定模块http响应结束
emit sig_reset_mod_finish(id, res, err);
}

if(mod == Modules::LOGINMOD){
emit sig_login_mod_finish(id, res, err);
}
}

对于登录请求,主要逻辑在sig_login_mod_finish信号发出看,该信号在LoginDialog中连接

1
2
3
//连接登录回包信号
connect(HttpMgr::GetInstance().get(), &HttpMgr::sig_login_mod_finish, this,
&LoginDialog::slot_login_mod_finish);

进而出发槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void LoginDialog::slot_login_mod_finish(ReqId id, QString res, ErrorCodes err)
{
if(err != ErrorCodes::SUCCESS){
showTip(tr("网络请求错误"),false);
return;
}

// 解析 JSON 字符串,res需转化为QByteArray
QJsonDocument jsonDoc = QJsonDocument::fromJson(res.toUtf8());
//json解析错误
if(jsonDoc.isNull()){
showTip(tr("json解析错误"),false);
return;
}

//json解析错误
if(!jsonDoc.isObject()){
showTip(tr("json解析错误"),false);
return;
}


//调用对应的逻辑,根据id回调。
_handlers[id](jsonDoc.object());

return;
}

槽函数中根据id获取提前注册好的回调函数

1
_handlers[id](jsonDoc.object());

之前的注册逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void LoginDialog::initHttpHandlers()
{
//注册获取登录回包逻辑
_handlers.insert(ReqId::ID_LOGIN_USER, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
enableBtn(true);
return;
}
auto email = jsonObj["email"].toString();

//发送信号通知tcpMgr发送长链接
_si = std::make_shared<ServerInfo>();

_si->_uid = jsonObj["uid"].toInt();
_si->_chat_host = jsonObj["chathost"].toString();
_si->_chat_port = jsonObj["chatport"].toString();
_si->_token = jsonObj["token"].toString();

_si->_res_host = jsonObj["reshost"].toString();
_si->_res_port = jsonObj["resport"].toString();


qDebug()<< "email is " << email << " uid is " << _si->_uid <<" chat host is "
<< _si->_chat_host << " chat port is "
<< _si->_chat_port << " token is " << _si->_token
<< " res host is " << _si->_res_host
<< " res port is " << _si->_res_port;
emit sig_connect_tcp(_si);
// qDebug() << "send thread is " << QThread::currentThread();
// emit sig_test();
});
}

所以当消息到来时会出发上面的lambda表达式,进而发出sig_connect_tcp信号, 该信号链接槽函数slot_tcp_connect

1
2
//连接tcp连接请求的信号和槽函数
connect(this, &LoginDialog::sig_connect_tcp, TcpMgr::GetInstance().get(), &TcpMgr::slot_tcp_connect);

槽函数内根据host地址链接指定的tcpserver

1
2
3
4
5
6
7
8
9
void TcpMgr::slot_tcp_connect(std::shared_ptr<ServerInfo> si)
{
qDebug()<< "receive tcp connect signal";
// 尝试连接到服务器
qDebug() << "Connecting to chat server...";
_host = si->_chat_host;
_port = static_cast<uint16_t>(si->_chat_port.toUInt());
_socket.connectToHost(_host, _port);
}

_socket 是QTcpSocket类型,发出连接请求后,如果和服务器建立好连接后,会触发链接成功的回调, 在TcpMgr的构造函数中提前注册了消息和回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
TcpMgr::TcpMgr():_host(""),_port(0),_b_recv_pending(false),_message_id(0),_message_len(0),_bytes_sent(0),_pending(false)
{
registerMetaType();
QObject::connect(&_socket, &QTcpSocket::connected, this, [&]() {
qDebug() << "Connected to server!";
// 连接建立后发送消息
emit sig_con_success(true);
});

QObject::connect(&_socket, &QTcpSocket::readyRead, this, [&]() {
// 当有数据可读时,读取所有数据
// 读取所有数据并追加到缓冲区
_buffer.append(_socket.readAll());

forever {
//先解析头部
if(!_b_recv_pending){
// 检查缓冲区中的数据是否足够解析出一个消息头(消息ID + 消息长度)
if (_buffer.size() < static_cast<int>(sizeof(quint16) * 2)) {
return; // 数据不够,等待更多数据
}

// ✅ 每次都重新创建stream
QDataStream stream(_buffer);
stream.setVersion(QDataStream::Qt_5_0);
stream >> _message_id >> _message_len;
_buffer.remove(0, sizeof(quint16) * 2); // 使用remove代替mid赋值
qDebug() << "Message ID:" << _message_id << ", Length:" << _message_len;

}

//buffer剩余长读是否满足消息体长度,不满足则退出继续等待接受
if(_buffer.size() < _message_len){
_b_recv_pending = true;
return;
}

_b_recv_pending = false;
// 读取消息体
QByteArray messageBody = _buffer.mid(0, _message_len);
qDebug() << "receive body msg is " << messageBody ;

_buffer = _buffer.mid(_message_len);
handleMsg(ReqId(_message_id),_message_len, messageBody);
}

});

//5.15 之后版本
// QObject::connect(&_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred), [&](QAbstractSocket::SocketError socketError) {
// Q_UNUSED(socketError)
// qDebug() << "Error:" << _socket.errorString();
// });

// 处理错误(适用于Qt 5.15之前的版本)
QObject::connect(&_socket, static_cast<void (QTcpSocket::*)(QTcpSocket::SocketError)>(&QTcpSocket::error),
this,
[&](QTcpSocket::SocketError socketError) {
qDebug() << "Error:" << _socket.errorString() ;
switch (socketError) {
case QTcpSocket::ConnectionRefusedError:
qDebug() << "Connection Refused!";
emit sig_con_success(false);
break;
case QTcpSocket::RemoteHostClosedError:
qDebug() << "Remote Host Closed Connection!";
break;
case QTcpSocket::HostNotFoundError:
qDebug() << "Host Not Found!";
emit sig_con_success(false);
break;
case QTcpSocket::SocketTimeoutError:
qDebug() << "Connection Timeout!";
emit sig_con_success(false);
break;
case QTcpSocket::NetworkError:
qDebug() << "Network Error!";
break;
default:
qDebug() << "Other Error!";
break;
}
});

// 处理连接断开
QObject::connect(&_socket, &QTcpSocket::disconnected, this,[&]() {
qDebug() << "Disconnected from server.";
//并且发送通知到界面
emit sig_connection_closed();
});
//连接发送信号用来发送数据
QObject::connect(this, &TcpMgr::sig_send_data, this, &TcpMgr::slot_send_data);

//连接发送信号
QObject::connect(&_socket, &QTcpSocket::bytesWritten, this, [this](qint64 bytes) {
//更新发送数据
_bytes_sent += bytes;
//未发送完整
if (_bytes_sent < _current_block.size()) {
//继续发送
auto data_to_send = _current_block.mid(_bytes_sent);
_socket.write(data_to_send);
return;
}

//发送完全,则查看队列是否为空
if (_send_queue.isEmpty()) {
//队列为空,说明已经将所有数据发送完成,将pending设置为false,这样后续要发送数据时可以继续发送
_current_block.clear();
_pending = false;
_bytes_sent = 0;
return;
}

//队列不为空,则取出队首元素
_current_block = _send_queue.dequeue();
_bytes_sent = 0;
_pending = true;
qint64 w2 = _socket.write(_current_block);
qDebug() << "[TcpMgr] Dequeued and write() returned" << w2;
});


//关闭socket
connect(this, &TcpMgr::sig_close, this, &TcpMgr::slot_tcp_close);
//注册消息
initHandlers();

}

当客户端和服务器建立连接后,会出发下面的lambda表达式

1
2
3
4
5
QObject::connect(&_socket, &QTcpSocket::connected, this, [&]() {
qDebug() << "Connected to server!";
// 连接建立后发送消息
emit sig_con_success(true);
});

从而发出sig_con_success信号, 信号连接了槽函数slot_tcp_con_finish

1
2
//连接tcp管理者发出的连接成功信号
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_con_success, this, &LoginDialog::slot_tcp_con_finish);

槽函数里链接资源服务器

1
2
3
4
5
6
7
8
9
void LoginDialog::slot_tcp_con_finish(bool bsuccess)
{
if(bsuccess){
showTip(tr("聊天服务连接成功,正在连接资源服务器..."),true);
emit sig_connect_res_server(_si);
}else{
showTip(tr("网络异常"), false);
}
}

该信号连接了槽函数

1
2
3
//连接tcp连接资源服务器请求的信号和槽函数
connect(this, &LoginDialog::sig_connect_res_server,
FileTcpMgr::GetInstance().get(), &FileTcpMgr::slot_tcp_connect);

链接资源服务器的逻辑和之前链接ChatServer类似

1
2
3
4
5
6
7
8
9
void FileTcpMgr::slot_tcp_connect(std::shared_ptr<ServerInfo> si)
{
qDebug() << "receive tcp connect signal";
// 尝试连接到服务器
qDebug() << "Connecting to server...";
_host = si->_res_host;
_port = static_cast<uint16_t>(si->_res_port.toUInt());
_socket.connectToHost(_host, _port);
}

当客户端链接资源服务器成功了,就会出发lambda表达式

1
2
3
4
QObject::connect(&_socket, &QTcpSocket::connected, this, [&]() {
qDebug() << "Connected to server!";
emit sig_con_success(true);
});

信号sig_con_success和槽函数链接

1
connect(FileTcpMgr::GetInstance().get(), &FileTcpMgr::sig_con_success, this, &LoginDialog::slot_res_con_finish);

进而触发这个函数,内部发送登录请求给ChatServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void LoginDialog::slot_res_con_finish(bool bsuccess)
{
if(bsuccess){
showTip(tr("聊天服务连接成功,正在登录..."),true);
QJsonObject jsonObj;
jsonObj["uid"] = _si->_uid;
jsonObj["token"] = _si->_token;

QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Indented);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_CHAT_LOGIN, jsonData);

}else{
showTip(tr("网络异常"),false);
enableBtn(true);
}
}

这里略去ChatServer的处理,客户端收到ID_CHAT_LOGIN_RSP回复后,出发lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
_handlers.insert(ID_CHAT_LOGIN_RSP, [this](ReqId id, int len, QByteArray data){
Q_UNUSED(len);
qDebug()<< "handle id is "<< id ;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if(jsonDoc.isNull()){
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();
qDebug()<< "data jsonobj is " << jsonObj ;

if(!jsonObj.contains("error")){
int err = ErrorCodes::ERR_JSON;
qDebug() << "Login Failed, err is Json Parse Err" << err ;
emit sig_login_failed(err);
return;
}

int err = jsonObj["error"].toInt();
if(err != ErrorCodes::SUCCESS){
qDebug() << "Login Failed, err is " << err ;
emit sig_login_failed(err);
return;
}

auto uid = jsonObj["uid"].toInt();
auto name = jsonObj["name"].toString();
auto nick = jsonObj["nick"].toString();
auto icon = jsonObj["icon"].toString();
auto sex = jsonObj["sex"].toInt();
auto desc = jsonObj["desc"].toString();
auto user_info = std::make_shared<UserInfo>(uid, name, nick, icon, sex,"",desc);

UserMgr::GetInstance()->SetUserInfo(user_info);
UserMgr::GetInstance()->SetToken(jsonObj["token"].toString());
if(jsonObj.contains("apply_list")){
UserMgr::GetInstance()->AppendApplyList(jsonObj["apply_list"].toArray());
}

//添加好友列表
if (jsonObj.contains("friend_list")) {
UserMgr::GetInstance()->AppendFriendList(jsonObj["friend_list"].toArray());
}

emit sig_swich_chatdlg();
});

将用户信息,以及好友列表,申请列表等数据组织好后存储UserMgr中,然后发送信号sig_swich_chatdlg跳转到登录界面

链接信号

1
2
//连接创建聊天界面信号
connect(TcpMgr::GetInstance().get(),&TcpMgr::sig_swich_chatdlg, this, &MainWindow::SlotSwitchChat);

跳转逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13

void MainWindow::SlotSwitchChat()
{
_chat_dlg = new ChatDialog();
_chat_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_chat_dlg);
_chat_dlg->show();
_login_dlg->hide();
this->setMinimumSize(QSize(1050,900));
this->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
_ui_status = CHAT_UI;
_chat_dlg->loadChatList();
}

至此完成登录界面加载以及聊天记录加载,聊天记录加载具体流程看下面

加载聊天记录

其中加载聊天记录核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void ChatDialog::loadChatList()
{
showLoadingDlg(true);
//发送请求逻辑
QJsonObject jsonObj;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj["uid"] = uid;
int last_chat_thread_id = UserMgr::GetInstance()->GetLastChatThreadId();
jsonObj["thread_id"] = last_chat_thread_id;


QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_LOAD_CHAT_THREAD_REQ, jsonData);
}

发送ID_LOAD_CHAT_THREAD_REQ逻辑给ChatServer,略去服务器处理流程,客户端会受到会话列表的回复数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
_handlers.insert(ID_LOAD_CHAT_THREAD_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();

if (!jsonObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "chat thread json parse failed " << err;
return;
}

int err = jsonObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "get chat thread rsp failed, error is " << err;
return;
}

qDebug() << "Receive chat thread rsp Success";

auto thread_array = jsonObj["threads"].toArray();
std::vector<std::shared_ptr<ChatThreadInfo>> chat_threads;
for (const QJsonValue& value : thread_array) {
auto cti = std::make_shared<ChatThreadInfo>();
cti->_thread_id = value["thread_id"].toInt();
cti->_type = value["type"].toString();
cti->_user1_id = value["user1_id"].toInt();
cti->_user2_id = value["user2_id"].toInt();
chat_threads.push_back(cti);
}

bool load_more = jsonObj["load_more"].toBool();
int next_last_id = jsonObj["next_last_id"].toInt();
//发送信号通知界面
emit sig_load_chat_thread(load_more, next_last_id, chat_threads);
});

解析会话列表,按照会话id请求每个会话具体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void ChatDialog::slot_load_chat_thread(bool load_more, int last_thread_id,
std::vector<std::shared_ptr<ChatThreadInfo>> chat_threads)
{
for (auto& cti : chat_threads) {
//先处理单聊,群聊跳过,以后添加
if (cti->_type == "group") {
continue;
}

auto uid = UserMgr::GetInstance()->GetUid();
auto other_uid = 0;
if (uid == cti->_user1_id) {
other_uid = cti->_user2_id;
}
else {
other_uid = cti->_user1_id;
}

auto chat_thread_data = std::make_shared<ChatThreadData>(other_uid, cti->_thread_id, 0);
UserMgr::GetInstance()->AddChatThreadData(chat_thread_data, other_uid);

auto* chat_user_wid = new ChatUserWid();
chat_user_wid->SetChatData(chat_thread_data);
QListWidgetItem* item = new QListWidgetItem;
//qDebug()<<"chat_user_wid sizeHint is " << chat_user_wid->sizeHint();
item->setSizeHint(chat_user_wid->sizeHint());
ui->chat_user_list->addItem(item);
ui->chat_user_list->setItemWidget(item, chat_user_wid);
_chat_thread_items.insert(cti->_thread_id, item);
}

UserMgr::GetInstance()->SetLastChatThreadId(last_thread_id);

if (load_more) {
//发送请求逻辑
QJsonObject jsonObj;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj["uid"] = uid;
jsonObj["thread_id"] = last_thread_id;


QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_LOAD_CHAT_THREAD_REQ, jsonData);
return;
}

showLoadingDlg(false);
//继续加载聊天数据
loadChatMsg();
}

如果load_more为true,说明有会话没加载完,需要继续加载,等到所有会话信息加载成功后,load_more为false,则将当前会话的消息列表添加到聊天界面。如果会话列表加载完成了,则继续加载会话内部的多个消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ChatDialog::loadChatMsg() {

//发送聊天记录请求
_cur_load_chat = UserMgr::GetInstance()->GetCurLoadData();
if (_cur_load_chat == nullptr) {
return;
}

showLoadingDlg(true);

//发送请求给服务器
//发送请求逻辑
QJsonObject jsonObj;
jsonObj["thread_id"] = _cur_load_chat->GetThreadId();
jsonObj["message_id"] = _cur_load_chat->GetLastMsgId();

QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_LOAD_CHAT_MSG_REQ, jsonData);
}

回包消息处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
_handlers.insert(ID_LOAD_CHAT_MSG_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();

if (!jsonObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "parse create private chat json parse failed " << err;
return;
}

int err = jsonObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "get create private chat failed, error is " << err;
return;
}

qDebug() << "Receive create private chat rsp Success";

int thread_id = jsonObj["thread_id"].toInt();
int last_msg_id = jsonObj["last_message_id"].toInt();
bool load_more = jsonObj["load_more"].toBool();

std::vector<std::shared_ptr<ChatDataBase>> chat_datas;
for (const QJsonValue& data : jsonObj["chat_datas"].toArray()) {
auto send_uid = data["sender"].toInt();
auto msg_id = data["msg_id"].toInt();
auto thread_id = data["thread_id"].toInt();
auto unique_id = data["unique_id"].toInt();
auto msg_content = data["msg_content"].toString();
QString chat_time = data["chat_time"].toString();
int status = data["status"].toInt();
int msg_type = data["msg_type"].toInt();
int recv_id = data["receiver"].toInt();
if (msg_type == int(ChatMsgType::TEXT)) {
auto chat_data = std::make_shared<TextChatData>(msg_id, thread_id, ChatFormType::PRIVATE,
ChatMsgType::TEXT, msg_content, send_uid, status, chat_time);
chat_datas.push_back(chat_data);
continue;
}

if (msg_type == int(ChatMsgType::PIC)) {
auto uid = UserMgr::GetInstance()->GetUid();
QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QString img_path_str = storageDir + "/user/" + QString::number(uid) + "/chatimg/" + QString::number(send_uid);
QString img_path = img_path_str + "/" + msg_content;
//文件不存在,则创建空白图片占位,同时组织数据准备发送
if (QFile::exists(img_path) == false) {

CreatePlaceholderImgMsgL(img_path_str, msg_content,
msg_id, thread_id, send_uid, recv_id, status, chat_time,
chat_datas);
continue;
}
//如果文件存在
//如果文件存在则直接构建MsgInfo
// 获取文件大小
QFileInfo fileInfo(img_path);
qint64 file_size = fileInfo.size();
//从文件路径加载QPixmap
QPixmap pixmap(img_path);
//如果图片加载失败,也是创建占位符,然后组织发送
if (pixmap.isNull()) {
CreatePlaceholderImgMsgL(img_path_str, msg_content,
msg_id, thread_id, send_uid, recv_id, status, chat_time,
chat_datas);
continue;
}

//说明图片加载正确,构建真实图片
auto file_info = std::make_shared<MsgInfo>(MsgType::IMG_MSG, img_path_str,
pixmap, msg_content, file_size, "");
file_info->_msg_id = msg_id;
file_info->_sender = send_uid;
file_info->_receiver = recv_id;
file_info->_thread_id = thread_id;
//设置文件传输的类型
file_info->_transfer_type = TransferType::Download;
//设置文件传输状态
file_info->_transfer_state = TransferState::None;
//放入chat_datas列表
auto chat_data = std::make_shared<ImgChatData>(file_info,"", thread_id, ChatFormType::PRIVATE,
ChatMsgType::PIC, send_uid, status, chat_time);
chat_datas.push_back(chat_data);
continue;
}
}

//发送信号通知界面
emit sig_load_chat_msg(thread_id, last_msg_id, load_more, chat_datas);
});

这里加载了thread会话信息,以及每个会话的消息列表,如果消息列表没有加载完全,则继续发送信号sig_load_chat_msg信号继续加载消息。

最终在此处将所有会话的所有消息加载完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void ChatDialog::slot_load_chat_msg(int thread_id, int msg_id, bool load_more, 
std::vector<std::shared_ptr<ChatDataBase>> msglists)
{
_cur_load_chat->SetLastMsgId(msg_id);
//加载聊天信息
for (auto& chat_msg : msglists) {
_cur_load_chat->AppendMsg(chat_msg->GetMsgId(), chat_msg);
}

//还有未加载完的消息,就继续加载
if (load_more) {
//发送请求给服务器
//发送请求逻辑
QJsonObject jsonObj;
jsonObj["thread_id"] = _cur_load_chat->GetThreadId();
jsonObj["message_id"] = _cur_load_chat->GetLastMsgId();

QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_LOAD_CHAT_MSG_REQ, jsonData);
return;
}

//获取下一个chat_thread
_cur_load_chat = UserMgr::GetInstance()->GetNextLoadData();
//都加载完了
if(!_cur_load_chat){
//更新聊天界面信息
SetSelectChatItem();
SetSelectChatPage();
showLoadingDlg(false);
return;
}

//继续加载下一个聊天
//发送请求给服务器
//发送请求逻辑
QJsonObject jsonObj;
jsonObj["thread_id"] = _cur_load_chat->GetThreadId();
jsonObj["message_id"] = _cur_load_chat->GetLastMsgId();

QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_LOAD_CHAT_MSG_REQ, jsonData);
}

资源消息加载

核心的资源消息加载是在ID_LOAD_CHAT_MSG_RSP回报的逻辑里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
_handlers.insert(ID_LOAD_CHAT_MSG_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();

if (!jsonObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "parse create private chat json parse failed " << err;
return;
}

int err = jsonObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "get create private chat failed, error is " << err;
return;
}

qDebug() << "Receive create private chat rsp Success";

int thread_id = jsonObj["thread_id"].toInt();
int last_msg_id = jsonObj["last_message_id"].toInt();
bool load_more = jsonObj["load_more"].toBool();

std::vector<std::shared_ptr<ChatDataBase>> chat_datas;
for (const QJsonValue& data : jsonObj["chat_datas"].toArray()) {
auto send_uid = data["sender"].toInt();
auto msg_id = data["msg_id"].toInt();
auto thread_id = data["thread_id"].toInt();
auto unique_id = data["unique_id"].toInt();
auto msg_content = data["msg_content"].toString();
QString chat_time = data["chat_time"].toString();
int status = data["status"].toInt();
int msg_type = data["msg_type"].toInt();
int recv_id = data["receiver"].toInt();
if (msg_type == int(ChatMsgType::TEXT)) {
auto chat_data = std::make_shared<TextChatData>(msg_id, thread_id, ChatFormType::PRIVATE,
ChatMsgType::TEXT, msg_content, send_uid, status, chat_time);
chat_datas.push_back(chat_data);
continue;
}

if (msg_type == int(ChatMsgType::PIC)) {
auto uid = UserMgr::GetInstance()->GetUid();
QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QString img_path_str = storageDir + "/user/" + QString::number(uid) + "/chatimg/" + QString::number(send_uid);
QString img_path = img_path_str + "/" + msg_content;
//文件不存在,则创建空白图片占位,同时组织数据准备发送
if (QFile::exists(img_path) == false) {

CreatePlaceholderImgMsgL(img_path_str, msg_content,
msg_id, thread_id, send_uid, recv_id, status, chat_time,
chat_datas);
continue;
}
//如果文件存在
//如果文件存在则直接构建MsgInfo
// 获取文件大小
QFileInfo fileInfo(img_path);
qint64 file_size = fileInfo.size();
//从文件路径加载QPixmap
QPixmap pixmap(img_path);
//如果图片加载失败,也是创建占位符,然后组织发送
if (pixmap.isNull()) {
CreatePlaceholderImgMsgL(img_path_str, msg_content,
msg_id, thread_id, send_uid, recv_id, status, chat_time,
chat_datas);
continue;
}

//说明图片加载正确,构建真实图片
auto file_info = std::make_shared<MsgInfo>(MsgType::IMG_MSG, img_path_str,
pixmap, msg_content, file_size, "");
file_info->_msg_id = msg_id;
file_info->_sender = send_uid;
file_info->_receiver = recv_id;
file_info->_thread_id = thread_id;
//设置文件传输的类型
file_info->_transfer_type = TransferType::Download;
//设置文件传输状态
file_info->_transfer_state = TransferState::None;
//放入chat_datas列表
auto chat_data = std::make_shared<ImgChatData>(file_info,"", thread_id, ChatFormType::PRIVATE,
ChatMsgType::PIC, send_uid, status, chat_time);
chat_datas.push_back(chat_data);
continue;
}
}

//发送信号通知界面
emit sig_load_chat_msg(thread_id, last_msg_id, load_more, chat_datas);
});

核心逻辑是这部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

if (msg_type == int(ChatMsgType::TEXT)) {
auto chat_data = std::make_shared<TextChatData>(msg_id, thread_id, ChatFormType::PRIVATE,
ChatMsgType::TEXT, msg_content, send_uid, status, chat_time);
chat_datas.push_back(chat_data);
continue;
}

if (msg_type == int(ChatMsgType::PIC)) {
auto uid = UserMgr::GetInstance()->GetUid();
QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QString img_path_str = storageDir + "/user/" + QString::number(uid) + "/chatimg/" + QString::number(send_uid);
QString img_path = img_path_str + "/" + msg_content;
//文件不存在,则创建空白图片占位,同时组织数据准备发送
if (QFile::exists(img_path) == false) {

CreatePlaceholderImgMsgL(img_path_str, msg_content,
msg_id, thread_id, send_uid, recv_id, status, chat_time,
chat_datas);
continue;
}
//如果文件存在
//如果文件存在则直接构建MsgInfo
// 获取文件大小
QFileInfo fileInfo(img_path);
qint64 file_size = fileInfo.size();
//从文件路径加载QPixmap
QPixmap pixmap(img_path);
//如果图片加载失败,也是创建占位符,然后组织发送
if (pixmap.isNull()) {
CreatePlaceholderImgMsgL(img_path_str, msg_content,
msg_id, thread_id, send_uid, recv_id, status, chat_time,
chat_datas);
continue;
}

//说明图片加载正确,构建真实图片
auto file_info = std::make_shared<MsgInfo>(MsgType::IMG_MSG, img_path_str,
pixmap, msg_content, file_size, "");
file_info->_msg_id = msg_id;
file_info->_sender = send_uid;
file_info->_receiver = recv_id;
file_info->_thread_id = thread_id;
//设置文件传输的类型
file_info->_transfer_type = TransferType::Download;
//设置文件传输状态
file_info->_transfer_state = TransferState::None;
//放入chat_datas列表
auto chat_data = std::make_shared<ImgChatData>(file_info,"", thread_id, ChatFormType::PRIVATE,
ChatMsgType::PIC, send_uid, status, chat_time);
chat_datas.push_back(chat_data);
continue;
}

rpc通知客户端异步下载聊天图片

Posted on 2026-02-08 | In C++聊天项目

前情回顾

前面我们搞定了1,2,3以及5过程

image-20260208131004814

今天主要完成

4,6和7

资源服务器grpc设置

因为资源服务器要通知ChatServer,所以要设置grpc客户端

完善下proto协议,新增消息通知请求

image-20260208133832656

具体代码逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
message NotifyChatImgReq{
int32 from_uid = 1;
int32 to_uid = 2;
int32 message_id = 3;
string file_name = 4;
int64 total_size = 5;
int32 thread_id =6;
}

message NotifyChatImgRsp{
int32 error = 1;
int32 from_uid = 2;
int32 to_uid = 3;
int32 message_id = 4;
string file_name = 5;
int64 total_size = 6;
int32 thread_id =7;
}

service ChatService {
rpc NotifyAddFriend(AddFriendReq) returns (AddFriendRsp) {}
rpc RplyAddFriend(RplyFriendReq) returns (RplyFriendRsp) {}
rpc SendChatMsg(SendChatMsgReq) returns (SendChatMsgRsp) {}
rpc NotifyAuthFriend(AuthFriendReq) returns (AuthFriendRsp) {}
rpc NotifyTextChatMsg(TextChatMsgReq) returns (TextChatMsgRsp){}
rpc NotifyKickUser(KickUserReq) returns (KickUserRsp){}
rpc NotifyChatImgMsg(NotifyChatImgReq) returns (NotifyChatImgRsp){}
}

实现grpc客户端逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#pragma once
#include "const.h"
#include "Singleton.h"
#include "ConfigMgr.h"
#include "message.grpc.pb.h"
#include "message.pb.h"
#include <grpcpp/grpcpp.h>
#include <queue>
#include <condition_variable>
using grpc::Channel;
using grpc::Status;
using grpc::ClientContext;


using message::ChatService;
using message::NotifyChatImgReq;
using message::NotifyChatImgRsp;

class ChatServerConPool {
public:
ChatServerConPool(size_t poolSize, std::string host, std::string port)
: poolSize_(poolSize), host_(host), port_(port), b_stop_(false) {
for (size_t i = 0; i < poolSize_; ++i) {

std::shared_ptr<Channel> channel = grpc::CreateChannel(host + ":" + port,
grpc::InsecureChannelCredentials());

connections_.push(ChatService::NewStub(channel));
}
}

~ChatServerConPool() {
std::lock_guard<std::mutex> lock(mutex_);
Close();
while (!connections_.empty()) {
connections_.pop();
}
}

std::unique_ptr<ChatService::Stub> getConnection() {
std::unique_lock<std::mutex> lock(mutex_);
cond_.wait(lock, [this] {
if (b_stop_) {
return true;
}
return !connections_.empty();
});
//如果停止则直接返回空指针
if (b_stop_) {
return nullptr;
}
auto context = std::move(connections_.front());
connections_.pop();
return context;
}

void returnConnection(std::unique_ptr<ChatService::Stub> context) {
std::lock_guard<std::mutex> lock(mutex_);
if (b_stop_) {
return;
}
connections_.push(std::move(context));
cond_.notify_one();
}

void Close() {
b_stop_ = true;
cond_.notify_all();
}

private:
atomic<bool> b_stop_;
size_t poolSize_;
std::string host_;
std::string port_;
std::queue<std::unique_ptr<ChatService::Stub>> connections_;
std::mutex mutex_;
std::condition_variable cond_;
};

class ChatServerGrpcClient :public Singleton<ChatServerGrpcClient>
{
friend class Singleton<ChatServerGrpcClient>;
public:
~ChatServerGrpcClient() {

}
NotifyChatImgRsp NotifyChatImgMsg(int message_id, std::string chatserver);
private:
ChatServerGrpcClient();
//sever_ip到连接池的映射, <chatserver1,std::unique_ptr<ChatServerConPool>>
std::unordered_map<std::string, std::unique_ptr<ChatServerConPool>> _hash_pools;
};

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include "ChatServerGrpcClient.h"
#include "MysqlMgr.h"

NotifyChatImgRsp ChatServerGrpcClient::NotifyChatImgMsg(int message_id,std::string chatserver)
{
ClientContext context;
NotifyChatImgRsp reply;
NotifyChatImgReq request;
request.set_message_id(message_id);
if (_hash_pools.find(chatserver) == _hash_pools.end()) {
reply.set_error(ErrorCodes::ServerIpErr);
return reply;
}
auto chat_msg = MysqlMgr::GetInstance()->GetChatMsgById(message_id);
request.set_file_name(chat_msg->content);
request.set_from_uid(chat_msg->sender_id);
request.set_to_uid(chat_msg->recv_id);
request.set_thread_id(chat_msg->thread_id);
// 资源文件路径
auto file_dir = ConfigMgr::Inst().GetFileOutPath();
//该消息是接收方客户端发送过来的,服务器将资源存储在发送方的文件夹中
auto uid_str = std::to_string(chat_msg->sender_id);
auto file_path = (file_dir / uid_str / chat_msg->content);
boost::uintmax_t file_size = boost::filesystem::file_size(file_path);
request.set_total_size(file_size);

auto &pool_ = _hash_pools[chatserver];
auto stub = pool_->getConnection();
Status status = stub->NotifyChatImgMsg(&context, request, &reply);
Defer defer([&stub, &pool_, this]() {
pool_->returnConnection(std::move(stub));
});
if (status.ok()) {
return reply;
}
else {
reply.set_error(ErrorCodes::RPCFailed);
return reply;
}
}

ChatServerGrpcClient::ChatServerGrpcClient()
{
auto& gCfgMgr = ConfigMgr::Inst();
std::string host1 = gCfgMgr["chatserver1"]["Host"];
std::string port1 = gCfgMgr["chatserver1"]["Port"];
_hash_pools["chatserver1"] = std::make_unique<ChatServerConPool>(5, host1, port1);

std::string host2 = gCfgMgr["chatserver2"]["Host"];
std::string port2 = gCfgMgr["chatserver2"]["Port"];
_hash_pools["chatserver2"] = std::make_unique<ChatServerConPool>(5, host2, port2);
}

实现了通知接口,用来通知ChatServer图片消息上传完成,让ChatServer再通知其他客户端.

ChatServer响应资源服务器通知

ChatServer的proto也需要进行同样配置,这里略去

具体在ChatServiceImpl中添加响应消息通知的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Status ChatServiceImpl::NotifyChatImgMsg(::grpc::ServerContext* context, const ::message::NotifyChatImgReq* request, ::message::NotifyChatImgRsp* response)
{
//查找用户是否在本服务器
auto uid = request->to_uid();
auto session = UserMgr::GetInstance()->GetSession(uid);

Defer defer([request, response]() {
//设置具体的回包信息
response->set_error(ErrorCodes::Success);
response->set_message_id(request->message_id());
});

//用户不在内存中则直接返回
if (session == nullptr) {
//这里只是返回1个状态
return Status::OK;
}

//在内存中则直接发送通知对方
session->NotifyChatImgRecv(request);
//这里只是返回1个状态
return Status::OK;
}

通过Session通知客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CSession::NotifyChatImgRecv(const ::message::NotifyChatImgReq* request) {
Json::Value rtvalue;
rtvalue["error"] = ErrorCodes::Success;
rtvalue["message_id"] = request->message_id();
rtvalue["sender_id"] = request->from_uid();
rtvalue["receiver_id"] = request->to_uid();
rtvalue["img_name"] = request->file_name();
rtvalue["total_size"] = std::to_string(request->total_size());
rtvalue["thread_id"] = request->thread_id();

std::string return_str = rtvalue.toStyledString();
//通知图片聊天信息
Send(return_str, ID_NOTIFY_IMG_CHAT_MSG_REQ);
return;
}

客户端获取通知

客户端收到服务器通知后,会优先查看本地资源是否存在,如果存在则直接加载图片,添加聊天记录到页面。

如果不存在则组织下载,但是也需要将消息添加到聊天界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
_handlers.insert(ID_NOTIFY_IMG_CHAT_MSG_REQ, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();
qDebug() << "receive notify img chat msg req success" ;


//收到消息后转发给页面
auto thread_id = jsonObj["thread_id"].toInt();
auto sender_id = jsonObj["sender_id"].toInt();
auto message_id = jsonObj["message_id"].toInt();
auto receiver_id = jsonObj["receiver_id"].toInt();
auto img_name = jsonObj["img_name"].toString();
auto total_size_str = jsonObj["total_size"].toString();
auto total_size = total_size_str.toLongLong();
auto uid = UserMgr::GetInstance()->GetUid();
//客户端存储聊天记录,按照如下格式存储C:\Users\secon\AppData\Roaming\llfcchat\chatimg\uid, uid为对方uid
QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QString img_path_str = storageDir +"/user/"+ QString::number(uid)+ "/chatimg/" + QString::number(sender_id);
auto file_info = UserMgr::GetInstance()->GetTransFileByName(img_name);
//正常情况是找不到的,所以这里初始化一个文件信息放入UserMgr中管理
if (!file_info) {
//预览图先默认空白,md5为空
file_info = std::make_shared<MsgInfo>(MsgType::IMG_MSG, img_path_str, CreateLoadingPlaceholder(200, 200), img_name, total_size, "");
UserMgr::GetInstance()->AddTransFile(img_name, file_info);
}

file_info->_msg_id = message_id;
file_info->_sender = sender_id;
file_info->_receiver = receiver_id;
file_info->_thread_id = thread_id;
//设置文件传输的类型
file_info->_transfer_type = TransferType::Download;
//设置文件传输状态
file_info->_transfer_state = TransferState::Uploading;

auto img_chat_data_ptr = std::make_shared<ImgChatData>(file_info, "",
thread_id, ChatFormType::PRIVATE, ChatMsgType::PIC,
sender_id, MsgStatus::READED);


emit sig_img_chat_msg(img_chat_data_ptr);

//组织请求,准备下载
QJsonObject jsonObj_send;
jsonObj_send["name"] = img_name;
jsonObj_send["seq"] = file_info->_seq;
jsonObj_send["trans_size"] = "0";
jsonObj_send["total_size"] = QString::number(file_info->_total_size);
jsonObj_send["token"] = UserMgr::GetInstance()->GetToken();
jsonObj_send["sender_id"] = sender_id;
jsonObj_send["receiver_id"] = receiver_id;
jsonObj_send["message_id"] = message_id;
jsonObj_send["uid"] = uid;
//客户端存储聊天记录,按照如下格式存储C:\Users\secon\AppData\Roaming\llfcchat\chatimg\uid, uid为对方uid
QDir chatimgDir(img_path_str);
jsonObj["client_path"] = img_path_str;
if (!chatimgDir.exists()) {
chatimgDir.mkpath("."); // 创建当前路径
}

QJsonDocument doc(jsonObj_send);
auto send_data = doc.toJson();
FileTcpMgr::GetInstance()->SendData(ID_IMG_CHAT_DOWN_REQ, send_data);
});

收到服务器通知后,开始构造json数据,发送ID_IMG_CHAT_DOWN_REQ请求

聊天记录添加

客户端在请求服务器资源的时候,因为本地没有资源,可以先在聊天界面生成一个预览的空白图片,同时显示进度条

这部分逻辑是在客户端的tcpmgr中处理服务器通知聊天消息的逻辑里

1
2
3
4
5
_handlers.insert(ID_NOTIFY_IMG_CHAT_MSG_REQ, [this](ReqId id, int len, QByteArray data) {
//...
//发送给界面显示
emit sig_img_chat_msg(img_chat_data_ptr);
}

客户端将图片消息发送给界面显示

在ChatDialog的构造函数中添加信号槽链接

1
2
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_img_chat_msg,
this, &ChatDialog::slot_img_chat_msg);

ChatDialog收到该信号后,会触发添加消息的逻辑

1
2
3
4
5
6
7
8
9
10
void ChatDialog::slot_img_chat_msg(std::shared_ptr<ImgChatData> imgchat) {
//更新数据
auto thread_id = imgchat->GetThreadId();
auto thread_data = UserMgr::GetInstance()->GetChatThreadByThreadId(thread_id);
thread_data->AddMsg(imgchat);
if (_cur_chat_thread_id != thread_id) {
return;
}
ui->chat_page->AppendOtherMsg(imgchat);
}

添加其他消息的逻辑, 此处都是将其他人发送的图片消息添加到聊天界面显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
void ChatPage::AppendOtherMsg(std::shared_ptr<ChatDataBase> msg) {
auto self_info = UserMgr::GetInstance()->GetUserInfo();
ChatRole role;
if (msg->GetSendUid() == self_info->_uid) {
role = ChatRole::Self;
ChatItemBase* pChatItem = new ChatItemBase(role);

pChatItem->setUserName(self_info->_name);
SetSelfIcon(pChatItem, self_info->_icon);
QWidget* pBubble = nullptr;
if (msg->GetMsgType() == ChatMsgType::TEXT) {
pBubble = new TextBubble(role, msg->GetMsgContent());
}
else if (msg->GetMsgType() == ChatMsgType::PIC) {
auto img_msg = dynamic_pointer_cast<ImgChatData>(msg);
auto pic_bubble = new PictureBubble(img_msg->_msg_info->_preview_pix, role, img_msg->_msg_info->_total_size);
pic_bubble->setMsgInfo(img_msg->_msg_info);
pBubble = pic_bubble;
//连接暂停和恢复信号
connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::pauseRequested,
this, &ChatPage::on_clicked_paused);
connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::resumeRequested,
this, &ChatPage::on_clicked_resume);
}

pChatItem->setWidget(pBubble);
auto status = msg->GetStatus();
pChatItem->setStatus(status);
ui->chat_data_list->appendChatItem(pChatItem);
_base_item_map[msg->GetMsgId()] = pChatItem;
}
else {
role = ChatRole::Other;
ChatItemBase* pChatItem = new ChatItemBase(role);
auto friend_info = UserMgr::GetInstance()->GetFriendById(msg->GetSendUid());
if (friend_info == nullptr) {
return;
}
pChatItem->setUserName(friend_info->_name);

// 使用正则表达式检查是否是默认头像
QRegularExpression regex("^:/res/head_(\\d+)\\.jpg$");
QRegularExpressionMatch match = regex.match(friend_info->_icon);
if (match.hasMatch()) {
pChatItem->setUserIcon(QPixmap(friend_info->_icon));
}
else {
// 如果是用户上传的头像,获取存储目录
QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
auto uid = UserMgr::GetInstance()->GetUid();
QDir avatarsDir(storageDir + "/user/" + QString::number(uid) + "/avatars");
auto file_name = QFileInfo(self_info->_icon).fileName();
// 确保目录存在
if (avatarsDir.exists()) {
QString avatarPath = avatarsDir.filePath(file_name); // 获取上传头像的完整路径
QPixmap pixmap(avatarPath); // 加载上传的头像图片
if (!pixmap.isNull()) {
pChatItem->setUserIcon(pixmap);
}
else {
qWarning() << "无法加载上传的头像:" << avatarPath;
auto icon_label = pChatItem->getIconLabel();
LoadHeadIcon(avatarPath, icon_label, file_name, "self_icon");
}
}
else {
qWarning() << "头像存储目录不存在:" << avatarsDir.path();
//创建目录
avatarsDir.mkpath(".");
auto icon_label = pChatItem->getIconLabel();
QString avatarPath = avatarsDir.filePath(file_name);
LoadHeadIcon(avatarPath, icon_label, file_name, "self_icon");
}
}

QWidget* pBubble = nullptr;
if (msg->GetMsgType() == ChatMsgType::TEXT) {
pBubble = new TextBubble(role, msg->GetMsgContent());
}
else if (msg->GetMsgType() == ChatMsgType::PIC) {
auto img_msg = dynamic_pointer_cast<ImgChatData>(msg);
auto pic_bubble = new PictureBubble(img_msg->_msg_info->_preview_pix, role, img_msg->_msg_info->_total_size);
pic_bubble->setMsgInfo(img_msg->_msg_info);
pBubble = pic_bubble;
//连接暂停和恢复信号
connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::pauseRequested,
this, &ChatPage::on_clicked_paused);
connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::resumeRequested,
this, &ChatPage::on_clicked_resume);
}
pChatItem->setWidget(pBubble);
auto status = msg->GetStatus();
pChatItem->setStatus(status);
ui->chat_data_list->appendChatItem(pChatItem);
_base_item_map[msg->GetMsgId()] = pChatItem;
}
}

资源服务器响应下载请求

LogicWorker中增加请求的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
_fun_callbacks[ID_IMG_CHAT_DOWN_REQ] = [this](std::shared_ptr<CSession> session, const short& msg_req_id,
const string& msg_data) {

Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);

auto seq = root["seq"].asInt();
auto name = root["name"].asString();
auto total_size_str = root["total_size"].asString();
auto trans_size_str = root["trans_size"].asString();
auto file_path = ConfigMgr::Inst().GetFileOutPath();
auto message_id = root["message_id"].asInt();
auto sender = root["sender_id"].asInt();
auto receiver = root["receiver_id"].asInt();
auto token = root["token"].asString();
auto uid = root["uid"].asInt();

auto callback = [=](const Json::Value& result) {
// 在异步任务完成后调用
Json::Value rtvalue = result;
rtvalue["error"] = ErrorCodes::Success;
rtvalue["name"] = name;
rtvalue["sender_id"] = sender;
rtvalue["receiver_id"] = receiver;
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_IMG_CHAT_DOWN_RSP);
};

// 使用 std::hash 对字符串进行哈希
std::hash<std::string> hash_fn;
size_t hash_value = hash_fn(name); // 生成哈希值
int index = hash_value % DOWN_LOAD_WORKER_COUNT;
std::cout << "Hash value: " << hash_value << std::endl;


//第一个包校验一下token是否合理
if (seq == 1) {
//从redis获取用户token是否正确
std::string uid_str = std::to_string(uid);
std::string token_key = USERTOKENPREFIX + uid_str;
std::string token_value = "";
bool success = RedisMgr::GetInstance()->Get(token_key, token_value);
Json::Value rtvalue;
if (!success) {
rtvalue["error"] = ErrorCodes::UidInvalid;
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_IMG_CHAT_DOWN_RSP);
return;
}

if (token_value != token) {
rtvalue["error"] = ErrorCodes::TokenInvalid;
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_IMG_CHAT_DOWN_RSP);
return;
}
}

auto sender_str = std::to_string(sender);
//转化为字符串
auto uid_str = std::to_string(uid);
auto file_path_str = (file_path / sender_str / name).string();

auto down_load_task = std::make_shared<DownloadTask>(session, uid, name, seq, file_path_str, callback);

FileSystem::GetInstance()->PostDownloadTaskToQue(down_load_task,index);
};

LogicWorker将请求投递给FileSystem队列,FileSystem队列排队处理消息,被DownloaderWorker处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
void DownloadWorker::task_callback(std::shared_ptr<DownloadTask> task)
{
// 解码
auto file_path_str = task->_file_path;

//std::cout << "file_path_str is " << file_path_str << std::endl;

boost::filesystem::path file_path(file_path_str);

Json::Value result;
result["error"] = ErrorCodes::Success;

if (!boost::filesystem::exists(file_path)) {
std::cerr << "文件不存在: " << file_path_str << std::endl;
result["error"] = ErrorCodes::FileNotExists;
task->_callback(result);
return;
}

std::ifstream infile(file_path_str, std::ios::binary);
if (!infile) {
std::cerr << "无法打开文件进行读取。" << std::endl;
result["error"] = ErrorCodes::FileReadPermissionFailed;
task->_callback(result);
return;
}

std::shared_ptr<FileInfo> file_info = nullptr;

if (task->_seq == 1) {
// 获取文件大小
infile.seekg(0, std::ios::end);
std::streamsize file_size = infile.tellg();
infile.seekg(0, std::ios::beg);
//如果为空,则创建FileInfo 构造数据存储
file_info = std::make_shared<FileInfo>();
file_info->_file_path_str = file_path_str;
file_info->_name = task->_name;
file_info->_seq = 1;

file_info->_total_size = file_size;
file_info->_trans_size = 0;
// 立即保存到 Redis,覆盖旧数据,设置过期时间
RedisMgr::GetInstance()->SetDownLoadInfo(task->_name, file_info);
std::cout << "[新下载] 文件: " << task->_name
<< ", 大小: " << file_size << " 字节" << std::endl;
}
else {
//断点续传,从 Redis 获取历史信息
file_info = RedisMgr::GetInstance()->GetDownloadInfo(task->_name);
if (file_info == nullptr) {
// Redis 中没有信息(可能过期了)
std::cerr << "断点续传失败,Redis 中无下载信息: " << task->_name << std::endl;
result["error"] = ErrorCodes::RedisReadErr;
task->_callback(result);
infile.close();
return;
}
// 验证序列号是否匹配
if (task->_seq != file_info->_seq) {
std::cerr << "序列号不匹配,期望: " << file_info->_seq
<< ", 实际: " << task->_seq << std::endl;
result["error"] = ErrorCodes::FileSeqInvalid;
task->_callback(result);
infile.close();
return;
}

std::cout << "[续传] 文件: " << task->_name
<< ", seq: " << task->_seq
<< ", 进度: " << file_info->_trans_size
<< "/" << file_info->_total_size << std::endl;
}

// 计算当前偏移量
std::streamsize offset = ((std::streamsize)task->_seq - 1) * MAX_FILE_LEN;
if (offset >= file_info->_total_size) {
std::cerr << "偏移量超出文件大小。" << std::endl;
result["error"] = ErrorCodes::FileOffsetInvalid;
task->_callback(result);
infile.close();
return;
}

// 定位到指定偏移量
infile.seekg(offset);

// 读取最多MAX_FILE_LEN字节
char buffer[MAX_FILE_LEN];
infile.read(buffer, MAX_FILE_LEN);
//获取read实际读取多少字节
std::streamsize bytes_read = infile.gcount();

if (bytes_read <= 0) {
std::cerr << "读取文件失败。" << std::endl;
result["error"] = ErrorCodes::FileReadFailed;
task->_callback(result);
infile.close();
return;
}

// 将读取的数据进行base64编码
std::string data_to_encode(buffer, bytes_read);
std::string encoded_data = base64_encode(data_to_encode);

// 检查是否是最后一个包
std::streamsize current_pos = offset + bytes_read;
bool is_last = (current_pos >= file_info->_total_size);

// 设置返回结果
result["data"] = encoded_data;
result["seq"] = task->_seq;
result["total_size"] = std::to_string(file_info->_total_size);
result["current_size"] = std::to_string(current_pos);
result["is_last"] = is_last;

infile.close();

if (is_last) {
std::cout << "文件读取完成: " << file_path_str << std::endl;
RedisMgr::GetInstance()->DelDownLoadInfo(task->_name);
}
else {
//更新信息
file_info->_seq++;
file_info->_trans_size = offset + bytes_read;
//更新redis
RedisMgr::GetInstance()->SetDownLoadInfo(task->_name, file_info);
}

if (task->_callback) {
task->_callback(result);
}

}

资源服务器每次收到请求后,由DownloadWorker从队列中获取请求,查询服务器资源,将资源按照seq计算偏移量最后读取数据发送给客户端。

客户端存储下载的资源

客户端需要存储服务器传输的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
_handlers.insert(ID_IMG_CHAT_DOWN_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id << " data is " << data;
// 将QByteArray转换为QJsonDocument

QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();

if (!jsonObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "parse create private chat json parse failed " << err;
return;
}

int err = jsonObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "get create private chat failed, error is " << err;
return;
}

qDebug() << "Receive download file info rsp success";

QString base64Data = jsonObj["data"].toString();
int seq = jsonObj["seq"].toInt();
bool is_last = jsonObj["is_last"].toBool();
QString total_size_str = jsonObj["total_size"].toString();
qint64 total_size = total_size_str.toLongLong(nullptr);
QString current_size_str = jsonObj["current_size"].toString();
qint64 current_size = current_size_str.toLongLong(nullptr);
QString name = jsonObj["name"].toString();

auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
if (file_info == nullptr) {
qDebug() << "file: " << name << " not found";
return;
}

file_info->_current_size = current_size;
file_info->_rsp_size = current_size;
file_info->_total_size = total_size;
auto clientPath = file_info->_text_or_url;

//Base64解码
QByteArray decodedData = QByteArray::fromBase64(base64Data.toUtf8());
auto file_path = clientPath + "/" + name;
QFile file(file_path);

// 根据 seq 决定打开模式
QIODevice::OpenMode mode;
if (seq == 1) {
// 第一个包,覆盖写入
mode = QIODevice::WriteOnly;
}
else {
// 后续包,追加写入
mode = QIODevice::WriteOnly | QIODevice::Append;
}

if (!file.open(mode)) {
qDebug() << "Failed to open file for writing:" << clientPath;
qDebug() << "Error:" << file.errorString();
return;
}


qint64 bytesWritten = file.write(decodedData);
if (bytesWritten != decodedData.size()) {
qDebug() << "Failed to write all data. Written:" << bytesWritten
<< "Expected:" << decodedData.size();
}

file.close();

qDebug() << "Successfully wrote" << bytesWritten << "bytes to file";
qDebug() << "Progress:" << current_size << "/" << total_size
<< "(" << (current_size * 100 / total_size) << "%)";

if (is_last) {
qDebug() << "File download completed:" << clientPath;
UserMgr::GetInstance()->RmvTransFileByName(name);
//通知界面下载完成
emit sig_download_finish(file_info, file_path);
}
else {
//继续请求
file_info->_seq = seq + 1;
file_info->_last_confirmed_seq = seq;
if (file_info->_transfer_state == TransferState::Paused) {
//暂停状态,则直接返回
return;
}
//组织请求,准备下载
QJsonObject jsonObj_send;
jsonObj_send["name"] = name;
jsonObj_send["seq"] = file_info->_seq;
jsonObj_send["trans_size"] = QString::number(file_info->_current_size);
jsonObj_send["total_size"] = QString::number(file_info->_total_size);
jsonObj_send["token"] = UserMgr::GetInstance()->GetToken();
jsonObj_send["sender_id"] = file_info->_sender;
jsonObj_send["receiver_id"] = file_info->_receiver;
jsonObj_send["message_id"] = file_info->_msg_id;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj_send["uid"] = uid;
QJsonDocument doc(jsonObj_send);
auto send_data = doc.toJson();
FileTcpMgr::GetInstance()->SendData(ID_IMG_CHAT_DOWN_REQ, send_data);
//todo...通知界面更新进度
emit sig_update_download_progress(file_info);
}
});

通过QFile类实现文件写入。

客户端进度显示

为了让客户端更为直观的显示下载进度,可以在收到服务器消息后,将文件下载进度同步给界面,同时显示支持暂停和继续

进度通知在上述逻辑中

1
2
3
4
5

_handlers.insert(ID_IMG_CHAT_DOWN_RSP, [this](ReqId id, int len, QByteArray data) {
//...
emit sig_update_download_progress(file_info);
});

同样是在ChatDialog构造函数中添加消息链接

1
2
3
//接收tcp返回的下载进度信息
connect(FileTcpMgr::GetInstance().get(), &FileTcpMgr::sig_update_download_progress,
this, &ChatDialog::slot_update_download_progress);

进度处理槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ChatDialog::slot_update_download_progress(std::shared_ptr<MsgInfo> msg_info) {
auto chat_data = UserMgr::GetInstance()->GetChatThreadByThreadId(msg_info->_thread_id);
if (chat_data == nullptr) {
return;
}

//更新消息,其实不用更新,都是共享msg_info的一块内存,这里为了安全还是再次更新下

chat_data->UpdateProgress(msg_info);

if (_cur_chat_thread_id != msg_info->_thread_id) {
return;
}


//更新聊天界面信息
ui->chat_page->UpdateFileProgress(msg_info);
}

在ChatPage中详细处理更新

1
2
3
4
5
6
7
8
9
10
11
12
13
void ChatPage::UpdateFileProgress(std::shared_ptr<MsgInfo> msg_info) {
auto iter = _base_item_map.find(msg_info->_msg_id);
if (iter == _base_item_map.end()) {
return;
}

if (msg_info->_msg_type == MsgType::IMG_MSG) {
auto bubble = iter.value()->getBubble();
PictureBubble* pic_bubble = dynamic_cast<PictureBubble*>(bubble);
pic_bubble->setProgress(msg_info->_rsp_size, msg_info->_total_size);
}

}

PicBubble中完成状态显示

1
2
3
4
5
6
7
8
9
10
11
void PictureBubble::setProgress(int value, int total_value)
{
if (m_total_size != total_value) {
m_total_size = total_value;
}
float percent = (value / (m_total_size*1.0))*100;
m_progressBar->setValue(percent);
if (percent >= 100) {
setState(TransferState::Completed);
}
}

断点续传

因为在客户端收到服务器通知的图片聊天信息的时候,已经通过sig_img_chat_msg将消息发送给ChatDialog添加到页面上了。同时传输了图片的状态为下载中。

点击继续和暂停的逻辑可以复用PicBubble的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void PictureBubble::onPictureClicked()
{
switch (m_state) {
case TransferState::Downloading:
case TransferState::Uploading:
// 暂停
setState(TransferState::Paused);
emit pauseRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
break;

case TransferState::Paused:
// 继续
resumeState(); //
emit resumeRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
break;

case TransferState::Failed:
// 重试
emit resumeRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
break;

default:
// 其他状态可以实现查看大图等功能
break;
}
}

接下来我们响应暂停和继续,这部分逻辑也已经复用之前的逻辑即可

暂停逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ChatPage::on_clicked_paused(QString unique_name, TransferType transfer_type)
{
UserMgr::GetInstance()->PauseTransFileByName(unique_name);
}

void UserMgr::PauseTransFileByName(QString name) {
std::lock_guard<std::mutex> mtx(_trans_mtx);
auto iter = _name_to_msg_info.find(name);
if (iter == _name_to_msg_info.end()) {
return;
}

iter.value()->_transfer_state = TransferState::Paused;
}

恢复逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

void ChatPage::on_clicked_resume(QString unique_name, TransferType transfer_type)
{
UserMgr::GetInstance()->ResumeTransFileByName(unique_name);
//继续发送或者下载
if (transfer_type == TransferType::Upload) {
FileTcpMgr::GetInstance()->ContinueUploadFile(unique_name);
return;
}

if (transfer_type == TransferType::Download) {
FileTcpMgr::GetInstance()->ContinueDownloadFile(unique_name);
return;
}
}

void UserMgr::ResumeTransFileByName(QString name)
{
std::lock_guard<std::mutex> mtx(_trans_mtx);
auto iter = _name_to_msg_info.find(name);
if (iter == _name_to_msg_info.end()) {
return;
}

if (iter.value()->_transfer_type == TransferType::Download) {
iter.value()->_transfer_state = TransferState::Downloading;
return;
}

if (iter.value()->_transfer_type == TransferType::Upload) {
iter.value()->_transfer_state = TransferState::Uploading;
return;
}
}

发送继续下载信号通知FileTcpMgr继续下载

1
2
3
void FileTcpMgr::ContinueDownloadFile(QString unique_name) {
emit sig_continue_download_file(unique_name);
}

FileTcpMgr响应下载逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void FileTcpMgr::slot_continue_download_file(QString unique_name) {
auto file_info = UserMgr::GetInstance()->GetTransFileByName(unique_name);
if (file_info == nullptr) {
return;
}

if (file_info->_current_size >= file_info->_total_size) {
qDebug() << "file has received finished";
return;
}

//组织请求,准备下载
QJsonObject jsonObj_send;
jsonObj_send["name"] = unique_name;
jsonObj_send["seq"] = file_info->_seq;
jsonObj_send["trans_size"] = QString::number(file_info->_current_size);
jsonObj_send["total_size"] = QString::number(file_info->_total_size);
jsonObj_send["token"] = UserMgr::GetInstance()->GetToken();
jsonObj_send["sender_id"] = file_info->_sender;
jsonObj_send["receiver_id"] = file_info->_receiver;
jsonObj_send["message_id"] = file_info->_msg_id;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj_send["uid"] = uid;
QJsonDocument doc(jsonObj_send);
auto send_data = doc.toJson();
FileTcpMgr::GetInstance()->SendData(ID_IMG_CHAT_DOWN_REQ, send_data);
}

通过上述逻辑可以实现客户端的断点下载和暂停。

效果演示

image-20260208162803229

源码链接

https://gitee.com/secondtonone1/llfcchat

注意第二季分支为Season_2

Untitled

Posted on 2026-02-06

title: 几种简单并行算法的实现(for_each,find以及partial_sum)
date: 2024-01-29 19:56:02
tags: C++
categories: C++


简介

前文介绍了几种数据划分的方式,包括按照线程数量划分,按照递归方式划分,以及按照任务类型划分等。

本文结合之前的划分方式,基于stl的find, for_each以及partial_sum等算法实现并行版本。

并行版本for_each

实现并行的for_each,最简单的方式就是将数据划分,每个线程分别处理一段连续的数据即可。

在介绍并行版本之前,我们先实现一个管理线程 的类join_threads,用来管控线程防止线程过早退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class join_threads
{
std::vector<std::thread>& threads;
public:
explicit join_threads(std::vector<std::thread>& threads_) :
threads(threads_)
{}
~join_threads()
{
for (unsigned long i = 0; i < threads.size(); ++i)
{
if (threads[i].joinable())
threads[i].join();
}
}
};

接下来我们实现第一种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
template<typename Iterator, typename Func>
void parallel_for_each(Iterator first, Iterator last, Func f)
{
unsigned long const length = std::distance(first, last);
if (!length)
return;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::vector<std::future<void>> futures(num_threads - 1); //⇽-- - 1
std::vector<std::thread> threads(num_threads - 1);
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
std::packaged_task<void(void)> task( // ⇽-- - 2
[=]()
{
std::for_each(block_start, block_end, f);
});
futures[i] = task.get_future();
threads[i] = std::thread(std::move(task)); //⇽-- - 3
block_start = block_end;
}
std::for_each(block_start, last, f);
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
futures[i].get(); // ⇽-- - 4
}
}

1 我们规定如果处理的数量不超过25个则用单线程。否则根据处理的数量划分任务,计算开辟的线程数,如果要开辟的线程数大于内核线程的数量,则以内核线程数为准。

2 根据实际开辟的线程数num_threads计算每个线程处理的块大小。并且初始化两个vector,分别用来存储处理结果的future和处理任务的线程。

3 我们在(2处)代码生成了一个任务task,然后获取future赋值给vector对应下标为i的future元素,并且把任务绑定给对应下标为i的thread。

4 numthreads-1个线程并行处理for_each,剩下的主线程处理余下的for_each,最后通过futures.get汇总

第二种划分方式是我们采取递归的方式,我们知道采用递归的方式无法提前开辟准确数量的线程,我们采用async帮我们完成这个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename Iterator, typename Func>
void async_for_each(Iterator first, Iterator last, Func f)
{
unsigned long const length = std::distance(first, last);
if (!length)
return;
unsigned long const min_per_thread = 25;
if (length < (2 * min_per_thread))
{
std::for_each(first, last, f); //⇽-- - 1
}
else
{
Iterator const mid_point = first + length / 2;
//⇽-- - 2
std::future<void> first_half = std::async(&async_for_each<Iterator, Func>,
first, mid_point, f);
//⇽-- - 3
async_for_each(mid_point, last, f);
// ⇽-- - 4
first_half.get();
}
}

async可以帮助我们判断是否需要开启线程还是自动串行执行。每次我们将要处理的数据一分为2,前半部分交给一个async开辟线程处理,后半部分在本线程处理。而所谓的本线程不一定是主线程,因为我们通过async递归执行parallel_for_each,也就相当于在一个线程里独立执行了。

find的并行实现

find 的并行查找方式还是分两种,一种是将要查找的区间划分为几个段,每段交给一个线程查找。

另一种是采用递归的方式每次折半,前半部分交给一个线程查找,后半部分留在本线程查找。

我们先说第一种

find比较特殊,我们要防止线程忙等待,也要防止线程在其他线程已经查找到值后做无谓的浪费。可以用一个共享的全局atomic变量表示是否找到目标。

因为主线程要获取某个线程查找到的迭代器位置,所以我们用promise 设置 value为迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
template<typename Iterator, typename MatchType>
Iterator parallel_find(Iterator first, Iterator last, MatchType match)
{
struct find_element //⇽-- - 1
{
void operator()(Iterator begin,Iterator end,
MatchType match,
std::promise<Iterator>*result,
std::atomic<bool>*done_flag)
{
try
{
for (; (begin != end) && !done_flag->load(); ++begin) //⇽-- - 2
{
if (*begin == match)
{
result->set_value(begin); //⇽-- - 3
done_flag->store(true); //⇽-- - 4
return;
}
}
}
catch (...) //⇽-- - 5
{
try
{
result->set_exception(std::current_exception()); //⇽-- - 6
done_flag->store(true);
}
catch (...) //⇽-- - 7
{}
}
}
};
unsigned long const length = std::distance(first, last);
if (!length)
return last;
unsigned long const min_per_thread = 25;
unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads = std::thread::hardware_concurrency();
unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::promise<Iterator> result; //⇽-- - 8
std::atomic<bool> done_flag(false); //⇽-- - 9
std::vector<std::thread> threads(num_threads - 1); //⇽-- - 10
{
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
// ⇽-- - 11
threads[i] = std::thread(find_element(), block_start, block_end, match, &result, &done_flag);
block_start = block_end;
}
// ⇽-- - 12
find_element()(block_start, last, match, &result, &done_flag);
}

// ⇽-- - 13
if (!done_flag.load())
{
return last;
}
//⇽-- - 14
return result.get_future().get();
}

1 find_element重载了()运算符,接受四个参数,分别是迭代器的开始,迭代起的结束,要查找的数值,以及用来通知外部的promise,还有线程之间用来检测是否有某个线程完成查找的原子变量。

2 find_element重载()的逻辑就是查找这个区间内满足某个值的位置,并将这个位置的迭代起设置到promise中,然后将完成的原子变量标记为true。

说第二种方式,利用递归折半查找,我们可以用async帮助我们完成并行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
template<typename Iterator, typename MatchType>
Iterator parallel_find_impl(Iterator first, Iterator last, MatchType match,
std::atomic<bool>& done) // ⇽-- - 1
{
try
{
unsigned long const length = std::distance(first,last);
unsigned long const min_per_thread = 25; // ⇽-- - 2
if (length < (2 * min_per_thread)) //⇽-- - 3
{
for (; (first != last) && !done.load(); ++first) //⇽-- - 4
{
if (*first == match)
{
done = true; //⇽-- - 5
return first;
}
}
return last; //⇽-- - 6
}
else
{
//⇽-- - 7
Iterator const mid_point = first + (length / 2);
//⇽-- - 8
std::future<Iterator> async_result = std::async(&parallel_find_impl<Iterator,MatchType>,
mid_point,last,match,std::ref(done));
//⇽-- - 9
Iterator const direct_result = parallel_find_impl(first,mid_point,match,done);
//⇽-- - 10
return (direct_result == mid_point) ?async_result.get() : direct_result;
}
}
catch (...)
{
// ⇽-- - 11
done = true;
throw;
}
}
template<typename Iterator, typename MatchType>
Iterator parallel_find_async(Iterator first, Iterator last, MatchType match)
{
std::atomic<bool> done(false);
//⇽-- - 12
return parallel_find_impl(first, last, match, done);
}

1 并行查找的方式种我们先根据长度是否小于50决定是否开启并行任务,如果小于50则采取单线程方式。

2 如果采用并行的方式,我们将长度折半,前半部分交给async,后半部分交给本线程。

3 最后我们在主线程中汇合,获取结果。

partial_sum并行版本

C++ 提供了累计计算求和的功能,比如一个vector中存储的数据为{1,2,3},那么经过计算,第一个元素仍然为1,第二个元素为1+2, 第三个元素为1+2+3,结果为{1,3,6}.

关于并行版本我们可以这么思考,假设元数组为{1,2,3,4,5,6,7},那我们可以划分为三个部分,第一部分为{1,2,3}交给第一个线程处理, 第二部分{4,5,6}交给第二个线程处理,7交给本线程处理。

但是我们要考虑的一个问题是线程2要用到线程1最后计算的结果,线程1计算后{1,3,6},线程2需要用到6做累加,我们可以先让线程1计算出第3个元素值6,再将这个6传递给线程2,剩下的就可以并行计算了。同样的道理本线程要处理最后一个元素的累加结果,他需要等到线程2处理完第6个元素的值。

所以基本思路是每个线程优先处理分区的最后一个元素,通过promise设置给其他线程,在这个阶段线程之间是串行的,等到所有线程都开始计算其他位置后就是并行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
template<typename Iterator>
void parallel_partial_sum(Iterator first, Iterator last)
{
typedef typename Iterator::value_type value_type;

struct process_chunk //⇽-- - 1
{
void operator()(Iterator begin, Iterator last,
std::future<value_type>* previous_end_value,
std::promise<value_type>* end_value)
{
try
{
Iterator end = last;
++end;
std::partial_sum(begin, end, begin); //⇽-- - 2
if (previous_end_value) //⇽-- - 3
{
value_type addend = previous_end_value->get(); // ⇽-- - 4
*last += addend; // ⇽-- - 5
if (end_value)
{
end_value->set_value(*last); //⇽-- - 6
}
// ⇽-- - 7
std::for_each(begin, last, [addend](value_type& item)
{
item += addend;
});
}
else if (end_value)
{
// ⇽-- - 8
end_value->set_value(*last);
}
}
catch (...) // ⇽-- - 9
{
if (end_value)
{
end_value->set_exception(std::current_exception()); // ⇽-- - 10
}
else
{
throw; // ⇽-- - 11
}

}
}
};
unsigned long const length = std::distance(first, last);

if (!length) {
return;
}
unsigned long const min_per_thread = 25; //⇽-- - 12
unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads = std::thread::hardware_concurrency();
unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
typedef typename Iterator::value_type value_type;

std::vector<std::thread> threads(num_threads - 1); // ⇽-- - 13

std::vector<std::promise<value_type> > end_values(num_threads - 1); // ⇽-- - 14

std::vector<std::future<value_type> > previous_end_values; // ⇽-- - 15
previous_end_values.reserve(num_threads - 1); // ⇽-- - 16
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
Iterator block_last = block_start;
std::advance(block_last, block_size - 1); // ⇽-- - 17
// ⇽-- - 18
threads[i] = std::thread(process_chunk(), block_start, block_last,
(i != 0) ? &previous_end_values[i - 1] : 0,
&end_values[i]);
block_start = block_last;
++block_start; // ⇽-- - 19
previous_end_values.push_back(end_values[i].get_future()); // ⇽-- - 20
}
Iterator final_element = block_start;
std::advance(final_element, std::distance(block_start, last) - 1); // ⇽-- - 21
// ⇽-- - 22
process_chunk()(block_start, final_element, (num_threads > 1) ? &previous_end_values.back() : 0,
0);

}

1 定义了process_chunk类,重载了()运算符,在重载的逻辑里我们先计算区间内的partial_sum累计求和(2处)

2 因为我们处理的区间不一定是首个区间,也就是他还需要加上前面区间处理得出的最后一个元素的值,所以我们通过previouse_end_value判断本区间不是首个区间,并且加上前面处理的结果。优先将最后一个值计算出来设置给promise。然后在利用for_each遍历计算其他位置的值。

总结

本文介绍了如何并行设计stl的相关算法,读者有好的思路可以互相交流一下。

测试代码和项目代码链接:

https://gitee.com/secondtonone1/boostasio-learn/tree/master/concurrent/day21-ParallenAlgorithm

视频链接

https://space.bilibili.com/271469206/channel/collectiondetail?sid=1623290

聊天图片资源续传和进度显示

Posted on 2026-01-02 | In C++聊天项目

增加上传可视化进度

之前我们传输图片的时候,只能通过服务器查看上传进度。客户端无法感知上传进度,所以考虑在图片传输基础上,显示上传进度。

封装可点击标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#pragma once
#include <QLabel>
#include <QWidget>
#include <QIcon>

class ClickableLabel :
public QLabel
{
Q_OBJECT
public:
explicit ClickableLabel(QWidget* parent = nullptr);
void setIconOverlay(const QIcon& icon); //设置遮罩图标
void showIconOverlay(bool show); //显示/隐藏遮罩图标
protected:
void mousePressEvent(QMouseEvent* event) override;
void enterEvent(QEvent* event) override;
void leaveEvent(QEvent* event) override;
void paintEvent(QPaintEvent* event) override;

signals:
void clicked();

private:
QIcon m_overlayIcon;
bool m_showOverlay;
bool m_hovered;
};

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include "ClickableLabel.h"
#include <QMouseEvent>
#include <QPainter>
// ClickableLabel.cpp
ClickableLabel::ClickableLabel(QWidget* parent)
: QLabel(parent)
, m_showOverlay(false)
, m_hovered(false)
{
setCursor(Qt::PointingHandCursor);
setMouseTracking(true);
}

void ClickableLabel::mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton) {
emit clicked();
}
QLabel::mousePressEvent(event);
}

void ClickableLabel::enterEvent(QEvent* event)
{
m_hovered = true;
update();
QLabel::enterEvent(event);
}

void ClickableLabel::leaveEvent(QEvent* event)
{
m_hovered = false;
update();
QLabel::leaveEvent(event);
}

void ClickableLabel::paintEvent(QPaintEvent* event)
{
QLabel::paintEvent(event);

if (m_showOverlay && !m_overlayIcon.isNull()) {
QPainter painter(this);

// 绘制半透明遮罩
if (m_hovered) {
painter.fillRect(rect(), QColor(0, 0, 0, 100));
}
else {
painter.fillRect(rect(), QColor(0, 0, 0, 60));
}

// 绘制图标
int iconSize = qMin(width(), height()) / 3; // 图标大小为图片的1/3
QRect iconRect(
(width() - iconSize) / 2,
(height() - iconSize) / 2,
iconSize,
iconSize
);

m_overlayIcon.paint(&painter, iconRect);
}
}

void ClickableLabel::setIconOverlay(const QIcon& icon)
{
m_overlayIcon = icon;
update();
}

void ClickableLabel::showIconOverlay(bool show)
{
m_showOverlay = show;
update();
}

图片设置可点击和进度条

image-20260102111204491

在图片中添加可点击的标签,并且添加进度条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#ifndef PICTUREBUBBLE_H
#define PICTUREBUBBLE_H

#include "BubbleFrame.h"
#include <QHBoxLayout>
#include <QPixmap>
#include "ClickableLabel.h"
#include <QProgressBar>
#include "global.h"
class PictureBubble : public BubbleFrame
{
Q_OBJECT
public:


PictureBubble(const QPixmap& picture, ChatRole role,int total, QWidget* parent = nullptr);

void setProgress(int value);
void showProgress(bool show);
void setState(TransferState state);
void resumeState();
void setMsgInfo(std::shared_ptr<MsgInfo> msg);
TransferState state() const { return m_state; }

signals:
void pauseRequested(QString unique_name, TransferType transfer_type); // 请求暂停
void resumeRequested(QString unique_name, TransferType transfer_type); // 请求继续
void cancelRequested(QString unique_name, TransferType transfer_type); // 请求取消

private slots:
void onPictureClicked();

private:
void updateIconOverlay();
void adjustSize();

private:
ClickableLabel* m_picLabel;
QProgressBar* m_progressBar;
TransferState m_state;

QIcon m_pauseIcon;
QIcon m_playIcon;
QIcon m_downloadIcon;
QSize m_pixmapSize;
QVBoxLayout* m_vLayout;
int m_total_size;
std::shared_ptr<MsgInfo> _msg_info;
};

#endif // PICTUREBUBBLE_H

构造函数里添加进度条和样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PictureBubble::PictureBubble(const QPixmap &picture, ChatRole role, int total, QWidget *parent)
:BubbleFrame(role, parent), m_state(TransferState::None), m_total_size(total)
{
// 加载图标(使用Qt内置图标或自定义图标)
m_pauseIcon = style()->standardIcon(QStyle::SP_MediaPause);
m_playIcon = style()->standardIcon(QStyle::SP_MediaPlay);
m_downloadIcon = style()->standardIcon(QStyle::SP_ArrowDown);

// 创建容器
QWidget* container = new QWidget();
m_vLayout = new QVBoxLayout(container);
m_vLayout->setContentsMargins(0, 0, 0, 0);
m_vLayout->setSpacing(5);


// 创建可点击的图片标签
m_picLabel = new ClickableLabel();
m_picLabel->setScaledContents(true);
QPixmap pix = picture.scaled(QSize(PIC_MAX_WIDTH, PIC_MAX_HEIGHT),
Qt::KeepAspectRatio, Qt::SmoothTransformation);
m_pixmapSize = pix.size();
m_picLabel->setPixmap(pix);
m_picLabel->setFixedSize(pix.size());

connect(m_picLabel, &ClickableLabel::clicked,
this, &PictureBubble::onPictureClicked);

// 创建进度条
m_progressBar = new QProgressBar();
m_progressBar->setFixedWidth(pix.width());
m_progressBar->setFixedHeight(10);
m_progressBar->setRange(0, 100);
m_progressBar->setValue(0);
m_progressBar->setTextVisible(true);
setState(TransferState::None);

// 样式美化
m_progressBar->setStyleSheet(
"QProgressBar {"
" border: 1px solid #ccc;"
" border-radius: 3px;"
" text-align: center;"
" background-color: #f0f0f0;"
" font-size: 10px;"
"}"
"QProgressBar::chunk {"
" background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
" stop:0 #4CAF50, stop:1 #45a049);"
" border-radius: 2px;"
"}"
);

m_vLayout->addWidget(m_picLabel);
m_vLayout->addWidget(m_progressBar);
this->setWidget(container);
adjustSize();
}

设置传输状态为None,进度条初始值为0

调整界面设置,将图片拉伸包括进度条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void PictureBubble::adjustSize()
{
int left_margin = this->layout()->contentsMargins().left();
int right_margin = this->layout()->contentsMargins().right();
int v_margin = this->layout()->contentsMargins().bottom();

int width = m_pixmapSize.width() + left_margin + right_margin;
int height = m_pixmapSize.height() + v_margin * 2;

if (m_progressBar->isHidden() == false) {
height += m_progressBar->height() + m_vLayout->spacing();
}

setFixedSize(width, height);
}

封装设置进度函数

1
2
3
4
5
6
7
8
void PictureBubble::setProgress(int value)
{
float percent = (value / (m_total_size*1.0))*100;
m_progressBar->setValue(percent);
if (percent >= 100) {
setState(TransferState::Completed);
}
}

展示进度条

1
2
3
4
5
void PictureBubble::showProgress(bool show)
{
m_progressBar->show();
adjustSize();
}

当传输停止后,点击后会恢复,恢复传输或者下载状态,逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void PictureBubble::resumeState() {
if (_msg_info->_transfer_type == TransferType::Download) {
_msg_info->_transfer_state = TransferState::Downloading;
m_state = TransferState::Downloading;
updateIconOverlay();
return;
}

if (_msg_info->_transfer_type == TransferType::Upload) {
_msg_info->_transfer_state = TransferState::Uploading;
m_state = TransferState::Uploading;
updateIconOverlay();
return;
}
}

设置状态逻辑如下, 状态分为None,下载,上传,暂停,传输完成,传输失败等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void PictureBubble::setState(TransferState state)
{
m_state = state;
if (_msg_info) {
_msg_info->_transfer_state = state;
}

updateIconOverlay();

// 根据状态显示/隐藏进度条
switch (state) {
case TransferState::Downloading:
case TransferState::Uploading:
case TransferState::Paused:
showProgress(true);
break;
case TransferState::Completed:
// 完成后延迟隐藏进度条
QTimer::singleShot(1000, this, [this]() {
showProgress(false);
});
break;
case TransferState::None:
case TransferState::Failed:
showProgress(false);
break;
}
}

为了更好的控制传输逻辑,将std::shared_ptr<MsgInfo>作为成员变量设置给PictureBubble

这样在点击图片后,可以发送图片关联的消息id和unique_name

点击图片触发槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void PictureBubble::onPictureClicked()
{
switch (m_state) {
case TransferState::Downloading:
case TransferState::Uploading:
// 暂停
setState(TransferState::Paused);
emit pauseRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
break;

case TransferState::Paused:
// 继续
resumeState(); //
emit resumeRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
break;

case TransferState::Failed:
// 重试
emit resumeRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
break;

default:
// 其他状态可以实现查看大图等功能
break;
}
}

根据状态发送不同的信号,如果是下载或者暂停状态,则发送暂停信号。

如果是暂停状态,则发送继续下载或者继续上传状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void PictureBubble::updateIconOverlay()
{
switch (m_state) {
case TransferState::Downloading:
case TransferState::Uploading:
m_picLabel->setIconOverlay(m_pauseIcon);
m_picLabel->showIconOverlay(true);
break;

case TransferState::Paused:
m_picLabel->setIconOverlay(m_playIcon);
m_picLabel->showIconOverlay(true);
break;

case TransferState::Failed:
m_picLabel->setIconOverlay(m_downloadIcon); // 或重试图标
m_picLabel->showIconOverlay(true);
break;

default:
m_picLabel->showIconOverlay(false);
break;
}
}

链接状态信号

在ChatPage的构造函数中添加信号和槽函数链接

1
2
3
4
5
6
//链接暂停信号
connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::pauseRequested,
this, &ChatPage::on_clicked_paused);
//链接恢复信号
connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::resumeRequested,
this, &ChatPage::on_clicked_resume);

槽函数中设置文件为暂停状态

1
2
3
4
void ChatPage::on_clicked_paused(QString unique_name, TransferType transfer_type)
{
UserMgr::GetInstance()->PauseTransFileByName(unique_name);
}

槽函数中设置为续传或者下载状态,并且调用FileMgr续传之前未传递完成的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
void ChatPage::on_clicked_resume(QString unique_name, TransferType transfer_type)
{
UserMgr::GetInstance()->ResumeTransFileByName(unique_name);
//继续发送或者下载
if (transfer_type == TransferType::Upload) {
FileTcpMgr::GetInstance()->ContinueUploadFile(unique_name);
return;
}

if (transfer_type == TransferType::Download) {
return;
}
}

续传逻辑

FileTcpMgr发送信号sig_continue_upload_file, 这么做的好处是,发送信号的线程和接收信号的线程不一致,也可以跨线程调用。

1
2
3
void FileTcpMgr::ContinueUploadFile(QString unique_name) {
emit sig_continue_upload_file(unique_name);
}

在FileTcpMgr中链接了这个信号

1
2
//链接续传信号
QObject::connect(this, &FileTcpMgr::sig_continue_upload_file, this, &FileTcpMgr::slot_continue_upload_file);

槽函数slot_continue_upload_file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void FileTcpMgr::slot_continue_upload_file(QString unique_name) {
auto msg_info = UserMgr::GetInstance()->GetTransFileByName(unique_name);
if (msg_info == nullptr) {
return;
}
//将待发送序列号更新为已经确认接收的序列号,然后基于此序列号再递增。
msg_info->_seq = msg_info->_last_confirmed_seq;

if ((msg_info->_seq) * MAX_FILE_LEN >= msg_info->_total_size) {
qDebug() << "file has sent finished";
return;
}

if (MAX_CWND_SIZE - _cwnd_size == 0) {
return;
}

//打开
QFile file(msg_info->_text_or_url);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Could not open file: " << file.errorString();
return;
}

//文件偏移到已经发送的位置,继续读取发送
file.seek(msg_info->_seq * MAX_FILE_LEN);

bool b_last = false;
//再次组织数据发送
for (; MAX_CWND_SIZE - _cwnd_size > 0; ) {

QByteArray buffer;
msg_info->_seq++;
//放入发送未回包集合
msg_info->_flighting_seqs.insert(msg_info->_seq);
//每次读取MAX_FILE_LEN字节发送
buffer = file.read(MAX_FILE_LEN);
QJsonObject sendObj;
//将文件内容转换为base64编码
QString base64Data = buffer.toBase64();
sendObj["md5"] = msg_info->_md5;
sendObj["name"] = msg_info->_unique_name;
sendObj["seq"] = msg_info->_seq;
msg_info->_current_size = buffer.size() + (msg_info->_seq - 1) * MAX_FILE_LEN;
sendObj["trans_size"] = msg_info->_current_size;
sendObj["total_size"] = msg_info->_total_size;

b_last = false;
if (buffer.size() + (msg_info->_seq - 1) * MAX_FILE_LEN >= msg_info->_total_size) {
sendObj["last"] = 1;
b_last = true;
}
else {
sendObj["last"] = 0;
}

sendObj["data"] = base64Data;
sendObj["last_seq"] = msg_info->_max_seq;
sendObj["uid"] = UserMgr::GetInstance()->GetUid();
QJsonDocument doc(sendObj);
auto send_data = doc.toJson();
//直接发送,其实是放入tcpmgr发送队列
SendData(ID_IMG_CHAT_CONTINUE_UPLOAD_REQ, send_data);
_cwnd_size++;
//如果
if (b_last) {
break;
}
}

file.close();
}

ID_IMG_CHAT_CONTINUE_UPLOAD_REQ为断点续传请求

1
2
ID_IMG_CHAT_CONTINUE_UPLOAD_REQ = 1043,  //续传聊天图片资源请求
ID_IMG_CHAT_CONTINUE_UPLOAD_RSP = 1044, //续传聊天图片资源回复

响应续传回复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
_handlers.insert(ID_IMG_CHAT_CONTINUE_UPLOAD_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
_cwnd_size--;
// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject recvObj = jsonDoc.object();
qDebug() << "data jsonobj is " << recvObj;

if (!recvObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "icon upload_failed, err is Json Parse Err" << err;
//todo ... 提示上传失败
//emit upload_failed();
return;
}

int err = recvObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "Login Failed, err is " << err;
//emit upload_failed();
return;
}

auto name = recvObj["name"].toString();
auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
if (!file_info) {
return;
}

auto md5 = file_info->_md5;
auto seq = recvObj["seq"].toInt();
//根据seq从未接收集合移动到已接收集合中
file_info->_flighting_seqs.erase(seq);
//将seq放入已收到集合中
file_info->_rsp_seqs.insert(seq);
//计算当前最后确认的序列号
while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
++file_info->_last_confirmed_seq;
}



qDebug() << "recv : " << name << "file seq is " << seq;
//判断最大序列和最后确认序列号相等,说明收全了
if (file_info->_last_confirmed_seq == file_info->_max_seq) {
//更新已经传输的文件大小
file_info->_rsp_size = file_info->_total_size;
//通知界面显示
emit sig_update_upload_progress(file_info);
UserMgr::GetInstance()->RmvTransFileByName(name);
//todo 此处添加发送其他待发送的文件
auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
if (free_file == nullptr) {
return;
}
BatchSend(free_file);
return;
}

//更新已经传输的文件大小
file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;

//发送信号,更新图片上传进度
emit sig_update_upload_progress(file_info);
//如果传输状态不为上传,则直接返回。
if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
return;
}
BatchSend(file_info); });

sig_update_upload_progress信号为更新进度,因为在接收到服务器回包前,文件状态可能被设置为暂停,所以要判断一下文件状态是否为暂停,如果为暂停,则不发送后续内容。否则直接发送后续内容。

sig_update_upload_progress信号,也会在正常上传服务器,服务器回复给客户端时,客户端发送

比如上传图片信息回复逻辑里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
_handlers.insert(ID_IMG_CHAT_UPLOAD_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
_cwnd_size--;
// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject recvObj = jsonDoc.object();
qDebug() << "data jsonobj is " << recvObj;

if (!recvObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "icon upload_failed, err is Json Parse Err" << err;
//todo ... 提示上传失败
//emit upload_failed();
return;
}

int err = recvObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "Login Failed, err is " << err;
//emit upload_failed();
return;
}

auto name = recvObj["name"].toString();
auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
if (!file_info) {
return;
}

auto md5 = file_info->_md5;
auto seq = recvObj["seq"].toInt();
//根据seq从未接收集合移动到已接收集合中
file_info->_flighting_seqs.erase(seq);
//将seq放入已收到集合中
file_info->_rsp_seqs.insert(seq);
//计算当前最后确认的序列号
while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
++file_info->_last_confirmed_seq;
}



qDebug() << "recv : " << name << "file seq is " << seq;
//判断最大序列和最后确认序列号相等,说明收全了
if (file_info->_last_confirmed_seq == file_info->_max_seq) {
//更新已经传输的文件大小
file_info->_rsp_size = file_info->_total_size;
//通知界面显示
emit sig_update_upload_progress(file_info);
UserMgr::GetInstance()->RmvTransFileByName(name);
//todo 此处添加发送其他待发送的文件
auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
if (free_file == nullptr) {
return;
}
BatchSend(free_file);
return;
}

//更新已经传输的文件大小
file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;

//发送信号,更新图片上传进度
emit sig_update_upload_progress(file_info);
//如果传输状态不为上传,则直接返回。
if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
return;
}
BatchSend(file_info); });

_handlers.insert(ID_IMG_CHAT_CONTINUE_UPLOAD_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
_cwnd_size--;
// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject recvObj = jsonDoc.object();
qDebug() << "data jsonobj is " << recvObj;

if (!recvObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "icon upload_failed, err is Json Parse Err" << err;
//todo ... 提示上传失败
//emit upload_failed();
return;
}

int err = recvObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "Login Failed, err is " << err;
//emit upload_failed();
return;
}

auto name = recvObj["name"].toString();
auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
if (!file_info) {
return;
}

auto md5 = file_info->_md5;
auto seq = recvObj["seq"].toInt();
//根据seq从未接收集合移动到已接收集合中
file_info->_flighting_seqs.erase(seq);
//将seq放入已收到集合中
file_info->_rsp_seqs.insert(seq);
//计算当前最后确认的序列号
while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
++file_info->_last_confirmed_seq;
}



qDebug() << "recv : " << name << "file seq is " << seq;
//判断最大序列和最后确认序列号相等,说明收全了
if (file_info->_last_confirmed_seq == file_info->_max_seq) {
//更新已经传输的文件大小
file_info->_rsp_size = file_info->_total_size;
//通知界面显示
emit sig_update_upload_progress(file_info);
UserMgr::GetInstance()->RmvTransFileByName(name);
//todo 此处添加发送其他待发送的文件
auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
if (free_file == nullptr) {
return;
}
BatchSend(free_file);
return;
}

//更新已经传输的文件大小
file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;

//发送信号,更新图片上传进度
emit sig_update_upload_progress(file_info);
//如果传输状态不为上传,则直接返回。
if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
return;
}
BatchSend(file_info); });
}

同步信息回复也会发送进度上传信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
_handlers.insert(ID_FILE_INFO_SYNC_RSP, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject recvObj = jsonDoc.object();
qDebug() << "data jsonobj is " << recvObj;

if (!recvObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "icon upload_failed, err is Json Parse Err" << err;
//todo ... 提示上传失败,将来可能断点重传等
//emit upload_failed();
return;
}

int err = recvObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "Login Failed, err is " << err;
//emit upload_failed();
return;
}

//为了简单起见,先处理网络正常情况
auto seq = recvObj["seq"].toInt();
auto name = recvObj["name"].toString();

auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
if (!file_info) {
return;
}

//根据seq从未接收集合移动到已接收集合中
file_info->_flighting_seqs.erase(seq);
//将seq放入已收到集合中
file_info->_rsp_seqs.insert(seq);

//计算当前最后确认的序列号
while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
++file_info->_last_confirmed_seq;
}

qDebug() << "recv : " << name << "file seq is " << seq;
//判断最大序列和最后确认序列号相等,说明收全了
if (file_info->_last_confirmed_seq == file_info->_max_seq) {
//更新已经传输的文件大小
file_info->_rsp_size = file_info->_total_size;
//通知界面显示
emit sig_update_upload_progress(file_info);
UserMgr::GetInstance()->RmvTransFileByName(name);
//todo 此处添加发送其他待发送的文件
auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
if (free_file == nullptr) {
return;
}
BatchSend(free_file);
return;
}
//更新已经传输的文件大小
file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;
//通知界面显示
emit sig_update_upload_progress(file_info);
//如果传输状态不为上传,则直接返回。
if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
return;
}
BatchSend(file_info);
});

在ChatPage中链接进度上传信号和槽函数

1
2
3
//接收tcp返回的上传进度信息
connect(FileTcpMgr::GetInstance().get(), &FileTcpMgr::sig_update_upload_progress,
this, &ChatDialog::slot_update_upload_progress);

接收进度上传信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ChatDialog::slot_update_upload_progress(std::shared_ptr<MsgInfo> msg_info) {
auto chat_data = UserMgr::GetInstance()->GetChatThreadByThreadId(msg_info->_thread_id);
if (chat_data == nullptr) {
return;
}

//更新消息,其实不用更新,都是共享msg_info的一块内存,这里为了安全还是再次更新下

chat_data->UpdateProgress(msg_info);

if (_cur_chat_thread_id != msg_info->_thread_id) {
return;
}


//更新聊天界面信息
ui->chat_page->UpdateFileProgress(msg_info);
}

调用ChatPage的UpdateFileProgress函数

1
2
3
4
5
6
7
8
9
10
11
12
void ChatPage::UpdateFileProgress(std::shared_ptr<MsgInfo> msg_info) {
auto iter = _base_item_map.find(msg_info->_msg_id);
if (iter == _base_item_map.end()) {
return;
}

if (msg_info->_msg_type == MsgType::IMG_MSG) {
auto bubble = iter.value()->getBubble();
PictureBubble* pic_bubble = dynamic_cast<PictureBubble*>(bubble);
pic_bubble->setProgress(msg_info->_rsp_size);
}
}

根据消息类型为图片,则调用PictureBubble更新图片进度

服务器

ResoureServer服务器要响应断点续传请求

LogicWorker注册消息处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
_fun_callbacks[ID_IMG_CHAT_CONTINUE_UPLOAD_REQ] = [this](shared_ptr<CSession> session, const short& msg_id,
const string& msg_data) {
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
auto md5 = root["md5"].asString();
auto seq = root["seq"].asInt();
auto name = root["name"].asString();
auto total_size = root["total_size"].asInt();
auto trans_size = root["trans_size"].asInt();
auto last = root["last"].asInt();
auto file_data = root["data"].asString();
auto file_path = ConfigMgr::Inst().GetFileOutPath();
auto uid = root["uid"].asInt();
//转化为字符串
auto uid_str = std::to_string(uid);
auto file_path_str = (file_path / uid_str / name).string();
Json::Value rtvalue;

auto callback = [=](const Json::Value& result) {

// 在异步任务完成后调用
Json::Value rtvalue = result;
rtvalue["error"] = ErrorCodes::Success;
rtvalue["total_size"] = total_size;
rtvalue["seq"] = seq;
rtvalue["name"] = name;
rtvalue["trans_size"] = trans_size;
rtvalue["last"] = last;
rtvalue["md5"] = md5;
rtvalue["uid"] = uid;
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
};

// 使用 std::hash 对字符串进行哈希
std::hash<std::string> hash_fn;
size_t hash_value = hash_fn(name); // 生成哈希值
int index = hash_value % FILE_WORKER_COUNT;
std::cout << "Hash value: " << hash_value << std::endl;

//第一个包
if (seq == 1) {
//构造数据存储
auto file_info = std::make_shared<FileInfo>();
file_info->_file_path_str = file_path_str;
file_info->_name = name;
file_info->_seq = seq;
file_info->_total_size = total_size;
file_info->_trans_size = trans_size;
bool success = RedisMgr::GetInstance()->SetFileInfo(name, file_info);
if (!success) {
rtvalue["error"] = ErrorCodes::FileSaveRedisFailed;
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
return;
}
}
else {
auto file_info = RedisMgr::GetInstance()->GetFileInfo(name);
if (file_info == nullptr) {
rtvalue["error"] = ErrorCodes::FileNotExists;
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
return;
}
file_info->_seq = seq;
file_info->_trans_size = trans_size;
bool success = RedisMgr::GetInstance()->SetFileInfo(name, file_info);
if (!success) {
rtvalue["error"] = ErrorCodes::FileSaveRedisFailed;
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
return;
}
}
FileSystem::GetInstance()->PostMsgToQue(
std::make_shared<FileTask>(session, ID_IMG_CHAT_CONTINUE_UPLOAD_REQ, uid, file_path_str, name, seq, total_size,
trans_size, last, file_data, callback),
index
);
};

在FileWorker响应图片上传逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//处理续传图片请求
_handlers[ID_IMG_CHAT_CONTINUE_UPLOAD_REQ] = [this](std::shared_ptr<FileTask> task) {
// 解码
std::string decoded = base64_decode(task->_file_data);

auto file_path_str = task->_path;
auto last = task->_last;
//std::cout << "file_path_str is " << file_path_str << std::endl;

boost::filesystem::path file_path(file_path_str);
boost::filesystem::path dir_path = file_path.parent_path();
// 获取完整文件名(包含扩展名)
std::string filename = file_path.filename().string();
Json::Value result;
result["error"] = ErrorCodes::Success;

// Check if directory exists, if not, create it
if (!boost::filesystem::exists(dir_path)) {
if (!boost::filesystem::create_directories(dir_path)) {
std::cerr << "Failed to create directory: " << dir_path.string() << std::endl;
result["error"] = ErrorCodes::FileNotExists;
task->_callback(result);
return;
}
}


std::ofstream outfile;
//第一个包
if (task->_seq == 1) {
// 打开文件,如果存在则清空,不存在则创建
outfile.open(file_path_str, std::ios::binary | std::ios::trunc);
}
else {
// 保存为文件
outfile.open(file_path_str, std::ios::binary | std::ios::app);
}


if (!outfile) {
std::cerr << "无法打开文件进行写入。" << std::endl;
result["error"] = ErrorCodes::FileWritePermissionFailed;
task->_callback(result);
return;
}

outfile.write(decoded.data(), decoded.size());
if (!outfile) {
std::cerr << "写入文件失败。" << std::endl;
result["error"] = ErrorCodes::FileWritePermissionFailed;
task->_callback(result);
return;
}

outfile.close();
if (last) {
std::cout << "文件已成功保存为: " << task->_name << std::endl;
//todo...更新数据库聊天图像上传状态
//todo...通过grpc通知ChatServer
}

if (task->_callback) {
task->_callback(result);
}
};

效果展示

image-20260102101925154

image-20260102102645373

<123…41>

401 posts
18 categories
21 tags
RSS
GitHub ZhiHu
© 2026 恋恋风辰 本站总访问量次 | 本站访客数人
Powered by Hexo
|
Theme — NexT.Muse v5.1.3