从打字机效果实现详解整包/流式传输、长/短连接、SSE、Streamable-HTTP、长轮询、WebSocket

前置知识

流式响应就是将一次耗时较长的响应拆成多个分块,分多次传输给客户端,生成一部分响应后就传递到客户端。客户端接收到一个分块,立刻追加展示给用户,给用户一个较好的体验,这种效果被称为打印机效果。为了实现打字机效果,目前有几种主流实现方式:SSE、Streamable-HTTP、长轮询。

我们需要理解计算机网络传输中的两组概念:整包传输/分块传输短连接/长连接
PS:非常重要,被问了没答上来。

  • 整包传输:当客户端发起请求后,服务端一次性将所有数据传输到客户端,称为整包传输,目前绝大多数的响应都是整包传输。

  • 分块传输 :当客户端发起请求后,服务端将响应分多次传输到客户端,每次传输一个分块,称为分块传输。分块传输在传输较大响应时经常使用,每个块前面有大小标识,标识这个块的数据大小,最后以一个零长度块表示结束,多次分块传输算一次响应。要开启分块传输,需要在响应的请求头中添加Transfer-Encoding: chunked

  • 短连接:一次请求对应一次响应,一次请求建立一次TCP连接,收到响应后立刻关闭TCP连接。

  • 长连接 :和短连接相比,当服务端完成本次响应后,客户端不关闭TCP连接,下次发送请求时再次复用本次TCP连接,减少三次握手和四次挥手的资源浪费。长连接本质是复用TCP连接,多次请求可以在同一个TCP中,响应完成后TCP连接不断开。要开启长连接,需要在请求头中设置Keep-Alive: True参数。

整包传输/分块传输短连接/长连接这两组概念相互独立,也可以互相组合使用,所以有短连接分块传输、短连接整包传输、长连接分块传输、长连接整包传输四种概念。

协议详情

整包传输HTTP

复制代码
HTTP头部示例:
GET /bulk HTTP/1.1
Connection: close  # 传输完成即关闭

工作流程:
1. 客户端发送请求
2. 服务器处理数据(可能耗时)
3. 服务器一次性发送所有数据
4. 连接立即关闭

SSE协议

复制代码
HTTP头部示例:
HTTP/1.1 200 OK
Transfer-Encoding: chunked  # 关键头部
Content-Type: text/plain

chunked传输格式:
chunk-size\r\n
chunk-data\r\n
0\r\n  # 结束标志
\r\n

Streamble-HTTP

复制代码
事件流格式:
data: 消息内容\n\n
event: 事件类型\n
data: 消息内容\n\n
id: 事件ID\n
data: 消息内容\n\n

客户端API:
const source = new EventSource('/sse');
source.onmessage = (event) => { ... };
source.onerror = () => { ... };

长轮询

复制代码
实现模式:
客户端      服务器
  |--请求--->|
  |          | (等待新数据或超时)
  |<--响应---|
  |--立即新请求->|
  |<--响应---|

WebSocket

复制代码
握手过程:
1. HTTP Upgrade请求
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...

2. 服务器响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...

代码演示

demo.py

python 复制代码
from fastapi import FastAPI, Request, WebSocket, HTTPException
from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
import asyncio
import json
from datetime import datetime
import time
import uuid
from typing import AsyncGenerator, Dict
from contextlib import asynccontextmanager


# 应用生命周期管理
@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动时
    print("🚀 服务器启动中...")
    yield
    # 关闭时
    print("🛑 服务器关闭")


app = FastAPI(title="网络传输协议演示", lifespan=lifespan)

# 全局状态存储
polling_states: Dict[str, dict] = {}

# 演示文本 - 简化版,确保可以正常显示
TEXT = "Hello! 网络传输协议演示。"
CHAR_DELAY = 0.15  # 稍微增加延迟,确保稳定


# ========== SSE函数 ==========
async def sse_generator(request: Request):
    """
    SSE生成器
    关键点:保持简单的SSE格式,确保正确换行
    """
    try:
        for i, char in enumerate(TEXT):
            # 检查连接是否还在
            if await request.is_disconnected():
                print(f"SSE连接断开于字符 {i}")
                break

            # 延迟模拟打字效果
            await asyncio.sleep(CHAR_DELAY)

            # 方案1:创建简单的事件数据
            event_data = {
                "char": char,
                "index": i,
                "total": len(TEXT),
                "timestamp": datetime.now().isoformat()
            }

            # 确保正确的SSE格式:data: {json}\n\n
            message = f"data: {json.dumps(event_data)}\n\n"
            yield message

            print(f"SSE发送: {char} (第{i + 1}/{len(TEXT)}个字符)")

        # 发送完成事件
        yield "event: complete\ndata: {\"status\": \"done\"}\n\n"
        print("SSE传输完成")

    except Exception as e:
        print(f"SSE生成器错误: {e}")
        yield f"event: error\ndata: {{\"message\": \"{str(e)}\"}}\n\n"


@app.get("/sse")
async def sse_endpoint(request: Request):
    """
    SSE端点 - 添加正确的响应头
    """
    print("新的SSE连接建立")

    # 使用正确的SSE响应头
    headers = {
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Content-Type": "text/event-stream",
        "X-Accel-Buffering": "no",  # 禁用nginx缓冲
        "Access-Control-Allow-Origin": "*"
    }

    return StreamingResponse(
        sse_generator(request),
        media_type="text/event-stream",
        headers=headers
    )


# ========== 长轮询函数 ==========
@app.get("/long-polling")
async def long_polling_endpoint(request: Request, client_id: str = None, last_index: int = 0):
    """
    长轮询实现
    """
    # 生成或使用客户端ID
    if not client_id:
        client_id = str(uuid.uuid4())

    # 初始化客户端状态
    if client_id not in polling_states:
        polling_states[client_id] = {
            "index": 0,
            "start_time": time.time(),
            "last_active": time.time()
        }
        print(f"新客户端连接: {client_id}")

    client_state = polling_states[client_id]

    # 更新最后活动时间
    client_state["last_active"] = time.time()

    # 清理过期客户端(超过30秒无活动)
    expired_clients = [
        cid for cid, state in polling_states.items()
        if time.time() - state["last_active"] > 30
    ]
    for cid in expired_clients:
        del polling_states[cid]

    # 检查是否有新数据
    if last_index < client_state["index"]:
        # 已经有新数据,立即返回
        if client_state["index"] < len(TEXT):
            char_index = client_state["index"] - 1
            response_data = {
                "char": TEXT[char_index] if char_index < len(TEXT) else "",
                "index": client_state["index"],
                "total": len(TEXT),
                "complete": client_state["index"] >= len(TEXT),
                "client_id": client_id,
                "timestamp": datetime.now().isoformat()
            }
            return JSONResponse(content=response_data)

    # 等待新数据
    max_wait = 25  # 最大等待25秒
    check_interval = 0.5  # 检查间隔0.5秒

    for _ in range(int(max_wait / check_interval)):
        await asyncio.sleep(check_interval)

        # 检查连接是否还在
        if await request.is_disconnected():
            print(f"客户端 {client_id} 断开连接")
            return JSONResponse(content={"status": "disconnected"})

        # 检查是否有新数据
        if last_index < client_state["index"]:
            break

        # 模拟生成新数据(每2个检查周期生成一个字符)
        if _ % 4 == 0 and client_state["index"] < len(TEXT):
            client_state["index"] += 1
            print(f"长轮询生成新字符给 {client_id}: 索引 {client_state['index']}")
            break

    # 返回数据或超时
    if last_index < client_state["index"] and client_state["index"] <= len(TEXT):
        char_index = client_state["index"] - 1
        response_data = {
            "char": TEXT[char_index],
            "index": client_state["index"],
            "total": len(TEXT),
            "complete": False,
            "client_id": client_id,
            "timestamp": datetime.now().isoformat()
        }

        # 如果是最后一个字符,标记完成
        if client_state["index"] == len(TEXT):
            response_data["complete"] = True

        return JSONResponse(content=response_data)
    else:
        # 超时返回
        return JSONResponse(content={
            "status": "timeout",
            "client_id": client_id,
            "timestamp": datetime.now().isoformat()
        })


# ========== 其他协议保持简单 ==========
@app.get("/bulk")
async def bulk_transfer():
    """整包传输"""
    await asyncio.sleep(1)
    return {
        "protocol": "整包传输",
        "text": TEXT,
        "total_chars": len(TEXT),
        "timestamp": datetime.now().isoformat()
    }


async def stream_generator():
    """流式传输"""
    for i, char in enumerate(TEXT):
        await asyncio.sleep(CHAR_DELAY)
        yield f"字符 {i + 1}: {char}\n"


@app.get("/stream")
async def stream_response():
    """流式传输端点"""
    return StreamingResponse(
        stream_generator(),
        media_type="text/plain"
    )


# ========== WebSocket ==========
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    print("WebSocket连接建立")

    try:
        data = await websocket.receive_json()

        if data.get("action") == "start":
            for i, char in enumerate(TEXT):
                await asyncio.sleep(CHAR_DELAY)
                await websocket.send_json({
                    "type": "char",
                    "char": char,
                    "index": i + 1,
                    "total": len(TEXT),
                    "protocol": "WebSocket"
                })

            await websocket.send_json({
                "type": "complete",
                "message": "传输完成"
            })

    except Exception as e:
        print(f"WebSocket错误: {e}")
    finally:
        print("WebSocket连接关闭")


