news 2026/10/4 17:20:20

SSE知识梳理(1)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSE知识梳理(1)

作者:没有四次元口袋的蓝胖
日期:2026-10-03
标签:SSE, 流式响应


SSE知识梳理(1)

你有没有注意过 ChatGPT 的回答是一个字一个字"打"出来的?这不是前端特效,而是后端真的在一边生成一边发送数据——这就是流式响应。实现流式响应有多种技术方案,其中SSE(Server-Sent Events)是最轻量、最适合"服务端单向推送"场景的方案。

这篇笔记聚焦 SSE 的基础知识:协议是什么、数据格式长什么样、和 WebSocket 有什么区别,以及如何在 Python/Java/前端实现同步流式调用。掌握这些内容,面试时就能把 SSE 的基本面讲清楚。

核心掌握:SSE 协议原理、数据格式、与 WebSocket 对比、FastAPI StreamingResponse、前端 EventSource、Java SseEmitter。


一、SSE是什么

1.1 基本概念

SSE(Server-Sent Events,服务器推送事件)是一种基于 HTTP 的单向流式传输技术。它允许服务器在建立连接后,持续向客户端推送文本数据,而客户端只需被动接收。

核心特点:

  • 单向通信:服务器 → 客户端,客户端不能通过 SSE 向服务器发送数据
  • 基于 HTTP:使用标准 HTTP 协议,不需要额外协议握手
  • 文本格式:只支持文本数据(UTF-8),不支持二进制
  • 自动重连:浏览器原生 EventSource API 自带断线重连机制
  • 轻量简单:比 WebSocket 简单得多,不需要独立的服务器组件

要点:SSE 的本质是"服务器往客户端推数据的单向管道"。它不改变 HTTP 的请求-响应模型,只是让响应体变得"无限长"——服务器可以持续往里写数据,客户端持续读取。

1.2 SSE在HTTP协议层面的工作原理

理解 SSE,需要先理解它是如何在 HTTP 协议层面工作的。

传统 HTTP 请求-响应:客户端发送请求 → 服务器处理 → 一次性返回完整响应 → 连接关闭。

SSE 的 HTTP 请求-响应:客户端发送请求 → 服务器返回响应头(Content-Type: text/event-stream) →连接不关闭→ 服务器持续往响应体中写数据 → 客户端持续读取 → 直到服务器发送结束标记或连接断开。

这里的关键是 HTTP 的Chunked Transfer Encoding(分块传输编码)。当服务器使用 chunked 编码时,响应头中没有Content-Length,服务器可以将响应体分成多个"块"(chunk)逐步发送。每个 chunk 包含一块数据和长度信息,客户端收到一个 chunk 就处理一个 chunk,不需要等全部数据到达。

HTTP/1.1 200 OK Content-Type: text/event-stream Transfer-Encoding: chunked Cache-Control: no-cache Connection: keep-alive # 第一个chunk data: {"content": "你"} # 第二个chunk(100ms后发送) data: {"content": "好"} # 第三个chunk(200ms后发送) data: {"content": "!"} # 结束标记 data: [DONE]

为什么要理解 Chunked Transfer Encoding:面试中如果被追问"SSE 和普通的 HTTP 长连接有什么区别",核心答案就是 chunked 编码。普通的 HTTP 响应是有Content-Length的,客户端知道什么时候接收完毕;SSE 的响应使用 chunked 编码,没有Content-Length,客户端需要一直等待,直到收到结束标记。这就是 SSE 能实现"流式"的底层 HTTP 机制。

1.3 SSE的数据格式

SSE 使用纯文本格式传输数据,每条消息以data:开头,以\n\n结尾:

# SSE协议格式 event: message\n id: 1\n retry: 5000\n data: {"text": "Hello"}\n \n data: {"text": "World"}\n \n # 解读: # event: 事件类型(可选,默认"message") # id: 消息ID(可选,用于断线重连时的 Last-Event-ID) # retry: 重连间隔毫秒数(可选,客户端多久后尝试重连) # data: 消息内容(必填,可以多行,每行一个 data: 前缀) # \n\n: 空行表示一条消息结束

字段说明:

