惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
C
Cybersecurity and Infrastructure Security Agency CISA
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Know Your Adversary
Know Your Adversary
S
Securelist
I
Intezer
AWS News Blog
AWS News Blog
L
LINUX DO - 热门话题
P
Privacy International News Feed
Recent Announcements
Recent Announcements
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
The GitHub Blog
The GitHub Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Schneier on Security
Schneier on Security
N
Netflix TechBlog - Medium
爱范儿
爱范儿
B
Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
Hacker News: Ask HN
Hacker News: Ask HN
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
Blog — PlanetScale
Blog — PlanetScale
WordPress大学
WordPress大学
S
Secure Thoughts
K
Kaspersky official blog
N
News | PayPal Newsroom
O
OpenAI News
Last Week in AI
Last Week in AI
C
Check Point Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cyberwarzone
Cyberwarzone
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tor Project blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
D
Docker
Hugging Face - Blog
Hugging Face - Blog
T
Threat Research - Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
博客园 - 司徒正美
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
P
Palo Alto Networks Blog

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的?
使用Redis的Stream模块实现群聊功能
Jiajun Huang · 2020-04-09 · via Jiajun的技术笔记

Redis 5.0 加入了一个新的模块:Stream,在这篇文章中,我们使用它来实现IM中的群聊。

首先我们来看看我们的IM有哪些功能,回忆一下我们使用的最多的IM—微信,聊天的形式有两种:

  • 单聊
  • 群聊

单聊在数据上不算难,比如最简单的,我们可以使用一个关系型数据库来存储每一条聊天记录,或者每一个一对一的关系,我们使用一个 AOF文件来存储,诸如此类,今天我们的重点是群聊。我们继续分析:

  • 微信的群聊是不保存历史记录的,意味着从一个手机切换到另外一个手机之后,历史记录就不存在了
  • 群聊是多对多的,意味着每一个人都可以发送和接收消息

而Redis的Stream模块,完美的提供了我们所需要的功能。在使用Stream之前,我们得了解什么是Stream,下面是简单的描述,如果你想 更详细的了解它,可以参考 这里

Stream的中文意思是流,你可以把它想象成水流。水流中流动的是水,而Redis中的流里,流动的是信息。就像一根水管,我们可以 从一段注水,从另一端放水一样,在Redis中,我们可以从一端注入信息,我们称之为产生信息(产生信息的一端叫做生产者),也可以 在另一端消费信息(消费信息的一端叫做消费者)。

Stream中产生消息,用的是 XADD 这个命令,而消费则是用 XREAD

接下来,我们就要用这两个命令来实现一个简单的群聊对话。

MVP:阻塞版本

from redis import Redis  # 导入包

r = Redis()  # 初始化Redis对象实例,这里没有填参数,因此会连接本地的redis: 127.0.0.1:6379

name = input("what's your name? ")  # 首先要求输入一个名字,作为待会儿群聊时的身份认证标识
chat_stream = "my_chat_stream"  # 这是这个群聊的一个标识,相当于一个群的名字

while True:  # 进入死循环
    user_input = input("what you wanna say? ")  # 首先输出你想说啥?提示用户输入内容

    r.xadd(chat_stream, {name: user_input})  # 然后发送输入的内容到群聊内容里

    print(r.xread({chat_stream: "$"}, None, 0))  # 输出从群里读出来的内容

运行一下,起一个终端,执行 python main.py

$ python main.py 
what's your name? jhon
what you wanna say? hello world
[[b'hello', [(b'1586415762129-0', {b'marry': b'nothing just kidding'})]]]
what you wanna say? hello, marry
[[b'hello', [(b'1586415773558-0', {b'marry': b'well'})]]]

起另外一个终端,同样执行 python main.py

$ python main.py 
what's your name? marry
what you wanna say? nothing just kidding
[[b'hello', [(b'1586415768566-0', {b'jhon': b'hello, marry'})]]]
what you wanna say? well

可以看到,它们互相可以看到内容,但是有一个缺点,那就是,每次都要等读取完之后,才可以进行下一次输入,接下来我们着手 改进这一点。

