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

推荐订阅源

Recent Commits to openclaw:main
Recent Commits to openclaw:main
N
News | PayPal Newsroom
TaoSecurity Blog
TaoSecurity Blog
Google Online Security Blog
Google Online Security Blog
NISL@THU
NISL@THU
T
Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Engineering at Meta
Engineering at Meta
AWS News Blog
AWS News Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Privacy International News Feed
B
Blog
PCI Perspectives
PCI Perspectives
Martin Fowler
Martin Fowler
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
T
Tenable Blog
F
Fortinet All Blogs
G
GRAHAM CLULEY
V2EX - 技术
V2EX - 技术
C
Check Point Blog
Project Zero
Project Zero
P
Palo Alto Networks Blog
J
Java Code Geeks
W
WeLiveSecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
The Exploit Database - CXSecurity.com
博客园 - 司徒正美
P
Privacy & Cybersecurity Law Blog
S
SegmentFault 最新的问题
Last Week in AI
Last Week in AI
Forbes - Security
Forbes - Security
C
Cybersecurity and Infrastructure Security Agency CISA
Security Latest
Security Latest
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Vercel News
Vercel News
Recent Announcements
Recent Announcements
博客园 - Franky
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Recorded Future
Recorded Future
The Last Watchdog
The Last Watchdog
MongoDB | Blog
MongoDB | Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
Cisco Talos Blog
Cisco Talos Blog
量子位
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日
2026-04-14 · via 暗无天日

你有没有想过,有人用 GIF 动画在浏览器里玩游戏?

这不是玩笑。有人用 Haskell 写了一个贪吃蛇游戏,但它的画面输出不是通过窗口或 Canvas,而是通过 GIF 动画流 ——服务器不断向浏览器推送新的 GIF 帧,浏览器自动显示,就这样实现了一个"实时"游戏。打开浏览器,访问一个 URL,就能看到不断变化的 GIF 画面,按 WASD 键还能控制蛇的移动方向。

这听起来很奇怪,但仔细想想又很巧妙:GIF 是一种几乎所有浏览器都支持的图片格式,不需要 JavaScript,不需要 WebSocket,甚至不需要现代浏览器——只要能显示 GIF 就行。

那么,这种"GIF 流"的技术原理是什么?除了玩游戏,它还能做哪些奇怪的事?让我们一起来探索。

参考资料: https://hookrace.net/blog/haskell-game-programming-with-gif-streams/

GIF 流的原理

GIF 流的核心技术是 HTTP 的 multipart/x-mixed-replace Content-Type。这是一种 服务器推送 (Server Push)技术,诞生于 Netscape 时代。

普通的 HTTP 响应是这样的:客户端发请求,服务器返回一个完整的响应(一个 HTML 页面、一张图片等),然后连接关闭。

multipart/x-mixed-replace 允许服务器在 同一个 HTTP 连接 上连续发送多个"部分",每个部分都会 替换 前一个。浏览器收到新的部分后,会自动用新内容替换旧内容。

原始的 HTTP 响应看起来是这样的:

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame

--frame
Content-Type: image/gif

<GIF 二进制数据:第 1 帧>
--frame
Content-Type: image/gif

<GIF 二进制数据:第 2 帧>
--frame
Content-Type: image/gif

<GIF 二进制数据:第 3 帧>
...

服务器 永远不会关闭这个连接 ,而是持续不断地发送新的 GIF 帧。浏览器收到每一帧后立即显示,然后再等待下一帧。就这样,一个静态的 <img> 标签就变成了"实时视频"。

这项技术最常见于 IP 摄像头的监控画面 (MJPEG 流),但用 GIF 来实现同样可行,而且 GIF 格式还有个优势:支持 256 色调色板和透明色,对于简单图形(如游戏画面、监控图表)来说文件体积可能比 JPEG 更小。注意 GIF 的 256 色限制可能导致颜色失真,但对于线条、文本等简单图形效果不错。

理解了原理后,接下来我们来看看 GIF 流能做哪些有趣的事。

用途 1:用 Python 实现一个 GIF 流服务器

理解了原理,我们来动手实现一个最简 GIF 流服务器。用 Python 标准库 + Pillow 就够了。

安装依赖

pip install --user Pillow

完整代码

