<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title><![CDATA[明熙的Blog]]></title> 
<atom:link href="https://mingxi.icu/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[认真是一种态度，态度也许不认真呢。]]></description>
<link>https://mingxi.icu/</link>
<language>zh-cn</language>
<generator>emlog</generator>

<item>
    <title>FastAPI实战：WebSocket长连接保持与心跳机制，从入门到填坑</title>
    <link>https://mingxi.icu/web/9.html</link>
    <description><![CDATA[<h2>摘要</h2>
<p>本文通过一个真实的上线案例，详细讲解FastAPI与JavaScript实现WebSocket长连接保持的心跳机制。你会了解为什么连接会断、心跳原理是什么、前后端代码怎么写，以及那些文档里没写的调优陷阱。照着做，让你的实时通信稳如老狗。</p>
<p>你是不是也遇到过——WebSocket连接动不动就断开，尤其是在移动端，用户切换个Wi-Fi或者电梯里信号晃一下，消息就收不到了？用户投诉说“APP消息延迟”，你一查日志，满屏都是<code>WebSocket disconnected</code>，然后疯狂重连，服务器压力山大，用户体验稀碎。</p>
<p>有些项目图省事，觉得WebSocket连上就行了，结果线上跑了半天，运维小哥就发来报警：连接数忽高忽低，很多连接存活不到2分钟。查日志，好家伙，Nginx默认<code>proxy_read_timeout</code> 60秒，加上移动网络运营商会掐掉长时间无流量的连接，双向夹击，连接全断了。</p>
<p>核心结论：WebSocket长连接保持，不能靠“连上就不管”，必须引入心跳机制——就像两个人打电话，每隔一会儿问一句“喂，还在吗？”。今天我就把FastAPI后端 + JavaScript前端的完整心跳实现，掰开了揉碎了讲给你听，顺便把我踩过的坑标红。</p>
<h2>本文路线图</h2>
<ul>
<li>为什么WebSocket会断？—— 中间件超时、网络状态变化</li>
<li>心跳原理：ping-pong 还是 pong-ping？</li>
<li>FastAPI后端：接收心跳消息 + 超时管理</li>
<li>JavaScript前端：定时发送心跳 + 断线重连</li>
<li>完整可运行代码示例</li>
<li>那些年我踩过的坑（间隔设置、重复定时器、服务端主动断开）</li>
</ul>
<h2>🧠 第一部分：连接为什么会断？</h2>
<p>把WebSocket想象成一条水管，数据就是水。如果水管一直流水，它就不会堵。但要是你半天不放水，中间的路由器、防火墙就觉得“嘿，这管子是不是废弃了？”——咔嚓一刀给你掐了。尤其是在移动网络下，运营商的NAT网关空闲超时可能只有30秒到几分钟。还有我们常用的Nginx，默认<code>proxy_read_timeout</code>是60秒，一旦60秒内没有数据从后端发到客户端，Nginx就会自作主张断开连接。</p>
<p>所以，要想让连接长存，唯一的方法就是定期发送一些“无用”的数据，告诉中间件：“我还活着，别砍我！”——这就是心跳。</p>
<h2>💓 第二部分：心跳机制的两种姿势</h2>
<p>心跳本质是一种<code>ping/pong</code>模式。WebSocket协议本身有控制帧<code>Ping</code>和<code>Pong</code>，但浏览器原生JS的WebSocket API并没有直接暴露发送Ping帧的方法，所以我们一般用普通消息模拟：</p>
<ul>
<li>方案A：客户端定时发送<code>ping</code>消息，服务器收到后立即回复<code>pong</code>。</li>
<li>方案B：服务器定时发送<code>ping</code>，客户端回复<code>pong</code>。但同样，客户端需要能解析并回复。</li>
</ul>
<p>更常见的做法是客户端主动发心跳，服务器只需响应或记录。为啥？因为客户端更能感知网络变化，且断开后能立即重连。下面我就以客户端发心跳为例，上代码。</p>
<h2>⚙️ 第三部分：FastAPI后端实战</h2>
<p>先搭一个最简单的FastAPI WebSocket端点。这里我用了<code>/ws</code>路径，接收心跳消息（约定JSON格式<code>{"type": "ping"}</code>），并回复<code>{"type": "pong"}</code>。同时，为了及时清理死连接，我会记录每个连接的最后心跳时间，启动一个后台任务检查超时（比如60秒没收到心跳就主动close）。</p>
<pre><code class="language-python">from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio
import json
from datetime import datetime, timedelta

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: dict[WebSocket, datetime] = {}
        self._heartbeat_check_interval = 30   # 每30秒检查一次
        asyncio.create_task(self.heartbeat_checker())

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections[websocket] = datetime.utcnow()
        print(f"新连接加入，当前连接数：{len(self.active_connections)}")

    def disconnect(self, websocket: WebSocket):
        if websocket in self.active_connections:
            del self.active_connections[websocket]
            print(f"连接断开，当前连接数：{len(self.active_connections)}")

    async def handle_messages(self, websocket: WebSocket):
        try:
            while True:
                data = await websocket.receive_text()
                try:
                    msg = json.loads(data)
                except:
                    continue
                # 如果是心跳ping，更新最后心跳时间并回复pong
                if msg.get("type") == "ping":
                    self.active_connections[websocket] = datetime.utcnow()
                    await websocket.send_text(json.dumps({"type": "pong"}))
                else:
                    # 其他业务消息，按需处理
                    await websocket.send_text(json.dumps({"type": "echo", "data": msg}))
        except WebSocketDisconnect:
            self.disconnect(websocket)

    async def heartbeat_checker(self):
        while True:
            await asyncio.sleep(self._heartbeat_check_interval)
            now = datetime.utcnow()
            timeout = timedelta(seconds=70)  # 超过70秒没心跳就断开
            dead_conns = []
            for ws, last_ping in self.active_connections.items():
                if now - last_ping &gt; timeout:
                    dead_conns.append(ws)
            for ws in dead_conns:
                try:
                    await ws.close(code=1000, reason="heartbeat timeout")
                except:
                    pass
                self.disconnect(ws)

manager = ConnectionManager()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    await manager.handle_messages(websocket)</code></pre>
<h2>💻 第四部分：JavaScript前端实现</h2>
<p>前端主要做三件事：建立连接、定时发心跳、监听断开自动重连。我习惯把WebSocket封装成一个类，方便复用。直接上代码：</p>
<pre><code class="language-javascript">class WebSocketClient {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.heartbeatInterval = 30000; // 30秒一次心跳
        this.reconnectInterval = 3000;   // 断线后3秒重连
        this.heartbeatTimer = null;
        this.reconnectTimer = null;
        this.connect();
    }

    connect() {
        this.ws = new WebSocket(this.url);
        this.ws.onopen = () =&gt; {
            console.log('WebSocket 已连接');
            // 连接成功后，启动心跳
            this.startHeartbeat();
            // 如果之前有重连定时器，清掉
            if (this.reconnectTimer) {
                clearTimeout(this.reconnectTimer);
                this.reconnectTimer = null;
            }
        };

        this.ws.onmessage = (event) =&gt; {
            const data = JSON.parse(event.data);
            if (data.type === 'pong') {
                console.log('收到心跳pong，连接正常');
                // 可以在这里更新UI显示最后心跳时间，但不必须
            } else {
                // 处理其他业务消息
                console.log('业务消息', data);
            }
        };

        this.ws.onclose = (e) =&gt; {
            console.log('WebSocket 关闭', e.reason);
            // 停止心跳
            this.stopHeartbeat();
            // 尝试重连
            this.reconnect();
        };

        this.ws.onerror = (err) =&gt; {
            console.error('WebSocket 错误', err);
            this.ws.close();
        };
    }

    startHeartbeat() {
        this.heartbeatTimer = setInterval(() =&gt; {
            if (this.ws &amp;&amp; this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({ type: 'ping' }));
                console.log('发送心跳ping');
            } else {
                console.warn('连接未开启，停止发送心跳');
                this.stopHeartbeat();
            }
        }, this.heartbeatInterval);
    }

    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
    }

    reconnect() {
        this.stopHeartbeat();
        if (!this.reconnectTimer) {
            this.reconnectTimer = setTimeout(() =&gt; {
                console.log('尝试重连...');
                this.connect();
            }, this.reconnectInterval);
        }
    }

    // 主动关闭连接（比如页面卸载时）
    close() {
        this.stopHeartbeat();
        if (this.ws) {
            this.ws.close();
        }
    }
}