改进:同时收发

由于我们收取消息的时候,超时时间填的是0,也就是说,没有收取到消息就一直等待,所以如果没有群聊消息来,我们就只能一直 等着,而不能输入新的消息。比较简单的一个改进,是改成没有收到消息的时候不等待,直接进入下一次循环,不过这样就有另外 一个缺点,那就是如果不输入内容,就一直看不到新的消息。

有没有办法让收消息和发消息互相不干扰呢?有办法,我们要使用线程:一个线程负责收消息,一个负责发消息:

import threading

from redis import Redis  # 导入包

r = Redis()  # 初始化Redis对象实例,这里没有填参数,因此会连接本地的redis: 127.0.0.1:6379

name = input("what's your name? ")  # 首先要求输入一个名字,作为待会儿群聊时的身份认证标识
chat_stream = "my_chat_stream"  # 这是这个群聊的一个标识,相当于一个群的名字


def send_msgs():
    while True:  # 进入死循环
        user_input = input("what you wanna say? ")  # 首先输出你想说啥?提示用户输入内容
        r.xadd(chat_stream, {name: user_input})  # 然后发送输入的内容到群聊内容里


def recv_msgs():
    while True:
        print(r.xread({chat_stream: "$"}, None, 0))  # 输出从群里读出来的内容


if __name__ == "__main__":
    threading.Thread(target=recv_msgs).start()
    send_msgs()

如果你运行一下,就会发现现在已经可以了,但是仍然有缺点,那就是两端都会把自己的信息打印出来,这是因为我们对收到的信息 没有做处理,而是直接打印出来了。下一步,就是改进这些:

import threading

from redis import Redis  # 导入包

r = Redis()  # 初始化Redis对象实例,这里没有填参数,因此会连接本地的redis: 127.0.0.1:6379

name = input("what's your name? ")  # 首先要求输入一个名字,作为待会儿群聊时的身份认证标识
chat_stream = "my_chat_stream"  # 这是这个群聊的一个标识,相当于一个群的名字


def send_msgs():
    while True:  # 进入死循环
        user_input = input("what you wanna say? ")  # 首先输出你想说啥?提示用户输入内容
        if user_input:
            r.xadd(chat_stream, {name: user_input})  # 然后发送输入的内容到群聊内容里


def handle_msgs(msgs):
    # msgs结构是:[[b'my_chat_stream', [(b'1586416610013-0', {b'jhon': b'nothing'})]]]
    for msg in msgs:  # 迭代,因此msg是 [b'my_chat_stream', [(b'1586416610013-0', {b'jhon': b'nothing'})]]
        _, msg_list = msg  # 解包,因此 msg_list 是 [(b'1586416610013-0', {b'jhon': b'nothing'})]
        for _, content in msg_list:  # 再次解包并迭代,因此 content是 {b'jhon': b'nothing'}
            for user_name, user_input in content.items():  # 迭代,因此user_name是 b'jhon' 而 user_input 是 b'nothing'
                decoded_user_name = user_name.decode()
                decoded_user_input = user_input.decode()
                if decoded_user_name == name:
                    continue

                print("[{}]: {}".format(decoded_user_name, decoded_user_input))


def recv_msgs():
    while True:
        msgs = r.xread({chat_stream: "$"}, None, 0)  # 获取从群里读出来的内容
        handle_msgs(msgs)  # 因为逻辑不算简单,为了这里看起来简单易懂,我们把处理消息的逻辑放在另外一个函数里


if __name__ == "__main__":
    threading.Thread(target=recv_msgs).start()
    send_msgs()

总结

这一篇文章中,我们通过一步一步的迭代,借助Redis的Stream功能,实现了一个简单的群聊功能,首先我们用一个简单的死循环, 输入后输入内容,接着我们进行改进,让输入和输出分别处理,互补干扰,最后我们对处理消息的逻辑进行进一步优化,使得 输出的内容看起来干净整洁。


参考资料: