wenmo8 发布的文章

前言

Redis在我们的日常开发工作中,使用频率非常高,已经变成了必不可少的技术之一。

Redis的使用场景也很多。

比如:保存用户登录态,做限流,做分布式锁,做缓存提升数据访问速度等等。

那么问题来了,Redis的性能要如何优化?

为了充分发挥Redis的性能,这篇文章跟大家一起聊聊Redis性能优化的18招,希望对你会有所帮助。

1. 选择合适的数据结构

Redis支持多种数据结构,如字符串、哈希、列表、集合和有序集合。根据实际需求选择合适的数据结构可以提高性能。

如果要存储用户信息,考虑使用哈希而不是多个字符串:

jedis.hset("user:1001", "name", "Alice");
jedis.hset("user:1001", "age", "30");

这样可以高效地存储和访问多个属性。

2. 避免使用过大的key和value

较长的key和value会占用更多内存,还可能影响性能。

保持key简短,并使用简洁的命名约定。

比如:
将“user:1001:profile”简化为“u:1001:p”。

还可以做压缩等其他优化。

如果对大key问题,比较感兴趣可以看看我的另一篇文章《
从2s优化到0.1s,我用了这5步
》,里面有非常详细的介绍。

3. 使用Redis Pipeline

对多个命令的批量操作,使用Pipeline可以显著降低网络延迟,提升性能。

比如,批量设置key可以这样做:

Pipeline p = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
    p.set("key:" + i, "value:" + i);
}
p.sync();

这样一次性可以发送多个命令,减少了网络往返时间,能够提升性能。

4. 控制连接数量

过多的连接会造成资源浪费,使用
连接池
可以有效管理连接数量。

比如,使用JedisPool:

JedisPool pool = new JedisPool("localhost");
try (Jedis jedis = pool.getResource()) {
    jedis.set("key", "value");
}

有了连接池,这样连接就会被复用,而不是每次都创建新连接,使用完之后,又放回连接池。

能有效的节省连接的创建和销毁时间。

5. 合理使用过期策略

设置合理的过期策略,能防止内存被不再使用的数据占满。

例如,缓存热点数据可以设置过期时间。

比如,对会话数据设置过期时间:

jedis.setex("session:12345", 3600, "data");

Redis内部会定期清理过期的缓存。

6. 使用Redis集群

数据量增大时,使用Redis集群可以将数据分散到多个节点,提升并发性能。

可以将数据哈希分片到多个Redis实例。

这样可以避免单个Redis实例,数据太多,占用内存过多的问题。

7. 充分利用内存优化

选择合适的内存管理策略,Redis支持LRU(Least Recently Used)策略,可以自动删除不常用的数据。

比如,配置Redis的maxmemory:

maxmemory 256mb
maxmemory-policy allkeys-lru

8. 使用Lua脚本

Lua脚本让多条命令在Redis中原子性执行,减少网络延迟。

比如,使用Lua防止多个命令的网络延迟:

EVAL "redis.call('set', KEYS[1], ARGV[1]) return redis.call('get', KEYS[1])" 1 "key" "value"

使用Lua脚本,可以保证Redis的多个命令是原子性操作。

9. 监控与调优

使用INFO命令监控Redis性能数据,如命令支持、内存使用等,及时调优。

比如,使用命令获取监控信息:

INFO memory
INFO clients

10. 避免热点key

热点key会造成单一节点的压力,通过随机化访问来避免。

比如,可以为热点key加随机后缀:

String key = "hotkey:" + (System.currentTimeMillis() % 10);
jedis.incr(key);

11. 使用压缩

存储大对象时,考虑使用压缩技术来节省内存。

比如,可以使用
GZIP
压缩JSON数据:

byte[] compressed = gzipCompress(jsonString);
jedis.set("data", compressed);

12. 使用Geo位置功能

Redis支持地理位置存储和查询,使用
GEOADD
可以高效管理地理数据。

比如,存储地点信息:

jedis.geoadd("locations", longitude, latitude, "LocationName");

13. 控制数据的持久化

合理设置
RDB

AOF
的持久化策略,避免频繁写盘造成性能下降。

示例:
设置持久化的时间间隔:

save 900 1
appendonly yes

14. 尽量减少事务使用

在高并发场景下,避免过度使用MULTI/EXEC,因为事务会锁住key。

可以直接使用单条命令替代事务。

15. 合理配置客户端

调整客户端的连接超时和重连策略,以适应高负载场景,确保连接稳定。

例如:

JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(128); // 最大连接数
poolConfig.setMaxIdle(64); // 最大空闲连接
poolConfig.setMinIdle(16); // 最小空闲连接
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);

JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379, 2000); // 连接超时2000ms

16. 使用Redis Sentinel

使用
Sentinel
进行监控,实现高可用性,确保系统在故障时能够快速切换。

配置Sentinel进行主从复制。

17. 优化网络配置

保证Redis服务器有良好的网络带宽,避免网络瓶颈。

使用服务器内部专线,减少延迟。

18. 定期清理不必要的数据

生命周期管理很关键,定期删除过期或不必要的数据,保持内存高效利用。

可以设置
Cron
任务定期清理。

虽说Redis内部会清理过期的数据,但有些长期存在的垃圾数据,也建议及时清理。

总结

以上就是Redis性能优化的18条军规,灵活应用这些策略能够为你的项目带来显著的性能提升。希望能帮助到你,欢迎分享你的优化经验!

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

技术背景

在前面的一篇
博客
中我们介绍了MindSpore-2.4-gpu的安装和其中可能出现的一些问题。这里我们在安装完成之后,可以尝试使用一些MindSpore新版本的特性。那么在安装之后,如果是使用VSCode作为IDE,可以使用ctrl+shift+P快捷键,然后搜索
python:sele
将Python解释器切换到我们所需要的最新MindSpore环境下。

设备管理和资源监测

在mindspore-2.4版本中增加了
mindspore.hal
接口,可以用于管理设备、监测设备以及执行流的处理等等。例如,常用的获取设备的数量:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.device_count(device_target))
# 2

这个输出表明我们的环境下有两个GPU卡。也可以打印这两块显卡的名称:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.get_device_name(0, device_target))
print(ms.hal.get_device_name(1, device_target))
# Quadro RTX 4000
# Quadro RTX 4000

以及设备的可用状态:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.is_available(device_target))
# True

查询设备是否被初始化:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.is_initialized(device_target))
A = ms.Tensor([0.], ms.float32)
A2 = (A+A).asnumpy()
print(ms.hal.is_initialized(device_target))
# False
# True

这也说明,只有在计算的过程中,MindSpore才会将Tensor的数据传输到计算后端。除了设备管理之外,新版本的MindSpore还支持了一些内存监测的功能,对于性能管理非常的实用:

import mindspore as ms
import numpy as np
ms.set_context(device_target="GPU")
A = ms.Tensor(np.random.random(1000), ms.float32)
A2 = (A+A).asnumpy()
print(ms.hal.max_memory_allocated())
# 8192

这里输出的占用最大显存的Tensor的大小。需要说明的是,这里不能直接按照浮点数占用空间来进行计算,应该说MindSpore在构建图的过程中会产生一些额外的数据结构,这些数据结构也会占用一定的显存,但是显存增长的趋势是准确的。除了单个的打印,还可以整个的输出一个summary:

import mindspore as ms
import numpy as np
ms.set_context(device_target="GPU")
A = ms.Tensor(np.random.random(1000), ms.float32)
A2 = (A+A).asnumpy()
print(ms.hal.memory_summary())

输出的结果为:

|=============================================|
|               Memory summary                |
|=============================================|
| Metric               | Data                 |
|---------------------------------------------|
| Reserved memory      |   1024 MB            |
|---------------------------------------------|
| Allocated memory     |   4096 B             |
|---------------------------------------------|
| Idle memory          |   1023 MB            |
|---------------------------------------------|
| Eager free memory    |      0 B             |
|---------------------------------------------|
| Max reserved memory  |   1024 MB            |
|---------------------------------------------|
| Max allocated memory |   8192 B             |
|=============================================|

ForiLoop

其实简单来说就是一个内置的for循环的操作,类似于Jax中的fori_loop:

import mindspore as ms
import numpy as np
from mindspore import ops
ms.set_context(device_target="GPU")

@ms.jit
def f(_, x):
    return x + x

A = ms.Tensor(np.ones(10), ms.float32)
N = 3
AN = ops.ForiLoop()(0, N, f, A).asnumpy()
print (AN)
# [8. 8. 8. 8. 8. 8. 8. 8. 8. 8.]

有了这个新的for循环体,我们可以对整个循环体做端到端自动微分:

import mindspore as ms
import numpy as np
from mindspore import ops, grad
ms.set_context(device_target="GPU", mode=ms.GRAPH_MODE)

@ms.jit
def f(_, x):
    return x + x

@ms.jit
def s(x, N):
    return ops.ForiLoop()(0, N, f, x)

A = ms.Tensor(np.ones(10), ms.float32)
N = 3
AN = grad(s, grad_position=(0, ))(A, N).asnumpy()
print (AN)
# [8. 8. 8. 8. 8. 8. 8. 8. 8. 8.]

流计算

CUDA Stream流计算是CUDA高性能编程中必然会用到的一个特性,其性能优化点来自于数据传输和浮点数运算的分离,可以做到在不同的Stream中传输数据,这样就达到了一边传输数据一边计算的效果。相比于单个Stream的
传输-计算-等待-传输-计算
这样的模式肯定是要更快一些,而有些深度学习框架其实很早就已经支持了Stream的调度,MindSpore目前也是跟上了节奏。关于Stream计算适用的一些场景,首先我们来看这样一个例子:

import mindspore as ms
import numpy as np
np.random.seed(0)
from mindspore import numpy as msnp
ms.set_context(device_target="GPU", mode=ms.GRAPH_MODE)

@ms.jit
def U(x, mu=1.0, k=1.0):
    return msnp.sum(0.5 * k * (x-mu) ** 2)

x = ms.Tensor(np.ones(1000000000), ms.float32)
energy = U(x)
print (energy)

在本地环境下执行就会报错:

