<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Http on 张欣耕的个人博客</title><description>Recent content in Http on 张欣耕的个人博客</description><link>https://shanechang.com/zh-cn/tags/http/</link><language>zh-cn</language><lastBuildDate>Wed, 13 May 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/zh-cn/tags/http/index.xml" rel="self" type="application/rss+xml"/><item><title>买烤面包机的艺术</title><link>https://shanechang.com/zh-cn/p/networking-journey-buying-a-toaster/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/networking-journey-buying-a-toaster/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.CGOZWnV8_Z1qvbwJ.webp&quot; alt=&quot;Featured image of post 买烤面包机的艺术&quot; /&gt;&lt;p&gt;如果你是新读者:在上一篇&lt;a href=&quot;../networking-journey-portainer-and-switches/&quot;&gt;交换机到底在干嘛&lt;/a&gt;里,我承认自己在网络这事上”嘴上有功底”,然后认真补课,终于理解了:为什么一个监听在 127.0.0.1 的服务,会对那个通过 10.88.0.1 过来的容器完全隐身。&lt;/p&gt;
&lt;p&gt;对老朋友们:这篇是一次有意为之的小小”岔路”。&lt;/p&gt;
&lt;h2 id=&quot;小尴尬&quot;&gt;小尴尬&lt;/h2&gt;
&lt;p&gt;写第一篇的中途,我试图把新学的东西讲给一个不写代码的朋友听。晚餐时她礼貌地问:你这周在发什么技术疯?&lt;/p&gt;
&lt;p&gt;我说了大概三句话,她的眼睛就开始出现那种神奇的微光。&lt;/p&gt;
&lt;p&gt;你懂的——那种”我爱你我在场,但我已经听不懂中文了”的微光。我已经说出了 TCP, 内核, bridge interface,下一步就要爆出 MAC address,幸好我及时刹车。&lt;/p&gt;
&lt;p&gt;这时我发现一个问题:我现在会解释交换机了;我能解释虚拟桥;我能解释为什么 Portainer 容器看不见我的 SSH 隧道。可如果她问我——从她在网站上点下”购买”的那一刻起,到页面回个”下单成功”为止,整件事到底一秒不落地发生了什么? ——我会直接开摆。我有很多零件,但没有一个完整的故事。&lt;/p&gt;
&lt;p&gt;于是我决定写一个。一个真的故事。有角色,有场景,有戏剧冲突。尽量不说术语。甚至要做到能在餐桌上讲完的那种。&lt;/p&gt;
&lt;p&gt;“解码环”(每个角色在计算机世界里到底是谁)在文末。请先看故事。尤其是你从没认真想过网站”到底怎么工作”的话,更要先看。&lt;/p&gt;
&lt;h2 id=&quot;出场人物&quot;&gt;出场人物&lt;/h2&gt;
&lt;p&gt;先认识一下小小的演员表,他们出场快,退场也快。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jack。一个在家网购的男人。他只想买台烤面包机。这就是他全部的人设。&lt;/li&gt;
&lt;li&gt;浏览器小助手(Browser-Bot)。住在 Jack 的笔记本里。写便条, 打包包裹, 交给快递。忠诚, 龟毛。&lt;/li&gt;
&lt;li&gt;信使之海(The Sea of Couriers)。一整片在 Jack 家和商店之间的接力快递大军。每个人只知道”下一个该交给谁”。他们不看信,只转运。&lt;/li&gt;
&lt;li&gt;前门(The Front Gate)。遥远写字楼门口冷静的小门童。只接收投递到”这栋楼”的包裹,其他一律拒收。不会读书。&lt;/li&gt;
&lt;li&gt;内核妈妈(Mama Kernel)。办公室经理。知道每个门, 每张桌, 每位住户。她桌上有三样法宝:整栋楼的分机黄页, 一本改地址的规则书, 和一大本”今天收了啥我怎么处理的”的流水账。冷静, 稳重, 百战成精。&lt;/li&gt;
&lt;li&gt;前置文员与后置文员(Pre-Clerk &amp;#x26; Post-Clerk)。成对上班的小助理。前置文员负责每个入站包裹,有时会改改包裹外面的地址;后置文员管出站的包裹——并且手抄了一份前置文员做过的所有修改,好在出门前把地址”改回去”。&lt;/li&gt;
&lt;li&gt;走廊礼宾(The Hallway Concierge)。管楼内走廊。认得每位住户的脸。他的唯一超能力:把包裹沿着走廊”横着”送到对的门口——不上不下,只走平面。&lt;/li&gt;
&lt;li&gt;前端弗里达(Frieda the Frontend)。住在走廊里,10 号。脾气好,像个前台:接到访客的包裹,看看要求,要么自己处理,要么礼貌地转交给能处理的人。&lt;/li&gt;
&lt;li&gt;后端布鲁图(Brutus the Backend)。10 号隔壁,11 号。粗声粗气的账房先生。不和外人说话。只接弗里达递过来的包裹。真正干活的——开账本, 扣库存, 记订单。&lt;/li&gt;
&lt;li&gt;包裹(The Parcels)。故事里的每条消息都是俄罗斯套娃:一封信装进一个信封,塞进一个小包裹,再塞进一个大邮袋。每一层都写给不同的人,走不同的路。我们会看它们一层层拆开, 又一层层重新包好。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;齐活儿,开戏。&lt;/p&gt;
&lt;h2 id=&quot;第一幕点击&quot;&gt;第一幕——点击&lt;/h2&gt;
&lt;p&gt;Jack 想要一台烤面包机。&lt;/p&gt;
&lt;p&gt;其实他已经惦记了好几周。旧的上周日伴着一缕青烟英勇就义,吓得猫当场换了个房间。他一直在拖,因为网购总感觉比它应有的”更郑重”。但今天,他下定决心。&lt;/p&gt;
&lt;p&gt;他滑啊滑。挑中一个。点下”购买”。&lt;/p&gt;
&lt;p&gt;Jack 电脑角落里,浏览器小助手噌地弹起,打着领结,手持小夹板。他写下一张短短的便条:这位顾客想买一台烤面包机。然后开始为它披挂远行。&lt;/p&gt;
&lt;p&gt;先把便条装进一个小信封,上面写好小助手一手好字的地址。再把这个信封装进一个更大的包裹,写上”商店”的远方地址。然后整个包裹再塞进一个外层邮袋,这一层写的,是门口第一位接力快递的地址。&lt;/p&gt;
&lt;p&gt;三层。三个地址。每一层负责一段路。小助手今天已经这么干了一千遍。他把邮袋递出门外。&lt;/p&gt;
&lt;h2 id=&quot;第二幕穿越信使之海&quot;&gt;第二幕——穿越信使之海&lt;/h2&gt;
&lt;p&gt;第一位接力员早就等着。她接过邮袋,看一眼最外层的地址,撒腿就跑。&lt;/p&gt;
&lt;p&gt;她跑到下一位,撕掉外层邮袋——这一层使命已毕——看内层的地址,再用”她自己”的新邮袋包上,写上下一位接力的地址,交棒。&lt;/p&gt;
&lt;p&gt;如此往复。&lt;/p&gt;
&lt;p&gt;每位接力员都做同样的事:撕掉只用于上一段路的外层,看看里面的地址,再为下一段路重新打包。那个内层包裹——写着商店真正地址的那个——从不被打开。它是神圣的。它是被指定要”端到端旅行”的。&lt;/p&gt;
&lt;p&gt;如果你能把时间快进,从高空俯瞰,你会看到一个光点在接力员之间跳跃,穿越半个大陆,身后落满被丢弃的邮袋。最里面的信,相对它的信封没有移动;内层包裹也没有改变。只有最外面的那层,不断地重生。&lt;/p&gt;
&lt;p&gt;最终,在人类时间里只是眨眼一瞬,包裹抵达它命定的那栋写字楼。最后一名接力把它交到前门手里,敬个礼,转身继续夜色中的奔波。&lt;/p&gt;
&lt;h2 id=&quot;第三幕剥洋葱&quot;&gt;第三幕——剥洋葱&lt;/h2&gt;
&lt;p&gt;前门对邮袋内容不感兴趣。它只关心最外层写的是不是”这栋楼的名字”。确认无误,收下。它的使命完成。&lt;/p&gt;
&lt;p&gt;包裹被送进后勤办公室,内核妈妈正等着。&lt;/p&gt;
&lt;p&gt;她轻轻拾起,撕下外层邮袋——那只服务于最后一段路。她把里面的包裹放在桌上。这一层的地址的确是写给这栋楼的,而且是”更正式”的写法。对我们有利。&lt;/p&gt;
&lt;p&gt;她正要再拆一层,前置文员出现在她身边。&lt;/p&gt;
&lt;p&gt;“这件是 10 号公寓的,“他说,“销售咨询。弗里达管。”&lt;/p&gt;
&lt;p&gt;内核妈妈点点头。前置文员拿起铅笔,轻轻在包裹外面动了两笔——把”整栋楼”的收件名,改成了”10 号公寓”的具体门牌。他翻开账本认真记下:包裹 #8472,原送达大楼,重定向至 10 号。回程记得改回。记得二字他画了三道杠。他对工作极其较真。&lt;/p&gt;
&lt;p&gt;内核妈妈拿着改好抬头的包裹,翻目录。10 号——在走廊那边,礼宾的地盘。她又加了一层小小的”楼内快递套”,直接写明”收:弗里达”,从窗口递给走廊礼宾。&lt;/p&gt;
&lt;h2 id=&quot;第四幕走廊&quot;&gt;第四幕——走廊&lt;/h2&gt;
&lt;p&gt;礼宾早已把走廊住户的脸背得滚瓜烂熟。前端弗里达,10 号——稳。三步并作两步,唰地把包裹从她门缝塞入。&lt;/p&gt;
&lt;p&gt;弗里达心情一如既往地阳光。她拆套——楼内快递套, 楼层那层, 一路上的原始旅行包裹——最后打开小信封,读到里面的便条。&lt;/p&gt;
&lt;p&gt;“这位顾客想买一台烤面包机。”&lt;/p&gt;
&lt;p&gt;“好嘞! “弗里达自言自语。“这是布鲁图的活儿。”&lt;/p&gt;
&lt;p&gt;她抽出一张白纸,写下”有顾客想买一台烤面包机——请处理”。装进她自己的小信封,抬头写”11 号”,再套上她自己的小快递套,从门缝推出去给礼宾。&lt;/p&gt;
&lt;p&gt;这点很关键。弗里达没有把”原包裹”转发给布鲁图;她是写了”一份新的”包裹,代表她自己向布鲁图提出同一请求。原包裹就像一张凭据,被她留在桌上。待会儿还要用。&lt;/p&gt;
&lt;p&gt;礼宾接过弗里达的新包裹——沿走廊三步——塞入布鲁图的门缝。&lt;/p&gt;
&lt;p&gt;11 号屋里,布鲁图没抬头,听见包裹落地,叹了口气。拆。读。嘟囔:“烤面包机。顾客。行吧。”&lt;/p&gt;
&lt;p&gt;他翻开大账本。扣掉一台烤面包机库存。在 Jack 的名下记一笔。砸上一个鲜红的大章”CONFIRMED”。再写一小条回执——“已接单,将发货,ID #42”——装进写给弗里达的小信封,从门缝推回。&lt;/p&gt;
&lt;p&gt;礼宾。三步。弗里达家门缝。&lt;/p&gt;
&lt;p&gt;弗里达看完布鲁图的回信,笑了。她转回桌上那封”原包裹”,提笔写好给”最初发件人——Jack”的回信:“订单已确认,烤面包机正在路上,感谢选购。“照来时的套娃法打包:信封里信封,外套里外套。门缝,递出。&lt;/p&gt;
&lt;h2 id=&quot;第五幕回家&quot;&gt;第五幕——回家&lt;/h2&gt;
&lt;p&gt;回信按来时的路径”镜像”折返。礼宾到内核妈妈。内核妈妈翻目录:发件人在”楼外”,“得从前门出”。&lt;/p&gt;
&lt;p&gt;但她正要为出站打包,后置文员来了,啪地翻到包裹 #8472 那页。&lt;/p&gt;
&lt;p&gt;“别忘了,“他说,“这包裹进门时,我同事把抬头从大楼改成了 10 号。现在得改回。弗里达的门牌号不能出现在外发的包裹上——对外界来说,这封回信用的是’大楼’的地址,不是任何一个具体住户。”&lt;/p&gt;
&lt;p&gt;他用铅笔把抬头改回,合上账本。&lt;/p&gt;
&lt;p&gt;内核妈妈给改好抬头的包裹穿上全新的外邮袋,交给前门。前门转手给门口第一位接力。信使之海再次接力,邮袋一层层重生,直到最后一位快递员冲上 Jack 的小路,把包裹从他家门缝塞进来。&lt;/p&gt;
&lt;p&gt;浏览器小助手郑重其事地拆开,抚平回信,朗读。&lt;/p&gt;
&lt;p&gt;“订单已确认。你的烤面包机正在路上。感谢选购。”&lt;/p&gt;
&lt;p&gt;Jack 的浏览器里,Buy 按钮静静变绿,写成”✓ 已下单”。某处,一只猫从阳光房里抬了抬眼。Jack 笑了。&lt;/p&gt;
&lt;p&gt;他心想:也太顺了吧。&lt;/p&gt;
&lt;h2 id=&quot;解码器&quot;&gt;解码器&lt;/h2&gt;
&lt;p&gt;其实,一点也不顺。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;从 Jack 点击到屏幕上收到响应的整段旅程:浏览器里的分层打包;每一跳路由器重写以太网帧;Linux 主机上的数据通路;让回复流量&amp;quot;反向解 NAT&amp;quot;的 conntrack 快照;在 podman0 桥上完全内部完成的前端到后端的那一次横向通信;以及对称的回程路径。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1491&quot; height=&quot;1055&quot; src=&quot;/_astro/how_a_web_request_travels.BRiMf_cn_Z1n5NSi.webp&quot; srcset=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;以下是每位角色”真实身份”的演职员表。像看谢幕后名单一样看它,然后意识到:刚刚那场戏,比你以为的精巧多了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jack 与他的浏览器小助手:一位用户和他电脑上的浏览器。浏览器把”我点了购买”这种人类意图,翻译成服务器必须收到的那串消息。&lt;/li&gt;
&lt;li&gt;小助手写的”便条”与那一层层套娃:这才是整出戏的灵魂,得多说两句。
&lt;ul&gt;
&lt;li&gt;每条穿越互联网的消息,都是层层嵌套的套娃。最里面那张便条是实际内容——在我们这里是”买一台烤面包机”。它叫 HTTP 请求,是浏览器和网站说话的语言。&lt;/li&gt;
&lt;li&gt;其外是一层 TCP segment——加上可靠性功能(序列号, 重传, 确认回执),万一路上有丢件,能发现并补发。TCP 让互联网”像回事儿”;没有它,打开网页就像抛硬币。&lt;/li&gt;
&lt;li&gt;再外是一层 IP packet——写着端到端”谁发给谁”的地址。它说”这是发往商店公网地址的”。按故事设定,这一层基本不会在中途被打开或修改,它从端到端”穿越”。&lt;/li&gt;
&lt;li&gt;最外是一层 Ethernet frame——最大号的邮袋。它只认识”下一跳是谁”。每一跳上路前,这一层都会被撕掉, 重写,再交给下一位。&lt;br&gt;
这套嵌套——HTTP 信里,TCP 外套,IP 再外套,以太网最外套——正是学校里讲的”OSI 模型”在人话里的模样:一个给不同受众的”包里包”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;信使之海:互联网上的路由器。每台只知道”下一跳”,合在一起便能接力跨洲。那只”IP 包端到端不变,只有以太网帧每跳重写”的事实,才是互联网能跑起来的关键:里层能活到终点,外层随时换新。&lt;/li&gt;
&lt;li&gt;前门:面向外网的网卡(NIC)。在 Linux 主机上通常叫 eth0。职责如故事所述:收下发给我们的帧,丢掉其余,递给内核。&lt;/li&gt;
&lt;li&gt;内核妈妈:Linux 内核本尊。她拥有所有接口, 所有路由决定, 所有地址改写, 所有在飞行中的连接。她查的”黄页”是内核路由表(routing table),身边那本”改地址规则书”是 iptables/netfilter,她记的大账本是 conntrack 表。&lt;/li&gt;
&lt;li&gt;前置文员与后置文员:iptables 的 PREROUTING 与 POSTROUTING 挂钩。它们做的是 NAT(网络地址转换)。故事里的”进站改目的地到内部门牌”是 DNAT(目的地址转换)。后置文员要在出站”改回”的原因,是让外界只看见大楼的公网地址,而不是任何内部门牌。&lt;/li&gt;
&lt;li&gt;走廊礼宾:内核里的虚拟桥(virtual bridge)。在跑容器的 Linux 主机上,常叫 docker0 或 podman0。它的行为和我们在上一篇里研究的”交换机”一模一样:按名字在同一走廊里把包裹送到对应住户;至于它是软件不是铁盒子,只是实现细节。&lt;/li&gt;
&lt;li&gt;前端弗里达与后端布鲁图:两只容器(containers),各自住在隔离的”小公寓”里。弗里达可能跑着 nginx 这样的前端;布鲁图可能是数据库或订单处理 API。它们都插在同一条内部走廊(bridge)上,但彼此——以及对整栋楼以外——都有严格隔离,细节我们后面还会聊。
&lt;ul&gt;
&lt;li&gt;弗里达”重写一份新包裹给布鲁图”而不是”简单转发原包裹”的那个瞬间,正是反向代理的真实工作方式:它把一个对外会话,拆成了两个独立的会话,中间用应用层”粘”起来;不是一根管子直通。这个区别比听上去重要,我们后面会展开。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;10 号与 11 号, 走廊与大楼:这些就是你在上一篇和本文里反复看到的 IP。弗里达的门牌是 10.88.0.10,布鲁图是 10.88.0.11,走廊网段是 10.88.0.0/16,主机(内核妈妈在走廊上的座位)是 10.88.0.1。大楼的公网地址,则取决于外界怎么看到这台主机。&lt;/li&gt;
&lt;li&gt;那段”外界看不见”的弗里达与布鲁图的对话:这就是桥上的容器间通信。注意它没过 NAT,没走什么复杂路由,没碰外部接口。两间公寓,一条走廊,礼宾横向递包。便宜, 快, 对外隐形。这也是为什么 bridge 上容器互通飞快——内核几乎没离开底层。&lt;/li&gt;
&lt;li&gt;两本”对上暗号”的账本:连接跟踪(conntrack)。内核记住每条活动连接以及为它做过的改写,让回复流量能完美”反解”回去,尽管改写当初是”单边”的。没有它,地球上没有任何 NAT 能工作;有了它,现代互联网(你家路由器每天为家里每台设备干的事)就能悄无声息地运转。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;啊哈时刻&quot;&gt;啊哈时刻&lt;/h2&gt;
&lt;p&gt;我那位饭搭子一听”互联网”,脑海里大概飘过一根气动管道:这头塞东西,那头吐出来,中间是魔法。&lt;/p&gt;
&lt;p&gt;可中间不是魔法。中间是一串小而笨而相同的角色,每个包裹只做一件小事情,彼此并不知情,也不关心内容。所谓”聪明”,全在两端——浏览器小助手把每层地址写对,接收方把层层包裹拆开并回信。中间只是好心路人传纸条。&lt;/p&gt;
&lt;p&gt;在写字楼里,这种”接力”继续。内核妈妈比任何一个接力员都复杂——她能改地址, 能路由, 能记账——但她本质上仍是一串小机器:门口收件,撕最外层,看地址,必要时改地址,查下一跳,重新包好,从正确的内门送出去。这是条流水线,不是魔法阵。只要你别把它想象成”一大步”,就能把它整个装进脑子里。&lt;/p&gt;
&lt;p&gt;在我脑中,第一声”咔哒”是交换机那一刻——它让我看清”一瞬间, 一地点发生了什么”。第二声”咔哒”是这台烤面包机的旅程——让我看清”一整段路上发生了什么”。两者拼在一起,就顺了。&lt;/p&gt;
&lt;h2 id=&quot;我还没想明白的&quot;&gt;我还没想明白的&lt;/h2&gt;
&lt;p&gt;故事里有个地方我悄悄糊弄过去了。&lt;/p&gt;
&lt;p&gt;包裹到了弗里达那里时,我说”她的本地内核”剥了外衣,“她的门缝”收了快递,“她的桌上”放着原始包裹。看起来,弗里达似乎有她自己的小宇宙:自己的大门, 自己的 loopback, 自己的门牌, 自己的一切。&lt;/p&gt;
&lt;p&gt;可弗里达不是一台单独的电脑。她只是跑在和内核妈妈同一台主机上的一个进程。整栋楼只有一个内核,只有一套硬件。&lt;/p&gt;
&lt;p&gt;那弗里达的”公寓”究竟怎么”存在”的? 她如何拥有自己的内部地址, 自己的 loopback, 自己看见的走廊,却又互不踩到隔壁布鲁图的地盘? 同一个内核,怎么能并排跑出多套”平行网络世界”,而每一套都以为”世界里只有我”?&lt;/p&gt;
&lt;p&gt;答案就是我在上一篇挥手路过, 这篇终于要扣的那枚扣子:network namespace(网络命名空间)。一旦它”咔哒”落位,你就会明白为什么容器”像是独立的小电脑”。它也是 VPN, 沙箱, 很多零信任网络的地基——也是我这整个系列慢慢铺垫的那个 bug 的底层缘由。&lt;/p&gt;
&lt;p&gt;我会讲到的。我保证。&lt;/p&gt;
&lt;p&gt;下回见——看我先掉进哪只兔子洞。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;(人类撰写,必要处由 AI 协助润色)&lt;/p&gt;</description><pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate></item><item><title>ASGI到底是什么?从一头雾水到豁然开朗的理解之旅</title><link>https://shanechang.com/zh-cn/p/understanding-asgi-from-confusion-to-clarity/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/understanding-asgi-from-confusion-to-clarity/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.D4v3duLW_217oEx.webp&quot; alt=&quot;Featured image of post ASGI到底是什么?从一头雾水到豁然开朗的理解之旅&quot; /&gt;&lt;h2 id=&quot;scope的隐身术-让人抓狂的神秘变量&quot;&gt;scope的隐身术: 让人抓狂的神秘变量&lt;/h2&gt;
&lt;p&gt;故事要从我开发一个需要实时推送数据的小功能说起。没啥花头, 就是想服务器一有新消息, 客户端立马能收得到。于是我自然而然地用上了FastAPI, 结果一头扎进文档, 发现满篇都是&lt;code&gt;scope&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;没错, 就是这个&lt;code&gt;scope&lt;/code&gt;。看文档到哪都能看到它的影子, 教程代码里有它, Stack Overflow的解答也是默契地假设你已经会用它了。但最让我抓狂的是: **我在自己的代码里根本没看到过它的踪影!**路由, WebSocket端点, 统统没露面。scope去哪了?难道是传说中的”空气变量”?&lt;/p&gt;
&lt;p&gt;更离谱的是, &lt;code&gt;receive&lt;/code&gt;和&lt;code&gt;send&lt;/code&gt;也差不多, 大家都在谈, 一到自己写代码就消失了。AI助手讲解得头头是道, 但我死活没法把这些虚空中的参数和自己写的Litestar, Advanced Alchemy代码联系起来。&lt;/p&gt;
&lt;p&gt;这时候我才幡然醒悟: 原来我一直在学各种框架, 却从没搞懂这些框架的地基是什么。只有”刨地三尺”, 搞懂Python异步Web服务器最底层的原理, 才能明白这些玄学参数到底干嘛用的——不是FastAPI的套路, 也不是Django Channels的玩法, 而是底层协议的真相。&lt;/p&gt;
&lt;p&gt;于是, 这就是我的ASGI探险记。如果你也曾经对ASGI, WebSocket一头雾水, 觉得Python异步Web开发跟传统Flask/Django完全不是一个画风, 这篇文章就是为你写的。读完你就能明白ASGI到底是什么, 它为啥存在, 以及它如何优雅统一了HTTP, SSE, WebSocket等各种通信方式。&lt;/p&gt;
&lt;h2 id=&quot;第一重顿悟-asgi其实就是个君子协定&quot;&gt;第一重顿悟: ASGI其实就是个”君子协定”&lt;/h2&gt;
&lt;p&gt;最让我拍案叫绝的发现是: &lt;strong&gt;ASGI不是框架, 也不是库, 而是一个规范——规定了Web服务器如何跟你的Python应用对话。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就像电源插座一样, 国家标准一出, 不管是小米还是海尔, 都能造出插得上的电器。ASGI也是一样, 定义了Web服务器和异步Python应用之间的”对接协议”。&lt;/p&gt;
&lt;p&gt;整个协议简单到极致, 只有三个参数:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;async def app(scope, receive, send):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 应用逻辑&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这么朴实无华。无论你用FastAPI, Starlette, Django Channels, 还是自己手撸, 从本质上看, ASGI应用就是一个可调用对象, 接收这三个参数:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/strong&gt;: 一个字典, 包含了连接的元信息。可以理解为”场景说明书”, 告诉你这是啥连接, 谁发起的, 要干啥。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;receive&lt;/code&gt;&lt;/strong&gt;: 一个异步函数, 你调用它能收到客户端发来的消息。就像查邮箱收信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;send&lt;/code&gt;&lt;/strong&gt;: 另一个异步函数, 用它给客户端发消息。等于往邮箱里塞信寄出去。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更妙的是, 这套”三件套”通吃所有场景——普通HTTP, 流式Server-Sent Events, 双向WebSocket, 甚至连应用的启动和关闭都能用这一套。只要scope说明了”这次玩哪种花样”, 你就能对症下药。&lt;/p&gt;
&lt;p&gt;不过刚开始让我迷糊的是——“会话”到底指啥?一次请求?一场对话?服务器的整个寿命?&lt;/p&gt;
&lt;p&gt;答案其实很灵性: **得看scope的type。**接下来, 咱们一个个举例聊聊。&lt;/p&gt;
&lt;h2 id=&quot;http-简单明了的一次性买卖&quot;&gt;HTTP: 简单明了的一次性买卖&lt;/h2&gt;
&lt;p&gt;先从最熟悉的HTTP说起。你可能早就用惯了, 但不一定知道ASGI在背后默默打工。&lt;/p&gt;
&lt;p&gt;当有客户端发起HTTP请求时, ASGI会生成一个scope, 大致长这样:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;method&quot;: &quot;GET&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/api/users/123&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;format=json&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54321),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;type: &quot;http&quot;&lt;/code&gt;, 这就是告诉你, 这是一锤子买卖, 请求-响应搞定即走。&lt;/p&gt;
&lt;p&gt;流程如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发请求 → 服务器生成scope并调用你的app&lt;/li&gt;
&lt;li&gt;你的应用用&lt;code&gt;receive()&lt;/code&gt;收请求体 (可能是分段的)&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;send()&lt;/code&gt;发响应头 (状态, 头信息)&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;send()&lt;/code&gt;发响应体 (具体内容)&lt;/li&gt;
&lt;li&gt;scope消失, 连接结束&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就像自动贩卖机: 你投币, 选饮料, 拿饮料, 完事走人。贩卖机根本不记得你是谁。&lt;/p&gt;
&lt;p&gt;重点: HTTP在ASGI里是&lt;strong&gt;无状态的&lt;/strong&gt;。每次请求独立, scope活不了一秒, 处理完就销毁。&lt;/p&gt;
&lt;p&gt;举个响应例子:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 先发响应头&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;application/json&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 再发响应体&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&apos;{&quot;user&quot;: &quot;Shane&quot;, &quot;id&quot;: 123}&apos;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;短平快, 简单明了。这是最基础的模式。&lt;/p&gt;
&lt;h2 id=&quot;server-sent-events-服务器单向直播&quot;&gt;Server-Sent Events: 服务器单向直播&lt;/h2&gt;
&lt;p&gt;如果说HTTP是买瓶饮料就走, Server-Sent Events (SSE) 则像是你在贩卖机旁边蹲着, 机器一上新立马通知你。&lt;/p&gt;
&lt;p&gt;SSE本质上还是HTTP, 但不再遵循一次性请求-响应的老路, 而是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发起HTTP请求&lt;/li&gt;
&lt;li&gt;服务器回响应头 (记得加&lt;code&gt;Content-Type: text/event-stream&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接保持不断开&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;服务器随时发送数据&lt;/li&gt;
&lt;li&gt;直到有一方主动断开&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;scope依旧是&lt;code&gt;type: &quot;http&quot;&lt;/code&gt;, 但玩法变了。你就像打电话点了”今日特价”, 电话那边不停播报新菜品上架。&lt;/p&gt;
&lt;p&gt;例子:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;text/event-stream&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 持续推送&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 42}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 43}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;more_body: True&lt;/code&gt;, 意思就是”别断, 还会有!”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSE还是HTTP&lt;/strong&gt;, 只是持久化了连接, 单向推送。客户端啥都不回, 只是一直听。&lt;/p&gt;
&lt;p&gt;适合场景:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实时仪表盘&lt;/li&gt;
&lt;li&gt;通知推送&lt;/li&gt;
&lt;li&gt;股票行情&lt;/li&gt;
&lt;li&gt;进度更新&lt;/li&gt;
&lt;li&gt;只需单向推送, 不需要客户端反馈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比WebSocket简单, 功能比普通HTTP强。&lt;/p&gt;
&lt;h2 id=&quot;websocket-双向持久对话&quot;&gt;WebSocket: 双向持久对话&lt;/h2&gt;
&lt;p&gt;轮到WebSocket出场, ASGI的奥义也就此揭晓。&lt;/p&gt;
&lt;p&gt;WebSocket跟HTTP/SSE都不一样, 因为它是&lt;strong&gt;双向, 持久, 状态化&lt;/strong&gt;的。不是你一句我一句的买卖, 而是持续的对话, 谁都可以主动说话。&lt;/p&gt;
&lt;p&gt;如果HTTP像写信, SSE像广播, &lt;strong&gt;WebSocket就是打电话&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;scope结构:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;websocket&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/ws/chat/room-42&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;user=shane&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54322),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type: &quot;websocket&quot;&lt;/code&gt;, 完全不同的玩法。&lt;/p&gt;
&lt;p&gt;消息类型:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;客户端到应用:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.connect&quot;}       # 客户端请求建立连接&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.receive&quot;, &quot;text&quot;: &quot;Hello!&quot;}  # 客户端发消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.disconnect&quot;}    # 客户端断开&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;应用到客户端:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.accept&quot;}        # 应用同意连接&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.send&quot;, &quot;text&quot;: &quot;Welcome!&quot;}   # 应用发消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.close&quot;}         # 应用断开&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有木有发现, HTTP是你问我答, WebSocket是持续在线, 谁都能随时插话。&lt;/p&gt;
&lt;p&gt;scope也能活很久, 可能几秒, 也可能几小时。你可以在一个scope里互发上百条消息, 状态一直保留着。&lt;/p&gt;
&lt;p&gt;适合场景:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聊天应用&lt;/li&gt;
&lt;li&gt;协同编辑 (比如在线文档)&lt;/li&gt;
&lt;li&gt;在线游戏&lt;/li&gt;
&lt;li&gt;实时推送&lt;/li&gt;
&lt;li&gt;需要真·双向实时通信的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这里还有个经典疑问……&lt;/p&gt;
&lt;h2 id=&quot;websocket握手的迷思-升级的秘密&quot;&gt;WebSocket握手的迷思: 升级的秘密&lt;/h2&gt;
&lt;p&gt;初学时我被”WebSocket升级”整蒙了: &lt;strong&gt;WebSocket不是HTTP, 怎么开始的?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案让人大跌眼镜: &lt;strong&gt;WebSocket一开始就是个HTTP请求。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;啥?不信?往下看。&lt;/p&gt;
&lt;p&gt;WebSocket的设计就是要兼容原有Web基础设施 (端口, 代理, SSL等), 所以不是重新发明协议, 而是走”先HTTP, 后升级”的套路。&lt;/p&gt;
&lt;p&gt;流程:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 客户端发特殊HTTP请求:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;GET /ws/chat HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Host: example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Key: X3JJHMbDL1EzLkh9GBhXDw==&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Version: 13&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是普通GET, 只是加了些特殊头, 表示”我要升级成WebSocket”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 服务器回101响应:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;HTTP/1.1 101 Switching Protocols&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Accept: &amp;#x3C;computed response&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;101 Switching Protocols&lt;/code&gt;就是同意升级, 从此用WebSocket协议交流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 协议切换, TCP连接保持&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;底层TCP连接没断, 但HTTP已结束, 之后两边说的是WebSocket的”暗号”。&lt;/p&gt;
&lt;p&gt;你可以这么理解: 打电话给餐厅 (HTTP), 请求转接到某个包间 (Upgrade), 服务员说”马上接通” (101响应), 之后你就跟包间的人直接聊天了 (WebSocket) 。线路没变, 协议变了。&lt;/p&gt;
&lt;h2 id=&quot;asgi在握手流程中扮演什么角色&quot;&gt;ASGI在握手流程中扮演什么角色?&lt;/h2&gt;
&lt;p&gt;最让我一头雾水的是: &lt;strong&gt;ASGI是在哪一环介入的?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是: ASGI只在协议升级 (握手) 之后才介入。&lt;/p&gt;
&lt;p&gt;分层解释:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务器层 (Uvicorn/Hypercorn等, 自动完成) :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收HTTP升级请求&lt;/li&gt;
&lt;li&gt;校验WebSocket头&lt;/li&gt;
&lt;li&gt;回复101响应&lt;/li&gt;
&lt;li&gt;切换TCP连接协议&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ASGI应用层 (你的代码) :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器创建WebSocket scope&lt;/li&gt;
&lt;li&gt;发送&lt;code&gt;websocket.connect&lt;/code&gt;消息给你的应用&lt;/li&gt;
&lt;li&gt;你来决定: 同意还是拒绝连接 (比如做认证)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样一分层, 豁然开朗:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协议层 (服务器)&lt;/strong&gt;: 判断是不是有效的WebSocket升级请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务层 (应用)&lt;/strong&gt;: 判断要不要让这个连接进来 (比如权限校验)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;握手早就由服务器搞定了, 你收到&lt;code&gt;websocket.connect&lt;/code&gt;时, 客户端已经连接上, 等你点头。你发&lt;code&gt;websocket.accept&lt;/code&gt;只是”业务同意”, 不是协议握手。&lt;/p&gt;
&lt;p&gt;代码示意:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 收到握手后服务器转发的消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;message = await receive()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# message = {&quot;type&quot;: &quot;websocket.connect&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 业务判断, 比如认证&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if user_is_authenticated:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.accept&quot;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 开始双向消息循环&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.close&quot;, &quot;code&quot;: 1008})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 拒绝连接&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解了这点, FastAPI, Starlette各种WebSocket玩法就都清楚了——协议交给服务器, 业务你说了算。&lt;/p&gt;
&lt;p&gt;你和客户端都能随时断开连接, 谁也不是大爷, 谁都能说”我不玩了”。&lt;/p&gt;
&lt;h2 id=&quot;lifespan-横空出世的第四种scope&quot;&gt;Lifespan: 横空出世的第四种scope&lt;/h2&gt;
&lt;p&gt;就在我以为ASGI只有HTTP/SSE/WebSocket三种模式时, 突然冒出来个lifespan, 彻底打乱了我的世界观。&lt;/p&gt;
&lt;p&gt;原以为lifespan是”一次连接的生存期”, 比如WebSocket连着多久。事实证明, &lt;strong&gt;我错得离谱&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;lifespan根本跟一次连接没关系, 而是&lt;strong&gt;整个应用的生命周期&lt;/strong&gt;——服务器进程从启动到关闭。&lt;/p&gt;
&lt;p&gt;你可以这么总结:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP scope: 活一次请求 (百毫秒)&lt;/li&gt;
&lt;li&gt;SSE scope: 活一次流式会话 (几分钟到几小时)&lt;/li&gt;
&lt;li&gt;WebSocket scope: 活一次双向会话 (几秒到几小时)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lifespan scope: 活整个应用 (几天到几个月)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器启动时, 先创建lifespan scope, 给你发startup消息;关机时再发shutdown消息。&lt;/p&gt;
&lt;p&gt;scope结构:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;lifespan&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;消息类型:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.startup&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.shutdown&quot;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;啥用?适合那些&lt;strong&gt;只需要在应用启动或关闭时做一次的事&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库连接池初始化&lt;/li&gt;
&lt;li&gt;机器学习模型加载到内存&lt;/li&gt;
&lt;li&gt;启动后台任务调度器&lt;/li&gt;
&lt;li&gt;缓存预热&lt;/li&gt;
&lt;li&gt;监控/指标采集器初始化&lt;/li&gt;
&lt;li&gt;优雅关闭各种资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打个比方: HTTP/SSE/WebSocket是你招待每个顾客, lifespan是你每天开店 (准备食材, 开火) 和打烊 (关门, 清扫) 。&lt;/p&gt;
&lt;p&gt;你肯定不想每次请求都加载2G模型吧?只需要在&lt;code&gt;lifespan.startup&lt;/code&gt;一次性加载, 所有请求复用。同理, 关机时优雅关闭连接池。&lt;/p&gt;
&lt;p&gt;FastAPI里的&lt;code&gt;@app.on_event(&quot;startup&quot;)&lt;/code&gt;和&lt;code&gt;@app.on_event(&quot;shutdown&quot;)&lt;/code&gt;, 其实就是在帮你处理这些lifespan消息。&lt;/p&gt;
&lt;p&gt;lifespan和其他三种scope是正交的, 不是通信场景, 而是应用本身的生命周期管理。&lt;/p&gt;
&lt;h2 id=&quot;asgi的终极思维模型&quot;&gt;ASGI的终极思维模型&lt;/h2&gt;
&lt;p&gt;经历了这些疑惑和顿悟, 我终于形成了这样一个清晰的思维框架:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASGI是个统一的接口, 用来描述”通信会话”, 会话的类型有:&lt;/strong&gt;&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;scope类型&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;存活时间&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;通信方向&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;典型场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;毫秒级&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;请求→响应&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;API调用, 页面加载, 表单提交&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SSE&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;分钟到小时&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;服务器→客户端&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;实时推送, 仪表盘, 通知&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;秒到小时&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;双向&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;聊天, 协作, 游戏, 实时控制&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Lifespan&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;应用生命周期&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;N/A&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;启动/关闭, 资源管理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这四种都用同一个接口: &lt;code&gt;async def app(scope, receive, send)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;scope字典告诉你遇上了啥会话, receive/send让你与之交互。合同不变, 玩法多变。&lt;/p&gt;
&lt;p&gt;这样一来, &lt;strong&gt;中间件也能通吃所有类型&lt;/strong&gt;。比如认证中间件, 可以根据scope类型分别处理HTTP, WebSocket, SSE。日志中间件也是一样, 写一次通用所有。&lt;/p&gt;
&lt;p&gt;这就是为啥你一旦理解ASGI, FastAPI用起来就很顺畅。所有路由, WebSocket端点, 启动事件, 本质上都是ASGI应用, 遵守同一个简单协议。&lt;/p&gt;
&lt;p&gt;隐身的&lt;code&gt;scope&lt;/code&gt;其实一直都在, 只是框架帮你藏起来了。等你需要深入理解WebSocket, 优雅管理资源, 写中间件的时候, 就会发现ASGI这层基础有多香。&lt;/p&gt;
&lt;h2 id=&quot;从迷茫到通透&quot;&gt;从迷茫到通透&lt;/h2&gt;
&lt;p&gt;刚开始时, &lt;code&gt;scope&lt;/code&gt;像个幽灵, 文档总假设你早就会。现在我终于明白: 它不过是个用来描述通信模式的字典。&lt;/p&gt;
&lt;p&gt;美妙的是, 这种理解一通百通。读FastAPI的WebSocket例子, 能明白底层怎么回事;Starlette的启动事件, 其实就是lifespan消息的处理;排查连接断了, 也能思考是HTTP scope正常结束, 还是WebSocket意外关闭。&lt;/p&gt;
&lt;p&gt;一旦下潜到”协议层”, 理解ASGI底层, 而非只会用框架, 迷雾就散了, 自信心up!&lt;/p&gt;
&lt;p&gt;如果你正在开发实时功能, 异步API, 或者单纯想吃透现代Python Web后端, 希望这段旅程能帮你像我一样拨云见日。下次看到文档里的scope, 别慌, 你已经明白它的底细。&lt;/p&gt;
&lt;p&gt;说不定哪天你也会撸出极简ASGI应用, 写出专属中间件, 或者终于搞懂框架背后的魔法。&lt;/p&gt;
&lt;p&gt;ASGI的兔子洞随时欢迎你深入探索——但至少现在, 你已经站在了入口。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;(文章原创, 部分内容用AI润色。)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate></item></channel></rss>