字段是否必须作用示例
data:✅ 必填消息内容,可以多行data: {"msg": "hello"}
event:可选事件类型,默认messageevent: notification
id:可选消息ID,用于断线重连id: 42
retry:可选重连间隔(毫秒)retry: 5000
空行\n\n✅ 必须表示一条消息结束(空行)

多行数据示例:

data: {"line1": "这是第一行"} data: {"line2": "这是第二行"} # 客户端收到的 event.data 会是: # {"line1": "这是第一行"}\n{"line2": "这是第二行"} # 注意多行之间用 \n 连接

一个实际的SSE响应示例(模拟AI对话流式输出):

HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive data: {"content": "你"} data: {"content": "好"} data: {"content": "!"} data: [DONE]

要点:Content-Type: text/event-stream是 SSE 的标志性响应头,浏览器据此识别这是一个 SSE 流。每条消息之间用空行(\n\n)分隔,这个空行不能省略。实际开发中,大部分场景只需要data:字段就够了,event:、id:、retry:在需要高级功能时才使用。

1.4 SSE vs WebSocket vs 长轮询

这是面试高频对比题:

维度SSEWebSocket长轮询
通信方向单向(服务器→客户端)双向请求-响应
协议标准 HTTPws:// / wss://标准 HTTP
数据格式文本(UTF-8)文本 + 二进制文本
浏览器支持原生 EventSource原生 WebSocket原生 XMLHttpRequest
断线重连自动重连(内置)需手动实现需手动实现
实现复杂度低中中
服务器兼容性好(标准HTTP)需独立WebSocket服务器好
代理/防火墙穿透好可能被拦截好
适用场景AI对话、通知推送、实时行情聊天室、游戏、协作编辑简单实时通知
并发连接开销中低高(频繁建立连接)

逐项解读:

  • 通信方向:SSE 是单向管道,服务器只能往客户端推数据;WebSocket 是全双工通道,双方都能随时发消息;长轮询本质还是请求-响应,只是服务器"hold住"请求直到有新数据才返回。
  • 协议:SSE 基于标准 HTTP,和普通的 GET 请求没有本质区别,只是响应体是持续的;WebSocket 需要先通过 HTTP 做协议升级(Upgrade: websocket),之后切换到 ws:// 协议。
  • 断线重连:这是 SSE 最大的优势之一。浏览器 EventSource 在连接断开时会自动重连,并在请求头中携带Last-Event-ID,服务端可以据此补发遗漏消息。WebSocket 的断线重连需要开发者自己实现。
  • 代理/防火墙穿透:SSE 走标准 HTTP,和普通的网页请求走一样的通道,几乎不会被拦截。WebSocket 使用自定义协议,有些公司防火墙会拦截 ws:// 协议的连接。

选型建议:

  • 只需要服务器向客户端推送 →SSE(如 ChatGPT 的流式输出、股票行情推送)
  • 需要双向实时通信 →WebSocket(如在线聊天、多人协作)
  • 兼容极老旧浏览器 →长轮询(现在几乎不需要了)

1.5 常见面试注意点

  • SSE 只支持文本:不能传二进制数据(图片、音频等),需要二进制用 WebSocket。
  • SSE 基于 HTTP:不需要像 WebSocket 那样做协议升级,天然兼容负载均衡、CDN、代理等基础设施。
  • 自动重连是内置的:浏览器 EventSource 在连接断开时会自动重连,并携带Last-Event-ID请求头,服务端可以据此补发遗漏的消息(这个特性在第三篇会详细讲)。
  • SSE 有浏览器连接数限制:同一域名下,HTTP/1.1 最多 6 个 SSE 连接。HTTP/2 下没有此限制(多路复用)。面试提到这点会加分。
  • SSE 的历史:SSE 最早由 WHATWG 在 HTML5 规范中定义,2009 年就被 Firefox 支持。虽然历史比 WebSocket 更早,但因为功能相对简单(只支持单向),一直没有 WebSocket 那么"出名"。直到 AI 大模型兴起,SSE 因为轻量、简单、适合单向推送的特点,才重新被广泛关注。
  • SSE 没有标准化客户端库的"统一体验":虽然 EventSource 是 W3C 标准,但不同浏览器在实现细节上有差异(如重连行为)。生产环境建议使用@microsoft/fetch-event-source等封装库来统一行为。

