深入浅出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()
相关推荐
IT_陈寒3 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码4 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo4 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志5 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..5 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects5 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰5 小时前
一些基础和问题解决
后端
陈随易6 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN6 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana6 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端