Traceback (most recent call last):
  File "/home/dechin/projects/gitee/dechin/tests/test_ms.py", line 13, in <module>
    energy = U(x)
  File "/home/dechin/anaconda3/envs/mindspore-master/lib/python3.9/site-packages/mindspore/common/api.py", line 960, in staging_specialize
    out = _MindsporeFunctionExecutor(func, hash_obj, dyn_args, process_obj, jit_config)(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/mindspore-master/lib/python3.9/site-packages/mindspore/common/api.py", line 188, in wrapper
    results = fn(*arg, **kwargs)
  File "/home/dechin/anaconda3/envs/mindspore-master/lib/python3.9/site-packages/mindspore/common/api.py", line 588, in __call__
    output = self._graph_executor(tuple(new_inputs), phase)
RuntimeError: 
----------------------------------------------------
- Memory not enough:
----------------------------------------------------
Device(id:0) memory isn't enough and alloc failed, kernel name: 0_Default/Sub-op0, alloc size: 4000000000B.

----------------------------------------------------
- C++ Call Stack: (For framework developers)
----------------------------------------------------
mindspore/ccsrc/runtime/graph_scheduler/graph_scheduler.cc:1066 Run

说明出现了内存不足的情况。通常情况下,可能需要手动做一个拆分,然后使用循环体遍历:

import time
import mindspore as ms
import numpy as np
from mindspore import numpy as msnp
ms.set_context(device_target="GPU", mode=ms.GRAPH_MODE)

@ms.jit
def U(x, mu=1.0, k=1.0):
    return msnp.sum(0.5 * k * (x-mu) ** 2)

def f(x, N=1000, size=1000000):
    ene = 0.
    start_time = time.time()
    for i in range(N):
        x_tensor = ms.Tensor(x[i*size:(i+1)*size], ms.float32)
        ene += U(x_tensor)
    end_time = time.time()
    print ("The calculation time cost is: {:.3f} s".format(end_time - start_time))
    return ene.asnumpy()

x = np.ones(1000000000)
energy = f(x)
print (energy)
# The calculation time cost is: 11.732 s
# 0.0

这里至少没有报内存错误了,因为每次只有在计算的时候我们才把相应的部分拷贝到显存中。接下来使用流计算,也就是边拷贝边计算的功能:

def f_stream(x, N=1000, size=1000000):
    ene = 0.
    s1 = ms.hal.Stream()
    s2 = ms.hal.Stream()
    start_time = time.time()
    for i in range(N):
        if i % 2 == 0:
            with ms.hal.StreamCtx(s1):
                x_tensor = ms.Tensor(x[i*size:(i+1)*size], ms.float32)
                ene += U(x_tensor)
        else:
            with ms.hal.StreamCtx(s2):
                x_tensor = ms.Tensor(x[i*size:(i+1)*size], ms.float32)
                ene += U(x_tensor)
    ms.hal.synchronize()
    end_time = time.time()
    print ("The calculation with stream time cost is: {:.3f} s".format(end_time - start_time))
    return ene.asnumpy()

因为要考虑到程序编译对性能带来的影响,所以这里使用与不使用Stream的对比需要分开执行。经过多次测试之后,不使用Stream的运行时长大约为:

The calculation time cost is: 10.925 s
41666410.0

而使用Stream的运行时长大约为:

The calculation with stream time cost is: 9.929 s
41666410.0

就直观而言,Stream计算在MindSpore中有可能带来一定的加速效果,但其实这种加速效果相比于直接写CUDA Stream带来的效果增益其实要弱一些,可能跟编译的逻辑有关系。但至少现在有了Stream这样的一个工具可以在MindSpore中直接调用,就可以跟很多同类型的框架同步竞争了。

总结概要

接上一篇对于MindSpore-2.4-gpu版本的安装介绍,本文主要介绍一些MindSpore-2.4版本中的新特性,例如使用hal对设备和流进行管理,进而支持Stream流计算。另外还有类似于Jax中的fori_loop方法,MindSpore最新版本中也支持了ForiLoop循环体,使得循环的执行更加高效,也是端到端自动微分的强大利器之一。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/ms24.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

探讨 Unity 实时多人游戏的现状,不同游戏类型中的不同网络架构。

网络架构模式

游戏开发者使用各种网络架构模式来确保多人游戏中玩家之间可靠且快速的互动。每种模式都有其自身的优缺点,选择合适的模式取决于您正在使用的特定游戏类型和互动场景。

在本节中,我们将讨论以下模式:锁步、回滚、快照插值和延迟补偿。此外,我们将讨论最适合不同类型或游戏的模式。

锁帧(LockStep)

锁帧(LockStep)
是最古老的网络游戏同步方法之一,如今仍然经常使用。虽然这种架构可以采用多种形式,但我们将重点介绍最常见的实现,然后讨论必要的条件、限制和可能的配置。

在锁步架构中,每个玩家将其输入发送到其他所有玩家,然后在收到所有玩家的输入后立即推进其模拟——就是这样!游戏在互联网上完全同步,使用非常少的带宽,每个玩家始终看到事件的展开方式与其他所有人完全相同。

为了使游戏与锁步架构兼容,必须满足几个条件。让我们讨论一下。

确定性

锁步架构的主要要求是游戏模拟必须支持严格的位级[确定性](https://www.whatgamesare.com/determinism.html)。由于网络代码只同步输入,因此游戏模拟必须在每个帧中使用相同的输入数据在每台机器上计算出相同的结果。否则,游戏模拟将不同步、发散并偏离彼此——最终导致游戏看起来完全不同。

这通常是一个难以满足的条件,游戏必须经过精心设计才能保持确定性。开发人员通常在每个帧或帧中的多个点检查游戏状态校验和,并将这些校验和在参与者之间进行比较,以帮助跟踪和修复在游戏测试期间发生不同步的非确定性来源。

此外,任何使用浮点运算的游戏(即大多数现代游戏)都需要额外的考虑。如果您的游戏在多个平台上运行,由于平台和编译器之间的差异,浮点确定性可能特别难以实现。每个编译器可以使用不同的指令集、重新排序指令或自动矢量化。每个系统都可以以不同的方式实现超越函数,例如余弦、正弦和正切。所有这些都可能导致平台之间甚至构建之间出现不同步。一些开发人员完全使用定点运算或软件模拟浮点运算来实现其游戏模拟,以绕过使用浮点数产生的非确定性。

其他非确定性来源包括使用不同的种子生成随机数或以不同的顺序处理对象,例如物理中的接触。所有这些考虑因素也极大地限制了可以用作游戏模拟一部分的第三方库,例如碰撞检测库、物理引擎等。因此,这对于许多类型的游戏和引擎来说可能不切实际。

固定帧率

锁步还要求所有玩家同步每个输入和滴答所代表的时间单位。换句话说,游戏将以固定的帧率进行。一些游戏将渲染帧率锁定为与固定模拟帧率匹配。其他游戏允许以任意帧率进行渲染,并在固定滴答结果之间显示插值。

问题和限制

虽然锁步架构是最容易实现的架构之一,并且没有视觉伪影,但它存在一些问题和限制。在某些情况下,可以通过对游戏实现做出一些妥协来克服这些限制。在其他情况下,这些限制可能只是限制了这种架构对特定游戏的适用性。

输入延迟

锁步通过等待直到收到所有相关信息才推进,从而防止任何延迟引起的视觉伪影。这种等待有一个缺点:输入延迟。

“输入延迟”通常是指用户按下按钮到在屏幕上看到响应之间的时间。从用户的角度来看,当他们在屏幕上看到该输入的结果时,他们会感知到对他们输入的响应。如果用户必须等到所有其他玩家收到并处理他们的输入才能看到此响应,这会导致明显的输入延迟。

在对反应性要求很高的游戏中,例如射击游戏,输入延迟可能是关键因素,因为它会使游戏响应速度变慢,更难玩。

回滚

接下来,
回滚
是一种流行的网络代码架构,广泛应用于现代竞技游戏,尤其是在格斗游戏中。

回滚可以看作是经典锁步架构的扩展。在回滚架构中,玩家在每一帧发送他们的命令,并在不等待其他玩家的命令的情况下继续他们的游戏。游戏在不等待来自远程玩家的数据的情况下进行——这被称为“客户端预测”,因为远程输入数据尚不可知,客户端必须对其他玩家的未来行动做出假设。

与锁步架构(它提供帧到帧的完美顺序,但存在输入延迟)相比,回滚架构提供即时命令响应,但以顺序为代价。

在回滚过程中,游戏会进行,并且一旦本地玩家的命令发送出去,就会立即显示出来。但是,由于尚未收到来自远程玩家的命令,因此显示的信息是预测。一旦收到来自远程玩家的数据,就必须将此新信息与预测进行协调。如果信息与预测不匹配,则游戏将回滚以“纠正”错误。

什么是“回滚”?

假设在客户端准备渲染第 7 帧时,收到了来自远程玩家的第 5 帧的新数据。在这种情况下,客户端需要执行以下步骤:

  1. 加载/恢复整个游戏的状态,使其处于第 4 帧,即所有命令都已知的上一个不可预测帧。

  2. 使用第 5 帧的原始本地命令以及从远程玩家新收到的命令,将游戏移动到第 5 帧。

  3. 通过对每一帧应用本地命令继续向前推进,直到我们到达当前帧(在我们的示例中,这是第 7 帧)。

图片

完成这些步骤后,我们可能会注意到,远程玩家在第 5 帧中的操作可能导致的结果与预测的结果不同;现在将在游戏中显示这些更正后的结果。

因此,在这种情况下,“回滚”意味着游戏“回滚”到所有内容都已知的最后一帧,然后使用新信息“重播”向前。这提供了纠正预测错误的能力。

需要注意的是,为了使此方法有效,游戏必须能够快速保存和恢复其完整状态,以及使用任意命令向前移动任意数量的帧。根据游戏的复杂性,这可能需要大量的计算资源。

快照插值

快照插值
是一种由游戏 Quake 推广的技术,此后已广泛应用于源自 Quake 的游戏和游戏引擎。事实上,这种模型特别适合射击游戏。

快照插值方法基于游戏对象两个独立的时间流的概念:一个反映过去对象的状态,另一个反映未来对象预期状态。

客户端(玩家)将其命令发送到服务器,服务器处理这些命令,更改游戏状态,然后将游戏当前状态的“快照”发送回客户端。此快照包含有关游戏中的所有对象在创建快照时状态的信息。

为了保持游戏的响应速度,客户端会立即将部分命令应用于某些对象,预测其行为。这会导致对象在玩家的屏幕上同时以不同的状态存在:

  • 插值对象
    以它们在过去某个时间点的状态表示。

  • 预测对象
    以它们在未来某个时间点预期存在的状态表示。

这会产生一种有趣的动态,例如,当您预测的角色试图躲避一个正在插值的传入弹丸时。这可能比看起来更难,因为您的角色正在未来移动,而弹丸则在过去。

尽管如此,这种模型还是有一些优点:

  • 游戏会立即响应玩家命令;与锁步模型不同,不需要输入延迟。

  • 与完整的回滚架构相比,客户端需要更少的处理时间。

  • 对象在从服务器获得的已知状态之间进行插值,因此对象只通过它们已经处于的状态。

需要注意的是,为了有效地使用这种模型,游戏必须能够快速处理和传输游戏状态的快照,以及立即响应玩家命令。

不同游戏类型的最佳架构

每种游戏类型都会对网络延迟、稳定性和吞吐量施加其自身的限制。例如,MMO 游戏需要高吞吐量、低延迟和高稳定性。同时,第一人称射击 (FPS) 游戏需要低延迟和高吞吐量。在这两种类型中,建议使用具有权威游戏服务器和基于状态和输入的数据交换的服务器-客户端拓扑。

下表提供了不同视频游戏类型的网络需求、数据交换格式、推荐的网络拓扑和网络模式的比较概述。

图片

通过分析此表,我们可以得出一些结论:

  1. 网络需求在很大程度上取决于类型:例如,RTS 和 FPS 游戏需要低延迟才能确保流畅且逼真的游戏体验。

  2. 数据交换格式也因类型而异:大多数游戏使用状态和输入交换,但有些游戏,例如 RTS 和动作游戏,只使用输入。

  3. 网络模式和拓扑是根据每种类型的特定需求选择的:动作和体育游戏通常使用预测和回滚,而 MMO 和 RPG 游戏通常使用快照预测和插值。

继续前进

每款游戏都有其自身的一套需求和类型交叉,这会对其网络互动施加其自身的限制。在许多情况下,选择经过验证的解决方案将比独立开发更好。

在这篇文章中,我们回顾了实时多人游戏中网络架构的主要模式。我们还汇总了一个针对不同类型推荐解决方案的通用表格。当然,这些建议并非最终的和通用的,但它们可以作为您选择架构解决方案时的起点。

自研
原生鸿蒙NEXT5.0 API12 ArkTS
仿微信
app聊天
模板
HarmonyOSChat

harmony-wechat
原创重磅实战
纯血鸿蒙OS ArkUI+ArkTs仿微信App
聊天实例。包括
聊天、通讯录、我、朋友圈
等模块,实现类似
微信消息UI布局、编辑器光标处输入文字+emo表情图片/GIF动图、图片预览、红包、语音/位置UI、长按语音面板
等功能。

版本信息

DevEco Studio 5.0.3.906HarmonyOS5.0.0API12 Release SDK
commandline
-tools-windows-x64-5.0.3.906

纯血鸿蒙OS元年已来,华为大力推广自主研发的全场景分布操作系统HarmonyOS,赶快加入鸿蒙原生应用开发,未来可期!

基于鸿蒙os
ArkTs

ArkUI
实现下拉刷新、右键长按/下拉菜单、自定义弹窗、朋友圈等功能。

项目框架结构

基于最新版
DevEco Studio 5.0.3.906
编码工具构建鸿蒙app聊天项目模板。

https://developer.huawei.com/consumer/cn/deveco-studio/

HarmonyOS-Chat聊天app项目已经发布到我的原创作品集,有需要的可以去拍哈~

https://gf.bilibili.com/item/detail/1107424011

如果大家想快速的入门到进阶开发,先把官方文档撸一遍,然后找个实战项目案例练练手。

华为鸿蒙os开发官网
https://developer.huawei.com/consumer/cn/
HarmonyOS开发设计规范
https://developer.huawei.com/consumer/cn/design/
ArkUI方舟UI框架
https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/arkui-declarative-comp-V5

路由页面JSON文件

HarmonyOS ArkUI自定义顶部导航条

项目中所有顶部标题导航栏均是自定义封装ArkUI组件实现功能效果。之前有写过一篇专门的分享介绍,感兴趣的可以去看看下面这篇文章。

HarmonyOS NEXT 5.0自定义增强版导航栏组件|鸿蒙ArkUI自定义标题栏

https://www.cnblogs.com/xiaoyan2017/p/18517517

Index.ets入口模板

//自定义页面
@Builder customPage() {if(this.pageIndex === 0) {
IndexPage()
}
else if(this.pageIndex === 1) {
FriendPage()
}
else if(this.pageIndex === 2) {
MyPage()
}
}

build() {
Navigation() {
this.customPage()
}
.toolbarConfiguration(
this.customToolBar)
.height(
'100%')
.width(
'100%')
.backgroundColor($r(
'sys.color.background_secondary'))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}

//自定义底部菜单栏
@Builder customToolBar() {
Row() {
Row() {
Badge({
count:
8,
style: {},
position: BadgePosition.RightTop
}) {
Column({space:
2}) {
SymbolGlyph($r(
'sys.symbol.ellipsis_message_fill'))Text('聊天').fontSize(12)}
}
}
.layoutWeight(
1)
.justifyContent(FlexAlign.Center)
.onClick(()
=>{this.pageIndex = 0})

Row() {
Column({space:
2}) {
SymbolGlyph($r(
'sys.symbol.person_2'))Text('通讯录').fontSize(12)}
}
.layoutWeight(
1)
.justifyContent(FlexAlign.Center)
.onClick(()
=>{this.pageIndex = 1})

Row() {
Badge({
value:
'',
style: { badgeSize:
8, badgeColor: '#fa2a2d'}
}) {
Column({space:
2}) {
SymbolGlyph($r(
'sys.symbol.person_crop_circle_fill_1'))Text('我').fontSize(12)}
}
}
.layoutWeight(
1)
.justifyContent(FlexAlign.Center)
.onClick(()
=>{this.pageIndex = 2})
}
.height(
56)
.width(
'100%')
.backgroundColor($r(
'sys.color.background_secondary'))
.borderWidth({top:
1})
.borderColor($r(
'sys.color.background_tertiary'))
}

HarmonyOS实现登录/注册/倒计时验证

登录模板

/**
* 登录模板
* @author andy
*/import { router, promptAction } from'@kit.ArkUI'@Entry
@Component
struct Login {
@State name: string
= ''@State pwd: string= '' //提交 handleSubmit() {if(this.name === '' || this.pwd === '') {
promptAction.showToast({ message:
'账号或密码不能为空'})
}
else{//登录接口逻辑... promptAction.showToast({ message:'登录成功'})
setTimeout(()
=>{
router.replaceUrl({ url:
'pages/Index'})
},
2000)
}
}

build() {
Column() {
Column({space:
10}) {
Image(
'pages/assets/images/logo.png').height(50).width(50)
Text(
'HarmonyOS-Chat').fontSize(18).fontColor('#0a59f7')
}
.margin({top:
50})
Column({space:
15}) {
TextInput({placeholder:
'请输入账号'})
.onChange((value)
=>{this.name =value
})
TextInput({placeholder:
'请输入密码'}).type(InputType.Password)
.onChange((value)
=>{this.pwd =value
})
Button(
'登录').height(45).width('100%')
.linearGradient({ angle:
135, colors: [['#0a59f7', 0.1], ['#07c160', 1]] })
.onClick(()
=>{this.handleSubmit()
})
}
.margin({top:
30})
.width(
'80%')
Row({space:
15}) {
Text(
'忘记密码').fontSize(14).opacity(0.5)
Text(
'注册账号').fontSize(14).opacity(0.5)
.onClick(()
=>{
router.pushUrl({url:
'pages/views/auth/Register'})
})
}
.margin({top:
20})
}
.height(
'100%')
.width(
'100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
}

Stack({alignContent: Alignment.End}) {
TextInput({placeholder:
'验证码'})
.onChange((value)
=>{this.code =value
})
Button(`${
this.codeText}`).enabled(!this.disabled).controlSize(ControlSize.SMALL).margin({right: 5})
.onClick(()
=>{this.handleVCode()
})
}

鸿蒙arkts实现60s倒计时验证码

//验证码参数
@State codeText: string = '获取验证码'@State disabled:boolean = false@State time: number= 60

//获取验证码
handleVCode() {if(this.tel === '') {
promptAction.showToast({ message:
'请输入手机号'})
}
else if(!checkMobile(this.tel)) {
promptAction.showToast({ message:
'手机号格式错误'})
}
else{
const timer
= setInterval(() =>{if(this.time > 0) {this.disabled = true this.codeText = `获取验证码(${this.time--})`
}
else{
clearInterval(timer)
this.codeText = '获取验证码' this.time = 5 this.disabled = false}
},
1000)
}
}

鸿蒙os下拉刷新/九宫格图像/长按菜单

  • 下拉刷新组件
Refresh({
refreshing: $$
this.isRefreshing,
builder:
this.customRefreshTips
}) {
List() {
ForEach(
this.queryData, (item: RecordArray) =>{
ListItem() {
//... }
.stateStyles({pressed:
this.pressedStyles, normal: this.normalStyles})
.bindContextMenu(
this.customCtxMenu, ResponseType.LongPress)
.onClick(()
=>{//... })
}, (item: RecordArray)
=>item.cid.toString())
}
.height(
'100%')
.width(
'100%')
.backgroundColor(
'#fff')
.divider({ strokeWidth:
1, color: '#f5f5f5', startMargin: 70, endMargin: 0})
.scrollBar(BarState.Off)
}
.pullToRefresh(
true)
.refreshOffset(
64)//当前刷新状态变更时触发回调 .onStateChange((refreshStatus: RefreshStatus) =>{
console.info(
'Refresh onStatueChange state is ' +refreshStatus)this.refreshStatus =refreshStatus
})
//进入刷新状态时触发回调 .onRefreshing(() =>{
console.log(
'onRefreshing...')
setTimeout(()
=>{this.isRefreshing = false},2000)
})
  • 自定义刷新提示
@State isRefreshing: boolean = false@State refreshStatus: number= 1

//自定义刷新tips
@Builder customRefreshTips() {
Stack() {
Row() {
if(this.refreshStatus == 1) {
SymbolGlyph($r(
'sys.symbol.arrow_down')).fontSize(24)
}
else if(this.refreshStatus == 2) {
SymbolGlyph($r(
'sys.symbol.arrow_up')).fontSize(24)
}
else if(this.refreshStatus == 3) {
LoadingProgress().height(
24)
}
else if(this.refreshStatus == 4) {
SymbolGlyph($r(
'sys.symbol.checkmark')).fontSize(24)
}
Text(`${
this.refreshStatus == 1 ? '下拉刷新':this.refreshStatus == 2 ? '释放更新':this.refreshStatus == 3 ? '加载中...':this.refreshStatus == 4 ? '完成' : ''}`).fontSize(16).margin({left:10})
}
.alignItems(VerticalAlign.Center)
}
.align(Alignment.Center)
.clip(
true)
.constraintSize({minHeight:
32})
.width(
'100%')
}
  • 长按右键菜单

.bindContextMenu(
this
.customCtxMenu, ResponseType.LongPress)

//自定义长按右键菜单
@Builder customCtxMenu() {
Menu() {
MenuItem({
content:
'标为已读'})
MenuItem({
content:
'置顶该聊天'})
MenuItem({
content:
'不显示该聊天'})
MenuItem({
content:
'删除'})
}
}
  • 下拉菜单

.bindMenu([ ... ])

Image($r('app.media.plus')).height(24).width(24)
.bindMenu([
{
icon: $r(
'app.media.message_on_message'),
value:
'发起群聊',
action: ()
=>{}
},
{
icon: $r(
'app.media.person_badge_plus'),
value:
'添加朋友',
action: ()
=> router.pushUrl({url: 'pages/views/friends/AddFriend'})
},
{
icon: $r(
'app.media.line_viewfinder'),
value:
'扫一扫',
action: ()
=>{}
},
{
icon: $r(
'app.media.touched'),
value:
'收付款',
action: ()
=>{}
}
])

HarmonyOS arkui自定义dialog弹框组件



支持参数配置如下:

//标题(支持字符串|自定义组件)
@BuilderParam title: ResourceStr | CustomBuilder =BuilderFunction//内容(字符串或无状态组件内容)
@BuilderParam message: ResourceStr | CustomBuilder =BuilderFunction//响应式组件内容(自定义@Builder组件是@State动态内容)
@BuilderParam content: () => void =BuilderFunction//弹窗类型(android | ios | actionSheet)
@Prop type: string//是否显示关闭图标
@Prop closable: boolean
//关闭图标颜色
@Prop closeColor: ResourceColor//是否自定义内容
@Prop custom: boolean
//自定义操作按钮
@BuilderParam buttons: Array<ActionItem> | CustomBuilder = BuilderFunction

调用方式非常简单。

//自定义退出弹窗
logoutController: CustomDialogController = newCustomDialogController({
builder: HMPopup({
type:
'android',
title:
'提示',
message:
'确定要退出当前登录吗?',
buttons: [
{
text:
'取消',
color:
'#999'},
{
text:
'退出',
color:
'#fa2a2d',
action: ()
=>{
router.replaceUrl({url:
'pages/views/auth/Login'})
}
}
]
}),
maskColor:
'#99000000',
cornerRadius:
12,
width:
'75%'})
//自定义公众号弹窗
@Builder customQRContent() {
Column({space:
15}) {
Image(
'pages/assets/images/qrcode.png').height(150).objectFit(ImageFit.Contain)
Text(
'扫一扫,加我公众号').fontSize(14).opacity(.5)
}
}
qrController: CustomDialogController
= newCustomDialogController({
builder: HMPopup({
message:
this.customQRContent,
closable:
true}),
cornerRadius:
12,
width:
'70%'})

好了,以上就是harmonyos next实战开发聊天app的一些知识分享,希望对大家有所帮助~

整个项目涉及到的知识点非常多,限于篇幅就先分享到这里。感谢大家的阅读与支持。

https://www.cnblogs.com/xiaoyan2017/p/18396212

https://www.cnblogs.com/xiaoyan2017/p/18437155

https://www.cnblogs.com/xiaoyan2017/p/18467237

目录

参考资料

什么是WebRTC?

  • WebRTC(Web实时通信)技术
  • 浏览器之间交换任意数据,而无需中介
  • 不需要用户安装插件或任何其他第三方软件

能做什么?

与Media Capture和Streams API一起
  • 支持音频和视频会议
  • 文件交换
  • 屏幕共享
  • 身份管理
  • 以及与传统电话系统的接口,包括支持发送DTMF(按键拨号)信号

架构图

个人理解(类比)

官方文档晦涩难懂,所以按照自己的思路,整理总结。

核心知识点

先整理官方核心知识点,这里不理解,没关系,我们继续按自己的思路总结
  • ICE(框架)允许您的Web浏览器与对等端连接
  • STUN(协议)用于发现您的公共地址并确定路由器中阻止与对等体直接连接的任何限制
  • NAT 用于为您的设备提供公共IP地址
  • TURN 是指通过打开与TURN服务器的连接并通过该服务器中继所有信息来绕过对称NAT限制
  • SDP 从技术上讲会话描述协议(SDP并不是一个真正的协议,而是一种数据格式)

核心知识点类比

我们使用餐厅(或者其他)来类比WebRTC核心概念, 想象一下,你现在正在餐厅里面。
顾客(用户)
可以直接与
厨房(服务器)
进行交流,而不需要通过
服务员(中介)
。在这个餐厅里,顾客可以
点菜(发送音视频请求)

享用美食(接受音视频流)

还可以与其他顾客(其他用户)直接交流(数据传输)
,而这一切都
不要
额外的
工具或设备(插件)

备注:如果你明白,上面描述,那我们就继续。

ICE框架

想象一下ICE就像餐厅
整体布局和设计
,它确保
顾客(用户)
能够顺利找到座位并与
厨房(对等端)
建立联系。ICE负责协调顾客和厨房之间的所有连接方式,确保他们能顺利交流。

STUN(协议)

STUN就像餐厅门口
接待员
,负责帮助顾客找到餐厅的公共入口。接待员会
告诉顾客他们的公共地址(公共IP地址)
,并帮助他们了解是否有任何
障碍(比如路由器的限制、防火墙等)阻止他们直接进入餐厅(与对等端直接连接)

NAT(网络地址转换)

NAT就像餐厅外墙,它为餐厅提供一个
公共门牌号(公共IP地址)
。虽然餐厅内部有很多
桌子(设备)
,但外面的人只知道这个公共门牌号,而不知道内部具体位置。

TURN

TURN就像餐厅的
外卖服务
。如果顾客无法直接进入餐厅(由于对称NAT限制),他们可以选择通过
外卖服务(TURN服务器)来获取食物
。所有的订单和交流通过外卖服务进行,这样即使顾客无法直接到达餐厅,他们仍然可以享用美食。

SDP(会话描述协议)

SDP就像餐厅的
菜单
,它描述了可供选择的菜品和饮料(音视频流的格式和参数),虽然菜单本身不是一个真正协议,但它提供顾客和厨房之间所需的信息,以便他们能达成共识,确保顾客点的菜品能够被厨房正确准备。

WebRTC的核心API

  • getUserMedia(点菜):

这个API就像顾客在餐厅里点菜。顾客告诉厨房他们想要什么(音频或视频),厨房就会准备好这些食材(获取用户的音频和视频流)。

  • RTCPeerConnection(厨房的工作台):

这个API就像厨房的工作台,负责处理顾客的订单(建立连接)。它确保顾客和厨房之间的交流顺畅,处理音视频流的传输,就像厨房准备和发送食物一样。

  • RTCDataChannel(顾客之间的交流):
    这个API就像顾客之间的对话。顾客可以直接与其他顾客交流(传输数据),比如分享他们的用餐体验或交换食谱,而不需要通过服务员。
    总结
    在这个餐厅的类比中,WebRTC就像一个高效的餐厅,顾客可以直接与厨房和其他顾客交流,享受美食和分享信息,而不需要中介的干预。核心API则是实现这一切的工具,帮助顾客点菜、厨房准备食物和顾客之间的交流。这样,WebRTC使得实时通信变得简单而高效。

现在开始做饭

如果你看到这里,恭喜你,我们达成共识,现在开始做饭。

准备阶段

环境准备

安装Docker、Nginx、Nodejs等,请查询其他文档
  • 一台服务器
  • Debian 12 x86_64 操作系统
  • Docker
  • Nginx
  • Nodejs

服务器搭建

首先我们需要两个服务,STUN/TURN、Signal Server, What's Signal Server? 别紧张我待会,会解释现在我们先专注与STUN/TURN,再次之前我们需要了解  Coturn TURN server(开源框架,感谢开发人员)
  • STUN/TURN
  • Signal Server 信令服务

Coturn TURN server(开源服务) 部署

对的你没有看错,就一行命令,这就是我为什么推荐使用Docker的原因,详细的Dockerfile请看 参考资料
docker run -d --network=host coturn/coturn

测试

打开我们的测试网站
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
添加服务器,等等我们的密码哪里来的?

用户名和密码

用户名和密码在Dockerfile文件里面,我使用的是默认配置,没设置任何配置文件,所以密码是默认密码,自己可以修改
https://github.com/coturn/coturn/blob/master/docker/coturn/debian/Dockerfile

Signal Server信令服务

想象一下,在这个餐厅中,顾客(用户)需要与厨房(对等端)进行交流,但他们并不能直接看到厨房内部情况,信令服务器就像餐厅的接待员或前台,负责协调顾客之间的交流和信息传递。
  • 传递消息,比如顾客A想与顾客B进行视频通话,顾客A请求会先发送到信令服务器,然后由信令服务器转发给顾客B

信令服务与客户端源代码

注意事项: WebRTC需要使用 SSL/TLS 证书,也就是https 协议。

测试

总结

  • 搭建Signal Server信令服务
  • 搭建STUN/TURN 服务
  • Docker 部署Coturn TURN server(节省大量部署时间)

常见问题

  • 稍后补充

源代码