二、同步流式调用

2.1 服务端逐chunk返回(Python FastAPI)

流式响应的核心思想:不等待整个响应生成完毕,而是边生成边发送。以 Python 的 FastAPI 为例:

fromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponseimporttime app=FastAPI()defgenerate_content():"""模拟AI逐字生成内容"""content="你好,我是AI助手,很高兴为你服务!"forcharincontent:yieldf"data: {{\"content\": \"{char}\"}}\n\n"time.sleep(0.1)# 模拟生成延迟yield"data: [DONE]\n\n"# 结束标记@app.get("/api/chat")defchat():returnStreamingResponse(generate_content(),media_type="text/event-stream",headers={"Cache-Control":"no-cache","Connection":"keep-alive","X-Accel-Buffering":"no",# 禁用 Nginx 缓冲})

关键点解读:

配置作用
yieldPython 生成器,每次产出一块数据,函数不退出,保持连接
text/event-streamSSE 标准 Content-Type,浏览器据此识别为 SSE 流
Cache-Control: no-cache禁止缓存,每条数据都要实时送达
X-Accel-Buffering: no告诉 Nginx 不要缓冲响应,直接转发给客户端

要点:yield是同步流式的核心。Python 的生成器(generator)每次执行到yield时会暂停并返回一个值,下次迭代时从暂停处继续。StreamingResponse会不断调用生成器,把每次yield的数据写入 HTTP 响应体并立即发送给客户端。

生成器的工作原理:

# 生成器的执行流程:# 1. 调用 generate_content() 时,不会立即执行函数体# 2. 第一次 next() 时,执行到第一个 yield,返回 "data: {...你...}\n\n"# 3. StreamingResponse 把这个数据写入 HTTP 响应体并发送# 4. 第二次 next() 时,从 time.sleep(0.1) 之后继续执行# 直到下一个 yield,返回 "data: {...好...}\n\n"# 5. 重复步骤 3-4,直到循环结束# 6. 最后一个 yield 返回 "data: [DONE]\n\n"# 7. 生成器抛出 StopIteration,StreamingResponse 关闭响应# 这就是"流"的本质:函数没有退出,而是一点一点地"流"出数据

2.2 客户端同步接收

前端有两种方式接收 SSE 数据:

方式1:EventSource API(最简单,浏览器原生支持)

consteventSource=newEventSource('/api/chat');// EventSource 有三个状态(readyState):// 0 = CONNECTING 正在连接// 1 = OPEN 连接已建立,可以接收数据// 2 = CLOSED 连接已关闭eventSource.onopen=function(){console.log('SSE连接已建立');};eventSource.onmessage=function(event){console.log('收到数据:',event.data);if(event.data==='[DONE]'){eventSource.close();// 收到结束标记,关闭连接console.log('生成完毕');return;}constdata=JSON.parse(event.data);displayText+=data.content;// 追加到页面renderToDOM(displayText);// 渲染到页面(打字机效果)};eventSource.onerror=function(error){console.error('SSE连接错误:',error);// 注意:如果不手动 close(),EventSource 会自动重连// 如果不想重连,需要在这里手动 closeeventSource.close();};

EventSource 的进阶用法——监听自定义事件类型:

// 如果服务端发送了 event: notification 类型的事件// 不能用 onmessage 接收,需要用 addEventListenereventSource.addEventListener('notification',function(event){console.log('收到通知:',event.data);});eventSource.addEventListener('price_update',function(event){console.log('价格更新:',event.data);});// onmessage 只接收默认类型(没有 event 字段或 event: message)的消息// addEventListener 可以接收指定类型的事件

方式2:fetch + ReadableStream(更灵活,支持POST请求)