// 使用示例
const client = new WebSocketClient('ws://你的域名/ws');
// 页面关闭前主动清理
window.addEventListener('beforeunload', () =&gt; client.close());</code></pre>
<h2>🧪 第五部分：跑起来看看效果</h2>
<p>启动FastAPI（<code>uvicorn main:app --reload</code>），打开浏览器控制台，你会看到每隔30秒发送一次ping，服务器立即回复pong。即使你断开Wi-Fi再打开，客户端也会自动重连，并且重连后心跳继续。🎯</p>
<h2>💣 第六部分：那些年我踩过的坑（必看）</h2>
<ul>
<li>坑1：心跳间隔太短，服务器压力大——1秒一次纯属自残，30秒一次足够，既保活又省资源。</li>
<li>坑2：服务端没做超时主动断开——客户端突然掉线（比如用户强制杀进程），服务端不知道，连接一直占着内存。所以后台心跳检查一定要有，超时就close。</li>
<li>坑3：重连时忘记清理旧定时器——每次重连都新建一个<code>setInterval</code>，导致多个心跳线程并发，消息爆炸。解决方案：重连前先<code>stopHeartbeat()</code>。</li>
<li>坑4：前后端心跳格式约定不一致——我的是<code>{"type":"ping"</code>，如果你后端用字段<code>heartbeat</code>，一定记得对齐，否则服务器不认，相当于没心跳。</li>
<li>坑5：没考虑SSL/加密连接——生产环境用<code>wss://</code>，证书配置要正确，否则连接直接被拒绝。</li>
</ul>]]></description>
    <pubDate>Wed, 18 Feb 2026 21:07:14 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/web/9.html</guid>
