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

推荐订阅源

Martin Fowler
Martin Fowler
D
DataBreaches.Net
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Apple Machine Learning Research
Apple Machine Learning Research
H
Help Net Security
M
MIT News - Artificial intelligence
美团技术团队
人人都是产品经理
人人都是产品经理
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Cloudflare Blog
有赞技术团队
有赞技术团队
L
LangChain Blog
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 【当耐特】
S
SegmentFault 最新的问题
V
Visual Studio Blog
Blog — PlanetScale
Blog — PlanetScale
Hugging Face - Blog
Hugging Face - Blog
B
Blog
I
InfoQ

暗无天日

读: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 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日
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 的其他有趣用途,欢迎在评论区分享。