"""最简 GIF 流服务器 - 在浏览器中显示实时变化的颜色"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from io import BytesIO
import time, math
from PIL import Image, ImageDraw

class GifStreamHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
                        self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.end_headers()
            html = '''<!DOCTYPE html>
<html><head><title>GIF Stream Demo</title></head>
<body style="background:#111;color:#fff;text-align:center">
<h1>GIF Stream Demo</h1>
<img src="/stream" style="border:2px solid #555">
<p>Open this page to see live GIF stream</p>
</body></html>'''
            self.wfile.write(html.encode('utf-8'))
        elif self.path == '/stream':
                        self.send_response(200)
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=frame')
            self.send_header('Cache-Control', 'no-cache')
            self.end_headers()

            t = 0
            while True:
                try:
                                        r = int((math.sin(t * 0.05) + 1) * 127)
                    g = int((math.sin(t * 0.05 + 2) + 1) * 127)
                    b = int((math.sin(t * 0.05 + 4) + 1) * 127)

                    img = Image.new('RGB', (200, 200), (r, g, b))
                    draw = ImageDraw.Draw(img)
                                        draw.text((50, 80), time.strftime('%H:%M:%S'), fill=(255, 255, 255))

                                        buf = BytesIO()
                    img.save(buf, format='GIF')
                    gif_data = buf.getvalue()

                                        self.wfile.write(b'--frame\r\n')
                    self.wfile.write(b'Content-Type: image/gif\r\n')
                    self.wfile.write(f'Content-Length: {len(gif_data)}\r\n'.encode())
                    self.wfile.write(b'\r\n')
                    self.wfile.write(gif_data)
                    self.wfile.write(b'\r\n')
                    self.wfile.flush()

                    t += 1
                    time.sleep(0.1)                  except (BrokenPipeError, ConnectionResetError):
                    break          else:
            self.send_error(404)

if __name__ == '__main__':
    server = HTTPServer(('0.0.0.0', 8080), GifStreamHandler)
    print('Listening on http://0.0.0.0:8080/')
    server.serve_forever()

运行效果

python3 /tmp/gif_stream_server.py

然后在浏览器中打开 http://localhost:8080/ ,你会看到一个 HTML 页面,其中的 GIF 图像不断变色,中间显示着实时时钟。

注意: multipart/x-mixed-replace 需要 <img> 标签来承载,不能直接在浏览器地址栏访问流地址。这是因为现代浏览器(特别是 Chrome )对直接导航到 multipart 流的支持有限。这也是 IP 摄像头监控画面的标准用法——通过 HTML 页面嵌入 <img src"/stream">= 来显示 MJPEG/GIF 流。

整个过程:

  • 服务器每 100ms 生成一帧 GIF 图像
  • 通过 multipart/x-mixed-replace 协议推送给浏览器
  • 浏览器自动替换显示,无需任何 JavaScript

你还可以用 curl 查看 GIF 流的原始数据,感受 multipart/x-mixed-replace 的格式:

curl -s http://localhost:8080/stream | xxd

你会看到 GIF 文件头( GIF87aGIF89a )以及边界标记 --frame 在二进制数据中交替出现。Pillow 对于简单的单帧图像默认输出 GIF87a (1987 年的原始格式),对于包含动画或透明色的图像则使用 GIF89a (1989 年增加了动画、透明色等特性)。两者在 GIF 流中都能正常工作。

用途 2:零依赖的实时系统监控仪表盘

回到 GIF 流的话题。既然我们可以用 Python 生成 GIF 帧并通过 HTTP 推送,那就可以做一个 不需要 WebSocket 、不需要 JavaScript 的实时监控仪表盘

核心思路:每隔一段时间采集系统状态(CPU、内存),用 Python 画一个柱状图,编码为 GIF 帧,通过 multipart/x-mixed-replace 推送给浏览器。

需要额外安装 psutil 来获取系统状态:

sudo pacman -S python-psutil
"""GIF 流系统监控仪表盘 - 纯 GIF,无需 JavaScript"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from io import BytesIO
import psutil
from PIL import Image, ImageDraw
import time

WIDTH, HEIGHT = 400, 200

def get_stats():
    """获取系统状态"""
    return {
        'cpu': psutil.cpu_percent(interval=0),
        'mem': psutil.virtual_memory().percent,
        'disk': psutil.disk_usage('/').percent,
    }

def draw_chart(stats):
    """绘制柱状图"""
    img = Image.new('RGB', (WIDTH, HEIGHT), (20, 20, 20))
    draw = ImageDraw.Draw(img)

    labels = [('CPU', stats['cpu'], (0, 180, 0)),
              ('MEM', stats['mem'], (0, 0, 180)),
              ('DISK', stats['disk'], (180, 0, 0))]

    for i, (label, value, color) in enumerate(labels):
        x = 50 + i * 120
        bar_height = int(value / 100 * 150)
                draw.rectangle([x, HEIGHT - 30 - bar_height, x + 60, HEIGHT - 30], fill=color)
                draw.text((x + 10, HEIGHT - 25), f'{label} {value}%', fill=(200, 200, 200))

    return img

class MonitorHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.end_headers()
            html = '''<!DOCTYPE html>
<html><head><title>System Monitor</title></head>
<body style="background:#111;color:#fff;text-align:center">
<h1>GIF Stream System Monitor</h1>
<img src="/stream" style="border:2px solid #555">
</body></html>'''
            self.wfile.write(html.encode('utf-8'))
        elif self.path == '/stream':
            self.send_response(200)
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=frame')
            self.send_header('Cache-Control', 'no-cache')
            self.end_headers()

            while True:
                try:
                    stats = get_stats()
                    img = draw_chart(stats)
                    buf = BytesIO()
                    img.save(buf, format='GIF')
                    gif_data = buf.getvalue()

                    self.wfile.write(b'--frame\r\n')
                    self.wfile.write(b'Content-Type: image/gif\r\n')
                    self.wfile.write(f'Content-Length: {len(gif_data)}\r\n'.encode())
                    self.wfile.write(b'\r\n')
                    self.wfile.write(gif_data)
                    self.wfile.write(b'\r\n')
                    self.wfile.flush()

                    time.sleep(1)                  except (BrokenPipeError, ConnectionResetError):
                    break  
if __name__ == '__main__':
    server = HTTPServer(('0.0.0.0', 8080), MonitorHandler)
    print('监控仪表盘: http://0.0.0.0:8080/')
    server.serve_forever()

运行后在浏览器中打开,你会看到一个实时更新的柱状图,显示 CPU、内存和磁盘使用率。

这个方案的特点:

  • 不需要前端框架 :没有 React、Vue,甚至没有 JavaScript
  • 不需要 WebSocket :纯 HTTP 长连接
  • 兼容性极强 :任何能显示 GIF 的浏览器都支持,包括 w3m 这类文本浏览器配合图片渲染器
  • 但带宽消耗大 :每秒 1 帧 GIF 的数据量虽然不大,但如果需要更高帧率就会显著增加

用途 3:在 GIF 帧延迟中藏数据

GIF 格式的每一帧都有一个 延迟时间 (Delay Time),以 1/100 秒为单位。正常情况下这个值是 10(即 100ms),但我们可以用它来编码秘密信息。

比如,延迟 20 表示二进制 1 ,延迟 10 表示二进制 0

"""在 GIF 帧延迟中隐藏消息"""
from PIL import Image
import random

def encode_message(message, output_path, width=10, height=10):
    """将消息编码到 GIF 帧延迟中"""
    binary = ''.join(format(ord(c), '08b') for c in message)
    binary += '00000000'  
    frames = []
    for bit in binary:
                pixels = [(random.randint(0, 255),) * 3 for _ in range(width * height)]
        img = Image.new('RGB', (width, height))
        img.putdata(pixels)
        frames.append(img)

            delays = [20 if b == '1' else 10 for b in binary]
    frames[0].save(
        output_path,
        save_all=True,
        append_images=frames[1:],
        duration=delays,
        loop=0,
    )
    print(f'已将 "{message}" 编码到 {output_path}{len(binary)} 帧)')

def decode_message(gif_path):
    """从 GIF 帧延迟中解码消息"""
    img = Image.open(gif_path)
    binary = ''
    try:
        while True:
            delay = img.info.get('duration', 10)
            binary += '1' if delay >= 20 else '0'
            img.seek(img.tell() + 1)
    except EOFError:
        pass

    message = ''
    for i in range(0, len(binary), 8):
        byte = binary[i:i+8]
        if byte == '00000000':
            break
        message += chr(int(byte, 2))
    return message

if __name__ == '__main__':
    encode_message('Hello GIF!', 'secret.gif')
    decoded = decode_message('secret.gif')
    print(f'从 secret.gif 解码出: "{decoded}"')

运行后你会得到一个看起来像随机噪点的 GIF 动画,但帧延迟中隐藏了秘密消息。肉眼完全看不出区别。

当然,这种隐写术很容易被分析——只要检查 GIF 帧的时间间隔分布就能发现异常(正常动画的帧延迟应该是均匀的,而编码后的会出现双峰分布)。但作为一个 GIF 的奇怪用途 ,它足够有趣了。

小结

GIF 流是一项"复古"的技术,诞生于上世纪 90 年代。在今天 WebSocket 、WebRTC 、Server-Sent Events 大行其道的时代,它看起来已经过时了。但正是这种简洁性赋予了它独特的价值:

  • 零前端依赖 :不需要 JavaScript,一个 <img> 标签就够了
  • 极简协议 :基于 HTTP 长连接 + multipart/x-mixed-replace ,实现起来只需要几十行代码
  • 广泛兼容 :几乎所有浏览器都支持,甚至包括一些非主流的浏览方式

GIF 证明了"老技术"也可以玩出新花样。下次当你看到一张 GIF 图片时,也许可以想想:它还能做什么更奇怪的事?

如果你也发现了 GIF 的其他有趣用途,欢迎在评论区分享。