</item>
<item>
    <title>手把手搞定FastAPI静态文件：安全、上传与访问</title>
    <link>https://mingxi.icu/web/8.html</link>
    <description><![CDATA[<p>你的FastAPI应用还在“裸奔”吗？超过70%的Web应用安全问题源于静态资源的不当配置！</p>
<p>这篇文章将带你系统掌握FastAPI中静态文件处理的方方面面，不止是简单的“挂载”，更涵盖安全防护、性能技巧和实战坑点，包含一个可直接运行的完整示例。</p>
<ul>
<li>📂 静态文件的加载、存储与应用场景</li>
<li>🛡️ 至关重要的静态文件安全设置</li>
<li>🎯 一行代码搞定网站favicon</li>
<li>🖼️ 处理图片等媒体文件的上传与访问</li>
</ul>
<h2>目录一览</h2>
<ul>
<li>🚀 起步：为什么需要处理静态文件？</li>
<li>📁 FastAPI的“文件管家”：StaticFiles</li>
<li>🔒 给静态文件加把“锁”：安全设置详解</li>
<li>⭐ 小图标大学问：Favicon的处理</li>
<li>🎬 不止于图片：媒体文件的上传与响应</li>
<li>🧪 实战演练：一个完整的示例应用</li>
</ul>
<h2>起步：为什么需要处理静态文件？</h2>
<p>你的API很酷，但用户访问 <code>http://localhost:8000/logo.png</code> 却得到404？这是因为FastAPI默认是个纯粹的API框架，它不会自动提供像图片、CSS、JavaScript这样的静态文件。</p>
<p>静态文件是那些内容固定、不经常改变的文件。它们对于构建一个完整的Web应用或API文档门户至关重要。</p>
<h2>FastAPI的“文件管家”：StaticFiles</h2>
<p>引入 <code>StaticFiles</code>，你就能轻松搭建一个文件服务器。其核心三步法：</p>
<ul>
<li>导入：<code>from fastapi.staticfiles import StaticFiles</code></li>
<li>挂载：将URL路径“挂载”到一个实际目录。</li>
<li>应用：使用 <code>app.mount</code> 方法。</li>
</ul>
<pre><code class="language-python">from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# 将路径 “/static” 映射到项目下的 “static” 目录
app.mount("/static", StaticFiles(directory="static"), name="static")</code></pre>
<p>理解“挂载”：它意味着所有访问 <code>/static/*</code> 的请求，都会由 <code>StaticFiles</code> 实例去 <code>./static</code> 目录下查找对应文件。它不是API路由，而是一个独立的子应用。</p>
<h2>🔒 给静态文件加把“锁”：安全设置详解</h2>
<p>开放文件访问是危险的！错误的配置可能导致敏感文件泄露（如 <code>.env</code>、<code>.git</code> 目录）。</p>
<ol>
<li>限制目录访问范围：永远不要将根目录“ <code>/</code> ”挂载到静态文件服务。务必使用一个独立的、权限明确的子目录（如 <code>static</code>、<code>assets</code>）。</li>
<li>使用“html=True”安全地提供HTML：如果你想提供单页应用（如Vue/React构建的产物），可以设置 <code>html=True</code>，并让 <code>index.html</code> 作为目录的默认页。</li>
</ol>
<pre><code class="language-python"># 安全地提供前端构建产物目录
app.mount("/app", StaticFiles(directory="./frontend/dist", html=True), name="spa_app")</code></pre>
<p>注意：<code>html=True</code> 只对提供前端SPA友好，它本身并不是一个安全漏洞。真正的安全在于对 <code>directory</code> 参数的控制。</p>
<h2>⭐ 小图标大学问：Favicon的处理</h2>
<p>浏览器会自动请求 <code>/favicon.ico</code>。如果你没处理，日志里就会堆满404错误，显得不专业。</p>
<p>最简单的方法：直接把它当成一个静态文件处理。</p>
<pre><code class="language-python"># 方法：为 favicon.ico 单独设置一个路径路由（或将它放在 static 目录）
from fastapi.responses import FileResponse

@app.get("/favicon.ico")
async def favicon():
    return FileResponse("static/favicon.ico")</code></pre>
<p>这能一劳永逸地消除那个烦人的404请求。</p>
<h2>🎬 不止于图片：媒体文件的上传与响应</h2>
<p>静态文件是“读”，媒体文件则常涉及“写”（上传）。FastAPI处理上传非常优雅。</p>
<ul>
<li>上传：使用 <code>File</code> 和 <code>UploadFile</code>。</li>
<li>存储：使用 <code>shutil</code> 或 <code>aiofiles</code> 写入特定目录（如 <code>media/</code>）。</li>
<li>访问：再次借助 <code>StaticFiles</code> 挂载 <code>media/</code> 目录。</li>
</ul>
<pre><code class="language-python">from fastapi import File, UploadFile
import shutil
import os

UPLOAD_DIR = "media"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/upload/image/")
async def upload_image(file: UploadFile = File(...)):
    file_path = os.path.join(UPLOAD_DIR, file.filename)
    with open(file_path, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    return {"filename": file.filename, "url": f"/media/{file.filename}"}

# 挂载上传的媒体文件目录
app.mount("/media", StaticFiles(directory="media"), name="media")</code></pre>
<h2>🧪 实战演练：一个完整的示例应用</h2>
<p>下面是一个整合了上述所有知识点完整的 <code>main.py</code> 文件，复制即可运行体验。</p>
<pre><code class="language-python">from fastapi import FastAPI, File, UploadFile, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, HTMLResponse
import shutil
import os
from pathlib import Path

app = FastAPI()

# 1. 创建必要的目录
static_dir = Path("static")
media_dir = Path("media")
static_dir.mkdir(exist_ok=True)
media_dir.mkdir(exist_ok=True)

# 2. 挂载静态文件目录（存放 CSS，JS，预置图片）
app.mount("/static", StaticFiles(directory=static_dir), name="static")

# 3. 挂载媒体文件目录（存放用户上传的图片）
app.mount("/media", StaticFiles(directory=media_dir), name="media")

# 4. 处理 favicon 请求
@app.get("/favicon.ico", include_in_schema=False)
async def get_favicon():
    # 假设你的 favicon.ico 放在 static 目录下
    return FileResponse(static_dir / "favicon.ico")

# 5. 文件上传接口
@app.post("/upload/")
async def create_upload_file(file: UploadFile = File(...)):
    # 保存上传的文件到 media 目录
    save_path = media_dir / file.filename
    with save_path.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    # 返回文件的访问 URL
    return {"url": f"/media/{file.filename}"}

# 6. 一个简单的前端页面，用于演示和测试上传
@app.get("/", response_class=HTMLResponse)
async def main():
    return """
    &lt;html&gt;
        &lt;head&gt;
            &lt;title&gt;FastAPI静态文件演示&lt;/title&gt;
        &lt;/head&gt;
        &lt;body&gt;
            &lt;h2&gt;上传图片测试&lt;/h2&gt;
            &lt;form action="/upload/" enctype="multipart/form-data" method="post"&gt;
            &lt;input name="file" type="file"&gt;
            &lt;input type="submit"&gt;
            &lt;/form&gt;
            &lt;br&gt;
            &lt;h3&gt;尝试访问：&lt;/h3&gt;
            &lt;ul&gt;
                &lt;li&gt;&lt;a href="/static/example.txt"&gt;预置的静态文件 (/static/example.txt)&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;上传后，访问：&lt;code&gt;/media/[你的文件名]&lt;/code&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/body&gt;
    &lt;/html&gt;
    """</code></pre>
<p>运行后，访问 <code>http://localhost:8000</code> 即可体验上传和访问文件。记得先在 <code>static/</code> 目录下放个 <code>example.txt</code> 文件。</p>]]></description>
    <pubDate>Thu, 01 Jan 2026 23:10:19 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/web/8.html</guid>
</item>
<item>
    <title>别再让你的 Python 傻等了：三分钟带你通过 asyncio 实现性能起飞</title>
    <link>https://mingxi.icu/web/3.html</link>
    <description><![CDATA[<h1>别再让你的 Python 傻等了：三分钟带你通过 asyncio 实现性能起飞</h1>
<ol>
<li>痛点场景：你是在“单线程”思考吗？</li>
</ol>
<p>想象你正在开发一个爬虫程序，需要下载 100 张高清图片。</p>
<p>如果你用传统的 requests 库，代码逻辑通常是这样的：</p>
<p>发起请求 A -&gt; 等待网络响应（500ms） -&gt; 保存图片 A。</p>
<p>发起请求 B -&gt; 等待网络响应（500ms） -&gt; 保存图片 B。</p>
<p>...以此类推。</p>
<p>问题出在哪里？在那 500ms 的网络等待时间里，你的 CPU 实际上在摸鱼！它明明可以处理剩下的 99 个请求，却非要死等这一个响应回来。这种模式叫“同步阻塞”，是导致程序运行缓慢的头号元凶。</p>
<p>解决方案：asyncio。它让 Python 学会了“分身术”，在等待 A 的时候，顺手把 B、C、D 全都发出去。</p>
<ol start="2">
<li>概念拆解：米其林餐厅的秘密</li>
</ol>
<p>生活化类比</p>
<p>为了理解 asyncio，我们把 CPU 比作餐厅厨师。</p>
<p>同步阻塞（Synchronous）：厨师把牛排丢进锅里，然后死死盯着锅，直到肉熟了才去切土豆。这时候，哪怕外面排了 10 个客人，厨师也什么都不干。</p>
<p>异步非阻塞（Asyncio）：厨师把牛排丢进锅里，定个闹钟（注册事件），转身就去切土豆或准备酱汁。等闹钟响了，他再回来翻牛排。</p>
<p>在这个比喻中：</p>
<p>事件循环 (Event Loop)：就是那个“闹钟管理器”。它负责监控哪些任务做好了，该切回哪一环。</p>
<p>协程 (Coroutine)：就是“牛排煎制”或“切土豆”这些可以中途挂起、之后再继续的任务。</p>
<ol start="3">
<li>动手实战：从 Hello World 到并发请求</li>
</ol>
<p>基础代码</p>
<pre><code class="language-python">import asyncio
import time

# 定义一个协程函数（使用 async 关键字）
async def fetch_data(id, delay):
    print(f"任务 {id}: 正在发起请求，预计耗时 {delay} 秒...")
    # 使用 await 挂起当前任务，模拟网络 I/O
    await asyncio.sleep(delay) 
    print(f"任务 {id}: 数据返回成功！")
    return f"结果 {id}"

async def main():
    start_time = time.perf_counter()

    # 创建任务并发执行
    print("--- 任务开始 ---")
    results = await asyncio.gather(
        fetch_data(1, 3),
        fetch_data(2, 1),
        fetch_data(3, 2)
    )

    end_time = time.perf_counter()
    print(f"--- 所有任务完成，总耗时: {end_time - start_time:.2f} 秒 ---")
    print(f"返回列表: {results}")

# 运行事件循环
if __name__ == "__main__":
    asyncio.run(main())</code></pre>
<p>代码解析</p>
<ul>
<li>async def: 告诉 Python 这是一个协程，调用它不会立即执行，而是返回一个协程对象。</li>
<li>await: 这是“暂停键”。它告诉事件循环：“我要在这儿等一会儿，你先去处理别人，等好了再叫我。”</li>
<li>asyncio.gather: 这是“集合指令”，它把多个协程打包，让事件循环同时启动它们。</li>
<li>结果分析: 虽然总等待时间是 3+1+2=6 秒，但你会发现程序运行只需 3 秒左右。因为最长的那个任务还没做完时，短的任务已经利用空隙做完了。</li>
</ul>
<ol start="4">
<li>进阶深潜：新手最容易掉进去的坑</li>
</ol>
<p>常见陷阱：在异步代码里写同步阻塞</p>
<p>很多新手会写出这样的代码：</p>
<pre><code class="language-python">async def broken_coroutine():
    time.sleep(5) # 致命错误！
    await some_async_func()</code></pre>
<p>后果：time.sleep(5) 会让整个线程停摆 5 秒。哪怕你有 1000 个协程，它们都会被这一行代码活生生卡死。在异步世界里，必须使用 await asyncio.sleep()。</p>
<p>最佳实践</p>
<ul>
<li>不要为了异步而异步：如果你的任务是计算密集型的（如：大矩阵运算、视频转码），asyncio 帮不了你，你应该用 multiprocessing（多进程）。</li>
<li>库的选择：传统的 requests 或 pymysql 是同步的，在 asyncio 中会失效。请使用对应的异步版本，如 aiohttp。</li>
</ul>]]></description>
    <pubDate>Fri, 26 Dec 2025 23:52:57 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/web/3.html</guid>
</item>
<item>
    <title>Spring Boot WebSocket方案终极指南：Netty与官方Starter对比与实践</title>
    <link>https://mingxi.icu/server/7.html</link>
    <description><![CDATA[<h1>Spring Boot WebSocket方案终极指南：Netty与官方Starter对比与实践</h1>
<h2>一、Maven依赖引入</h2>
<h3>1. Netty-WebSocket-Spring-Boot-Starter</h3>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.yeauty&lt;/groupId&gt;
    &lt;artifactId&gt;netty-websocket-spring-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;0.13.0&lt;/version&gt; &lt;!-- 请使用最新版本 --&gt;
&lt;/dependency&gt;</code></pre>
<h3>2. Spring官方WebSocket Starter</h3>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-websocket&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<h2>二、核心差异对比</h2>
<table>
<thead>
<tr>
<th>特性</th>
<th>Netty-WebSocket</th>
<th>Spring官方Starter</th>
</tr>
</thead>
<tbody>
<tr>
<td>底层框架</td>
<td>Netty NIO框架 (非阻塞IO)</td>
<td>Servlet容器 (Tomcat/Jetty)</td>
</tr>
<tr>
<td>协议支持</td>
<td>原生WebSocket + 自定义二进制协议</td>
<td>WebSocket + STOMP消息协议</td>
</tr>
<tr>
<td>线程模型</td>
<td>Reactor多线程模型 (Boss/Worker)</td>
<td>Servlet线程池模型</td>
</tr>
<tr>
<td>容器依赖</td>
<td>无 (可独立运行)</td>
<td>必须依赖Servlet容器</td>
</tr>
<tr>
<td>编程范式</td>
<td>事件驱动模型 (类似Netty Handler)</td>
<td>消息代理模型 (发布/订阅)</td>
</tr>
<tr>
<td>与Spring集成</td>
<td>中等 (需手动管理会话)</td>
<td>深度集成 (自动配置+安全支持)</td>
</tr>
<tr>
<td>学习曲线</td>
<td>较陡峭 (需理解Netty概念)</td>
<td>平缓 (Spring开发者友好)</td>
</tr>
<tr>
<td>适用场景</td>
<td>高频实时数据/自定义协议</td>
<td>企业消息系统/标准文本通信</td>
</tr>
</tbody>
</table>
<h2>三、使用场景决策指南</h2>
<h3>✅ 选择 Netty-WebSocket 当：</h3>
<ul>
<li>需要处理高频实时数据：金融行情推送、物联网传感器数据</li>
<li>使用自定义二进制协议：游戏数据包、音视频流传输</li>
<li>追求极致性能：要求1万+并发连接，低延迟响应</li>
<li>脱离Servlet容器：希望WebSocket服务独立部署</li>
</ul>
<h3>✅ 选择 Spring官方Starter 当：</h3>
<ul>
<li>开发企业级消息系统：聊天应用、实时通知系统</li>
<li>需要完整STOMP支持：利用消息代理和订阅机制</li>
<li>快速集成Spring生态：与Security、Data等组件协作</li>
<li>兼容旧浏览器：需要SockJS回退支持</li>
</ul>
<h2>四、核心代码实现对比</h2>
<h3>方案1：Netty-WebSocket实现（实时数据推送）</h3>
<pre><code class="language-java">@SpringBootApplication
@EnableNettyWebSocket // 启用Netty WebSocket服务器
public class DataPushApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataPushApplication.class, args);
    }
}

/**
 * 实时数据推送处理器
 * 特点：直接操作Session，手动管理连接
 */
@ServerEndpoint(host = "0.0.0.0", port = "8080", path = "/realtime")
public class DataPushHandler {

    // 存储所有活动会话
    private static final Set&lt;Session&gt; sessions = ConcurrentHashMap.newKeySet();

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
        session.sendText("CONNECTED|" + LocalTime.now());
    }

    @OnText
    public void onText(Session session, String message) {
        // 处理文本消息（如控制指令）
        String response = processCommand(message);
        session.sendText(response);
    }

    @OnBinary
    public void onBinary(Session session, byte[] bytes) {
        // 解析二进制数据（如传感器数据）
        SensorData data = SensorDecoder.decode(bytes);
        // 处理数据逻辑...
        byte[] response = SensorEncoder.encode(data);
        session.sendBinary(response);
    }

    @OnClose
    public void onClose(Session session, CloseReason reason) {
        sessions.remove(session);
    }

    // 广播数据给所有客户端
    public static void broadcast(byte[] data) {
        sessions.forEach(session -&gt; {
            if (session.isOpen()) {
                session.sendBinary(data);
            }
        });
    }
}</code></pre>
<h3>方案2：Spring官方Starter实现（完整聊天室）</h3>
<pre><code class="language-java">/**
 * WebSocket配置类
 * 特点：使用STOMP协议，配置消息代理
 */
@Configuration
@EnableWebSocketMessageBroker
public class ChatConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 客户端连接端点
        registry.addEndpoint("/chat-ws")
                .setAllowedOriginPatterns("*")
                .withSockJS(); // 浏览器兼容支持
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 启用内存消息代理
        registry.enableSimpleBroker("/topic", "/queue");

        // 设置应用消息前缀
        registry.setApplicationDestinationPrefixes("/app");

        // 设置用户私有队列前缀
        registry.setUserDestinationPrefix("/user");
    }
}