# ========== 改进的HTML页面 ==========
@app.get("/", response_class=HTMLResponse)
async def home():
    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>网络传输协议演示</title>
        <meta charset="utf-8">
        <style>
            body {{
                font-family: Arial, sans-serif;
                max-width: 1000px;
                margin: 0 auto;
                padding: 20px;
                background: #f5f5f5;
            }}
            h1 {{ color: #333; text-align: center; }}
            .protocol {{
                background: white;
                border-radius: 8px;
                padding: 20px;
                margin: 20px 0;
                box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            }}
            .output {{
                background: #f8f9fa;
                border: 1px solid #ddd;
                border-radius: 4px;
                padding: 15px;
                margin: 10px 0;
                min-height: 100px;
                font-family: monospace;
                white-space: pre-wrap;
                word-wrap: break-word;
            }}
            button {{
                background: #4CAF50;
                color: white;
                border: none;
                padding: 10px 20px;
                margin: 5px;
                border-radius: 4px;
                cursor: pointer;
                font-size: 14px;
            }}
            button:hover {{ background: #45a049; }}
            button:disabled {{
                background: #cccccc;
                cursor: not-allowed;
            }}
            .status {{
                color: #666;
                font-size: 14px;
                margin-top: 10px;
            }}
            .error {{ color: #d32f2f; }}
            .success {{ color: #388e3c; }}
        </style>
    </head>
    <body>
        <h1>🌐 网络传输协议演示</h1>
        <p>演示文本: <strong>"{TEXT}"</strong></p>

        <div class="protocol">
            <h3>📦 1. 整包传输</h3>
            <p>一次性返回所有数据,连接立即关闭</p>
            <button onclick="testBulk()">测试整包传输</button>
            <div class="output" id="output-bulk"></div>
            <div class="status" id="status-bulk">等待测试</div>
        </div>

        <div class="protocol">
            <h3>🌊 2. 流式传输</h3>
            <p>分块传输,保持连接直到完成</p>
            <button onclick="testStream()">测试流式传输</button>
            <div class="output" id="output-stream"></div>
            <div class="status" id="status-stream">等待测试</div>
        </div>

        <div class="protocol">
            <h3>🔔 3. SSE (Server-Sent Events)</h3>
            <p>长连接,服务器单向推送</p>
            <button onclick="startSSE()" id="sse-start">开始SSE</button>
            <button onclick="stopSSE()" id="sse-stop" disabled>停止SSE</button>
            <div class="output" id="output-sse"></div>
            <div class="status" id="status-sse">等待连接</div>
        </div>

        <div class="protocol">
            <h3>🔄 4. 长轮询</h3>
            <p>客户端轮询,服务器保持连接直到有数据</p>
            <button onclick="startPolling()" id="polling-start">开始长轮询</button>
            <button onclick="stopPolling()" id="polling-stop" disabled>停止轮询</button>
            <div class="output" id="output-polling"></div>
            <div class="status" id="status-polling">等待开始</div>
        </div>

        <div class="protocol">
            <h3>⚡ 5. WebSocket</h3>
            <p>全双工长连接,真正实时通信</p>
            <button onclick="connectWebSocket()" id="ws-connect">连接WebSocket</button>
            <button onclick="startWebSocket()" id="ws-start" disabled>开始传输</button>
            <button onclick="disconnectWebSocket()" id="ws-stop" disabled>断开连接</button>
            <div class="output" id="output-websocket"></div>
            <div class="status" id="status-websocket">未连接</div>
        </div>

        <script>
            const text = "{TEXT}";
            let sseEventSource = null;
            let pollingActive = false;
            let pollingClientId = null;
            let pollingLastIndex = 0;
            let ws = null;

            // 1. 整包传输
            async function testBulk() {{
                const output = document.getElementById('output-bulk');
                const status = document.getElementById('status-bulk');

                output.textContent = '';
                status.textContent = '请求中...';
                status.className = 'status';

                try {{
                    const start = Date.now();
                    const response = await fetch('/bulk');
                    const data = await response.json();
                    const time = Date.now() - start;

                    output.textContent = data.text;
                    status.textContent = `✅ 完成!耗时${{time}}ms,共${{data.total_chars}}个字符`;
                    status.className = 'status success';
                }} catch (error) {{
                    status.textContent = `❌ 错误: ${{error.message}}`;
                    status.className = 'status error';
                }}
            }}

            // 2. 流式传输
            async function testStream() {{
                const output = document.getElementById('output-stream');
                const status = document.getElementById('status-stream');

                output.textContent = '';
                status.textContent = '流式接收中...';
                status.className = 'status';

                try {{
                    const response = await fetch('/stream');
                    const reader = response.body.getReader();
                    const decoder = new TextDecoder();

                    while (true) {{
                        const {{done, value}} = await reader.read();
                        if (done) {{
                            status.textContent = '✅ 流式传输完成';
                            status.className = 'status success';
                            break;
                        }}

                        const text = decoder.decode(value);
                        output.textContent += text;
                    }}
                }} catch (error) {{
                    status.textContent = `❌ 错误: ${{error.message}}`;
                    status.className = 'status error';
                }}
            }}

            // 3. SSE
            function startSSE() {{
                const output = document.getElementById('output-sse');
                const status = document.getElementById('status-sse');
                const startBtn = document.getElementById('sse-start');
                const stopBtn = document.getElementById('sse-stop');

                output.textContent = '';
                status.textContent = '正在连接SSE...';
                status.className = 'status';
                startBtn.disabled = true;
                stopBtn.disabled = false;

                // 创建EventSource连接
                sseEventSource = new EventSource('/sse');

                sseEventSource.onopen = function() {{
                    status.textContent = '✅ SSE已连接,等待数据...';
                    status.className = 'status success';
                    console.log('SSE连接已打开');
                }};

                sseEventSource.onmessage = function(event) {{
                    try {{
                        if (event.data) {{
                            const data = JSON.parse(event.data);

                            if (data.status === 'done') {{
                                status.textContent = '✅ 传输完成';
                                stopSSE();
                                return;
                            }}

                            if (data.char) {{
                                output.textContent += data.char;
                                status.textContent = `接收中... ${{data.index + 1}}/${{data.total}}`;
                            }}
                        }}
                    }} catch (e) {{
                        console.log('SSE消息:', event.data);
                        // 可能是完成消息
                        if (event.data.includes('"status":"done"')) {{
                            status.textContent = '✅ 传输完成';
                            stopSSE();
                        }}
                    }}
                }};

                sseEventSource.onerror = function(event) {{
                    status.textContent = '❌ SSE连接错误,正在重连...';
                    status.className = 'status error';
                    console.error('SSE错误:', event);
                }};
            }}

            function stopSSE() {{
                if (sseEventSource) {{
                    sseEventSource.close();
                    sseEventSource = null;
                }}

                document.getElementById('sse-start').disabled = false;
                document.getElementById('sse-stop').disabled = true;
                document.getElementById('status-sse').textContent = '已停止';
            }}

            // 4. 长轮询
            async function doPoll() {{
                if (!pollingActive) return;

                const output = document.getElementById('output-polling');
                const status = document.getElementById('status-polling');

                try {{
                    const url = `/long-polling?client_id=${{pollingClientId}}&last_index=${{pollingLastIndex}}`;
                    const response = await fetch(url);
                    const data = await response.json();

                    if (data.status === 'disconnected' || data.status === 'timeout') {{
                        // 超时或断开,继续轮询
                        if (pollingActive) {{
                            setTimeout(doPoll, 100);
                        }}
                        return;
                    }}

                    if (data.char) {{
                        output.textContent += data.char;
                        pollingLastIndex = data.index;
                        status.textContent = `接收中... ${{data.index}}/${{data.total}}`;

                        if (data.complete) {{
                            status.textContent = '✅ 传输完成';
                            stopPolling();
                            return;
                        }}
                    }}

                    // 继续下一次轮询
                    if (pollingActive) {{
                        setTimeout(doPoll, 100);
                    }}

                }} catch (error) {{
                    console.error('轮询错误:', error);
                    status.textContent = '❌ 轮询错误,重试中...';
                    status.className = 'status error';

                    if (pollingActive) {{
                        setTimeout(doPoll, 1000);
                    }}
                }}
            }}

            function startPolling() {{
                pollingActive = true;
                pollingClientId = 'client_' + Date.now();
                pollingLastIndex = 0;

                const output = document.getElementById('output-polling');
                const status = document.getElementById('status-polling');
                const startBtn = document.getElementById('polling-start');
                const stopBtn = document.getElementById('polling-stop');

                output.textContent = '';
                status.textContent = '开始长轮询...';
                status.className = 'status';
                startBtn.disabled = true;
                stopBtn.disabled = false;

                // 开始轮询
                doPoll();
            }}

            function stopPolling() {{
                pollingActive = false;
                document.getElementById('polling-start').disabled = false;
                document.getElementById('polling-stop').disabled = true;
                document.getElementById('status-polling').textContent = '已停止';
            }}

            // 5. WebSocket
            function connectWebSocket() {{
                const status = document.getElementById('status-websocket');
                const connectBtn = document.getElementById('ws-connect');
                const startBtn = document.getElementById('ws-start');
                const stopBtn = document.getElementById('ws-stop');

                status.textContent = '正在连接WebSocket...';
                status.className = 'status';

                ws = new WebSocket(`ws://${{window.location.host}}/ws`);

                ws.onopen = function() {{
                    status.textContent = '✅ WebSocket已连接';
                    status.className = 'status success';
                    connectBtn.disabled = true;
                    startBtn.disabled = false;
                    stopBtn.disabled = false;
                }};

                ws.onmessage = function(event) {{
                    const data = JSON.parse(event.data);
                    const output = document.getElementById('output-websocket');
                    const status = document.getElementById('status-websocket');

                    if (data.type === 'char') {{
                        output.textContent += data.char;
                        status.textContent = `接收中... ${{data.index}}/${{data.total}}`;
                    }} else if (data.type === 'complete') {{
                        status.textContent = '✅ ' + data.message;
                        status.className = 'status success';
                    }}
                }};

                ws.onerror = function(error) {{
                    status.textContent = '❌ WebSocket连接错误';
                    status.className = 'status error';
                }};

                ws.onclose = function() {{
                    status.textContent = 'WebSocket已断开';
                    connectBtn.disabled = false;
                    startBtn.disabled = true;
                    stopBtn.disabled = true;
                }};
            }}

            function startWebSocket() {{
                if (ws && ws.readyState === WebSocket.OPEN) {{
                    document.getElementById('output-websocket').textContent = '';
                    ws.send(JSON.stringify({{action: 'start'}}));
                    document.getElementById('status-websocket').textContent = '传输中...';
                }}
            }}

            function disconnectWebSocket() {{
                if (ws) {{
                    ws.close();
                }}
            }}

            // 页面加载时初始化
            window.addEventListener('beforeunload', function() {{
                stopSSE();
                stopPolling();
                if (ws) ws.close();
            }});
        </script>
    </body>
    </html>
    """
    return HTMLResponse(content=html)


# ========== 健康检查端点 ==========
@app.get("/health")
async def health_check():
    """健康检查端点"""
    return {
        "status": "healthy",
        "timestamp": datetime.now().isoformat(),
        "protocols": {
            "bulk": "/bulk",
            "stream": "/stream",
            "sse": "/sse",
            "long_polling": "/long-polling",
            "websocket": "/ws"
        }
    }


# ========== 运行代码 ==========
if __name__ == "__main__":
    import uvicorn

    print("=" * 50)
    print("网络传输协议演示服务器")
    print("=" * 50)
    print(f"访问地址: http://localhost:8000")
    print(f"演示文本: {TEXT}")
    print(f"字符延迟: {CHAR_DELAY}秒")
    print("=" * 50)

    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        log_level="info",
        access_log=True
    )

代码运行

复制代码
python demo.py
# 然后打开浏览器访问http://localhost:8000

浏览器Network标签页观察

  • 整包传输:单个请求,一次性返回
  • 流式传输:单个请求,持续接收chunked数据
  • SSE:长连接,持续接收事件
  • 长轮询:多个快速连续的请求
  • WebSocket:单独的WebSocket连接

性能比较

  • 延迟:WebSocket < SSE < 长轮询 < 流式传输 < 整包传输

  • 连接开销:长轮询 > SSE ≈ WebSocket > 流式传输 > 整包传输

  • 实时性:WebSocket ≈ SSE > 长轮询 > 流式传输 > 整包传输

参考

相关推荐
大布布将军2 小时前
⚡️编排的艺术:BFF 的核心职能——数据聚合与 HTTP 请求
前端·网络·网络协议·程序人生·http·node.js·改行学it
_F_y2 小时前
Socket编程UDP
网络·网络协议·udp
不染尘.18 小时前
UDP客户服务器模型和UDP协议
服务器·网络·网络协议·计算机网络·udp
爬山算法20 小时前
Netty(21)Netty的SSL/TLS支持是如何实现的?
网络·网络协议·ssl
渡我白衣21 小时前
计算机组成原理(7):定点数的编码表示
汇编·人工智能·嵌入式硬件·网络协议·机器学习·硬件工程
Bruce_Liuxiaowei1 天前
网站敏感文件_目录大全(分类记忆+风险标注)
运维·网络·网络协议·http·网络安全·https
猛喝威士忌1 天前
【虚拟机】使用OpenWrt作为虚拟机集群的软路由(下)
linux·网络协议
2401_890443021 天前
传输层协议TCP
网络·网络协议·tcp/ip
玥轩_5211 天前
静态路由原理 及实验案例
网络·网络协议·网络安全·智能路由器·路由器·交换机