# 网络与并发编程指南
> **对标版本**:XTC v1.0-rc | **状态**:异步/并行/通道/状态机/调度器/选已落地
---
## 并发模型
玄铁采用 **双层架构**:
| 层级 | 机制 | 说明 |
|------|------|------|
| OS 线程层 | 工作窃取线程池 | 执行 CPU 密集型任务,线程数 = CPU 核数 |
| 用户态调度层 | 无栈状态机 + 调度器 | 管理 I/O 等待任务,就绪队列 + 定时器堆 |
`异步 { ... }` 的执行层由编译器按块内容自动选择(路由规则见 8.1.4)。
### 异步任务
`异步 { ... }` 将代码块作为并发任务执行,立即返回任务句柄。
```xuantie
设 task = 异步 {
示("任务执行中...")
时.睡(1000)
示("任务完成")
}
示("主线程继续执行...")
等待(task)
示("任务已结束")
```
**执行流程**:
1. 编译器统计异步块内的挂起点(`等待`/`收`/`发`/`选`/`时.睡`)位置
2. 仅在语句级 → 编译为无栈状态机(poll 函数 + 状态结构体),`xt_fiber_spawn` 直接提交用户态调度器
3. 循环体内含挂起点 → 整体编译为独立函数,`xt_async_spawn` 提交线程池
4. 主线程执行到 `等待` 或程序末尾时驱动调度器事件循环
5. 任务的 `返` 回值可被 `等待` 取回(无 `返` 则得 `空`)
### 并行执行
`并行 { {块1}, {块2}, ... }` 并发执行多个代码块。
```xuantie
设 results = 并行 {
{ 时.睡(500); 返 "A" }
{ 时.睡(300); 返 "B" }
{ 返 "C" }
}
// results = ["A", "B", "C"]
```
### 行为约定与禁忌(必读)
1. **主线程的 `收`/`发` 默认阻塞**(空等货、满等位)。主线程阻塞期间会顺带驱动 fiber 调度器,不会死锁;但「永远等不到货」的 `收` 会让程序永远停在那里——这不是卡死,是阻塞语义。
2. **fiber 任务的返回值只能取一次**。第二次 `等待` 同一个已完成的 fiber 得到的是 `空`(槽位已回收复用)。需要多次使用的结果请先存进变量。
3. **fiber 内捕获的外层局部变量是 spawn 时刻的值副本**。spawn 之后外层再修改,fiber 内看到的是旧值;fiber 内对它的修改也不会写回外层(但修改在 fiber 自身内部跨挂起点持久生效,v0.19 起)。需要共享请用通道传递,或改用全局变量。
4. **`等待(fiber)` 会驱动调度器直到全局静止**。若程序里还有长时间睡眠的其他 fiber,`等待` 会等它们一并结束才返回——设计如此,不是挂起。
5. **fiber 句柄上限 65535 个**。正常逐个 `等待` 的程序会触发槽位回收,实际远够;只创建不等待会持续占用槽位。
6. **不要把「循环内挂起」当成零成本并发**。它独占 worker 线程;线程池容量(CPU 核数量级)内没问题,成百上千个这种任务会退化。海量并发场景请把挂起点移到循环外(语句级),走 fiber。
7. **嵌套在回调函数内的异步块捕获外部变量**仍是已知局限。
### 自动路由规则(重要)
编译器统计异步块内的**挂起点**(`等待`、`收`、`发`、`选`、`时.睡`、`连`、`流.读`、`流.写`)出现的位置:
| 挂起点位置 | 执行层 | 代价 |
|-----------|--------|------|
| 仅在语句级(不在任何循环体里) | fiber 调度器 | 微秒级切换,几乎零占用 |
| 任意挂起点出现在循环体(`当`/`遍历`/`循`)内 | 线程池 | 运行期间独占一个 worker 线程 |
路由到线程池时编译器会给出诊断提示,例如:
```xuantie
编译提示: 异步块检测到 1 个循环内挂起点(等待/收/发/选/时.睡),已整体编译为线程池任务...
```
**为什么循环内挂起要走线程池**:fiber 是无栈状态机,挂起恢复依赖编译期生成的状态跳转;循环体内的挂起恢复需要保留循环索引等局部状态,当前实现选择把这类代码放到真正的 OS 线程上执行——阻塞语义天然正确,但会占用一个 worker。
**推荐模式**:
```xuantie
// ① 语句级挂起 → fiber,最轻量
设 任务 = 异步 { 时.睡(100); 返 42 }
设 返回值 = 等待(任务) // 返回值 = 42
// ② 循环内挂起 → 自动路由线程池,语义正确,放心写
设 消费者 = 异步 {
当 真 {
设 货 = 通道.收() // 阻塞等待,不占 fiber
示("收到: " & 货)
}
}
// ③ 非阻塞试探:用 选 的超时 0,而不是直接 收
设 idx = 选([通道], 0) // 立即返回:有货返下标,没货返 -1
```
---
## 通道
`道(容量)` 创建带缓冲的并发通道。v0.3 起支持**阻塞收发**(条件变量)。
### 基本用法
```xuantie
设 ch = 道(4) // 容量为 4 的通道
ch.发("hello") // 非阻塞发送(满则丢弃); 状态机中为阻塞发送
ch.发("world")
设 msg = ch.收() // 非阻塞接收(空则返回空); 状态机中为阻塞接收
示(msg)
```
### 阻塞收发(状态机内)
在 `异步` 块内使用 `.发()` / `.收()` 时,编译器自动使用阻塞版本:
```xuantie
设 ch = 道(3)
设 consumer = 异步 {
设 sum = 0
设 i = 0
当 i < 3 {
sum = sum + ch.收() // 阻塞等待,不占 OS 线程
i = i + 1
}
示("Sum: " & sum.转字())
}
设 producer = 异步 {
ch.发(1)
ch.发(2)
ch.发(3)
}
等待(producer)
等待(consumer)
```
### 多通道选择(选)
`选(通道数组, 超时毫秒)` 等待多个通道中第一个就绪的,返回通道索引。超时返回 -1。
```xuantie
设 fast = 道(1)
设 slow = 道(1)
设 sel = 异步 {
设 idx = 选([fast, slow], 5000)
若 idx == 0 { 示("fast 就绪") }
抑 idx == 1 { 示("slow 就绪") }
否 { 示("超时") }
}
```
> **主线程泵送语义**:主线程在 `选` 等待期间会驱动用户态调度器(与 `收`/`发` 的无限等待一致),因此发送方是 fiber(无循环挂起的异步块)时数据也能正常到达。代价:**fiber 内有长睡眠或线程池有在途任务时,`选` 的返回可能晚于你给的超时毫秒数**(等待会持续到 fiber 跑完一轮);超时到期仍未就绪才返回 -1。如果你需要严格守时的多路等待,把发送方移出 fiber(用含循环挂起的异步块走线程池),或把超时拆成多次短轮询。
### 线程安全
通道操作(`发`/`收`/`选`)已加互斥锁+条件变量保护,多线程并发安全。
---
## HTTP 请求
### URL 编码与解码
`求` **不对 URL 做任何编码**:地址里带中文、空格等非 ASCII/保留字符时必须自己先编码,否则服务端会拒答——单参 `求` 对这种拒答只报到"成功但体长为 0",很容易误判。两个内置函数成对使用:
```xuantie
设 地址 = "https://example.com/search?q=" & URL编码("玄铁 语言 & 文档")
示(地址) // ...?q=%E7%8E%84%E9%93%81%20%E8%AF%AD%E8%A8%80%20%26%20%E6%96%87%E6%A1%A3
示(URL解码("%E7%8E%84%E9%93%81")) // 玄铁
```
- `URL编码(串)`:按 RFC 3986,`A-Z a-z 0-9 - _ . ~` 直通,其余按 **UTF-8 字节**转 `%XX`(大写);空格 → `%20`(不是 form 的 `+`)。
- `URL解码(串)`:`%XX`(十六进制大小写均可)→ 原字节;`+` **原样保留**(RFC 3986,不做 form 的空格还原)。转义不完整、非十六进制、或解码结果不是合法 UTF-8,一律**明确报错退出**(提示带偏移)。
- `字节到字符串(字节)`:字节流按 UTF-8 严格校验后转字符串(非法字节明确报错退出);配合 `字节(容量)` 建缓冲与 `.追加(字节值)` 逐字节写入使用。
### 简单 GET(单参形态)
`求(url)` 发起 HTTP GET 请求,返回 `结果` 对象,成功时 `值` 为**最终**响应体文本。支持 `https://`(TLS 走系统原生通道,零外部依赖:Windows 用 Schannel,macOS 用 SecureTransport;Linux 当前无 TLS 通道,`https` 请求会明确失败)。
行为要点:
- **自动跟随重定向**:301/302/303/307/308 最多 5 跳,返回最终页内容(`Location` 支持绝对/协议相对/绝对路径/相对路径四种形态);
- **默认 User-Agent** 为完整 Chrome 标识(大量站点对空 UA 返回跳转壳或拒答);需要自定义时用双参形态的 `头` 选项;
- **超时**:连接 10 秒、收发静默 30 秒(超时返回失败而非挂死);DNS 解析走 `getaddrinfo`,多 A 记录站点逐 IP 故障转移;
- chunked 传输自动解码,二进制响应体安全定长处理。
```xuantie
设 resp = 求("http://api.example.com/data")
若 resp.成功 {
示("响应: " & resp.值)
} 否 {
示("请求失败: " & resp.错误)
}
```
### 完整客户端(双参形态)
`求(url, 选项)` 发起自定义请求,选项字典全部可选:
| 键 | 说明 | 默认 |
|----|------|------|
| `方法` | `"GET"`/`"POST"`/`"PUT"`/`"DELETE"` 等 | `"GET"` |
| `头` | 字典,逐个写入请求头,如 `{"Content-Type": "application/json"}` | 无 |
| `体` | 请求体文本(非 GET 时常配) | 无 |
| `超时` | 收发超时毫秒数(连接超时暂不支持) | 不限 |
成功时 `值` 为字典:`{"状态码": 整, "体": 字, "头": 字典}`;`Transfer-Encoding: chunked` 会自动解码。
```xuantie
设 r = 求("https://api.example.com/items", {
"方法": "POST",
"头": {"Content-Type": "application/json"},
"体": "{\"名\":\"玄铁\"}",
"超时": 8000
})
若 r.成功 {
设 v = r.值
示("状态码: " & v["状态码"])
示("响应体: " & v["体"])
示("Content-Type: " & v["头"]["Content-Type"])
}
```
**注意**:单参形态(8.3.1)若遇 chunked 响应不解码(原样返回含块帧的文本);`https` 请求建议用双参形态拿完整状态码/响应头。
### 求 与 异步块的关系(易踩坑)
`求` **不是** fiber 挂起点——它内部是完整的阻塞 HTTP 事务,不会像 `连`/`读`/`写` 那样让出调度器。推论:
```xuantie
// ✅ 推荐:整个块只有 求,没有别的挂起点 → 自动路由线程池,语义正确
设 抓取 = 异步 {
设 r = 求("https://example.com")
若 r.成功 { 示("拿到 " & r.值.长度 & " 字节") }
}
等待(抓取)
// ⚠️ 避免:同一个块里既有 fiber 挂起点(等待/收/发/选/时.睡/连/读/写)
// 又有 求 —— 块会变 fiber,求 会卡住调度线程直到事务完成
```
**并发抓取的正确姿势**:每个 URL 一个独立 `异步` 块(各自只含 `求`,自动去线程池),主线程逐个 `等待` 汇总:
```xuantie
设 们 = []
遍历 u 于 ["http://a.com/1", "http://a.com/2"] {
设 地址 = u // 循环内声明,逐迭代捕获
们.追加(异步 {
设 r = 求(地址)
若 r.成功 { 示(地址 & " -> " & r.值.长度 & " 字节") }
})
}
遍历 t 于 们 { 等待(t) }
```
---
## TCP 网络
玄铁的网络 I/O 有**两种执行上下文**,同一个 `读`/`写`/`连` 在两种上下文里的底层行为完全不同(语义一致,占用模型不同):
| 上下文 | 触发方式 | 底层行为 | 代价 |
|--------|---------|---------|------|
| 阻塞上下文 | 主线程 / 普通函数 / `听` 的回调 / 线程池异步任务 | 真正的阻塞 `recv/send`,独占当前线程 | 一个操作占一个线程 |
| fiber 上下文 | `异步 {}` 块内(语句级,不在循环体里) | 非阻塞 + 就绪挂起,等待时**不占任何线程** | 微秒级切换 |
> 「我这个写法会落到哪个上下文」由编译器按 8.1.4 的自动路由规则决定。本节所有示例都会标明落在哪一层。
### 服务端(监听)
`听(端口, 回调)` 启动 TCP 监听,**立即返回**(不等连接)。每来一个新连接,运行时为它在**独立的 OS 线程**里调用一次回调,回调参数就是该连接的 `流`。
**默认只绑回环。** 两参形态只监听 `127.0.0.1`(仅本机可连)——这是刻意的安全默认:服务在开发机上随手一跑就对整个局域网开门,是这类语言最常见的静默缺口。要对外提供服务,用三参形态显式写地址:
```xuantie
听(8000, 函(流) { /* ... */ }, "0.0.0.0") // 对外(所有网卡)
听(8000, 函(流) { /* ... */ }, "192.168.1.10") // 只绑某块网卡
```
地址不可用(端口被占、地址非法、第三个参数不是字符串)时 `听` 返回**失败结果**,错误消息里带具体地址与端口,不会静默回退成对外监听;参数个数不是 2 或 3 时编译期直接报错。
```xuantie
听(8000, 函(流) {
流.写("欢迎!\n")
循 {
设 数据 = 流.读()
若 数据 == 空 { 断 } // 对端关闭 → 读返回 空 → 退出循环
流.写("回显: " & 数据)
}
流.关()
})
输("按回车停止...") // 关键:主线程必须有事可做,否则程序直接退出
```
**关键点 1:回调里是阻塞上下文,可以放心用阻塞写法。** 因为每个连接独占一个真实线程,`循` 环里阻塞 `读` 天经地义——它只卡住「这一个连接」的线程,不影响其他连接,也不影响 fiber 调度器。
**关键点 2:`听` 不挡退出。** `听` 启动后立刻返回,如果主线程一路跑到程序末尾,进程退出、监听随之消失。所以服务端末尾通常用 `输(...)` 或一个永不结束的循环把主线程留住:
```xuantie
听(8000, 函(流) { /* ... */ })
示("服务已启动")
循 { 时.睡(1000) } // 或者:输("按回车停止...")
```
**关键点 3:多个回调并发跑,共享数据要走通道或全局。** 回调之间是真正的多线程并发。例如统计在线人数:
```xuantie
设 在线 = 0 // 全局变量,所有回调线程共享
听(8000, 函(流) {
在线 = 在线 + 1 // 简单计数可以;复杂共享状态请用 通道 汇总(见 8.2)
循 {
设 数据 = 流.读()
若 数据 == 空 { 断 }
流.写("当前在线: " & 在线)
}
在线 = 在线 - 1
流.关()
})
```
**关键点 4:目前没有「停止监听」原语。** `听` 一旦启动就到进程结束,这是已知边界;需要可关停的服务请把「是否继续」判断放在回调内部逻辑里。
**关键点 5(必知坑):TCP 没有消息边界,`读` 不等于「读一条消息」。** 阻塞路径下每次 `读` 最多返回 4096 字节,而且对端一次 `写` 的数据可能被拆成多次 `读` 到达(也可能多次 `写` 被合并成一次 `读`)。**不要假设 `读` 到的就是一整条消息。** 如果协议需要完整消息,自己加分隔符或长度头,循环拼:
```xuantie
// 按行协议:循环读直到凑出一行(\n 结尾)
函 读一行(流) {
设 缓冲 = ""
循 {
设 块 = 流.读()
若 块 == 空 { 返 空 } // 对端关闭
缓冲 = 缓冲 & 块
若 缓冲.包含?("\n") {
设 行集 = 缓冲.分割("\n")
返 行集[0] // 取第一行(分割语义见参考手册 6.4 节)
}
}
}
```
### 客户端(连接)
`连(地址)` 连接到 TCP 服务端,返回 `结果`——**必须判 `成功` 再取用**,地址格式 `"主机:端口"`:
```xuantie
设 联 = 连("127.0.0.1:8000")
若 联.成功 {
设 流 = 联.值
流.写("hello")
设 回 = 流.读()
若 回 != 空 { 示("服务端说: " & 回) }
流.关()
} 否 {
示("连接失败: " & 联.错误)
}
```
**关键点 1:`读` 返回 `空` 只有一个含义——对端关了(或已关)。** 它**不表示「暂时没数据」**:阻塞路径下没数据会一直等到有数据或对端关闭;fiber 路径下没数据会挂起等就绪。所以「读到 空」永远按连接结束处理:
```xuantie
设 数据 = 流.读()
若 数据 == 空 {
// 对端已关闭,别再 写/读,直接收尾
流.关()
返 空
}
```
**关键点 2:`写` 返回 `真`/`假`,且保证「写完全部」。** 写大数据时底层会自动分多次发送(部分发送的内部续传),返回 `真` 代表全部发出;返回 `假` 代表连接已坏。不需要自己处理「只发出去一半」的情况。
**关键点 3:`关` 通常可以不写。** socket 由 ARC 内存管理,引用归零自动关闭;长寿命循环(如服务端回调)里建议显式 `流.关()` 及时释放。
### Socket 方法
| 方法 | 说明 |
|------|------|
| `.读()` | 读取数据;有数据返回字符串,对端关闭返回 `空`。**阻塞路径每次最多 4096 字节** |
| `.读(上限)` | 同上,但最多读 `上限` 字节。**上限仅在 fiber 上下文生效**(见 8.4.4 细则 1) |
| `.写(内容)` | 写入字符串(自动分次发完),成功 `真` / 失败 `假` |
| `.发(内容)` | 同 `.写()` |
| `.关()` | 关闭连接 |
**闭包回调注意**:`听` 的回调经 `xt_closure_call1` 按 env 感知约定调用——捕获外层变量的闭包可正常使用。
### 异步块内的网络 I/O(fiber 感知,v0.19+)
#### 一分钟原理
`异步 {}` 块(语句级、不含循环内挂起点)会被编译成 fiber(无栈状态机),跑在 fiber 调度器上。块内的 `连`、`流.读()`、`流.写()` 是**挂起点**:socket 自动转为非阻塞;操作不能立即完成时,fiber 连人带状态挂进「socket 就绪等待表」,调度线程**立刻去驱动别的 fiber**;数据到达时 fiber 被精确唤醒,从挂起点继续——**等待期间不占用任何线程**。
收益:成百上千个并发连接不再需要成百上千个线程,一个调度线程就能撑起全部「大部分时间在等数据」的连接。
#### 标准客户端模板
```xuantie
设 客 = 异步 {
设 r = 连("127.0.0.1:8000") // 挂起点:连接建立前不占线程
若 非 r.成功 { 示("连接失败") ; 返 空 }
设 流 = r.值
流.写("你好") // 挂起点:写不完就挂起等可写,自动续传
设 回 = 流.读() // 挂起点:没数据就挂起等就绪
示("收到: " & 回)
流.关()
}
等待(客) // 主线程在这里驱动调度器直到 fiber 完成
```
与阻塞版逐字对比你会发现**写法完全一样**——差别只在底层不占线程。这就是 fiber 感知 I/O 的设计目标:同一套代码,放进 `异步 {}` 就自动获得高并发能力。
#### 多连接扇出模板(fiber 的主战场)
由于玄铁的 fiber 切换是微秒级,**几百个并发连接的正确写法是「每个连接一个 fiber」**,而不是开几百个线程:
```xuantie
设 们 = []
设 i = 0
当 i < 100 {
设 j = i // 循环内声明,逐迭代捕获(关键!写成直接用 i 会全拿到终值)
们.追加(异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 }
设 流 = r.值
流.写("我是 " & j)
设 回 = 流.读()
若 回 != 空 { 示("#{j} 号收到: " & 回) }
流.关()
})
i = i + 1
}
遍历 t 于 们 { 等待(t) } // 等全部 100 个 fiber 完成
```
**注意 `设 j = i` 这一行**:循环变量 `i` 只有一个存储,直接捕获会让所有 fiber 读到循环结束后的终值;`设 j = i` 在循环体内声明新变量,捕获按创建时刻快照(逐迭代语义),每个 fiber 拿到自己那轮的值。
#### 写法纪律(反直觉点,必读)
**纪律一:挂起语句独立成句,不要把挂起点包进 `若` 块。** fiber 是无栈状态机,含挂起点的 `若` 会被拆成多个「状态片」,片与片之间**块内语句会重复执行**(既有语义):
```xuantie
// ❌ 反例:若 块里有挂起点,块里的 示 可能被执行多次
异步 {
若 r.成功 {
示("连上了") // 状态片重放时这行会再打印一次!
设 回 = r.值.读()
示("收到: " & 回)
}
}
// ✅ 正例:挂起点全部独立成句,若 只包纯判断
异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 } // 若 块里没有挂起点,安全
设 流 = r.值
设 回 = 流.读()
若 回 != 空 { 示("收到: " & 回) }
}
```
**纪律二:需要「读循环」的连接处理,请交给线程池而不是 fiber。** 挂起点出现在循环体里时,整个异步块自动路由到线程池(8.1.4)——这不是降级,是设计:循环内挂起需要保留循环现场,fiber 做不到,线程天然正确。所以**服务端长连接收包循环**这样写就对:
```xuantie
// 循环里有 读 → 整块自动去线程池,阻塞语义,完全正确
设 长连处理 = 异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 }
设 流 = r.值
当 真 {
设 数据 = 流.读() // 线程池里阻塞读,独占一个 worker
若 数据 == 空 { 断 }
示("收到: " & 数据)
}
}
```
而**「一次性问答」式连接**(连上 → 写 → 读 → 关)全部语句级,走 fiber,最省:
```xuantie
设 问答 = 异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 }
设 流 = r.值
流.写("问")
设 答 = 流.读() // 全在语句级 → fiber,挂起不占线程
示("答: " & 答)
流.关()
}
```
#### 细则与边界(逐条都是实测行为)
1. **`读(上限)` 只在 fiber 内生效。** `流.读(16)` 在 fiber 里最多读 16 字节;在阻塞上下文(主线程/`听` 回调)该参数被忽略,固定每次最多 4096 字节。**结论:无论哪个上下文,都不要依赖单次 `读` 到的字节数**,要完整消息请用 8.4.1 关键点 5 的循环拼法。
2. **fiber 内 `.读()/.写()` 保留给 socket 流**(如同 `.收/.发` 保留给通道)。对字典/实例的同名方法在 fiber 内调用会显式报错,不会静默出错。`文件.读/写` 是模块函数,完全不受影响。
3. **fiber 内的变量跨挂起点持久**:`设` 的局部变量、对捕获变量的修改,在挂起恢复后都保持(v0.19 起;更早版本会丢成 `空`/旧值——如果你在老版本上写 fiber 网络代码,请升级)。
4. **同一个 socket 不要跨上下文混用。** socket 一旦被 fiber 碰过就会置为非阻塞;之后再拿到阻塞上下文用虽然做了兼容(内部重试,行为不变),但这是为容错准备的降级,不是设计用法。**一个流,从生到死待在同一个上下文里。**
5. **`读` 没有超时参数。** 需要「超时放弃」时,用**对端关闭触发**的模式:看门狗 fiber 到时后让对端(或本端持有方)`关` 流——挂起的 `读` 会被唤醒并返回 `空`,按连接结束统一处理:
```xuantie
设 看门 = 异步 {
时.睡(3000)
若 服务端流盒子[0] != 空 { 服务端流盒子[0].关() } // 对端关闭 → 读方唤醒得 空
}
```
6. **TLS(HTTPS)流暂不支持 fiber 感知 I/O**;`求` 走自己的阻塞路径,不受影响。
7. **fiber 句柄上限 65535**(见 8.1.3 第 5 条);socket 就绪等待表上限 1024 个并发挂起 I/O,超出显式报错退出(不静默丢)。
---
## HTTP 服务端(铁铺包 HTTP)
`HTTP.服务(端口, 处理函)` 以纯玄铁实现启动 HTTP/1.1 服务(基于 `听`)。处理函收**请求字典** `{"方法","路径","查询","头","体"}`,返回**响应字典** `{"状态码","头","体"}`(均可缺省,默认 200/空体)。每连接一请求一响应(Connection: close)。
```xuantie
引 "HTTP" 予 HTTP
HTTP.服务(8080, 函(请求) {
若 请求["路径"] == "/api" {
返 {"状态码": 200, "头": {"Content-Type": "application/json"}, "体": "{\"ok\":真}"}
}
返 {"状态码": 404, "体": "没有 " & 请求["路径"]}
})
示("服务已启动 :8080")
输("按回车停止...")
```
**关键点与写法**:
1. **处理函跑在连接线程里(阻塞上下文)**——基于 `听`,每连接一个独立线程。处理函里可以用 `文件.读` 等阻塞操作,也可以访问外层变量(闭包捕获正常)。
2. **读 POST 体**:`请求["体"]` 已按 `Content-Length` 收齐;`请求["方法"]` 区分动作。回显接口示例:
```xuantie
HTTP.服务(8080, 函(请求) {
若 请求["方法"] == "POST" 且 请求["路径"] == "/echo" {
返 {"状态码": 200, "体": "你发了: " & 请求["体"]}
}
返 {"状态码": 404, "体": "没有 " & 请求["路径"]}
})
```
3. **路径与查询串分离**:`/a?x=1` 进来,`请求["路径"]` 是 `/a`,`请求["查询"]` 是 `x=1`,解析查询参数用 `分割("&")` 再按 `=` 拆。
4. **不支持 keep-alive**:每个连接处理一个请求就关闭(HTTP/1.1 `Connection: close` 语义)。浏览器/fetch 会自然兼容(后续请求自动开新连接)。
5. **响应头自动补**:`Content-Length` 按 `体.字节数` 自动写入,`Connection: close` 自动带上;你只需给业务头。
6. **关停**:目前没有「停止监听」原语,服务随进程生命周期;开发期直接结束进程即可。
## 即时通讯 (IM) 完整示例
项目 `examples/im/` 包含完整的服务端+客户端示例。
```xuantie
// 服务端
听(8000, 函(流) {
示("新连接")
流.写("系统: 欢迎进入聊天室!")
循 {
设 数据 = 流.读()
若 数据 == 空 { 断 }
示("收到: " & 数据)
}
})
示("服务端已就绪")
输("按回车停止...")
```
---
## 线程安全速查
| 组件 | 线程安全性 | 说明 |
|------|-----------|------|
| 通道 (`道`) | **安全** ✓ | `xt_chan_mutex_t` + 条件变量全程持锁 |
| 通道(阻塞收发) | **安全** ✓ | 条件变量等待,不占 OS 线程 |
| 选 (`选`) | **安全** ✓ | 轮询+条件变量,超时安全 |
| 异步 (`异步`) | **安全** ✓ | 状态机 poll 函数由调度器单线程调用 |
| 并行 (`并行`) | **安全** ✓ | cloneEnv 隔离 |
| Socket I/O | **安全** ✓ | 每个 socket 独立,无共享状态 |
| ARC | **安全** ✓ | atomic_fetch_add/sub |
| 调度器 | **注意** ⚠ | 线程池回调唤醒 fiber,无锁竞态(当前单线程未触发) |
| 全局变量 | **不安全** ✗ | 用户代码需自行加锁 |
| 弱引用 | **安全** ✓ | `g_weak_mutex` 保护 |
---
## 命令速查
| 关键字 | 说明 |
|--------|------|
| `异步 { ... }` | 异步执行代码块(自动检测状态机) |
| `等待(task)` | 等待异步任务完成 |
| `并行 { {a}, {b} }` | 并行执行多个代码块 |
| `道(n)` | 创建容量为 n 的通道 |
| `.发(v)` | 通道/流 发送(状态机中阻塞) |
| `.收()` | 通道接收(状态机中阻塞) |
| `选(arr, ms)` | 多通道选择,返回索引,超时返回 -1 |
| `求(url)` | HTTP GET(返回体文本) |
| `求(url, 选项)` | 完整 HTTP 客户端(方法/头/体/超时,回 状态码/体/头) |
| `连(addr)` | TCP 连接(fiber 内非阻塞挂起) |
| `听(port, cb, 绑定地址?)` | TCP 监听(回调支持捕获闭包);缺省只绑回环 `127.0.0.1`,对外须显式写 `"0.0.0.0"` |
| `HTTP.服务(port, fn)` | HTTP 服务端(铁铺包 HTTP) |
| `.读()` / `.读(上限)` | Socket 读取(上限仅 fiber 内生效;fiber 内为就绪挂起) |
| `.写(s)` | Socket 写入(fiber 内为可写挂起,自动续传) |
| `.关()` | Socket 关闭 |