/**
 * 聊天控制器
 * 特点：使用高级消息抽象，自动处理订阅
 */
@Controller
public class ChatController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    // 处理公共聊天消息
    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public ChatMessage handlePublicMessage(@Payload ChatMessage message, 
                                        Principal principal) {
        message.setSender(principal.getName());
        message.setTimestamp(LocalDateTime.now());
        return message;
    }

    // 处理私有消息
    @MessageMapping("/private")
    public void handlePrivateMessage(@Payload ChatMessage message, 
                                   Principal principal) {
        message.setSender(principal.getName());
        message.setTimestamp(LocalDateTime.now());

        // 定向发送给接收者
        messagingTemplate.convertAndSendToUser(
            message.getRecipient(), 
            "/queue/private", 
            message
        );
    }

    // 用户上线处理
    @EventListener
    public void handleConnect(SessionConnectedEvent event) {
        String username = event.getUser().getName();
        // 通知所有用户更新在线列表
        messagingTemplate.convertAndSend("/topic/onlineUsers", 
            userService.getOnlineUsers());
    }
}

/**
 * 消息实体类
 */
public class ChatMessage {
    private String sender;      // 发送者
    private String recipient;   // 接收者（私聊使用）
    private String content;     // 消息内容
    private LocalDateTime timestamp; // 时间戳

