深入浅出sse协议,用代码自己实现

http大家肯定都不陌生了,入行就接触的老朋友了。

但是它的兄弟sse又是怎么一回事呢?

说它是http的兄弟,是因为它们同属应用层协议,具体一点,就是tcp的上一层。

然而,叫sse协议,只是一个口语上的习惯性表达,它本质上,只是一种特殊的http协议,而不是像websocket那样独立于http以外的协议。所以,可以叫他们亲兄弟也不为过。通过http本身就有的一系列规范,去做了一个有特定需求场景的协议。

sse是如何降临到世界上来的呢

起初,有了http,我们跟别人通信,我说一句,你回一句,很好很美满。

后来,大家在网络协议的基础上做出各种好玩的软件,游戏。发现了一个问题,就好像,当我要盯着股票软件看实时更新数据的时候。用http的话,那岂不是要自己不断地手点刷新盯盘?因为http只能一次一次地调用,调完这一次,下次它也不会主动发给你。

有的小聪明想到进一步的方法,那我就轮询呗,本地按照一定频率去访问服务端,那不就实现了自动刷新了吗。

然而这样做了一会儿,又暴露了新问题

第一,轮询的频率是多少,如果股票那段时间变动很慢,那么轮询快了是不是浪费网络资源。相反,轮询慢了,那么软件本身的信息功能性就大打折扣。

第二,轮询功能,大量频繁的连接创建和断开,你什么服务器能顶的起这样的压力?手机也顶不住这样的电力损耗吧。

第三,轮询一次要重新拉取所有信息,如果你需要的只是某一块信息,那么岂不是浪费带宽了。

既然从工程层面不好处理,那么网络大佬们开始动脑筋了。

压力主要是来自于轮询的反复创建销毁http连接,有没有可能,在用户用软件的这段时间里,就持续不断地送数据,但是不断开连接呢?

于是乎,sse闪亮登场了。

sse的本质

sse的本质,就是深入到tcp层面,跟传统的http做一个区分。当连接建立之后,不立刻断开,持续用这个连接推送数据,直到其中某一方主动断开连接,或者将数据发送完毕,再断开。这样不就解决了过度连接的资源损耗吗。

进一步,我们看到,sse并非是一个完全需要重新设计的逻辑,它与http非常相似,核心上只是多了一步特征,保持长连接。那么,在已有的http协议之上,我们使用keep-alive,是不是就能达到这个效果了呢?

sse的工作流程

1.请求端发出http请求,头部参数设置Accept: text/event-stream,告诉服务端,我要长连接。

2.服务端返还的第一个报文,响应头设置

yaml 复制代码
HTTP/1.1 200 OK

Content-Type: text/event-stream      // 告知浏览器,按照流式内容解析

Cache-Control: no-cache              // 禁止缓存,保证实时推送

X-Accel-Buffering: no                // 链路上的nginx/反向代理们不要攒包,直接转发报文。

Connection: keep-alive               // 维持HTTP长连接不断开

3.保持长连接不断,服务端按照特有的sse数据帧,在tcp链路中持续发送消息。格式为:

kotlin 复制代码
data: ...

4.重连机制。链路意外中断后,客户端感知,触发onerror。浏览器自动重新发一次get请求,额外带上Last-Event-ID这个请求头,服务端拿到这个ID,继续上次的传输。

5.连接关闭。客户端主动关闭,或者服务端发送204 N Content,否则客户端还是会重连。

注意:一次sse协议中,只有一次http的请求和响应,其他数据全在tcp链路层面传输。

自己实现简易的sse协议

只理解还是不过瘾的,想要更深入,那就自己实现一次sse协议。未完待续...

python 复制代码
# sse_server.py
import socket
import threading
import time

def handle_client(conn):
    try:
        # 解析http请求
        req = conn.recv(1024).decode("utf-8")
        print("客户端请求头:\n", req)

        # 构造个回复报文,返还回去,通知客户端和网络链路上的节点
        sse_response = (
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/event-stream; charset=utf-8\r\n"
            "Cache-Control: no-cache\r\n"
            "Connection: keep-alive\r\n"
            "X-Accel-Buffering: no\r\n"   # 👉 你要的这个头
            "\r\n"
        )

        # 推送
        conn.send(sse_response.encode("utf-8"))

        # 定义一段你想返回的长字符串
        long_text = """
        这是一段很长很长的文本内容。
        我要把它分成好几段,
        通过 SSE 协议一点点推送给客户端。
        每一段都会单独发送,
        客户端会按顺序接收并拼接起来。
        这就是 SSE 流式传输的效果。
        """

        # 把长文本按行拆分(你也可以按字数拆分)
        lines = long_text.strip().split("\n")

        # 循环一段一段发送
        for line in lines:
            # SSE 格式必须:data: 内容\n\n
            msg = f"data: {line}\n\n"

            # 发送一段(分段发送!)
            conn.send(msg.encode("utf-8"))

            # 可以加延迟,看得更清楚
            time.sleep(1)

    except Exception as e:
        print("客户端断开")
    finally:
        conn.close()

def run_server():
    # 创建socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 允许端口快速复用,防止服务重启时,报错端口被占用
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 绑定端口
    server.bind(("127.0.0.1", 9000))

    print("SSE 服务端已启动:127.0.0.1:9000")

    # 同时能处理的排队数量,再多的请求同时来就会直接拒绝
    server.listen(5)


    while True:
        # conn是客户端连接对象,addr是客户端地址
        conn, addr = server.accept()

        # 异步处理客户端请求
        t = threading.Thread(target=handle_client, args=(conn,))
        t.start()

if __name__ == "__main__":
    run_server()
swift 复制代码
# sse_client.py
import socket

def run_client():
    sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sk.connect(("127.0.0.1", 9000))

    # 标准 SSE 请求头
    req = (
        "GET /sse HTTP/1.1\r\n"
        "Host: 127.0.0.1:9000\r\n"
        "Accept: text/event-stream\r\n"
        "Cache-Control: no-cache\r\n"
        "Connection: keep-alive\r\n"
        "\r\n"
    )
    sk.send(req.encode())

    buffer = b""
    while True:
        chunk = sk.recv(1024)
        if not chunk:
            break
        buffer += chunk

        while b"\n\n" in buffer:
            msg, buffer = buffer.split(b"\n\n", 1)
            msg_str = msg.decode().strip()
            if msg_str.startswith("data:"):
                print("✅ 收到:", msg_str.replace("data:", "").strip())

    sk.close()

if __name__ == "__main__":
    run_client()
相关推荐
SamDeepThinking2 小时前
并发量就算只有2,该上锁还得上呀
java·后端·架构
永远不会的CC7 小时前
浙江华昱欣实习(4月23日~ 4月19日)
后端·学习
直奔標竿7 小时前
Java开发者AI转型第二十五课!Spring AI 个人知识库实战(四)——RAG来源追溯落地,拒绝AI幻觉
java·开发语言·人工智能·spring boot·后端·spring
嘟嘟MD7 小时前
程序员副业 | 2026年4月复盘
后端·创业
时空系8 小时前
认识Rust——我的第一个程序 Rust中文编程
开发语言·后端·rust
DevilSeagull8 小时前
Windows 批处理 (Batch) 编程: 从入门到入土. (一) 基础概念与环境配置
开发语言·windows·后端·batch·语言
CAE虚拟与现实8 小时前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
0xDevNull8 小时前
Java泛型详解
java·开发语言·后端
yeeanna8 小时前
GO函数的特殊性
开发语言·后端·golang