asyncfunctionfetchStream(){constresponse=awaitfetch('/api/chat');constreader=response.body.getReader();constdecoder=newTextDecoder();letdisplayText='';while(true){const{done,value}=awaitreader.read();if(done)break;constchunk=decoder.decode(value,{stream:true});// 解析SSE格式,提取data字段constlines=chunk.split('\n');for(constlineoflines){if(line.startsWith('data: ')){constdata=line.slice(6);if(data==='[DONE]')return;constparsed=JSON.parse(data);displayText+=parsed.content;renderToDOM(displayText);}}}}

两种方式的对比:

  • EventSource:使用简单,自带断线重连,但只支持 GET 请求,不能发送自定义请求头。
  • fetch + ReadableStream:灵活,支持 POST、自定义请求头,但需要自己处理缓冲区和重连。
  • 实际项目中,聊天场景因为需要 POST 传递消息体,更多用 fetch 方式。EventSource 适合简单的 GET 场景(如通知推送、行情订阅)。

2.3 Java实现示例(Spring Boot)

作为 Java 方向的面试,也需要了解 Java 端的实现。Spring Boot 提供了两种方式:

方式1:Spring WebFlux(响应式)

WebFlux 是 Spring 5 引入的响应式 Web 框架,基于 Reactor 模式。它天生支持非阻塞流式响应,是 Spring 生态中做 SSE 的"正统"方式。

@RestController@RequestMapping("/api")publicclassChatController{@GetMapping(value="/chat",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<ServerSentEvent<String>>chat(){// 使用 Reactor 的 Flux 实现流式响应Stringcontent="你好,我是AI助手,很高兴为你服务!";returnFlux.fromArray(content.split("")).delayElements(Duration.ofMillis(100))// 每个字符间隔100ms.map(char_->ServerSentEvent.<String>builder().data("{\"content\": \""+char_+"\"}").build()).concatWith(Flux.just(ServerSentEvent.<String>builder().data("[DONE]").build()));}}

核心概念解读:

  • Flux<T>:Reactor 中的响应式序列,表示 0 到 N 个元素的异步序列。类似于 Java 8 的Stream,但是异步非阻塞的。
  • delayElements():在每个元素之间插入延迟,不会阻塞线程,而是调度到事件循环。
  • ServerSentEvent<T>:Spring 提供的 SSE 消息封装,支持data、event、id、retry等字段。
  • MediaType.TEXT_EVENT_STREAM_VALUE:等同于"text/event-stream",Spring 的常量定义。

方式2:SseEmitter(传统 Servlet 方式,不依赖 Reactor)

如果你的项目是 Spring MVC(基于 Servlet),不想引入 WebFlux 的依赖,可以用SseEmitter。它是 Spring MVC 对 SSE 的原生支持。

@GetMapping(value="/chat/traditional",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterchatTraditional(){SseEmitteremitter=newSseEmitter(60_000L);// 超时60秒// 注册回调(可选)emitter.onCompletion(()->System.out.println("SSE连接完成"));emitter.onTimeout(()->System.out.println("SSE连接超时"));emitter.onError(e->System.err.println("SSE连接错误: "+e));CompletableFuture.runAsync(()->{try{Stringcontent="你好,我是AI助手!";for(charc:content.toCharArray()){emitter.send(SseEmitter.event().data("{\"content\": \""+c+"\"}"));Thread.sleep(100);}emitter.send(SseEmitter.event().data("[DONE]"));emitter.complete();// 标记完成}catch(Exceptione){emitter.completeWithError(e);// 标记错误}});returnemitter;}

Java 两种方式对比:

  • Flux<ServerSentEvent>:响应式编程,非阻塞,适合高并发场景,但学习曲线较陡。
  • SseEmitter:传统方式,异步但底层仍是 Servlet 模型,上手更快,适合 Spring MVC 项目。
  • 面试中如果被问到"SSE 在 Java 中怎么实现",能把两种方式都提到并说清区别,会非常加分。
  • 注意SseEmitter的超时设置:new SseEmitter(60_000L)表示 60 秒无数据则超时断开。AI 对话场景中可能需要设更大的值。

2.5 同步流式的局限性

同步流式虽然简单直观,但在实际场景中有明显的局限性:

  • 阻塞线程:time.sleep(0.1)或Thread.sleep(100)会阻塞当前线程。如果服务器只有 4 个 worker 线程,同时就只能处理 4 个 SSE 请求。第 5 个请求必须等待。
  • 无法并行调用外部服务:如果后端需要调用 AI API(响应时间几秒到几十秒),同步方式会长时间占用线程。
  • 不适合高并发:每个 SSE 连接独占一个线程,100 个并发用户就需要 100 个线程,资源消耗大。

同步流式的适用场景:

  • 数据量小、并发量低的场景(如内部工具、demo 演示)
  • 不需要调用外部服务的场景(如从本地文件或数据库读取数据)
  • 快速验证想法、原型开发

什么时候必须用异步流式:

  • 需要调用外部 AI API(OpenAI、通义千问等),响应时间长
  • 并发量大(几十上百个同时在线用户)
  • 对资源利用率有要求(不想为每个连接分配一个线程)

一个简单的性能对比:假设后端需要处理 100 个并发的 SSE 连接,每个连接持续 30 秒(模拟 AI 生成回答)。

方案线程数内存占用能否处理
同步(每连接一线程)100~800MB(每线程8MB栈空间)勉强可以,但资源浪费
异步(asyncio)1~50MB轻松处理

这就是异步的核心优势:同样的硬件,能处理更多的并发连接。对于 SSE 这种"长连接、低带宽"的场景,异步模型几乎是唯一合理的选择。

解决方案:这就是为什么需要异步流式——用 asyncio 的事件循环代替线程阻塞,单线程就能处理大量并发连接。这将在下一篇详细讲解。


🗺️ 思维导图速览

SSE基础:协议原理与同步流式 │ ├── SSE是什么 │ ├── 基于HTTP的单向流式传输 │ ├── 服务器→客户端,客户端被动接收 │ ├── Content-Type: text/event-stream │ ├── 只支持文本(UTF-8),不支持二进制 │ └── HTTP底层:Chunked Transfer Encoding │ ├── 无Content-Length,响应体持续写入 │ └── 客户端边读边处理,不等全部数据 │ ├── SSE数据格式 │ ├── data: 消息内容\n\n(必填) │ ├── event: 事件类型(可选,默认message) │ ├── id: 消息ID(可选,用于断线重连) │ ├── retry: 重连间隔毫秒数(可选) │ ├── 多行data用\n连接 │ └── 空行\n\n表示一条消息结束 │ ├── 技术对比(面试高频) │ ├── SSE:单向、HTTP、自动重连、简单 → AI对话/推送 │ ├── WebSocket:双向、ws协议、手动重连 → 聊天/游戏 │ └── 长轮询:请求-响应、HTTP、手动重连 → 旧项目兼容 │ ├── 同步流式实现 │ ├── Python │ │ ├── FastAPI + StreamingResponse + yield 生成器 │ │ ├── 生成器原理:yield暂停→返回→继续 │ │ ├── 关键配置:Cache-Control、X-Accel-Buffering │ │ └── [DONE]标记表示流结束 │ │ │ ├── Java │ │ ├── Spring WebFlux: Flux<ServerSentEvent>(响应式) │ │ ├── Spring MVC: SseEmitter(传统Servlet方式) │ │ └── WebFlux非阻塞 vs SseEmitter异步线程 │ │ │ └── 前端 │ ├── EventSource │ │ ├── readyState: CONNECTING/OPEN/CLOSED │ │ ├── onmessage / addEventListener(自定义事件) │ │ └── 简单、GET、自动重连 │ └── fetch+ReadableStream │ ├── 灵活、支持POST、自定义Header │ └── 需手动处理缓冲区和重连 │ ├── 同步流式局限性 │ ├── 阻塞线程(time.sleep/Thread.sleep) │ ├── 无法并行调用外部服务 │ └── 不适合高并发 → 需要异步流式 │ └── 关键注意点 ├── yield 是流式的核心:暂停→返回→继续 ├── [DONE] 标记表示流结束 ├── X-Accel-Buffering: no 禁用Nginx缓冲 ├── HTTP/1.1同一域名6个SSE连接限制 └── Chunked Transfer Encoding 是底层机制

📝 写在最后

本篇要点回顾

这篇笔记覆盖了 SSE 的基础面:

  • 协议是什么:基于 HTTP 的单向流式传输,底层依赖 Chunked Transfer Encoding
  • 数据格式:data:+\n\n,可选event:、id:、retry:字段
  • 与 WebSocket 的对比:单向 vs 双向、HTTP vs ws、自动重连 vs 手动重连、文本 vs 二进制
  • 同步流式调用:Python(FastAPI StreamingResponse + yield 生成器)和 Java(WebFlux Flux / SseEmitter)两端的实现
  • 前端接收:EventSource(简单,GET,自动重连)vs fetch + ReadableStream(灵活,POST,手动处理)
  • 同步流式的局限性:阻塞线程,不适合高并发,引出异步流式的需求

学习建议

  1. 先跑通最简单的例子:写一个 FastAPI 的 SSE 接口 + 一个 EventSource 的前端页面,看到逐字输出的效果,理解整个链路。
  2. 重点理解"流"的概念:流式响应不是"一次性返回所有数据",而是"边生成边发送"。yield是 Python 中实现这个概念的关键工具。理解生成器的"暂停→返回→继续"机制,就理解了流式响应的核心。
  3. 理解 HTTP 底层:SSE 之所以能"流式"传输,是因为 HTTP 的 Chunked Transfer Encoding。面试追问"和长连接有什么区别"时,能说出 chunked 编码会非常加分。
  4. 对比表格要记牢:SSE vs WebSocket 的对比是面试必考题。不需要逐字背诵,但要能说出核心区别:通信方向、协议、数据格式、重连机制、适用场景。
  5. Java 两种实现都了解:面试问到 Java 方向时,能说出 WebFlux 的响应式方式和 SseEmitter 的传统方式,并解释区别,会是很大的加分项。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 17:19:54

中断里调用malloc导致偶发死机?嵌入式RTOS故障排查实录

凌晨三点半&#xff0c;产线上的一台采集设备死机了。面板无响应&#xff0c;串口不再输出任何日志&#xff0c;看门狗也没能把它拉回来。重启后一切正常&#xff0c;可再过几小时或者一两天&#xff0c;同样的偶发死机又随机出现。这不是第一次了——三周内同一批设备已经报了…

作者头像 李华
网站建设 2026/10/4 17:19:32

AI Native不是口号:可落地的研发流程重构手册

AI Native这个词&#xff0c;最近被提到得有点泛滥了。不少团队号称“AI Native”&#xff0c;实际就是给IDE装了个补全插件&#xff0c;或者让程序员用ChatGPT查报错——这不叫范式转变&#xff0c;这叫“用AI辅助写代码”。真正的AI Native团队&#xff0c;是把AI嵌入到需求拆…

作者头像 李华
网站建设 2026/10/4 17:16:34

MCP协议重写:从Session到Stateless与MRTR的迁移实战

1. 这次重写到底动了什么&#xff1a;从 Session 到 Stateless 的底层逻辑MCP 把自己推翻重写这件事&#xff0c;在圈子里其实没引起太大动静&#xff0c;但我身边几个真正在生产环境里跑 MCP 服务的朋友&#xff0c;反应都挺一致&#xff1a;早该这么干了。原因很简单&#xf…

作者头像 李华
网站建设 2026/10/4 17:06:50

CPU正常运行时间过长导致系统卡顿?从原理到实践的彻底解决方案

电脑用着用着越来越卡&#xff0c;任务管理器打开一看&#xff0c;CPU占用并不高&#xff0c;但系统反应就是迟钝。这时候很多人会习惯性去看“性能”标签页&#xff0c;结果一眼看到“运行时间”那栏赫然写着“30天14小时23分”&#xff0c;心里大概就有数了——这台机器已经连…

作者头像 李华
网站建设 2026/10/4 17:05:30

插件加载失败排查全指南:从plugins机制到entries did not activate

说到 plugins&#xff0c;恐怕每个开发者都不陌生&#xff0c;但真正能把插件加载失败这件事一次说清楚的&#xff0c;反而不多。我最近连续处理了几个跟“failed to load plugins”相关的病例&#xff0c;有 IDE 里的插件管理器&#xff0c;有音乐播放器的音源扩展&#xff0c…

作者头像 李华
网站建设 2026/10/4 17:04:45

AutoLISP实现两期方格网土方量自动计算实战

做了这么多年测绘和土木项目管理&#xff0c;土方量计算大概是每个项目进场都要碰一遍的活。以前没有参考模板的时候&#xff0c;手算方格网能把人算到怀疑人生。从哪一天开始&#xff1f;大概是从我在CAD里用AutoLISP写了一版两期土方量自动计算程序之后&#xff0c;三五十个方…

作者头像 李华