    // getters &amp; setters
}</code></pre>
<h2>五、关键差异解析</h2>
<ol>
<li>
<p><strong>连接管理方式</strong></p>
<ul>
<li>Netty：手动维护<code>Session</code>集合，直接操作连接</li>
<li>Spring：自动管理连接，通过<code>SimpMessagingTemplate</code>发送消息</li>
</ul>
</li>
<li>
<p><strong>消息处理模式</strong></p>
</li>
</ol>
<pre><code>graph LR
A[客户端] --&amp;gt; B{Netty方案}
B --&amp;gt; C[直接处理二进制数据]
B --&amp;gt; D[自定义协议解析]

A --&amp;gt; E{Spring方案}
E --&amp;gt; F[STOMP消息代理]
E --&amp;gt; G[发布/订阅模式]</code></pre>
<ol start="3">
<li>
<p><strong>异常处理机制</strong></p>
<ul>
<li>Netty：通过<code>@OnError</code>捕获异常，需手动关闭问题会话</li>
<li>Spring：全局异常处理器<code>@MessageExceptionHandler</code>统一处理</li>
</ul>
</li>
<li>
<p><strong>集群支持</strong></p>
<ul>
<li>Netty：需自行实现分布式会话管理（如Redis）</li>
<li>Spring：天然支持通过消息代理（RabbitMQ/Redis）实现集群</li>
</ul>
</li>
</ol>
<h2>六、选型建议总结</h2>
<table>
<thead>
<tr>
<th>项目特征</th>
<th>推荐方案</th>
<th>理由说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>高频实时数据（&gt;1000 TPS）</td>
<td>Netty-WebSocket</td>
<td>低延迟、高吞吐量</td>
</tr>
<tr>
<td>企业级聊天系统</td>
<td>Spring官方Starter</td>
<td>STOMP协议支持完善</td>
</tr>
<tr>
<td>自定义二进制协议</td>
<td>Netty-WebSocket</td>
<td>直接操作字节数据</td>
</tr>
<tr>
<td>需要SockJS兼容旧浏览器</td>
<td>Spring官方Starter</td>
<td>内置SockJS支持</td>
</tr>
<tr>
<td>微服务架构中的独立服务</td>
<td>Netty-WebSocket</td>
<td>不依赖Servlet容器</td>
</tr>
<tr>
<td>需要深度整合Spring Security</td>
<td>Spring官方Starter</td>
<td>原生支持安全拦截</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>黄金实践法则</strong>：<br><br />
新项目若<strong>不需要处理二进制协议</strong>，优先选择Spring官方方案；<br><br />
现有系统需<strong>添加高性能实时通道</strong>，引入Netty作为独立服务模块；<br><br />
<strong>关键业务系统</strong>建议同时实现两种方案，Netty处理实时数据流，Spring处理业务消息。</p>
</blockquote>]]></description>
    <pubDate>Fri, 26 Dec 2025 08:22:12 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/server/7.html</guid>
