发现新版OpenClaw小龙虾不触发缓存了，发个解决办法！
众所周知，openclaw是token吞金兽。虽然现在gpt调用几乎是免费的，但是可能也有些朋友使用其他api渠道或者没有自己的号池在使用各家中转站。如果没办法正确的调通缓存触发，会让token的使用成本超超超级加倍！关乎钱包的问题必须马上折腾明白。 今天更新了3.23-1和3.23-2版本之后，发现缓存触发果然出问题了，解决思路如下： 修复前： 修复后： 适用场景： 你用的是 openai-responses 链路是 OpenClaw -> CPA/new-api/其他 OpenAI-compatible proxy -> 上游 OpenAI/Codex 升级到 2026.3.22+ / 2026.3.23+ 后，发现 prompt cache 基本不触发，cache_tokens 大量变成 0 一、根因 OpenClaw 在 2026-03-19 合入了这条逻辑： PR #49877 标题：fix: strip prompt_cache_key for non-OpenAI openai-responses endpoints 也就是： 如果 baseUrl 不是官方 OpenAI / Azure OpenAI OpenClaw 会把 prompt_cache_key 和 prompt_cache_retention 从请求里剥掉 这能避免某些第三方兼容接口报错，但会误伤实际上 支持 prompt cache 的 OpenAI-compatible proxy。 例如： 本地 new-api CLIProxyAPI 各种转发到真实 OpenAI / Codex 的代理 二、具体改哪个文件 1) 优先找实际安装目录 如果你是全局安装（npm -g / pnpm -g），通常 OpenClaw 会在类似下面的位置： /usr/lib/node_modules/openclaw/dist/ 或者： $(npm root -g)/openclaw/dist/ 2) 目标文件不是固定文件名，但目标函数名固定 需要找包含这个函数的 dist bundle： grep -RIl 'function shouldStripResponsesPromptCache(model)' /usr/lib/node_modules/openclaw/dist 如果是 npm 全局安装但不在 /usr/lib/node_modules，可以用： grep -RIl 'function shouldStripResponsesPromptCache(model)' "$(npm root -g)/openclaw/dist" 3) 我这台机器上命中的文件是 /usr/lib/node_modules/openclaw/dist/pi-embedded-CbCYZxIb.js 注意：你那边文件名可能不一样，以 grep 找到的实际文件为准。 三、具体怎么改 原始代码 找到这个函数： function shouldStripResponsesPromptCache(model) { if (typeof model.api !== "string" || !OPENAI_RESPONSES_APIS.has(model.api)) return false; if (typeof model.baseUrl !== "string" || !model.baseUrl.trim()) return false; return !isDirectOpenAIBaseUrl(model.baseUrl); } 最小修复版（手动放行指定代理） 如果你要放行自己的代理，比如： 127.0.0.1:13000 localhost:13000 my-proxy.example.com 可以改成： function shouldStripResponsesPromptCache(model) { if (typeof model.api !== "string" || !OPENAI_RESPONSES_APIS.has(model.api)) return false; if (typeof model.baseUrl !== "string" || !model.baseUrl.trim()) return false; if ( model.baseUrl.includes("127.0.0.1:13000") || model.baseUrl.includes("localhost:13000") || model.baseUrl.includes("my-proxy.example.com") ) return false; return !isDirectOpenAIBaseUrl(model.baseUrl); } 我实际给本机打的最小补丁 function shouldStripResponsesPromptCache(model) { if (typeof model.api !== "string" || !OPENAI_RESPONSES_APIS.has(model.api)) return false; if (typeof model.baseUrl !== "string" || !model.baseUrl.trim()) return false; if (model.baseUrl.includes("127.0.0.1:13000") || model.baseUrl.includes("localhost:13000")) return false; return !isDirectOpenAIBaseUrl(model.baseUrl); } 四、修改步骤（手动版） 1) 备份目标文件 cp /usr/lib/node_modules/openclaw/dist/pi-embedded-*.js /usr/lib/node_modules/openclaw/dist/pi-embedded-backup-$(date +%s).js 更稳一点：先用 grep 定位具体文件后再备份。 2) 编辑目标文件 用你习惯的编辑器打开 grep 找到的那个 js 文件，把上面的函数替换掉。 3) 重启 gateway openclaw gateway restart 4) 验证 openclaw health 然后再看你的 new-api / CPA 日志，确认是否重新出现： channel_affinity key_fp key_path: prompt_cache_key cache_tokens > 0 五、给懒得手动找文件的人：可直接发给自家 OpenClaw 的提示词 把下面这段直接发给自己的 OpenClaw，让它自己尝试定位并修改： 你帮我修一个 OpenClaw 的本地缓存兼容问题。 目标： - 我现在的 OpenClaw 升级后，会把发往 OpenAI-compatible proxy 的 `prompt_cache_key` 剥掉，导致 prompt cache 基本不触发。 - 请你在本机全局安装的 OpenClaw dist 文件里，找到 `shouldStripResponsesPromptCache(model)` 这个函数。 - 对我指定的代理 baseUrl / host 做白名单放行，让这些代理**不要再被 strip `prompt_cache_key`**。 请按以下步骤做： 1. 先定位 OpenClaw 的实际 dist 目录： - 优先看 `/usr/lib/node_modules/openclaw/dist` - 如果没有，再看 `$(npm root -g)/openclaw/dist` 2. 在 dist 目录中搜索： - `function shouldStripResponsesPromptCache(model)` 3. 找到包含该函数的真实 bundle 文件（不要改 `.bak` 备份文件）。 4. 先备份该文件。 5. 把这个函数： function shouldStripResponsesPromptCache(model) { if (typeof model.api !== "string" || !OPENAI_RESPONSES_APIS.has(model.api)) return false; if (typeof model.baseUrl !== "string" || !model.baseUrl.trim()) return false; return !isDirectOpenAIBaseUrl(model.baseUrl); } 改成带白名单放行的版本。 我希望放行这些代理（你按实际变量生成 OR 条件）： - 127.0.0.1:13000 - localhost:13000 - 以及我 OpenClaw 配置里 `models.providers.*` 中所有 `api = openai-responses` 且 `baseUrl` 不是官方 OpenAI/Azure OpenAI 的 provider baseUrl 也就是说： - 先从 `~/.openclaw/openclaw.json` 读取 provider 配置 - 自动把符合条件的 baseUrl 也加进白名单 6. 修改完成后： - 重启 gateway：`openclaw gateway restart` - 跑 `openclaw health` 7. 最后告诉我： - 你改了哪个文件 - 你放行了哪些 baseUrl/host - 是否重启成功 - health 是否正常 注意事项： - 只做最小修改，不要顺手改别的逻辑 - 目标是“对白名单代理保留 `prompt_cache_key`”，不是全局关闭 strip 逻辑 - 如果 bundle 文件名和当前版本不同，以实际 grep 结果为准 - 如果找不到函数，就把搜索结果告诉我，不要瞎改 六、补充说明 为什么不要直接全局禁用 strip？ 因为有些第三方接口确实会因为 prompt_cache_key 报 400。 所以更稳的方式是： 只对白名单代理放行 其他非 OpenAI 端点仍保持原逻辑 更理想的上游方案 OpenClaw 已经有人提 PR： #52017 fix(agents): add supportsPromptCache compat opt-in for third-party OpenAI proxies 如果这个 PR 后面合并了，理论上未来就不需要手打这种 dist 补丁了。 七、最短结论 如果你用的是： OpenClaw openai-responses 中间走本地/第三方 OpenAI-compatible proxy 升级后缓存突然不命中 那就去找： shouldStripResponsesPromptCache(model) 并对白名单代理加一句： if (model.baseUrl.includes("你的代理地址")) return false; 然后重启 gateway 即可。
Ubuntu 25.10 系统远程桌面RDP突然失效，最终定位到 FreeRDP 安全回归
最近在一台 Ubuntu 25.10 虚拟机上遇到一个比较坑的情况：系统自带远程桌面明明开着，3389 也在监听，但windows rdp客户端连接后还是黑屏、闪断，或者认证后立刻断开。 这类问题表面像是“网络不通”或者“密码不对”，实际根因在更底层：gnome-remote-desktop 依赖的 FreeRDP/WinPR 运行时库出了安全更新回归。 现象 系统设置里远程桌面已开启 gnome-remote-desktop.service 处于运行状态 3389 正在监听 客户端能发起连接，但会黑屏、断开，或者认证后被踢回 排查时日志里能看到这类报错： client disconnected during the handover Unexpected client message in state CONNECTION_STATE_FINALIZATION_FONT_LIST SetEvent: hEvent is not an event corrupted size vs. prev_size client authentication failure nla_recv() error: -1 这些日志很容易误导人。看起来像认证失败，其实很多时候是后面的会话接管或者用户态服务已经崩了，前面的连接层只能以“认证失败”或“连接被重置”的形式表现出来。 原理 GNOME 自带 RDP 不是单进程直连桌面的简单模型，它大致是两段式： RDP 客户端 -> system 级 gnome-remote-desktop 监听 3389 -> handover / 重定向 -> user 级 gnome-remote-desktop 接管会话，监听 3391 -> 进入真实 GNOME 会话 所以只看 3389 在不在，并不能说明整条链路是好的。 另外，虽然这里用的是 GNOME 自带远程桌面，但底层 RDP 协议栈依赖的是： libfreerdp3-3 libfreerdp-server3-3 libwinpr3-3 也就是说，就算没有单独装 FreeRDP 客户端，只要用 GNOME 自带 RDP，这几组库的更新回归一样会直接影响远程桌面。 故障点 本机最终确认下来的版本情况是： gnome-remote-desktop 49.0-0ubuntu1.1 gnome-settings-daemon 49.0-1ubuntu3.1 libfreerdp3-3 3.16.0+dfsg-2ubuntu0.3 libfreerdp-server3-3 3.16.0+dfsg-2ubuntu0.3 libwinpr3-3 3.16.0+dfsg-2ubuntu0.3 继续查 apt changelog libfreerdp3-3 后，发现官方已经发布了： 3.16.0+dfsg-2ubuntu0.4 而且 changelog 里明确写了，这是在处理 .3 引入的安全回归，核心信息是： SECURITY REGRESSION: crash with realloc(): invalid next size 这和本机用户态 gnome-remote-desktop 日志里的： SetEvent: hEvent is not an event corrupted size vs. prev_size 是能对上的。简单说就是：.3 这组安全更新把 GNOME 自带 RDP 依赖的运行时库弄出了新问题，导致会话接管阶段异常甚至崩溃；.4 则把有问题的补丁先撤掉了。 最小修复办法 不需要整机 full-upgrade，也不需要重装桌面环境，直接把相关运行库升到 .4 即可： sudo apt update sudo apt install -y libfreerdp3-3 libfreerdp-server3-3 libwinpr3-3 升级后重启 GNOME 自带远程桌面的 system/user 两条服务链路： sudo systemctl daemon-reload systemctl --user daemon-reload sudo systemctl restart gnome-remote-desktop.service systemctl --user restart gnome-remote-desktop.service 修复后应该看到什么 先确认版本已经上来： dpkg -l | rg 'gnome-remote-desktop|gnome-settings-daemon|libfreerdp3-3|libfreerdp-server3-3|libwinpr3-3' 正常情况下应该能看到： libfreerdp3-3 3.16.0+dfsg-2ubuntu0.4 libfreerdp-server3-3 3.16.0+dfsg-2ubuntu0.4 libwinpr3-3 3.16.0+dfsg-2ubuntu0.4 再确认两层监听都在： sudo ss -ltnp '( sport = :3389 or sport = :3391 )' 预期是： 3389 由 system 级 gnome-remote-desktop 监听 3391 由 user 级 gnome-remote-desktop 监听 这时再重新发起 RDP 连接，通常就恢复正常了。 总结 这次问题的核心不是“远程桌面没开”，也不是“客户端不兼容”，而是： GNOME 自带 RDP 实际上依赖 FreeRDP/WinPR 3.16.0+dfsg-2ubuntu0.3 引入了安全回归 回归会在 handover 和用户会话接管阶段表现成黑屏、闪断、认证后断开 升级到 3.16.0+dfsg-2ubuntu0.4 后恢复正常 如果你在 Ubuntu 25.10 上遇到“自带远程桌面已开启、3389 在监听，但就是连不上”的情况，优先检查这几个包的版本，而不是先怀疑网络、账户或者客户端。 参考 Ubuntu Launchpad Bug #2141992 https://bugs.launchpad.net/ubuntu/+source/gnome-remote-desktop/+bug/2141992 Ubuntu Launchpad Bug #2122660 https://bugs.launchpad.net/bugs/2122660 Questing 的 libfreerdp3-3 发布记录 https://launchpad.net/ubuntu/questing/amd64/libfreerdp3-3
记一次诡异的 Tailscale 子网路由劫持：当 Ping 能通但 TCP 端口死活连不上
📌 1. 问题背景与网络拓扑 最近在调整多节点网络，使用了 Tailscale 组网，并在一台内网的 Ubuntu 服务器上开启了 Subnet Router（子网路由），以便身在外地时能直接通过本地 IP 访问内网设备。 当前的网络拓扑： 外网设备（Windows）： Tailscale 虚拟 IP 100.116.47.82 网关节点（Ubuntu）： 局域网 IP 192.168.31.177 （开启了 Tailscale Subnet Router，开启了内核转发，装有 1Panel 面板和多个 Docker 容器） 目标服务器： 局域网 IP 192.168.31.220 （内网另一台机器，运行着 PostgreSQL 数据库，端口 5432） 核心诉求： 在 Windows 上通过 Tailscale 隧道，直连 192.168.31.220:5432。 👻 2. 诡异的半残网络现象 在配置好一切后，遇到了一个极其反直觉的现象： ICMP 畅通： 在 Windows 上 Ping 192.168.31.220，完美连通，延迟仅 5ms。说明 Tailscale 隧道和底层的三层路由毫无问题。 网关到目标畅通： 在 Ubuntu (177) 本机用 nc -zv 192.168.31.220 5432 测试，返回 open。说明 220 目标服务器的防火墙和数据库配置正常。 TCP 业务瘫痪： 在 Windows 上用客户端连接 220 的 5432 端口，死活连不上（超时）。 初步抓包诊断： 在 Ubuntu (177) 上执行 tcpdump -i any host 192.168.31.220 and port 5432 -nn，发现只有从 tailscale0 进来的包 (In)，没有任何发往物理网卡的包 (Out)。 阶段性结论： 数据包安全到达了 177 网关，但被 177 的内部网络栈“私吞”了，未能进入 FORWARD（转发）流程。 🕵️‍♂️ 3. 排查深入：内核级“天眼”追踪 既然常规的放行规则（在 FORWARD 链首部强行 ACCEPT）无效，说明包死在了更靠前的位置。为了不靠猜排错，我决定利用 iptables -j LOG 在 Linux Netfilter 的 6 个核心关卡埋下日志探头，追踪这个 TCP 包的完整生命周期。 执行部署探头命令： # 1. 刚接触到网卡，最原始的状态 (raw 表) sudo iptables -t raw -I PREROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S1-RAW-PRE: " # 2. 准备被修改标记 (mangle 表) sudo iptables -t mangle -I PREROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S2-MGL-PRE: " # 3. 准备被劫持或映射 (nat 表) sudo iptables -t nat -I PREROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S3-NAT-PRE: " # 4. 意外关卡：如果包被劫持给本机，会进入 INPUT 链 sudo iptables -I INPUT 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S-X-INPUT: " # 5. 正常进入转发通道 (FORWARD 链) sudo iptables -I FORWARD 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S4-FORWARD: " # 6. 即将离开物理网卡发往目标 (POSTROUTING 链) sudo iptables -t nat -I POSTROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S5-NAT-POST: " 随后在 Windows 上再次发起 5432 端口的连接请求，然后在 Ubuntu 提取过滤后的内核日志： sudo dmesg -T | grep -E "S1|S2|S3|S4|S5|S-X" | grep "DPT=5432" | tail -n 6 提取到的震撼日志： [四 2月 26 21:58:22 2026] S1-RAW-PRE: SRC=100.116.47.82 DST=192.168.31.220 DPT=5432 [四 2月 26 21:58:22 2026] S2-MGL-PRE: SRC=100.116.47.82 DST=192.168.31.220 DPT=5432 [四 2月 26 21:58:22 2026] S3-NAT-PRE: SRC=100.116.47.82 DST=192.168.31.220 DPT=5432 [四 2月 26 21:58:22 2026] S4-FORWARD: SRC=100.116.47.82 DST=192.168.31.247 DPT=5432 [四 2月 26 21:58:22 2026] S5-NAT-POST: SRC=100.116.47.82 DST=192.168.31.247 DPT=5432 日志判断依据与分析： 包在 S1、S2、S3 阶段，目标地址 (DST) 一直都是我预期的 192.168.31.220。 但是当包走到 S4-FORWARD（进入转发通道）时，DST 突然变成了 192.168.31.247！ 铁证如山： 存在一个极度隐蔽的 DNAT（目标地址转换） 规则，在 PREROUTING 链的尾部强行把我去往 220 的数据包，篡改地址发送给了局域网里的另一台机器（247）。 这也解释了为什么 Ping 能通： 因为这个劫持者只对 TCP 5432 端口感兴趣，ICMP 协议的 Ping 包得以幸免，原路走完了正常转发流程。 💥 4. 揪出元凶：1Panel 端口转发的“贪婪”逻辑 顺藤摸瓜，我直接搜查 nat 表中所有的 DNAT 规则： sudo iptables -t nat -S | grep -E "DNAT|REDIRECT" 结果抓到了 1Panel 面板 生成的一批端口转发规则： -A 1PANEL_PREROUTING -p tcp -m tcp --dport 3308 -j DNAT --to-destination 192.168.31.131:3306 -A 1PANEL_PREROUTING -p tcp -m tcp --dport 5432 -j DNAT --to-destination 192.168.31.247:5432 ... 这条规则翻译成人类语言是：“只要经过我这台机器的 TCP 包，目标端口是 5432 的，一律改写目标 IP 为 247。” 它缺少了一个极其重要的限定条件：限定目标 IP 必须是本机。 作为对比，同机器上的 Docker 端口映射规则是这样的： -A DOCKER ! -i docker0 -p tcp -m tcp --dport 3306 -j DNAT ... 在这之前，Docker 会通过 ADDRTYPE match dst-type LOCAL 进行总闸拦截，确保只有访问宿主机本身的流量才会被 DNAT，完美避开了路过的路由流量。 1Panel 这种“裸奔”的规则，导致充当网关的 177 把所有 Subnet 子网内同名端口的流量全部无差别“绑架”了。 🛠️ 5. 修复过程 直接删除规则会导致原本的端口转发功能失效。为了既满足“访问 177 转发到 247”，又不影响“路过 177 去 220”，我编写了一段 Shell 脚本。 它的逻辑是读取 1Panel 生成的每一条缺陷 DNAT 规则，将其删除，并原地替换为带有 -m addrtype --dst-type LOCAL 的完美限制版本： sudo iptables-save | grep 'A 1PANEL_PREROUTING' | grep DNAT | while read -r line; do # 提取原始端口和目标地址 port=$(echo "$line" | grep -oP -- '--dport \K\d+') dest=$(echo "$line" | grep -oP -- '--to-destination \K\S+') # 1. 拆除 1Panel 贪婪的旧规则 sudo iptables -t nat -D 1PANEL_PREROUTING -p tcp -m tcp --dport $port -j DNAT --to-destination $dest # 2. 插入带 LOCAL 限制的新规则 sudo iptables -t nat -A 1PANEL_PREROUTING -p tcp -m tcp --dport $port -m addrtype --dst-type LOCAL -j DNAT --to-destination $dest echo "✅ 已修复端口 $port 的转发规则 (目标: $dest)" done 最后，一键拆除之前部署的所有日志探头： sudo iptables -t raw -D PREROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S1-RAW-PRE: " sudo iptables -t mangle -D PREROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S2-MGL-PRE: " sudo iptables -t nat -D PREROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S3-NAT-PRE: " sudo iptables -D INPUT -p tcp -s 100.116.47.82 -j LOG --log-prefix "S-X-INPUT: " sudo iptables -D FORWARD -p tcp -s 100.116.47.82 -j LOG --log-prefix "S4-FORWARD: " sudo iptables -t nat -D POSTROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S5-NAT-POST: " 💡 6. 避坑总结 ICMP 与 TCP 的差别待遇：网络排错时，Ping 能通只代表底层的三层路由没问题，不代表上层协议没在 nat 或 mangle 表被劫持。 警惕透明代理与面板转发：把装有 1Panel、宝塔或软路由代理（如 Mihomo/Clash）的机器作为 Subnet 网关时，极易发生 PREROUTING 链的连环翻车。1Panel 现有的端口转发逻辑对做网关的机器非常不友好。 定位永远比瞎猜重要：利用 iptables -j LOG 在内核各个 Hook 点打日志，是解决 Linux 复杂网络迷路问题的快速分析定位思路。
【自动跟踪更新】Tailscale Derp docker 容器镜像 去除域名验证补丁版
简介 最近看到 tailscale 一直在保持更新（尤其是提高网络吞吐量相关的特性），我就去问了问 ai，如果 derp 服务器一直保持在旧版是不是在中转模式下吃不到新版本的特性？AI 说确实。。 所以自己搓了个 github actions 自动检查新版本 derp 拉过来打补丁 然后发布到 dockerhub 直观效果就是 去除域名验证，国内免备案 (ip + 端口可以直接用)，安全性由客户端验证保证 所以宿主机尽量也运行 tailscale 的客户端来确保安全（被扫到就会被白嫖）！ Github 仓库 [post url="https://github.com/DeepFal/tailscale-derp" title="GitHub" intro="DeepFal/tailscale-derp" cover="https://cc.deepfal.cn/2026/02/26/69a019972b8be.png" /] 快速启动： docker run -d \ --name tailscale-derp \ --restart unless-stopped \ -p 0.0.0.0:59443:36666 \ -p 0.0.0.0:3478:3478/udp \ -v /run:/var/run:ro \ deepfal/tailscale-derp:latest \ ./derper \ -hostname derp.deepfal.cn \ -a :36666 \ -certmode manual \ -certdir /ssl \ --verify-clients 自搭建 derp 服务器完整教程说明： [post cid="945" cover="https://cc.deepfal.cn/2026/02/26/69a019db3f91f.png"/]
Docker 部署 Mihomo (Clash Meta) 核心 + WebUI 指南
流程概览 前言 Mihomo（原名 Mihomo Meta，亦即 Clash Meta）是目前 Clash 核心最活跃、功能最强大的分支。由于原始 Clash 项目删库，Mihomo 已成为各平台客户端的首选核心。 值得注意的是，Mihomo 本身是一个纯命令行程序（Core），没有图形界面。为了方便日常管理和监控，我们需要为其搭配一个 Web UI（如 Metacubexd）。本文将详细介绍如何在 Docker 环境（包括 Linux 服务器、NAS、飞牛OS等）中优雅地部署 Mihomo + WebUI，并开启 TUN 模式实现更接管更底层的流量。 准备工作 在开始之前，请确保您的环境满足以下条件： Docker 环境：已安装 Docker 及 Docker Compose。 基础知识：熟悉基本的命令行操作（创建文件夹、编辑文件）。 网络环境：拥有可用的节点订阅链接。 第一步：编写配置文件 (config.yaml) Mihomo 的运行核心在于配置文件。我们需要先在宿主机上准备好这个文件。 新建一个文件夹，命名为 mihomo。 在其中创建一个名为 config.yaml 的文件。 参考以下模板填入内容。 ⚠️ 重点修改提示： 请务必修改下方模板中的 第 7 行（API访问密钥）、第 20、21 行（节点订阅信息）与 第 26 行（节点名称前缀）。修改时请去掉尖括号 < >。 mixed-port: 7890 allow-lan: true bind-address: '*' mode: rule log-level: info external-controller: '0.0.0.0:9090' secret: "<API访问密钥>" tun: enable: false stack: mixed dns-hijack: - "any:53" - "tcp://any:53" auto-route: true auto-redirect: true auto-detect-interface: true proxy-providers: <节点提供者名称>: url: "<节点订阅链接>" type: http interval: 86400 health-check: {enable: true,url: "https://www.gstatic.com/generate_204", interval: 300} override: additional-prefix: "<节点名称前缀>" proxies: - name: "直连" type: direct udp: true geodata-mode: true geox-url: geoip: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat" geosite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb" asn: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb" dns: enable: true ipv6: true respect-rules: true enhanced-mode: fake-ip fake-ip-filter: - "*" - "+.lan" - "+.local" - "+.market.xiaomi.com" nameserver: - https://120.53.53.53/dns-query - https://223.5.5.5/dns-query proxy-server-nameserver: - https://120.53.53.53/dns-query - https://223.5.5.5/dns-query nameserver-policy: "geosite:cn,private": - https://120.53.53.53/dns-query - https://223.5.5.5/dns-query "geosite:geolocation-!cn": - "https://dns.cloudflare.com/dns-query" - "https://dns.google/dns-query" proxy-groups: - name: 默认 type: select proxies: [自动选择,直连,香港,台湾,日本,新加坡,美国,其它地区,全部节点] - name: Google type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: Telegram type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: Twitter type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: 哔哩哔哩 type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: 巴哈姆特 type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: YouTube type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: 海外AI type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: NETFLIX type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: Spotify type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: Github type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] - name: 国内 type: select proxies: [直连,默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择] - name: 其他 type: select proxies: [默认,香港,台湾,日本,新加坡,美国,其它地区,全部节点,自动选择,直连] #分隔,下面是地区分组 - name: 香港 type: select include-all: true filter: "(?i)港|hk|hongkong|hong kong" - name: 台湾 type: select include-all: true filter: "(?i)台|tw|taiwan" - name: 日本 type: select include-all: true filter: "(?i)日|jp|japan" - name: 美国 type: select include-all: true filter: "(?i)美|us|unitedstates|united states" - name: 新加坡 type: select include-all: true filter: "(?i)(新|sg|singapore)" - name: 其它地区 type: select include-all: true filter: "(?i)^(?!.*(?:🇭🇰|🇯🇵|🇺🇸|🇸🇬|🇨🇳|港|hk|hongkong|台|tw|taiwan|日|jp|japan|新|sg|singapore|美|us|unitedstates)).*" - name: 全部节点 type: select include-all: true - name: 自动选择 type: url-test include-all: true tolerance: 10 rules: - GEOIP,lan,直连,no-resolve - GEOSITE,twitter,Twitter - GEOSITE,youtube,YouTube - GEOSITE,category-ai-!cn,海外AI - GEOSITE,github,Github - GEOSITE,google,Google - GEOSITE,telegram,Telegram - GEOSITE,netflix,NETFLIX - GEOSITE,bilibili,哔哩哔哩 - GEOSITE,bahamut,巴哈姆特 - GEOSITE,spotify,Spotify - GEOSITE,CN,国内 - GEOSITE,geolocation-!cn,其他 - GEOIP,CN,国内 - MATCH,其他 第二步：构建 Docker Compose 我们将同时部署 Mihomo 核心 和 Metacubexd (WebUI)。 在 mihomo 文件夹同级或任意位置，创建 docker-compose.yml。 🌟 关键配置说明 (TUN 模式支持)： 为了支持 TUN 模式（虚拟网卡），容器必须拥有网络管理权限。我们在配置中加入了 cap_add: [NET_ADMIN] 和 devices: [/dev/net/tun]，这是实现透明代理的关键。 version: '3' services: # Web 管理面板 metacubexd: container_name: metacubexd #image: ghcr.io/metacubex/metacubexd # 国内加速源 image: ghcr.1ms.run/metacubex/metacubexd restart: always ports: - '9097:80' # WebUI 访问端口 # Mihomo 核心 mihomo: container_name: mihomo #image: docker.io/metacubex/mihomo:latest # 国内加速源 image: docker.1ms.run/metacubex/mihomo:latest restart: always pid: host ipc: host network_mode: host # 推荐使用 Host 模式，性能最好且便于处理 IPv6 # --- TUN 模式核心配置 --- cap_add: - NET_ADMIN # 赋予网络管理权限 devices: - /dev/net/tun:/dev/net/tun # 映射宿主机的 TUN 设备 volumes: # ⚠️ 注意：冒号左边请修改为你 config.yaml 所在的实际绝对路径 - ./mihomo:/root/.config/mihomo 第三步：启动与初始化 1. 启动容器 在 docker-compose.yml 所在目录执行： sudo docker-compose up -d 如果是飞牛OS等图形化系统，请直接在 Docker 管理器中“使用现有 Compose 文件”创建项目即可。 2. 解决规则文件下载失败 (Fatal Error) 初次启动时，查看日志可能会发现如下报错： level=fatal msg="Parse config error... can't download MMDB: context deadline exceeded" 这是因为网络环境导致容器无法自动下载 GeoIP/GeoSite 数据库。 解决方法： 前往 MetaCubeX/meta-rules-dat 下载以下文件： geoip.dat geosite.dat country-lite.mmdb GeoLite2-ASN.mmdb 将这4个文件手动上传到宿主机的 mihomo 文件夹（与 config.yaml 同级）。 重启容器：sudo docker-compose restart mihomo 第四步：配置 WebUI 打开浏览器访问：http://设备IP:9097。 进入设置页面，添加后端： API Base URL: http://设备IP:9090 Secret: 在 config 第 6 行设置的 API 访问密钥 点击添加，连接成功后即可看到流量仪表盘和节点选择界面。 进阶：关于 TUN 模式与透明代理 TUN 模式允许 Mihomo 创建一个虚拟网卡，接管设备上所有层级的流量（包括不走系统代理的终端命令、游戏等）。 为什么需要特殊权限？ 在 Docker 中，默认容器是没有权限创建网络设备的。要启用 TUN 模式，必须在启动参数中显式声明： --cap-add=NET_ADMIN：允许容器修改网络堆栈。 --device=/dev/net/tun：允许容器访问 Linux 的 TUN 字符设备。 在本文提供的 docker-compose.yml 中，我们已经默认添加了这两项配置。 如何启用？ 确保 Docker 配置已包含上述权限参数。 在 WebUI 的设置页面中勾选 TUN 模式。 或者在 config.yaml 中将 tun: enable 设置为 true 并重启容器。 此时，配合路由表的设置，Mihomo 即可变身为一个网关，实现透明代理。 Q&A 常见问题 Q1: "Failed to fetch" 无法连接后端？ A: 检查 WebUI 中填写的后端地址端口是否为 9090（不是 9097），并确保 config.yaml 中 external-controller 监听的是 0.0.0.0:9090。 Q2: 为什么不使用在线版的 WebUI (如 yacd)？ A: 在线版面板（通常是 HTTPS）因浏览器安全策略（混合内容拦截），往往无法连接到你本地的 HTTP 后端（Mihomo）。使用 Docker 自建本地版 WebUI 最稳定。 Q3: 如何在其他设备上使用这个代理？ A: 在其他设备（手机/电脑）的 Wi-Fi 代理设置中，填入 IP: 宿主机IP，端口: 7890。 相关链接 Mihomo 项目仓库 Mihomo 官方文档 Metacubexd 面板仓库
Qwen3 模型原生支持 32,768 个 token 的上下文长度。对于超出此限制的长文本，官方推荐使用 YaRN (Yet another RoPE-based Neural network scaling) 扩展技术。YaRN 是一种基于旋转位置编码（RoPE）的扩展方法，已验证可将上下文长度扩展至 131,072 个 token，并被 transformers、llama.cpp、vllm 和 sglang 等多个推理框架支持。 要启用 YaRN，需配置以下关键参数： rope_scaling: 一个配置 RoPE 缩放策略的字典。 rope_type: 字符串类型，指定缩放类型。启用 YaRN 时，其值为 "yarn"。 factor: 浮点数，定义上下文窗口扩展的倍数。例如，4.0 表示将原始长度扩展四倍。 original_max_position_embeddings: 整数，表示模型的原始最大上下文长度，对于 Qwen3，其值为 32768。 max_model_len: 整数，最终设置的模型最大上下文长度，应与 factor 的乘积相匹配。 解决方案：两种配置方式 根据官方文档，有以下两种主要方法来启用 YaRN。 1. 修改模型配置文件（推荐） 这是最直接且不易出错的方法，完全规避了命令行参数解析的复杂性。 定位文件: 打开 Qwen3 模型目录下的 config.json 文件。 添加配置: 在文件中添加或修改 rope_scaling 字段，并设置 max_model_len。 JSON "rope_scaling": { "rope_type": "yarn", "factor": 4.0, "original_max_position_embeddings": 32768 }, "max_model_len": 131072 这种方法配置一次即可永久生效，特别适用于生产环境部署。 2. 命令行参数传递 对于不同的推理框架，命令行参数的格式有所不同。 vLLM: Bash VLLM_USE_MODELSCOPE=true vllm serve ... --rope-scaling '{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":32768}' --max-model-len 131072 sglang: Bash SGLANG_USE_MODELSCOPE=true python -m sglang.launch_server ... --json-model-override-args '{"rope_scaling":{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":32768}}' llama.cpp: Bash llama-server ... --rope-scaling yarn --rope-scale 4 --yarn-orig-ctx 32768 命令行参数的格式因框架而异，需特别注意。 推荐模型参数与最佳实践 在启用 YaRN 长文本支持后，为了获得最佳推理效果，还需关注以下生成参数和最佳实践： 采样参数 对于思考模式（enable_thinking=True），使用 Temperature=0.6、TopP=0.95、TopK=20 和 MinP=0。不要使用贪婪解码，因为它可能导致性能下降和无尽的重复。 对于非思考模式（enable_thinking=False），我们建议使用 Temperature=0.7、TopP=0.8、TopK=20 和 MinP=0。 对于支持的框架，您可以将 presence_penalty 参数调整在 0 到 2 之间以减少无尽的重复。然而，使用较高的值偶尔会导致语言混杂和模型性能轻微下降。 其他重要提示 性能考量: 大部分开源框架都实现了静态 YaRN。我们建议仅在需要处理长上下文时才启用 YaRN，因为它可能影响处理较短文本时的性能。 factor 调整: 官方建议根据典型上下文长度调整 factor，例如，若应用场景的上下文长度通常为 65,536 个 token，则将 factor 设置为 2.0。 版本兼容性: 若遇到 Unrecognized keys in rope_scaling 警告，请升级 transformers 至 4.51.0 或更高版本。 显存需求: 扩展上下文长度会显著增加显存消耗。处理 131,072 个 token 的文本可能需要比原生长度多占用 30-40% 的显存。 输出长度: 对于大多数查询，建议设置 32,768 个 token 的输出长度。对于复杂问题，最大输出长度可设置为 38,912，为模型提供足够的空间生成详细回答。
自搭建 Tailscale DERP 服务器并使用客户端验证(verify-clients)
Tailscale DERP（Distributed, Encrypted, and Resilient Packet Relay）服务器是 Tailscale 网络中用于中继流量的组件，尤其适用于网络环境复杂或直连困难的场景，但是国内网络连接官方提供的 derp 服务器时普遍存在延迟高，速度慢，甚至几乎无法使用的情况。所以产生了自建私有 derp 流量中转服务器的需求，以便在 p2p 无法直连打洞成功的场景下能够实现和使用低延迟，高速度的 tailscale 流量中继服务。以下是使用 Docker 搭建 Tailscale DERP 服务器的详细步骤： 环境准备 操作系统：推荐使用 Linux 系统，如 Ubuntu、Debian 等，因为 Docker 在 Linux 上的性能和兼容性更佳。 Docker 环境：确保已安装 Docker。可以通过运行 docker --version 命令来检查 Docker 是否已安装。 Tailscale 客户端：在宿主机上安装并启动 Tailscale 客户端，以便 DERP 服务器能够与 Tailscale 网络通信。 网络配置：确保服务器的网络配置正确，能够正常访问外网，且对应的端口没有被防火墙阻止。 搭建步骤 1. 安装并启动 Tailscale 客户端 在宿主机上安装 Tailscale 客户端，并启动它。安装方法因操作系统而异，具体可以参考 Tailscale 官方文档。 启动 Tailscale 客户端后，它会在 /var/run/tailscale 目录下创建一个名为 tailscaled.sock 的套接字文件。 2. 拉取 Docker 镜像 从 Docker Hub 拉取 deepfal/tailscale-derp 镜像。该镜像已经包含了搭建 DERP 服务器所需的全部依赖和配置。 docker pull deepfal/tailscale-derp:latest 3. 运行 DERP 服务器容器 使用以下命令启动 DERP 服务器容器。该命令会将 DERP 服务器映射到宿主机的指定端口，并挂载必要的文件和目录。 docker run -d \ --name tailscale-derp \ --restart unless-stopped \ -p 0.0.0.0:59443:36666 \ -p 0.0.0.0:3478:3478/udp \ -v /run:/var/run:ro \ deepfal/tailscale-derp:latest \ ./derper \ -hostname derp.deepfal.cn \ -a :36666 \ -certmode manual \ -certdir /ssl \ --verify-clients 参数解释 -d：后台运行容器。 --name tailscale-derp：指定容器名称为 tailscale-derp。 -p 0.0.0.0:59443:36666：将容器的 36666 端口映射到宿主机的 59443 端口，允许外部访问。 -p 0.0.0.0:3478:3478/udp：将容器的 3478 端口映射到宿主机的 3478 端口，用于 UDP 通信。 -v /run:/var/run:ro：将宿主机的 Tailscale 套接字文件挂载到容器中，以便 DERP 服务器与 Tailscale 守护进程通信，挂载目录是为了防止客户端重启导致 socks 突然获取不到了导致 derp 工作异常，挂目录比较稳。 deepfal/tailscale-derp:latest：指定使用的 Docker 镜像。 ./derper：指定容器启动时运行的命令。 -hostname "derp.deepfal.cn"：设置 DERP 服务器的主机名。 -a ":36666"：指定 DERP 服务器监听的端口。 -certmode "manual"：指定证书模式为手动。 -certdir "/ssl"：指定证书目录。 --verify-clients：启用客户端验证。 4. 验证 DERP 服务器状态 启动容器后，可以通过以下命令查看 DERP 服务器的运行状态： docker logs tailscale-derp 如果看到类似 DERP server started 的日志信息，表示 DERP 服务器已成功启动。 5. 配置 Tailscale 客户端 在 Tailscale 后台配置中，添加自建 DERP 服务器的信息。具体配置方法如下： { "derpMap": { "OmitDefaultRegions": false, "Regions": { "910": { "RegionID": 910, "RegionCode": "自建节点(名称)", "Nodes": [ { "Name": "910", "RegionID": 910, "HostName": "宿主机IP", "InsecureForTests": true, "DERPPort": 59443 } ], "RegionName": "自建节点(名称)" } } } } 将 宿主机IP 替换为 DERP 服务器的实际 IP 地址。 添加 derp 节点之后就可以立即使用 tailscale netcheck 检查 derp 服务器是否在线了 注意事项 端口映射：确保宿主机的 59443 (tcp) 和 3478 (udp) 端口没有被其他服务占用，并且防火墙允许外部访问这些端口。
一文读懂 QUIC 协议：更快、更稳、更高效的网络通信
一文读懂 QUIC 协议：更快、更稳、更高效的网络通信 ​ 你是否也有这样的困扰：打开 APP 巨耗时、刷剧一直在缓冲、追热搜打不开页面、信号稍微差点就直接加载失败…… ​ 如果有一个协议能让你的上网速度，在不需要任何修改的情况下就能提升 20%，特别是网络差的环境下能够提升 30%以上；如果有一个协议可以让你在 WiFi 和蜂窝数据切换时，网络完全不断开、直播不卡顿、视频不缓冲；你愿意去了解一下它吗？它就是 QUIC 协议。本文将从 QUIC 的背景、原理、实践部署等方面来详细介绍。 一：网络协议栈 1.1 什么叫网络协议？ ​ 类似于我们生活中签署的合同一样，比如买卖合同是为了约束买卖双方的行为按照合同的要求履行，网络协议是为了约束网络通信过程中各方（客户端、服务端及中间设备）必须按照协议的规定进行通信，它制定了数据包的格式、数据交互的过程等等，网络中的所有设备都必须严格遵守才可以全网互联。 ​ 在网络协议栈中，是有分层的，每一层负责不同的事务。我们讨论最多的有三个：应用层、传输层、网络层。应用层主要是针对应用软件进行约束，比如你访问网站需要按照 HTTP 协议格式和要求进行，你发送电子邮件需要遵守 SMTP 等邮件协议的格式和要求；传输层主要负责数据包在网络中的传输问题，比如如何保证数据传输的时候的安全性和可靠性、数据包丢了怎么处理；网络层，也叫路由转发层，主要负责数据包从出发地到目的地，应该怎样选择路径才能更快的到达。合理的网络协议能够让用户上网更快！ 1.2 HTTP/3 协议 ​ HTTP/3 是第三个主要版本的 HTTP 协议。与其前任 HTTP/1.1 和 HTTP/2 不同，在 HTTP/3 中，弃用 TCP 协议，改为使用基于 UDP 协议的 QUIC 协议实现。所以，HTTP/3 的核心在于 QUIC 协议。显然，HTTP/3 属于应用层协议，而它使用的 QUIC 协议属于传输层协议。 1.3 我们需要 HTTP/3 协议吗 ​ 很多人可能都会有这样一个疑问，为什么在 2015 年才标准化了 HTTP/2 ，这么快就需要 HTTP/3？ ​ 我们知道，HTTP/2 通过引入“流”的概念，实现了多路复用。简单来说，假设你访问某个网站需要请求 10 个资源，你使用 HTTP1.1 协议只能串行地发请求，资源 1 请求成功之后才能发送资源 2 的请求，以此类推，这个过程是非常耗时的。如果想 10 个请求并发，不需要串行等待的话，在 HTTP1.1 中，应用就需要为一个域名同时建立 10 个 TCP 连接才行（一般浏览器不允许建立这么多），这无疑是对资源的极大的浪费。HTTP/2 的多路复用解决了这一问题，能使多条请求并发。 ​ 但现实很残酷，为什么很多业务用了 HTTP/2，反倒不如 HTTP1.1 呢？ ​ 第一：多流并发带来了请求优先级的问题，因为有的请求客户端（比如浏览器）希望它能尽快返回，有的请求可以晚点返回；又或者有的请求需要依赖别的请求的资源来展示。流的优先级表示了这个请求被处理的优先级，比如客户端请求的关键的 CSS 和 JS 资源是必须高优先级返回的，图片视频等资源可以晚一点响应。流的优先级的设置是一个难以平衡或者难以做到公平合理的事情，如果设置稍微不恰当，就会导致有些请求很慢，这在用户看来，就是用了 HTTP/2 之后，怎么有的请求变慢了。 ​ 第二：HTTP/2 解决了 HTTP 协议层面的队头阻塞，但是 TCP 的队头阻塞仍然没有解决，所有的流都在一条 TCP 连接上，如果万一序号小的某个包丢了，那么 TCP 为了保证到达的有序性，必须等这个包到达后才能滑动窗口，即使后面的序号大的包已经到达了也不能被应用程序读取。这就导致了在多条流并发的时候，某条流的某个包丢了，序号在该包后面的其他流的数据都不能被应用程序读取。这种情况下如果换做 HTTP1.1，由于 HTTP1.1 是多条连接，某个连接上的请求丢包了，并不影响其他连接。所以在丢包比较严重的情况下，HTTP/2 整体效果大概率不如 HTTP1.1 ​ 事实上，我们并不是真的需要新的 HTTP 版本，而是需要对底层传输控制协议(TCP) 进行升级。 1.4 QUIC 协议栈 ​ QUIC 协议实现在用户态，建立在内核态的 UDP 的基础之上，集成了 TCP 的可靠传输特性，集成了 TLS1.3 协议，保证了用户数据传输的安全。 二：QUIC 协议的优秀特性 2.1 建连快 ​ 数据的发送和接收，要想保证安全和可靠，一定是需要连接的。TCP 需要，QUIC 也同样需要。连接到底是什么？连接是一个通道，是在一个客户端和一个服务端之间的唯一一条可信的通道，主要是为了安全考虑，建立了连接，也就是建立了可信通道，服务器对这个客户端“很放心”，对于服务器来说：你想跟我进行通信，得先让我认识一下你，我得先确认一下你是好人，是有资格跟我通信的。那么这个确认对方身份的过程，就是建立连接的过程。 ​ 传统基于 TCP 的 HTTPS 的建连过程为什么如此慢？它需要 TCP 和 TLS 两个建连过程。如图 1 所示（传统 HTTPS 请求流程图）： ​ 对于一个小请求（用户数据量较小）而言，传输数据只需要 1 个 RTT，但是光建连就花掉了 3 个 RTT，这是非常不划算的，这里建连包括两个过程：TCP 建连需要 1 个 RTT，TLS 建连需要 2 个 RTT。RTT：Round Trip Time，数据包在网络上一个来回的时间。 ​ 为什么需要两个过程？可恶就可恶在这个地方，TCP 和 TLS 没办法合并，因为 TCP 是在内核里完成的，TLS 是在用户态。也许有人会说把干掉内核里的 TCP，把 TCP 挪出来放到用户态，然后就可以和 TLS 一起处理了。首先，你干不掉内核里的 TCP，TCP 太古老了，全世界的服务器的 TCP 都固化在内核里了。所以，既然干不掉 TCP，那我不用它了，我再自创一个传输层协议，放到用户态，然后再结合 TLS，这样不就可以把两个建连过程合二为一了吗？是的，这就是 QUIC。 2.1.1 QUIC 的 1-RTT 建连 ​ 如图 2 所示，是 QUIC 的连接建立过程：初次建连只需要 1 个 RTT 即可完成建连。后续再次建连就可以使用 0-RTT 特性 ​ QUIC 的 1-RTT 建连：客户端与服务端初次建连（之前从未进行通信过），或者长时间没有通信过（0-RTT 过期了），只能进行 1-RTT 建连。只有先进行一次完整的 1-RTT 建连，后续一段时间内的通信才可以进行 0-RTT 建连。 ​ 如图 3 所示：QUIC 的 1-RTT 建连可以分成两个部分。QUIC 连接信息部分和 TLS1.3 握手部分。 ​ QUIC 连接：协商 QUIC 版本号、协商 quic 传输参数、生成连接 ID、确定 Packet Number 等信息，类似于 TCP 的 SYN 报文；保证通信的两端确认过彼此，是对的人。 ​ TLS1.3 握手：标准协议，非对称加密，目的是为了协商出对称密钥，然后后续传输的数据使用这个对称密钥进行加密和解密，保护数据不被窃取。 ​ 我们重点看 QUIC 的 TLS1.3 握手过程。 ​ 我们通过图 4 可以看到，整个握手过程需要 2 次握手（第三次握手是带了数据的），所以整个握手过程只需要 1-RTT（RTT 是指数据包在网络上的一个来回）的时间。 1-RTT 的握手主要包含两个过程： 客户端发送 Client Hello 给服务端； 服务端回复 Server Hello 给客户端； 我们通过下图中图 5 和图 6 来看 Client Hello 和 Server Hello 具体都做了啥： 第一次握手（Client Hello 报文） ​ 首先，Client Hello 在扩展字段里标明了支持的 TLS 版本（Supported Version：TLS1.3）。值得注意的是 Version 字段必须要是 TLS1.2，这是因为 TLS1.2 已经在互联网上存在了 10 年。网络中大量的网络中间设备都十分老旧，这些网络设备会识别中间的 TLS 握手头部，所以 TLS1.3 的出现如果引入了未知的 TLS Version 必然会存在大量的握手失败。 ​ 其次，ClientHello 中包含了非常重要的 key_share 扩展：客户端在发送之前，会自己根据 DHE 算法生成一个公私钥对。发送 Client Hello 报文的时候会把这个公钥发过去，那么这个公钥就存在于 key_share 中，key_share 还包含了客户端所选择的曲线 X25519。总之，key_share 是客户端提前生成好的公钥信息。 ​ 最后，Client Hello 里还包括了：客户端支持的算法套、客户端所支持的椭圆曲线以及签名算法、psk 的模式等等，一起发给服务端。 第二次握手：（Server Hello 报文） ​ 服务端自己根据 DHE 算法也生成了一个公私钥对，同样的，Key_share 扩展信息中也包含了 服务端的公钥信息。服务端通过 ServerHello 报文将这些信息发送给客户端。 ​ 至此为止，双方（客户端服务端）都拿到了对方的公钥信息，然后结合自己的私钥信息，生成 pre-master key，在这里官方的叫法是（client_handshake_traffic_secret 和 server_handshake_traffic_secret），然后根据以下算法进行算出 key 和 iv，使用 key 和 iv 对 Server Hello 之后所有的握手消息进行加密。 ​ 注意：在握手完成之后，服务端会发送一个 New Session Ticket 报文给客户端，这个包非常重要，这是 0-RTT 实现的基础。 2.1.2 QUIC 的 0-RTT 握手 ​ 这个功能类似于 TLS1.2 的会话复用，或者说 0-RTT 是基于会话复用功能的。 ​ 通过上面图 10 我们可以看到，client 和 server 在建连时，仍然需要两次握手，仍然需要 1 个 rtt，但是为什么我们说这是 0-rtt 呢，是因为 client 在发送第一个包 client hello 时，就带上了数据（HTTP 请求），从什么时候开始发送数据这个角度上来看，的确是 0-RTT。 我们通过抓包来看 0-RTT 的过程： ​ 所以真正在实现 0-RTT 的时候，请求的数据并不会跟 Initial 报文（内含 Client Hello）一起发送，而是单独一个数据包（0-RTT 包）进行发送，只不过是跟 Initial 包同时进行发送而已。 ​ 我们单独看 Initial 报文发现，除了 pre_share_key、early-data 标识等信息与 1-RTT 时不同，其他并无区别。 2.1.3 QUIC 建连需要注意的问题 ​ 第一，QUIC 实现的时候，必须缓存收到的乱序加密帧，这个缓存至少要大于 4096 字节。当然可以选择缓存更多的数据，更大的缓存上限意味着可以交换更大的密钥或证书。终端的缓存区大小不必在整个连接生命周期内保持不变。这里记住：乱序帧一定要缓存下来。如果不缓存，会导致连接失败。如果终端的缓存区不够用了，则其可以通过暂时扩大缓存空间确保握手完成。如果终端不扩大其缓存，则其必须以错误码 CRYPTO_BUFFER_EXCEEDED 关闭连接。 ​ 第二，0-RTT 存在前向安全问题，请慎用！ 2.2 连接迁移 ​ QUIC 通过连接 ID 实现了连接迁移。 ​ 我们经常需要在 WiFi 和 4G 之间进行切换，比如我们在家里时使用 WiFi，出门在路上，切换到 4G 或 5G，到了商场，又连上了商场的 WiFi，到了餐厅，又切换到了餐厅的 WiFi，所以我们的日常生活中需要经常性的切换网络，那每一次的切换网络，都将导致我们的 IP 地址发生变化。 ​ 传统的 TCP 协议是以四元组（源 IP 地址、源端口号、目的 ID 地址、目的端口号）来标识一条连接，那么一旦四元组的任何一个元素发生了改变，这条连接就会断掉，那么这条连接中正在传输的数据就会断掉，切换到新的网络后可能需要重新去建立连接，然后重新发送数据。这将会导致用户的网络会“卡”一下。 ​ 但是，QUIC 不再以四元组作为唯一标识，QUIC 使用连接 ID 来标识一条连接，无论你的网络如何切换，只要连接 ID 不变，那么这条连接就不会断，这就叫连接迁移！ 2.2.1 连接 ID ​ 每条连接拥有一组连接标识符，也就是连接 ID，每个连接 ID 都能标识这条连接。连接 ID 是由一端独立选择的，每个端（客户端和服务端统称为端）选择连接 ID 供对端使用。也就是说，客户端生成的连接 ID 供服务端使用（服务端发送数据时使用客户端生成的连接 ID 作为目的连接 ID），反过来一样的。 ​ 连接 ID 的主要功能是确保底层协议（UDP、IP 及更底层的协议栈）发生地址变更（比如 IP 地址变了，或者端口号变了）时不会导致一个 QUIC 连接的数据包被传输到错误的 QUIC 终端（客户端和服务端统称为终端）上。 2.2.2 QUIC 的连接迁移过程 ​ QUIC 限制连接迁移为仅客户端可以发起，客户端负责发起所有迁移。如果客户端接收到了一个未知的服务器发来的数据包，那么客户端必须丢弃这些数据包。 如图 14 所示，连接迁移过程总共需要四个步骤。 连接迁移之前，客户端使用 IP1 和服务端进行通信； 客户端 IP 变成 IP2，并且使用 IP2 发送非探测帧给服务端； 启动路径验证（双方都需要互相验证），通过 PATH_CHANLLENGE 帧和 PATH_RESPONSE 帧进行验证。 验证通过后，使用 IP2 进行通信。 2.3 解决 TCP 队头阻塞问题 在 HTTP/2 中引入了流的概念。目的是实现多个请求在同一个连接上并发，从而提升网页加载的效率。 ​ 由图 15 来看，假设有两个请求同时发送，红色的是请求 1，蓝色的是请求 2，这两个请求在两条不同的流中进行传输。假设在传输过程中，请求 1 的某个数据包丢了，如果是 TCP，即使请求 2 的所有数据包都收到了，但是也只能阻塞在内核缓冲区中，无法交给应用层。但是 QUIC 就不一样了，请求 1 的数据包丢了只会阻塞请求 1，请求 2 不会受到阻塞。 ​ 有些人不禁发问，不是说 HTTP2 也有流的概念吗，为什么只有 QUIC 才能解决呢，这个根本原因就在于，HTTP2 的传输层用的 TCP，TCP 的实现是在内核态的，而流是实现在用户态度，TCP 是看不到“流”的，所以在 TCP 中，它不知道这个数据包是请求 1 还是请求 2 的，只会根据 seq number 来判断包的先后顺序。 2.4 更优的拥塞控制算法 拥塞控制算法中最重要的一个参数是 RTT，RTT 的准确性决定了拥塞控制算法的准确性；然而，TCP 的 RTT 测量往往不准确，QUIC 的 RTT 测量是准确的。 ​ 如图 16 所示：由于网络中经常出现丢包，需要重传，在 TCP 协议中，初始包和重传包的序号是一样的，拥塞控制算法进行计算 RTT 的时候，无法区别是初始包还是重传包，这将导致 RTT 的计算值要么偏大，要么偏小。 ​ 如图 17 所示：QUIC 通过 Packet Number 来标识包的序号，而且规定 Packet Number 只能单调递增，这也就解决了初始包和重传包的二义性。从而保证 RTT 的值是准确的。 ​ 另外，不同于 TCP，QUIC 的拥塞控制算法是可插拔的，由于其实现在用户态，服务可以根据不同的业务，甚至不同的连接灵活选择使用不同的拥塞控制算法。（Reno、New Reno、Cubic、BBR 等算法都有自己适合的场景） 2.5 QUIC 的两级流量控制 ​ 很多人搞不清楚流量控制与拥塞控制的区别。二者有本质上的区别。 ​ 流量控制要解决的问题是：接收方控制发送方的数据发送的速度，就是我的接收能力就那么大点，你别发太快了，你发太快了我承受不住，会给你丢掉你还得重新发。拥塞控制要解决的问题是：数据在网络的传输过程中，是否网络有拥塞，是否有丢包，是否有乱序等问题。如果中间传输的时候网络特别卡，数据包丢在中间了，发送方就需要重传，那么怎么判断是否拥塞了，重传要怎么重传法，按照什么算法进行发送数据才能尽可能避免数据包在中间路径丢掉，这是拥塞控制的核心。 ​ 所以，流量控制解决的是接收方的接收能力问题，一般采用滑动窗口算法；拥塞控制要解决的是中间传输的时候网络是否拥堵的问题，一般采用慢启动、拥塞避免、拥塞恢复、快速重传等算法。 ​ QUIC 是双级流控，不仅有连接这一个级别的流控，还有流这个级别的流控。如下图所示，每个流都有自己的可用窗口，可用窗口的大小取决于最大窗口数减去发送出去的最大偏移数，跟中间已经发送出去的数据包，是否按顺序收到了对端的 ACK 无关。 3. QUIC 协议如何优化 QUIC 协议定义了很多优秀的功能，但是在实现的过程中，我们会遇到很多问题导致无法达到预期的性能，比如 0-RTT 率很低，连接迁移失败率很高等等。 3.1 QUIC 的 0-RTT 成功率不高 ​ 导致 0-RTT 成功率不高的原因一般有如下几个： 服务端一般都是集群，对于客户端来说，处理请求的服务端是不固定的，新的请求到来时，如果当前 client 没有请求过该服务器，则服务器上没有相关会话信息，会把该请求当做一个新的连接来处理，重新走 1-RTT。 ​ 针对此种情况，我们可以考虑集群中所有的服务器使用相同的 ticket 文件。 客户端 IP 不是固定的，在发生连接迁移时，服务端下发的 token 融合了客户端的 IP，这个 IP 变化了的话，携带 token 服务端校验不过，0-RTT 会失败。 ​ 针对这个问题，我们可以考虑采用如图 19 所示的方法，使用设备信息或者 APP 信息来生成 token，唯一标识一个客户端。 Session Ticket 过期时间默认是 2 天，超过 2 天后就会导致 0-RTT 失败，然后降级走 1-RTT。可以考虑增长过期时间。 3.2 实现连接迁移并不容易 ​ 连接迁移的实现，不可避开的两个问题：一个是四层负载均衡器对连接迁移的影响，一个是七层负载均衡器对连接迁移的影响。 ​ 四层负载均衡器的影响：LVS、DPVS 等四层负载均衡工具基于四元组进行转发，当连接迁移发生时，四元组会发生变化，该组件就会把同一个请求的数据包发送到不同的后端服务器上，导致连接迁移失败； ​ 七层负载均衡器的影响（QUIC 服务器多核的影响）：由于多核的影响，一般服务器会有多个 QUIC 服务端进程，每个进程负载处理不同的连接。内核收到数据包后，会根据二元组（源 IP、源 port）选择已经存在的连接，并把数据包交给对应的 socket。在连接迁移发生时，源地址发生改变，可能会让接下来的数据包去到不同的进程，影响 socket 数据的接收。 ​ 如何解决以上两个问题？DPVS 要想支持 QUIC 的连接迁移，就不能再以四元组进行转发，需要以连接 ID 进行转发，需要建立 连接 ID 与对应的后端服务器的对应关系； ​ QUIC 服务器也是一样的，内核就不能用四元组来进行查找 socket，四元组查找不到时，就必须使用连接 ID 进行查找 socket。但是内核代码又不能去修改（不可能去更新所有服务器的内核版本），那么我们可以使用 eBPF 的方法进行解决。如下图 20 所示： 3.3 UDP 被限速或禁闭 ​ 业内统计数据全球有 7%地区的运营商对 UDP 有限速或者禁闭，除了运营商还有很多企业、公共场合也会限制 UDP 流量甚至禁用 UDP。这对使用 UDP 来承载 QUIC 协议的场景会带来致命的伤害。对此，我们可以采用多路竞速的方式使用 TCP 和 QUIC 同时建连。除了在建连进行竞速以外，还可以对网络 QUIC 和 TCP 的传输延时进行实时监控和对比，如果有链路对 UDP 进行了限速，可以动态从 QUIC 切换到 TCP。 3.4 QUIC 对 CPU 消耗大 ​ 相对于 TCP，为什么 QUIC 更消耗资源？ QUIC 在用户态实现，需要更多的内核空间与用户空间的数据拷贝和上下文切换； QUIC 的 ACK 报文也是加密的，TCP 是明文的。 内核知道 TCP 连接的状态，不用为每一个数据包去做诸如查找目的路由、防火墙规则等操作，只需要在 tcp 连接建立的时候做一次即可，然而 QUIC 不行； ​ 总的来说，QUIC 服务端消耗 CPU 的地方主要有三个：密码算法的开销；udp 收发包的开销；协议栈的开销； ​ 针对这些，我们可以适当采取优化措施来： 使用 Intel 硬件加速卡卸载 TLS 握手 开启 GSO 功能。 数据在传输过程中，可以将一轮中所有的 ACK 解析后再同时进行处理，避免处理大量的 ACK。 适当将 QUIC 的包长限制调高（比如从默认的 1200 调到 1400 个字节） 减少协议栈的内存拷贝 4. QUIC 的性能 ​ 从公开的数据来看，国内各个厂（腾讯、阿里、字节、华为、OPPO、网易等等）使用了 QUIC 协议后，都有很大的提升，比如网易上了 QUIC 后，响应速度提升 45%，请求错误率降低 50%；比如字节火山引擎使用 QUIC 后，建连耗时降低 20%~30%；比如腾讯使用 QUIC 后，在腾讯会议、直播、游戏等场景耗时也降低 30%； 总结 ​ QUIC 协议的出现，为 HTTP/3 奠定了基础。这是近些年在 web 协议上最大的变革，也是最优秀的一次实践。面对新的协议，我们总是有着各种各样的担忧，诚然，QUIC 协议在稳定性上在成熟度上，的确还不如 TCP 协议，但是经过近几年的发展，成熟度已经相当不错了，Nginx 近期也发布了 1.25.0 版本，支持了 QUIC 协议。所以面对这样优秀的协议，我们希望更多的公司，更多的业务参与进来使用 QUIC，推动 QUIC 更好的发展，推动用户上网速度更快！版权属于：李龙彦原文链接：https://www.infoq.cn/article/Lddlsa5f21StY04LI3hP转载时须注明出处及本声明
MySQL经典四表查询(教师，学生，成绩，课程表)多表查询
MySQL经典四表查询(教师，学生，成绩，课程表)多表查询 1、表架构呈现 student(sid,sname,sage,ssex) 学生表 teacher(tid,tname) 教师表 course(cid,cname,tid) 课程表 sc(sid,cid,score) 成绩表 2、建表SQL语句并插入数据 -- 1.学生表 CREATE TABLE student( sid INT PRIMARY KEY AUTO_INCREMENT, sname VARCHAR(20), sage DATE, ssex ENUM ('男','女') ); -- 2.课程表中使用了外键教师编号，因而需要先建立教师表 CREATE TABLE teacher( tid INT PRIMARY KEY AUTO_INCREMENT, tname VARCHAR(20) ); -- 3.建立课程表 CREATE TABLE course( cid INT PRIMARY KEY AUTO_INCREMENT, cname VARCHAR(20), tid INT, FOREIGN KEY (tid) REFERENCES teacher (tid) ); -- 4.建立成绩表 CREATE TABLE sc( sid INT, cid INT, score INT ); -- 先给student表插入数据 INSERT INTO student VALUES (1,'赵雷','1990-01-01','男'), (2,'钱电','1990-12-21','男'), (3,'孙风','1990-05-20','男'), (4,'李云','1990-08-06','男'), (5,'周梅','1991-12-01','女'), (6,'吴兰','1992-03-01','女'), (7,'郑竹','1989-07-01','女'), (8,'王菊','1990-01-20','女'); -- 给teacher表插入数据,这里不可以先给course表插入数据,因为course表外键连接到teacher的主键 INSERT INTO teacher VALUES(1,'张三'),(2,'李四'),(3,'王五'); -- 给course表插入数据 INSERT INTO course VALUES(1,'语文',2),(2,'数学',1),(3,'英语',3); -- 最后给sc表插入数据 INSERT INTO sc VALUES(1,1,90),(1,2,80),(1,3,90),(2,1,70),(2,2,60),(2,3,80),(3,1,80), (3,2,80),(3,3,80),(4,1,50),(4,2,30),(4,3,20),(5,1,76),(5,2,87),(6,1,31),(6,3,34),(7,2,89),(7,3,98); 3、问题集锦 （1）查询 1 课程比 2 课程成绩高的学生的信息及课程分数 SELECT s.sid,s.sname,s.sage,s.ssex,sc1.score,sc2.score FROM student s ,sc sc1,sc sc2 WHERE sc1.`cid` = 1 AND sc2.`cid` = 2 AND sc1.`score` > sc2.`score` AND sc1.`sid` = s.`sid`AND sc2.`sid` = s.`sid`; （2）查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩 SELECT s.sid,s.sname,AVG(sc1.`score`) AS 'avg_score' FROM student s ,sc sc1 WHERE s.sid = sc1.`sid` GROUP BY s.sid HAVING avg_score >= 60 ORDER BY avg_score DESC; （3）查询名字中含有"风"字的学生信息 SELECT s.sid,s.sname,s.sage,s.ssex FROM student s WHERE sname LIKE '%风%'; （4）查询课程名称为"数学"，且分数低于60的学生姓名和分数 SELECT s.sname,sc1.score FROM student s ,sc sc1 WHERE s.sid = sc1.sid AND cid = 2 AND sc1.score < 60; （5）查询所有学生的课程及分数情况 SELECT sc1.sid,c.cname,sc1.score FROM course c,sc sc1 WHERE c.cid = sc1.cid; （6）查询没学过"张三"老师授课的同学的信息 第一种普通写法： SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE c.tid = t.tid AND c.cid = sc1.cid AND t.tname = '张三'; 第二种子查询写法： SELECT s.* FROM student s WHERE s.sid NOT IN ( SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE c.tid = t.tid AND c.cid = sc1.cid AND t.tname = '张三'); （7）查询学过"张三"老师授课的同学的信息 第一种普通写法： SELECT s.* FROM student s,sc sc1,course c,teacher t WHERE sc1.sid = s.sid AND t.tid = c.tid AND c.cid = sc1.cid AND t.tname = '张三'; 第二种子查询写法： SELECT s.* FROM student s WHERE s.sid IN ( SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE c.tid = t.tid AND c.cid = sc1.cid AND t.tname = '张三'); （8）查询学过编号为 1 并且也学过编号为 2 的课程的同学的信息 SELECT s.* FROM student s,sc sc1,sc sc2 WHERE s.sid = sc1.sid AND sc1.sid = sc2.sid AND sc1.cid = 1 AND sc2.cid = 2; （9）查询学过编号为 1 但是没有学过编号为 2 的课程的同学的信息 -- 第一步：先用student表左连接查出学过课程1的和学过课程2的所有学生信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid=1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid=2) sc2 ON s.sid = sc2.sid; -- 第二步：筛选学过编号为1但是没有学过编号为2的课程的同学的信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid=1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid=2) sc2 ON s.sid = sc2.sid WHERE (sc1.cid=1 AND sc2.cid IS NULL); （10）查询没有学全所有课程的同学的信息 -- 第一种写法： -- 第一步：先用student表左连接查出学过课程1、2、3及其它的所有学生信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid = 1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 2) sc2 ON s.sid = sc2.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 3) sc3 ON s.sid = sc3.sid; -- 筛选没有学全所有课程的同学的信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid = 1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 2) sc2 ON s.sid = sc2.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 3) sc3 ON s.sid = sc3.sid WHERE (sc1.cid IS NULL OR sc2.cid IS NULL OR sc3.cid IS NULL); -- 上面写法比较繁琐，建议用楼下写法 -- 第二种写法： -- 第一步：先把三个课程都学的学员编号查出来 SELECT sc1.sid FROM sc sc1,sc sc2,sc sc3 WHERE sc1.cid = 1 AND sc2.cid = 2 AND sc3.cid = 3 AND sc1.sid = sc2.sid AND sc1.sid = sc3.sid; -- 第二步：对立的查询思路，三个课程都学完的同学对立面是三个课程没学完或者一个都没学的 SELECT s.* FROM student s WHERE s.sid NOT IN (SELECT sc1.sid FROM sc sc1,sc sc2,sc sc3 WHERE sc1.cid = 1 AND sc2.cid = 2 AND sc3.cid = 3 AND sc1.sid = sc2.sid AND sc1.sid = sc3.sid) GROUP BY s.sid; （11）查询至少有一门课与学号为"1"的同学所学相同的同学的信息 SELECT DISTINCT s.* FROM student s,sc sc1 WHERE s.sid = sc1.sid AND sc1.cid IN(SELECT cid FROM sc WHERE sid = 1) AND s.sid <> 1; （12）查询和"1"号的同学学习的课程完全相同的其他同学的信息 -- 第一步：先查出1号同学学习的所有的课程编号 -- 当前数据设计1号同学选修了所有课程 SELECT cid FROM sc WHERE sid = 1; -- 第二步：查出选修了1号学生没有选修课程的学生编号，这一步很关键，我们用cid去过滤，所以后面再用not in的时候那个子集里一定有和1号学生选修的课程完全相同的同学 -- 因为1号同学选修了所有课程，所以没有符合条件的学生编号 SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1); -- 如果查询结果中有重复的sid，因为有的同学选修的1号同学没选的课程不止一门，可以使用distinct对sid进行去重处理 -- 第三步：查询选修的课程是1号学生选修课程的子集的学生编号 -- 因为第二步查出的是选修了1号学生没有选修课程的学生编号,逆向思维，再用not in，双重否定变肯定，查出的就是和1号同学有一门、多门、或者全部课程的同学编号 -- 可以使用distinct对sid进行去重处理 SELECT sid FROM sc WHERE sid NOT IN (SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1)); -- 第四步：从上述查询结果中，筛选出选修的课程数量与1号学生选修的课程数量相等的其他学生的编号 SELECT sid FROM sc WHERE sid NOT IN (SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1)) GROUP BY sid HAVING COUNT(*) = (SELECT COUNT(*) FROM sc WHERE sid =1) AND sid <> 1; -- COUNT(*):统计返回的行数 当sid=1时，有3行数据 SELECT COUNT(*) FROM sc WHERE sid = 1; -- 3 -- 第五步:以上述查询结果为筛选条件，从student表中查询出与1号学生学习的课程完全相同的其他学生的信息 SELECT s.* FROM student s WHERE sid IN (SELECT sid FROM sc WHERE sid NOT IN (SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1)) GROUP BY sid HAVING COUNT(*) = (SELECT COUNT(*) FROM sc WHERE sid =1) AND sid <> 1); /第十二题思路总结： 01号之外的其他学生可以分成两个大类，一类是选修了01号学生没有选修的课程的学生 另一类学生选修的课程是01号学生选修的课程的子集，这个子集是选修了和1号学生里的一门、多门、 采用逆向思维，可以先找出选修了01号学生没选课程的学生编号，然后以01号学生选修的课程数量为筛选条件， 从剩下的选修的课程是01号学生选修的课程的子集这类学生中筛选出与01号学生所选课程完全相同的学生编号， 此编号包含了01，以剔除了01之后的编号为筛选条件 从student表中选出和01号同学学习的课程完全相同的其他同学的信息/ （13）查询没学过"张三"老师讲授的任一门课程的学生信息 -- 第一步：张三老师tid是1，cid是2，逆向思维，查询学过"张三"老师讲授的任一门课程的学生编号 SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE t.tid = c.tid AND sc1.cid = c.cid AND t.tname = '张三'; -- 第二步：以上查询结果为筛选条件，从student表中查询出没学过"张三"老师讲授的任一门课程的学生信息 SELECT s.* FROM student s WHERE s.sid NOT IN (SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE t.tid = c.tid AND sc1.cid = c.cid AND t.tname = '张三'); （14）查询出只有两门课程的全部学生信息 SELECT s.* FROM student s,sc GROUP BY sc.sid HAVING COUNT(sc.sid) = 2 AND s.sid = sc.sid; （15）查询1990年出生的学生信息(注：student表中sage列的类型是datetime) -- 第一种写法： SELECT s.* FROM student s WHERE s.sage >= '1990-01-01' AND s.sage<='1990-12-31'; -- 第二种写法：模糊查询 SELECT s.* FROM student s WHERE s.sage LIKE '1990-%'; （16）查询每门课程的平均成绩，结果按平均成绩降序排列，平均成绩相同时，按课程编号升序排列 SELECT sc.cid,AVG(score) AS avg_score FROM sc GROUP BY sc.cid ORDER BY avg_score DESC,sc.cid ASC; （17）查询任何一门课程成绩在70分以上的姓名、课程名称和分数 SELECT s.sname,c.cname,sc.score FROM student s,course c,sc WHERE s.sid = sc.sid AND sc.cid = c.cid AND sc.score >= 70; （18）查询平均成绩大于等于85的所有学生的学号、姓名和平均成绩,并按照平均成绩降序排列 SELECT s.sid,s.sname,AVG(score) AS avg_score FROM student s,sc WHERE s.sid = sc.sid GROUP BY s.sid HAVING avg_score >= 85 ORDER BY avg_score DESC; （19）查询成绩不及格的课程和学生姓名 SELECT s.sname,c.cname,sc.score FROM student s,course c,sc WHERE s.sid = sc.sid AND sc.cid = c.cid AND sc.score < 60; （20）查询课程编号为1且课程成绩在80分以上的学生的学号和姓名 SELECT s.sid,s.sname FROM student s,sc WHERE s.sid = sc.sid AND sc.cid = 1 AND sc.score >= 80 GROUP BY s.`sid`; （21）求每门课程的学生人数 SELECT cid AS '课程编号',COUNT(sid) AS '课程人数' FROM sc GROUP BY sc.cid; （22）统计每门课程的学生选修人数（超过5人的课程才统计）。要求输出课程号和选修人数 SELECT cid AS '课程编号',COUNT(sid)AS '课程人数' FROM sc GROUP BY sc.cid HAVING COUNT(sid)>5 ORDER BY COUNT(sid),sc.cid ASC; （23）查询不同课程成绩相同的学生的学生编号、课程编号、学生成绩 SELECT s1.sid,sc1.cid,sc1.score,s2.sid,sc2.cid,sc2.score FROM student s1,student s2,sc sc1,sc sc2 WHERE s1.sid=sc1.sid AND s2.sid = sc2.sid AND sc1.cid <>sc2.cid AND sc1.score = sc2.score; （24）检索至少选修两门课程的学生学号 SELECT sid FROM sc GROUP BY sc.sid HAVING COUNT(cid)>=2; （25）查询选修了全部课程的学生信息 SELECT * FROM student s,sc WHERE s.sid =sc.sid GROUP BY s.sid HAVING COUNT(cid)=3; （26）查询各学生的年龄(年龄保留整数) SELECT s.sname,ROUND((TO_DAYS('2020-06-11')-TO_DAYS(s.sage))/365) AS age FROM student s; （27）查询本月过生日的学生姓名和出生年月 -- _____ :五个下划线长度 SELECT s.sname,s.sage FROM student s WHERE s.sage LIKE '_____07%'; （28）查询下月过生日的学生 SELECT s.sname,s.sage FROM student s WHERE s.sage LIKE '_____08%'; （29）查询学全所有课程的同学的信息 SELECT s.* FROM student s,sc WHERE s.sid = sc.sid GROUP BY s.sid HAVING COUNT(sc.cid) = 3;
SpringMVC注解式开发 1.1 @Controller ​ 传统的配置式开发中的控制器Controller类必须实现Controller接口，并实现接口中的HandleRequest()方法，还需要再配置文件中配置处理器映射，且一个处理器（控制器）只能有一个方法，为了实现程序功能，不得不创建大量的处理器（控制器）类，不够方便灵活。 ​ 而，注解开发中，将一个普通的类转化为控制器类只需要在该类的声明上添加@Controller注解即可。一个基于注解的控制器可以有多个方法，能大大减少处理器（控制器）类的数量。 ​ 要使注解被扫描到，必须在springmvc.xml中配置组件扫描器，代码如下： <context:component-scan base-package="com.deepfal.controller" /> 1.2 @RequestMapping定义请求规则 ​ 基于注解的控制器无须在xml配置文件中配置处理器映射器，仅需要用@RequestMapping对应控制器类中任意一个方法进行注解即可建立“请求/响应”映射关系，将客户端请求与处理器的方法一一对应。 ​ 通过@RequestMapping 注解可以定义处理器对于请求的映射规则。该注解可以注解在方法上，也可以注解在类上，但意义是不同的。value 属性值常以“/”开始。@RequestMapping 的 value 属性用于定义所匹配请求的 URI。 1.2.1 指定模块名称 ​ 一个@Controller 所注解的类中，可以定义多个处理器方法。当然，不同的处理器方法所匹配的 URI 是不同的。这些不同的 URI 被指定在注解于方法之上的@RequestMapping 的value 属性中。但若这些请求具有相同的 URI 部分，则这些相同的 URI部分可以被抽取到注解在类之上的@RequestMapping 的 value 属性中。此时的这个 URI 表示模块（相当于包）的名称。URI 的请求是相对于 Web 的根目录。换个角度说，要访问处理器的指定方法，必须要在方法指定 URI 之前加上处理器类前定义的模块名称。 示例： 提取后 @Controller @RequestMapping("/zar") public class HelloSpringMvc { //相当于一个控制器处理的方法 @RequestMapping("/hello") public String one() { return "main"; } @RequestMapping("/two") public String two() { return "main"; } //客户端的请求： // // } 1.2.2 对请求提交方式的定义 ​ 对于@RequestMapping，其有一个属性 method，用于对被注解方法所处理请求的提交方式进行限制，即只有满足该 method 属性指定的提交方式的请求，才会执行该被注解方法。Method 属性的取值为 RequestMethod 枚举常量。常用的为 RequestMethod.GET 与RequestMethod.POST，分别表示提交方式的匹配规则为 GET 与 POST 提交。 @RequestMapping(value = "/hello",method = RequestMethod.POST) public String one() { return "main"; } 以上处理器方法只能处理 POST 方式提交的请求。 客户端浏览器常用的请求方式，及其提交方式有以下几种： 也就是说，只要指定了处理器方法匹配的请求提交方式为 POST，则相当于指定了请求发送的方式：要么使用表单请求，要么使用 AJAX 请求。其它请求方式被禁用。 当然，若不指定 method 属性，则无论是 GET 还是 POST 提交方式，均可匹配。即对于请求的提交方式无要求。 （1）post提交方式 （2）get提交方式 1.3 数据提交的方式 前四种数据注入的方式，会自动进行类型转换。但无法自动转换日期类型。 （1）单个数据（基本数据类型）注入 在方法中声明一个和表单提交的参数名称相同的参数，由框架按照名称直接注入。 （2）对象封装注入 在方法中声明一个自定义的实体类参数，框架调用实体类中相应的setter方法注入属性值，只要保证实体类中成员变量的名称与提交请求的name属性值一致即可。 实体Bean含对象属性 比如：Student对象，其中有一个Address的对象属性，在Address对象中有country和city两个基本类型的属性。 对象数据提交 姓名： 年龄： 国家： 城市： （3）动态占位符提交/路径变量（仅用于超链接） ​ 使用框架提供的一个注解@PathVariable，将请求url中的值作为参数进行提取，只能是超链接。restful风格下的数据提取方式。restful是一种软件架构风格、设计风格，而不是标准，只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁，更有层次，更易于实现缓存等机制。 ​ RESTful风格是把请求参数变为请求路径的一种编程风格。通过路径变量的使用，可以实现RESTful风格的编程。 ​ 中文乱码： ​ 由于tomcat默认使用ISO-8859-1对接收的文本编码，因此要获得正确中文有两种解决方式： 自己转码 使用如下转码方式。先把name以ISO-8859-1再编码，还原成字节数组，再用UTF-8进行解码，即可获得正确中文。 String newName=new String(name.getBytes("ISO-8859-1"),"UTF-8"); 修改tomcat | conf | server.xml 在server.xml的Connect中添加URIEncoding="utf-8"，这样默认就是用utf-8解码了，参数绑定中文也可以正确显示： <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="utf-8"/> 对于maven中的tomcat插件，需要添加如下配置： <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <uriEncoding>UTF-8</uriEncoding> </configuration> </plugin> 另，web.xml配置的filter只对post请求有效,因此对此问题不是解决之道。 （4）请求参数名称与形参名称不一致 请求与形参中的名字不对应，可以使用 @RequestParam(value="name1",required=true) String namea来进行参数名称绑定。 （5）数组类型的请求参数 @RequestMapping("/arrayParam") public String arrayParam(String[] hobby){ System.out.println("我的爱好："); for(String s:hobby){ System.out.println(s); } return "main"; } （6）使用HttpServletRequest对象提取 在方法参数中声明一个request对象，使用request的getParameter()获取表单提交的数据，这样得到的数据还要手工进行数据类型的转换。 public String five(HttpServletRequest request){ int age=new Integer(request.getParameter("stuage")); String name=request.getParameter("stuname"); System.out.println(age+"*********"+name); return "main"; } 1.4 请求参数中文乱码解决 ​ 对于前面所接收的请求参数，若含有中文，则会出现中文乱码问题。Spring 对于请求参数中的中文乱码问题，给出了专门的字符集过滤器： spring-web-5.2.5.RELEASE.jar 的org.springframework.web.filter 包下的 CharacterEncodingFilter 类。 （1）解决方案 ​ 在 web.xml 中注册字符集过滤器，即可解决 Spring 的请求参数的中文乱码问题。不过，最好将该过滤器注册在其它过滤器之前。因为过滤器的执行是按照其注册顺序进行的。 （2）源码分析 1.5 处理器方法的返回值 使用@Controller 注解的处理器的方法，其返回值常用的有四种类型： Ø 第一种：ModelAndView Ø 第二种：String Ø 第三种：无返回值void Ø 第四种：返回对象类型 1.4.1 返回 ModelAndView ​ 若处理器方法处理完后，需要跳转到其它资源，且又要在跳转的资源间传递数据，此时处理器方法返回 ModelAndView 比较好。当然，若要返回 ModelAndView，则处理器方法中需要定义 ModelAndView 对象。在使用时，若该处理器方法只是进行跳转而不传递数据，或只是传递数据而并不向任何资源跳转（如对页面的 Ajax 异步响应），此时若返回 ModelAndView，则将总是有一部分多余：要么 Model 多余，要么 View 多余。即此时返回 ModelAndView 将不合适。较少使用。 1.4.2 返回 String ​ 处理器方法返回的字符串可以指定逻辑视图名，通过视图解析器解析可以将其转换为物理视图地址。 当然，也可以直接返回资源的物理视图名。不过，此时就不需要再在视图解析器中再配置前辍与后辍了。 ​ 使用ModelAndView对象作为控制器方法的返回值类型，可以很好地同时解决跳转路径和携带数据的问题，但String作为控制器方法的返回值类型只解决了跳转路径问题，如果跳转到目标页面同时还需要携带数据怎么办呢？有3个解决方案： 使用方法中的Model参数 使用方法中的HttpServletRequest对象 使用方法中的HttpSession对象 1.4.3 无返回值void ​ 对于处理器方法返回 void 的应用场景，应用在AJAX 响应处理。若处理器对请求处理后，无需跳转到其它任何资源，此时可以让处理器方法返回 void。我们SSM整合案例中的分页使用的就是无返回值。代码见后面。 通过jackson-databind把对象转换为json字符串。 1.4.4 返回对象Object ​ 处理器方法也可以返回 Object 对象。这个 Object 可以是 Integer，自定义对象，Map，List 等。但返回的对象不是作为逻辑视图出现的，而是作为直接在页面显示的数据出现的。返回对象，需要使用@ResponseBody 注解，将转换后的 JSON 数据放入到响应体中。Ajax请求多用于Object返回值类型。由于转换器底层使用了Jackson 转换方式将对象转换为JSON 数据，所以需要添加Jackson的相关依赖。 项目案例:使用ajax请求返回一个JSON结构的学生. 实现步骤: A.在pom.xml文件中添加依赖 <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId <version>2.9.8</version> </dependency> B.在页面添加jQuery的函数库的引用 C.发送ajax请求 function show() { $.ajax({ url:"${pageContext.request.contextPath}/ajax", type:"post", dataType:"json", success:function (stu) { $("#oneStu").html(stu.name+"------"+stu.age); } }); } D.开发action @Controller public class AjaxDemo { @RequestMapping("/ajax") @ResponseBody //此注解用来解析ajax请求 public Object ajax(){ Student stu = new Student("张三",22); return stu; } } E.在springmvc.xml文件中添加注解驱动 <mvc:annotation-driven /> F.index.jsp页面 ajax访问服务器,返回一个学生 Ajax/JSON专项突破： 服务端接收对象返回 JSON 字符串 objectMapper.writeValueAsString(user); 服务端接收Bean返回 JSON 对象 @ResponseBody 服务端接收属性返回 JSON 对象 客户端发送 JSON 字符串返回 JSON 字符串 数据接收与返回的限制 @RequestMapping(consumes="application/json",produces="application/json") consumes限制前台传递过来的数据格式必须是JSON produces设置返回的数据要转成JSON对象并回传给客户端 直接输出响应字符串 前端JSON字符串和对象的相互转换： //1.用于将服务端传回来的JSON字符串解释转化为JSON对象 var jsonobject = JSON.parse(data); //2.JSON对象转化为字符串 var jsonObj = {username:"张三",password:"123"};//定义一个JSON对象 var jsonStr = JSON.stringify(jsonObj); 1.6 SpringMVC的四种跳转方式 ​ 默认的跳转是请求转发，直接跳转到jsp页面展示，还可以使用框架提供的关键字redirect:，进行一个重定向操作，包括重定向页面和重定向action，使用框架提供的关键字forward:，进行服务器内部转发操作，包括转发页面和转发action。当使用redirect:和forward:关键字时，视图解析器中前缀后缀的拼接就无效了。 页面部分： 请求转发页面(默认) 请求转发action 重定向页面 重定向action Controller部分： @Controller public class JumpAction { @RequestMapping("/one") public String one(){ System.out.println("请求转发页面(默认)"); //以前的访问方式 //request.getRequestDispatcher("/admin/main.jsp").forward(request,response); //观察地址栏的变化: http://localhost:8080/one.action //return "main"; //默认的访问方式是自动拼接前缀和后缀进行跳转 return "forward:/fore/user.jsp";//只要使用了forward:就可以屏蔽前缀和后缀的拼接,自己手工构建返回的全部路径+.jsp } @RequestMapping("/two") public String two(){ System.out.println("请求转发action"); //观察地址栏的变化: http://localhost:8080/two.action return "forward:/other.action"; //不使用forward:,就会是这样的路径 /admin/other.action/.jsp } @RequestMapping("/three") public String three(){ System.out.println("重定向页面"); //观察地址栏的变化 http://localhost:8080/admin/main.jsp return "redirect:/admin/main.jsp";//只要使用了redirect:就可以屏蔽前缀和后缀的拼接 } @RequestMapping("/four") public String four(){ System.out.println("重定向action"); //观察地址栏的变化 http://localhost:8080/other.action return "redirect:/other.action";//只要使用了redirect:就可以屏蔽前缀和后缀的拼接 } } 1.7 SpringMVC支持的默认参数类型 这些类型只要写在方法参数中就可以使用了。 1）HttpServletRequest 对象 2）HttpServletResponse 对象 3）HttpSession 对象 4）Model/ModelMap 对象 5）Map<String,Object>对象 示例： @Controller public class ParamAction { @RequestMapping("/param") public String param(HttpServletRequest request, HttpServletResponse response, HttpSession session, Model model, ModelMap modelMap, Map map){ //Map ,Model,ModelMap,request都使用请求作用域进行传值, //所以必须使用请求转发的方式进行跳转,否则丢失数据 Student stu = new Student("张三",22); request.setAttribute("requestStu",stu); session.setAttribute("sessionStu",stu); modelMap.addAttribute("modelMapStu",stu); model.addAttribute("modelStu",stu); map.put("mapStu",stu); return "main"; //切记请求转发跳 // return "redirect:/admin/main.jsp";//会丢失数据 } } 注意Model,Map,ModelMap都使用的是request请求作用域,意味着只能是请求转发后,页面才可以得到值。 1.8 日期处理 1.8.1 日期注入 日期类型不能自动注入到方法的参数中。需要单独做转换处理。使用@DateTimeFormat注解，需要在springmvc.xml文件中添加标签。 （1）在方法的参数上使用@DateTimeFormat注解 @RequestMapping("/submitone") public String submitdateone( @DateTimeFormat(pattern="yyyy-MM-dd") Date mydate){ System.out.println(mydate); return "dateShow"; } （2）在类的成员setXXX()方法上使用@DateTimeFormat注解 @DateTimeFormat(pattern="yyyy-MM-dd") public void setDate(Date date) { this.date = date; } 但这种解决方案要在每个使用日期类型的地方都去添加使用@DateTimeFormat注解，比较麻烦，我们可以使用@InitBinder注解来进行类中统一日期类型的处理。 （3）@InitBinder注解解决类中日期问题 @InitBinder public void initBinder(WebDataBinder dataBinder) { SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd"); dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(sf, true)); } 这样在类中出现的所有日期都可以进行转换了。 1.8.2 日期显示 （1）JSON中的日期显示 需要在类中的成员变量的getXXX方法上加注解. @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") public Date getDate() { return date; } （2）JSP页面的日期显示 需要使用国际化标签，先添加依赖 <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> 导入国际化的标签库 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 再使用标签显示日期 <c:forEach items="${list}" var="stu"> ${stu.name}-------${stu.age}-------<fmt:formatDate value="${stu.date}" pattern="yyyy-MM-dd"></fmt:formatDate> </c:forEach> 1.9 < mvc:annotation-driven/>标签的使用 < mvc:annotation-driven/>会自动注册两个bean，分别为DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter。是springmvc为@Controller分发请求所必须的。除了注册了这两个bean，还提供了很多支持。 1）支持使用ConversionService 实例对表单参数进行类型转换； 2）支持使用 @NumberFormat 、@DateTimeFormat； 3）注解完成数据类型的格式化； 4）支持使用 @RequestBody 和 @ResponseBody 注解； 5）静态资源的分流也使用这个标签; 1.10 资源在WEB-INF目录下 ​ 很多企业会将动态资源放在WEB-INF目录下，这样可以保证资源的安全性。在WEB-INF目录下的动态资源不可以直接访问，必须要通过请求转发的方式进行访问。这样避免了通过地址栏直接对资源的访问。重定向也无法访问动态资源。 项目案例： 页面结构图： action: @Controller public class ShowAction { @RequestMapping("/showIndex") public String showIndex(){ System.out.println("index............."); return "index"; } @RequestMapping("/showMain") public String showMain(){ System.out.println("main............."); return "main"; } @RequestMapping("/showLogin") public String showLogin(){ System.out.println("login............."); return "login"; } @RequestMapping("/login") public String login(String name, String pwd, HttpServletRequest request){ if("admin".equals(name) && "123".equals(pwd)){ return "main"; } request.setAttribute("msg","用户名或密码不正确!"); return "login"; } } 运行结果：