</item>
<item>
    <title>Web攻防-Fuzz模糊测试篇&amp;JS算法口令&amp;隐藏参数&amp;盲Payload&amp;未知文件目录</title>
    <link>https://mingxi.icu/web/5.html</link>
    <description><![CDATA[<h1>Web攻防-Fuzz模糊测试篇&amp;JS算法口令&amp;隐藏参数&amp;盲Payload&amp;未知文件目录</h1>
<h2>知识点：</h2>
<p>1、Fuzz技术-用户口令-常规&amp;模块&amp;JS插件<br />
2、Fuzz技术-目录文件-目录探针&amp;文件探针<br />
3、Fuzz技术-未知参数名-文件参数&amp;隐藏参数<br />
4、Fuzz技术-构造参数值-漏洞攻击恶意Payload</p>
<p>Fuzz：是一种基于黑盒的自动化软件模糊测试技术,简单的说一种懒惰且暴力的技术融合了常见的以及精心构建的数据文本进行网站、软件安全性测试。</p>
<p>Fuzz的核心思想:<br />
口令Fuzz(弱口令)<br />
目录Fuzz(漏洞点)<br />
参数Fuzz(利用参数)<br />
PayloadFuzz(Bypass)</p>
<p>Fuzz应用场景：<br />
-爆破用户口令<br />
-爆破敏感目录<br />
-爆破文件地址<br />
-爆破未知参数名<br />
-Payload测漏洞（绕过等也可以用）<br />
在实战黑盒中，目标有很多没有显示或其他工具扫描不到的文件或目录等，我们就可以通过大量的字典Fuzz找到的隐藏的文件进行测试。</p>
<h2>一、Fuzz技术-用户口令-常规&amp;模块&amp;JS插件</h2>
<h3>1、无验证码密码明文</h3>
<p>这种情况下爆破密码或者账号密码都爆破</p>
<p>只爆破密码很简单，这里不做演示</p>
<p>如果要同时爆破账号和密码，在Intruder模块需要选择Cluster bomb</p>
<h3>2、无验证码密码弱加密（MD5）</h3>
<p>密码使用MD5加密</p>
<p>在爆破的时候可以选择加密语言</p>
<h3>3、无验证码密码复杂加密（通过js文件进行加密）</h3>
<p>密码强加密，有自己的加密算法，需要我们去逆向出加密算法</p>
<p>此处加密算法逆向案例可以参考我的博客：<a href="https://www.cnblogs.com/Kurisu-Christina/p/19402512">https://www.cnblogs.com/Kurisu-Christina/p/19402512</a></p>
<p>该博文的演示案例二有说明</p>
<p>那么这种情况如何从burpsuite中发包呢？</p>
<p>使用插件BurpCrypto，将加密源码粘贴放入，并创建一个方法，传参数p为密码</p>
<h2>二、Fuzz技术-目录文件-目录探针&amp;文件探针</h2>
<p>打开就是403的网站可以测试有没有其他目录</p>
<p>也可以使用工具来FUZZ</p>
<p><a href="https://github.com/maurosoria/dirsearch">https://github.com/maurosoria/dirsearch</a></p>
<p><a href="https://github.com/7kbstorm/7kbscan-WebPathBrute">https://github.com/7kbstorm/7kbscan-WebPathBrute</a></p>
<h2>三、Fuzz技术-未知参数名-文件参数&amp;隐藏参数</h2>
<p>当爆破出目录，再爆破出文件名后，有些文件是可以传参数的，这样不就造成漏洞了吗</p>
<p>试着爆破参数名：</p>
<p>忙猜这里参数名为do，我们可以给他传入其他参数值</p>
<h2>四、Fuzz技术-构造参数值-漏洞攻击恶意Payload</h2>
<p>忙猜这里参数名为do，我们可以给他传入其他参数值</p>
<p>有很多就执行成功了</p>
<p>爆破的地方有很多，包括提交方式，js文件等也可以爆破，不要把想法仅限于账号密码</p>
<h2>五、SRC泄露未授权漏洞</h2>
<h3>Fuzz手机加验证码突破绕过</h3>
<p>爆破手机号</p>
<h3>Fuzz访问URL挖未授权访问</h3>
<h3>Fuzz密码组合规则信息泄露</h3>]]></description>
    <pubDate>Tue, 09 Dec 2025 10:38:13 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/web/5.html</guid>
</item>
<item>
    <title>虚拟机操作系统选择指南（2025）</title>
    <link>https://mingxi.icu/other/2.html</link>
    <description><![CDATA[<h1>虚拟机操作系统选择指南（2025）</h1>
<h2>为什么要了解虚拟机操作系统？</h2>
<p>想象一下，你有一台物理电脑，但想在里面运行多个&quot;虚拟电脑&quot;，每个&quot;虚拟电脑&quot;都有自己独立的操作系统。这就是虚拟机！而选择哪个操作系统安装在虚拟机里，就像选择给不同的房间布置不同的装修风格。</p>
<p>对新手来说，了解这些系统能帮你：</p>
<ul>
<li>找到最适合学习的系统</li>
<li>避免不必要的安装麻烦</li>
<li>快速上手实践</li>
</ul>
<h2>虚拟机操作系统简介</h2>
<h3>Rocky Linux</h3>
<p>你可以把它看作是CentOS的&quot;接班人&quot;。CentOS官方支持已转向CentOS Stream，而Rocky Linux完美接替了原先CentOS的稳定位置。就像你习惯用的某个经典产品停产了，它的&quot;官方继承者&quot;出现了。</p>
<p><strong>特点</strong>：</p>
<ul>
<li>与CentOS几乎一样的使用体验</li>
<li>非常稳定，适合长期运行</li>
<li>社区活跃，资料越来越多</li>
</ul>
<p><strong>适合谁</strong>： 以前用过CentOS，或者需要稳定服务器环境的新手</p>
<h3>CentOS 7（经典稳定版）</h3>
<p>虽然&quot;年纪&quot;有点大了（2014年发布），但依然有很多教程和环境基于它。就像Windows 7虽然老了，但很多人还在用。</p>
<p><strong>注意</strong>： 官方主流支持已结束，但还有部分扩展支持</p>
<p><strong>适合谁</strong>：</p>
<ul>
<li>跟着老教程学习的新手</li>
<li>运行特定老旧软件</li>
<li>只是想在虚拟机里&quot;体验一下&quot;的人</li>
</ul>
<h3>Ubuntu Server（新手友好之选）</h3>
<p>如果你想找最容易上手的Linux服务器系统，这可能是最佳选择。它就像&quot;智能手机&quot;中的iPhone——开箱即用，设置简单，遇到问题一搜就有答案。</p>
<p><strong>特点</strong>：</p>
<ul>
<li>安装过程图形化，像安装普通软件</li>
<li>庞大的社区，海量中文教程</li>
<li>软件更新快，能用到最新技术</li>
</ul>
<p><strong>适合谁</strong>： 完全零基础，想快速上手Linux服务器的新手</p>
<h3>Debian（稳定可靠之选）</h3>
<p>如果说Ubuntu是&quot;时尚快消品&quot;，Debian就是&quot;经典工艺品&quot;。它不追求最新，但追求最稳定。很多企业服务器都使用它。</p>
<p><strong>特点</strong>：</p>
<ul>
<li>极其稳定，运行几个月不用重启</li>
<li>软件包管理优秀</li>
<li>安全性很好</li>
</ul>
<p><strong>适合谁</strong>： 希望系统稳定，不追求最新功能的学习者</p>
<h3>🐉 openEuler（国产新星）</h3>
<p>华为推出的开源系统，近年来发展迅速。如果你是IT相关专业，或者关注国产化技术，值得了解。</p>
<p><strong>特点</strong>：</p>
<ul>
<li>中文支持好</li>
<li>针对服务器优化</li>
<li>国内社区活跃</li>
</ul>
<p><strong>适合谁</strong>： 对国产技术感兴趣，或需要中文环境支持的新手</p>
<h2>如何选择适合你的系统？</h2>
<h3>根据你的目标选择</h3>
<ul>
<li>&quot;我只想试试Linux是什么&quot; → 选 Ubuntu Server，安装简单，跟着教程一步步来就行</li>
<li>&quot;我要学运维，以后找工作&quot; → 新手从 Ubuntu Server 入门，然后学 Rocky Linux</li>
<li>&quot;我要搭建一个长期运行的个人网站/服务&quot; → 选 Debian 或 Rocky Linux</li>
<li>&quot;我学校的教程/实验室用的都是CentOS 7&quot; → 跟着学校走，用 CentOS 7</li>
<li>&quot;我想了解国产操作系统&quot; → 选 openEuler</li>
</ul>
<h3>系统配置要求对比</h3>
<p>所有系统的基本要求都差不多：</p>
<ul>
<li>内存：至少2GB（4GB更流畅）</li>
<li>硬盘：20GB以上</li>
<li>CPU：现代CPU都没问题</li>
</ul>
<p>Ubuntu Server 对新手最友好，安装过程中会有很多提示和帮助。</p>
<p><strong>最终建议</strong>： 从 Ubuntu Server 22.04 LTS 开始。它就像学开车时用的教练车——安全、友好、有辅助。等你熟悉了，再尝试其他系统，就知道它们的区别和特点了。</p>
<p>虚拟机操作系统的世界很广阔，但第一步总是最简单的：下载一个，装上试试。动手操作半小时，比看十篇教程都有用。</p>]]></description>
    <pubDate>Mon, 10 Nov 2025 20:43:33 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/other/2.html</guid>
</item>
<item>
    <title>搜索数据库表的性能优化过程</title>
    <link>https://mingxi.icu/other/1.html</link>
    <description><![CDATA[<h1>搜索数据库表的性能优化过程</h1>
<h2>问题背景</h2>
<p>做一个数据库表查看、标注与分析的工具软件。Table是数据库中表的信息（information_schema.tables）；Documentation是Table的数据字典文档，存储在本地文件中；Annotation是对Table的额外标注信息，存储在另一个数据库中。每一条Table，最多关联到一条Documentation和一条Annotation。</p>
<p>现在想搜索Table。前端向后端提供3个参数，搜索关键词列表、当前页码、每页条数；后端的搜索逻辑是，如果一条完整数据（Table+Documentation+Annotation）包含所有搜索关键词，则将Table加入搜索结果中。</p>
<p>Table的数量目前为6000+，要做到秒级搜索。</p>
<h2>初步实现</h2>
<p>因为跨数据源，所以不能简单连表查询。</p>
<p>对于每个Table，查出Documentation、Annotation，然后将Table、Documentation、Annotation中要搜索的字段值取出来，用空格隔开拼接为字符串，形如&quot;Table字段值 Documentation字段值 Annotation字段值&quot;，我们称之为SearchKey（搜索键）。如果每个关键词都包含在SearchKey中，则将Table加入搜索结果。</p>
<p>搜索时，先获取所有Table，然后遍历每个Table，获取SearchKey并判断是否加入搜索结果。</p>
<p>为了提高速度，用Redis缓存SearchKey。</p>
<p>分析数据情况：</p>
<ul>
<li>Table只增、不删、不改，因此，搜索时要重新获取所有Table，确保搜索到新Table；不必考虑驱逐（evict）SearchKey的缓存。</li>
<li>Documentation不增、不删、不改，因此，不必考虑驱逐SearchKey的缓存。</li>
<li>Annotation增、删、改，因此，要在Annotation增删改之后驱逐对应SearchKey的缓存，确保搜索到Annotation的最新信息。</li>
</ul>
<p>实测结果：</p>
<ul>
<li>实现了功能，支持同时按Table、Documentation、Annotation的字段搜索。</li>
<li>有性能问题，即使缓存已经全部完成，但每次搜索都要耗时30s左右，原因是6000+个Table遍历从Redis获取SearchKey，每次耗时1~15ms，累计耗时非常长。</li>
</ul>
<h2>第一次性能优化</h2>
<p>优化缓存策略。</p>
<p>获取所有Table后，构建SearchKeyMap（Table→SearchKey），然后将SearchKeyMap缓存，这样，下一次搜索时，只需要从Redis获取一次，提高传输效率。</p>
<p>为了确保搜索到新Table，缓存SearchKeyMap时将Table列表的长度作为缓存键，如果新增了Table，则SearchKeyMap不会命中缓存，而是重新构建。</p>
<p>为了减少构建SearchKeyMap的时间，仍然保留单个SearchKey的缓存，仍然在Annotation增删改之后驱逐单个SearchKey的缓存，但不同的是，还要同时驱逐SearchKeyMap的缓存。</p>
<p>实测结果：</p>
<ul>
<li>性能提升明显，在缓存全部完成的情况下，搜索耗时降至1.3s左右。</li>
<li>仍然有性能问题，对一个Annotation做了增删改，会驱逐整个SearchKeyMap缓存，重建SearchKeyMap就又回到了遍历Table的情况，仍然要耗时30s左右。</li>
</ul>
<h2>第二次性能优化</h2>
<p>优化缓存策略。</p>
<p>取消单个SearchKey的缓存，只缓存SearchKeyMap。</p>
<p>搜索Table时，要获取SearchKeyMap。先获取现有的SearchKeyMap缓存（固定缓存键，不再使用列表长度作为缓存键；没有缓存则取得空Map），然后遍历Table，如果Table不在SearchKeyMap中，则计算SearchKey并放入SearchKeyMap。这样，第一次搜索时会计算每个Table的SearchKey，后续搜索就只需要计算新Table的SearchKey。</p>
<p>Annotation增删改后，要更新SearchKeyMap。先获取现有的SearchKeyMap缓存，然后重新计算指定Table的SearchKey并放入SearchKeyMap。这样，无需每次都重建整个SearchKeyMap。</p>
<p>实测结果：Annotation增删改后再搜索，耗时降至1.3s左右。</p>
<h2>第三次性能优化</h2>
<p>优化缓存实现方式。</p>
<p>既然现在只需要简单地缓存一个SearchKeyMap，那么不一定要用Redis。</p>
<p>使用Redis作为缓存（RedisCacheManager），虽然内网通信快，但仍有网络开销。实测平均1092.9ms。</p>
<p>使用Map作为缓存（ConcurrentMapCacheManager），其他代码完全不变。实测平均968.3ms。</p>
<p>修改代码，直接用类中的Map字段作为缓存，省去缓存管理器的开销。实测平均915.2ms。</p>
<p>可见，性能有提升，但幅度不大。由于软件在开发中，要频繁重新运行，Redis能保持缓存，Map不能，因此保持上一版方案不做修改。</p>
<h2>第四次性能优化</h2>
<p>第三次优化其实是盲目的，应该要用事实找出性能瓶颈。</p>
<p>对搜索过程计时分析发现，一次耗时1105ms的搜索，其中获取所有Table耗时1028ms，占比93%，是绝对的性能瓶颈。</p>
<p>思路1：先只获取所有表名，而不是Table对象，如果表名对应的SearchKey匹配，再获取Table。实测发现，如果匹配的表名很多（例如关键词列表为空时），则即使有表名→Table的缓存（Redis实现），逐个获取也远远慢于直接从数据库一次性获取。因此，此思路不可行。</p>
<p>思路2：Table只增、不删、不改，因此可以考虑增量获取。缓存Table列表，每次获取时跳过缓存的长度，只获取增量部分。然而，information_schema.tables中没有id，无法保证新Table一定排在最后。因此，此思路不可行。</p>
<p>思路3：获取所有Table说到底只是为了搜索到新Table，如果能知道什么时候新增了Table，就可以放心地使用Table列表的缓存，或者从数据库重新获取。那么怎么知道？由于Table只增，所以可以用Table的数量判断。缓存Table列表，每次先从数据库查出数量（比直接查出Table列表明显更快），如果数量与缓存一致，则用缓存，否则查库。实测，此思路可行。</p>
<p>实现思路3后，再次计时分析。无新增Table时，搜索耗时降至360ms左右（只查库数量）；有新增时，耗时升至1.5s左右（查库数量+列表）。由于搜索Table的频率远远高于新增Table，因此，总体性能提升显著。</p>
<h2>总结</h2>
<p>经过数次性能优化，在满足功能的前提下，搜索时间从30s左右降至稳定0.4s左右，效果显著。0.4s已经没有缓慢感，性能优化工作可以结束了。</p>
<p>从上述优化过程可见，做优化要因地制宜，具体问题具体分析，选择合适的策略；优化效果的衡量要以实测结果为准。</p>]]></description>
    <pubDate>Mon, 22 Sep 2025 23:49:31 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/other/1.html</guid>
</item>
<item>
    <title>H5 页面中实现跳转到其他 APP</title>
    <link>https://mingxi.icu/web/6.html</link>
    <description><![CDATA[<h1>H5 页面中实现跳转到其他 APP</h1>
<p>在 H5 页面中跳转到其他 APP，可以使用以下几种方式：</p>
<h2>1. URL Scheme（自定义协议）</h2>
<p>许多 APP 都支持 URL Scheme 方式的跳转，例如：</p>
<pre><code class="language-html">&lt;a href="weixin://"&gt;打开微信&lt;/a&gt;
&lt;a href="alipay://"&gt;打开支付宝&lt;/a&gt;
&lt;a href="yourapp://path"&gt;打开自定义 APP&lt;/a&gt;</code></pre>
<p><strong>注意</strong>：</p>
<ul>
<li>需要目标 APP 支持 URL Scheme，未安装 APP 时会无响应或报错。</li>
<li>在 iOS 9+ 之后，需在 <code>info.plist</code> 中配置 <code>LSApplicationQueriesSchemes</code>。</li>
</ul>
<h2>2. Universal Links（iOS）&amp; Deep Link（Android）</h2>
<p>Universal Links（iOS）和 Deep Link（Android）可以更安全地跳转到 APP，且未安装时可跳转至 Web 页面。</p>
<ul>
<li>需要服务端配置特定文件（如 <code>apple-app-site-association</code>）。</li>
<li>适用于 iOS 9+，不会弹出确认框，用户体验更好。</li>
</ul>
<p><strong>示例</strong>：</p>
<pre><code class="language-html">&lt;a href="https://yourdomain.com/path"&gt;打开 APP&lt;/a&gt;</code></pre>
<h2>3. Intent Scheme（Android 专属）</h2>
<p>在 Android 设备上可以使用 <code>intent://</code> 方案：</p>
<pre><code class="language-html">&lt;a href="intent://path#Intent;scheme=yourapp;package=com.example.app;end;"
  &gt;打开 APP&lt;/a&gt;</code></pre>
<ul>
<li>若 APP 已安装，则直接打开。</li>
<li>若 APP 未安装，则可跳转到 Google Play。</li>
</ul>
<h2>4. iframe 方式（部分浏览器支持）</h2>
<pre><code class="language-html">&lt;iframe src="yourapp://path" style="display: none;"&gt;&lt;/iframe&gt;</code></pre>
<ul>
<li>可用于尝试静默拉起 APP，但可能被浏览器拦截。</li>
</ul>
<h2>5. 混合方式（兼容性方案）</h2>
<p>综合以上方法，推荐使用 JS 处理：</p>
<pre><code class="language-html">&lt;script&gt;
  function openApp() {
    var schemeUrl = "yourapp://path";
    var storeUrl = "https://yourapp.com/download"; // APP 下载地址

    var ua = navigator.userAgent.toLowerCase();
    var isAndroid = ua.indexOf("android") &gt; -1;
    var isIOS = ua.indexOf("iphone") &gt; -1 || ua.indexOf("ipad") &gt; -1;

    if (isIOS) {
      window.location.href = schemeUrl;
      setTimeout(() =&gt; {
        window.location.href = storeUrl;
      }, 2000);
    } else if (isAndroid) {
      window.location.href = schemeUrl;
      setTimeout(() =&gt; {
        window.location.href = storeUrl;
      }, 2000);
    } else {
      window.location.href = storeUrl;
    }
  }
&lt;/script&gt;

&lt;button onclick="openApp()"&gt;打开 APP&lt;/button&gt;</code></pre>
<h2>总结</h2>
<table>
<thead>
<tr>
<th>方式</th>
<th>适用平台</th>
<th>适用场景</th>
<th>适配难度</th>
</tr>
</thead>
<tbody>
<tr>
<td>URL Scheme</td>
<td>iOS/Android</td>
<td>适用于已知 APP</td>
<td>低</td>
</tr>
<tr>
<td>Universal Links / Deep Link</td>
<td>iOS/Android</td>
<td>更安全，适用于已安装 APP</td>
<td>高</td>
</tr>
<tr>
<td>Intent Scheme</td>
<td>Android</td>
<td>适用于 Android</td>
<td>中</td>
</tr>
<tr>
<td>iframe</td>
<td>部分浏览器</td>
<td>适用于尝试拉起 APP</td>
<td>低</td>
</tr>
<tr>
<td>综合方案</td>
<td>iOS/Android</td>
<td>适用于多种情况</td>
<td>中</td>
</tr>
</tbody>
</table>
<p>如果 APP 需要兼容性更好的跳转方式，建议结合 Universal Links（iOS）和 Deep Link（Android）。</p>]]></description>
    <pubDate>Sun, 02 Feb 2025 20:38:34 +0800</pubDate>
    <dc:creator>明熙</dc:creator>
    <guid>https://mingxi.icu/web/6.html</guid>
</item>
</channel>
</rss>