排查 SSH 客户端无法绑定转发端口的问题(Windows)

问题现象

在 Windows 10 中,使用 Bitvise SSH Client 配置本地代理转发端口(127.0.0.1:10800)时失败,报错:

Could not enable SOCKS/HTTP proxy forwarding on 127.0.0.1:10800: Address is already in use; bind() failed: Windows error 10013

排查步骤与命令记录

1. 检查是否有进程监听 10800(无结果)
# PowerShell 方式
Get-NetTCPConnection -LocalPort 10800 | Select-Object OwningProcess

# 或使用 netstat(CMD/PowerShell 均可)
netstat -ano | findstr :10800

→ 无输出,说明没有常规 TCP 监听。

2. 检查是否有隐藏的端口转发规则
netsh interface portproxy show all

→ 无输出,排除 portproxy 占用。

3. 使用 Process Explorer 深度扫描(GUI 工具)
  • 下载 Process Explorer
  • 以管理员身份运行 → Ctrl+F → 搜索 10800
    → 未发现任何句柄或线程绑定。
4. 关键一步:检查 Windows “排除端口范围”
netsh int ipv4 show excludedportrange protocol=tcp

输出示例(问题存在时):

开始端口    结束端口
----------    --------
     10745       10844   ← 10800 被包含在此区间!
     10845       10944
     ...

结论:端口 10800 被 Windows NAT 服务动态保留,普通程序无法绑定。


解决方案:重启 winnat 服务重置端口预留

适用于个人开发机,不影响日常使用。若使用 WSL2/Docker,操作后可能需重启相关服务。

# 以管理员身份运行 PowerShell

# 停止 Windows NAT Driver
net stop winnat

# 启动 Windows NAT Driver(会重建排除范围,通常大幅缩小)
net start winnat
验证是否生效:
netsh int ipv4 show excludedportrange protocol=tcp

正常输出(问题解决后):

开始端口    结束端口
----------    --------
        80          80
      5357        5357
     50000       50059     *

10745–11144 的“端口块” 范围已消失,这样所需的 10800 就可用了!


验证
  1. 重新连接 SSH,并启用本地代理转发到 127.0.0.1:10800
  2. 成功启动,无报错!

总结

问题根源Windows 自动将一些 端口段 加入“排除端口范围”
表象报“Address already in use”,但 netstat 查不到
核心命令netsh int ipv4 show excludedportrange protocol=tcp
快速修复net stop winnat && net start winnat

(Qwen3-Max 帮助整理文档与格式)

在 .NET MiniAPI (Kestrel) 中实现静态文件服务 + 可排序目录浏览功能

在开发轻量级 API 时,.NET MiniAPI 凭借其简洁高效的特性成为首选。但实际场景中,我们常需要暴露静态文件(如 API 文档、配置文件、共享资源),甚至允许客户端浏览目录下的文件列表。默认情况下,MiniAPI 的静态文件服务不支持目录浏览,且原生目录列表无排序功能,本文将详细记录如何实现「指定目录暴露 + 目录浏览 + 列表排序」的完整方案,附完整代码和优化细节。

一、需求背景

在 MiniAPI 中实现以下核心功能:

  1. 暴露服务器上的指定物理目录(如 GeoIP 文件夹),支持客户端通过 URL 访问文件;
  2. 启用目录浏览功能,允许客户端列出目录下的文件/子目录;
  3. 目录列表支持按「名称、大小、修改时间」排序(点击表头切换升序/降序);
  4. 优化目录列表样式,保持简洁易用,同时兼容 AOT 编译。

二、实现步骤(基于 .NET MiniAPI > 8.0)

1. 环境准备

创建 .NET MiniAPI 项目(若已有项目可跳过):

dotnet new web -n MiniApiStaticFileDemo
cd MiniApiStaticFileDemo

2. 核心配置:暴露指定物理目录 + 启用目录浏览

Program.cs 中配置静态文件中间件和目录浏览功能,核心是指定物理目录、URL 访问路径,并关联自定义排序格式化器。

2.1 Program.cs 完整配置

using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using System.Text;

var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();

// 关键配置:指定要暴露的物理目录
string baseDir = AppContext.BaseDirectory; // 程序运行目录
string exposeDir = Path.Combine(baseDir, "GeoIP"); // 要暴露的目录(如 GeoIP 文件夹)
string urlPrefix = "/geoip"; // 客户端访问前缀(通过 /geoip 访问该目录)

// 1. 配置静态文件服务:暴露指定物理目录
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(exposeDir), // 物理目录路径
    RequestPath = urlPrefix, // URL 访问路径(例:http://localhost:5000/geoip/文件名称)
    OnPrepareResponse = ctx =>
    {
        // 可选:添加响应头,禁止缓存静态文件
        ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store");
    }
});

// 2. 启用目录浏览(默认禁用),并关联自定义排序格式化器
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(exposeDir),
    RequestPath = urlPrefix, // 与静态文件访问路径保持一致
    Formatter = new SortableDirectoryFormatter("GeoIP 目录列表") // 自定义格式化器(支持排序)
});

// 测试接口
app.MapGet("/", () => Results.Ok("MiniAPI 静态文件服务 + 可排序目录浏览已启用"));

app.Run("http://*:5000");

3. 关键实现:自定义可排序目录格式化器

默认的目录浏览列表无排序功能,且样式简陋。我们通过实现 IDirectoryFormatter 接口,自定义目录列表的 HTML 输出,添加「名称、大小、修改时间」排序功能,同时优化样式和安全性。

3.1 完整 SortableDirectoryFormatter 类

创建 SortableDirectoryFormatter.cs 文件,核心功能包括:

  • 过滤隐藏文件(以 . 开头的文件);
  • 目录在前、文件在后的默认排序;
  • 点击表头切换排序方向(升序/降序);
  • 支持按名称、文件大小、修改时间排序;
  • XSS 防护、跨平台路径处理。
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using System.Globalization;
using System.Text;

public class SortableDirectoryFormatter : IDirectoryFormatter
{
    private readonly string _pageTitle;
    // 时间格式:年-月-日 时:分:秒(UTC 时区)
    private const string CustomDateTimeFormat = "yyyy-MM-dd HH:mm:ss +00:00";

    // 构造函数:支持自定义页面标题
    public SortableDirectoryFormatter(string pageTitle = "目录列表")
    {
        _pageTitle = pageTitle ?? "目录列表";
    }

    public async Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents)
    {
        // 1. 过滤隐藏文件 + 初始排序(目录在前,文件在后)
        var safeContents = contents ?? Enumerable.Empty<IFileInfo>();
        var fileList = new List<IFileInfo>(safeContents.Where(f =>
            !string.IsNullOrEmpty(f.Name) && !f.Name.StartsWith(".")
        ));
        fileList.Sort((a, b) =>
        {
            if (a.IsDirectory != b.IsDirectory)
                return a.IsDirectory ? -1 : 1;
            return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
        });

        // 2. 生成面包屑导航(支持多层目录跳转)
        var breadcrumbBuilder = new StringBuilder();
        string currentPath = "/";
        breadcrumbBuilder.Append($"<a href=\"{HtmlEncode(currentPath)}\">/</a>");
        var requestPath = context.Request.Path.Value;
        if (!string.IsNullOrEmpty(requestPath))
        {
            var pathSegments = requestPath.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
            foreach (var segment in pathSegments)
            {
                currentPath = Path.Combine(currentPath, segment + "/").Replace(Path.DirectorySeparatorChar, '/');
                breadcrumbBuilder.Append($"<a href=\"{HtmlEncode(currentPath)}\">{HtmlEncode(segment)}/</a>");
            }
        }

        // 3. 构建 HTML 页面(含样式 + 排序脚本)
        var htmlBuilder = new StringBuilder(4096);
        htmlBuilder.AppendLine("<!DOCTYPE html>");
        htmlBuilder.AppendLine("<html lang=\"zh-CN\">");
        htmlBuilder.AppendLine("<head>");
        htmlBuilder.AppendLine($"<title>{_pageTitle} - {HtmlEncode(requestPath ?? "/")}</title>");
        // 样式:保持简洁,适配不同设备
        htmlBuilder.AppendLine(@"<style>
            body { font-family: ""Segoe UI"", ""Microsoft YaHei"", sans-serif; font-size: 14px; max-width: 1200px; margin: 0 auto; padding: 20px; }
            header h1 { font-size: 24px; font-weight: 400; margin: 0 0 20px 0; color: #333; }
            #index { width: 100%; border-collapse: separate; border-spacing: 0; border: 1px solid #eee; }
            #index th { background: #f8f9fa; padding: 10px; text-align: center; cursor: pointer; user-select: none; position: relative; border-bottom: 2px solid #ddd; }
            #index td { padding: 8px 10px; border-bottom: 1px solid #eee; }
            #index th:hover { background: #f1f3f5; }
            #index td.length, td.modified { text-align: right; }
            a { color: #127aac; text-decoration: none; }
            a:hover { color: #13709e; text-decoration: underline; }
            .sort-arrow { position: absolute; right: 8px; font-size: 0.8em; color: #999; }
            .dir-name { font-weight: 500; }
        </style>");
        htmlBuilder.AppendLine("</head>");
        htmlBuilder.AppendLine("<body>");
        htmlBuilder.AppendLine($"<header><h1>{_pageTitle}:{breadcrumbBuilder}</h1></header>");
        htmlBuilder.AppendLine("<table id=\"index\">");
        htmlBuilder.AppendLine("<thead><tr>");
        htmlBuilder.AppendLine("<th data-col=\"name\">名称 <span class=\"sort-arrow\">↑</span></th>");
        htmlBuilder.AppendLine("<th data-col=\"size\">大小 <span class=\"sort-arrow\"></span></th>");
        htmlBuilder.AppendLine("<th data-col=\"modified\">最后修改时间 <span class=\"sort-arrow\"></span></th>");
        htmlBuilder.AppendLine("</tr></thead><tbody>");

        // 4. 生成文件/目录列表行
        if (fileList.Count == 0)
        {
            htmlBuilder.AppendLine("<tr><td colspan=\"3\" style=\"text-align:center; padding:20px; color:#666;\">目录无可用文件</td></tr>");
        }
        else
        {
            foreach (var file in fileList)
            {
                var fileName = file.IsDirectory ? $"{file.Name}/" : file.Name;
                var fileClass = file.IsDirectory ? "dir-name" : "";
                var fileSize = file.IsDirectory ? "-" : FormatFileSizeWithComma(file.Length);
                var fileModified = file.LastModified.ToUniversalTime().ToString(CustomDateTimeFormat, CultureInfo.InvariantCulture);
                var encodedFileName = Uri.EscapeDataString(file.Name);
                var fileUrl = $"{context.Request.Path}/{encodedFileName}".Replace("//", "/");

                htmlBuilder.AppendLine("<tr>");
                htmlBuilder.AppendLine($"<td class=\"name {fileClass}\"><a href=\"{HtmlEncode(fileUrl)}\">{HtmlEncode(fileName)}</a></td>");
                htmlBuilder.AppendLine($"<td class=\"length\">{HtmlEncode(fileSize)}</td>");
                htmlBuilder.AppendLine($"<td class=\"modified\">{HtmlEncode(fileModified)}</td>");
                htmlBuilder.AppendLine("</tr>");
            }
        }

        htmlBuilder.AppendLine("</tbody></table>");
        // 5. 排序核心脚本(点击表头切换排序)
        htmlBuilder.AppendLine(@"<script>
            document.addEventListener('DOMContentLoaded', function() {
                const table = document.getElementById('index');
                if (!table) return;
                const headers = table.querySelectorAll('thead th[data-col]');
                const tbody = table.querySelector('tbody');
                let currentSort = { col: 'name', order: 'asc' };

                // 表头点击事件
                headers.forEach(th => {
                    th.addEventListener('click', function() {
                        const newCol = this.dataset.col;
                        currentSort.order = (currentSort.col === newCol) ? (currentSort.order === 'asc' ? 'desc' : 'asc') : 'asc';
                        currentSort.col = newCol;
                        updateSortArrows();
                        sortTable();
                    });
                });

                // 更新排序箭头
                function updateSortArrows() {
                    headers.forEach(th => {
                        const arrow = th.querySelector('.sort-arrow');
                        arrow.textContent = (th.dataset.col === currentSort.col) ? (currentSort.order === 'asc' ? '↑' : '↓') : '';
                    });
                }

                // 排序逻辑
                function sortTable() {
                    const rows = Array.from(tbody.querySelectorAll('tr'));
                    if (!rows.length) return;

                    rows.sort((a, b) => {
                        const cellA = a.querySelector(`td.${getCellClass(currentSort.col)}`).textContent.trim();
                        const cellB = b.querySelector(`td.${getCellClass(currentSort.col)}`).textContent.trim();

                        switch (currentSort.col) {
                            case 'name':
                                const isDirA = cellA.endsWith('/');
                                const isDirB = cellB.endsWith('/');
                                if (isDirA !== isDirB) return isDirA ? -1 : 1;
                                return currentSort.order === 'asc' ? cellA.localeCompare(cellB, 'zh-CN') : cellB.localeCompare(cellA, 'zh-CN');
                            case 'size':
                                if (cellA === '-') return -1;
                                if (cellB === '-') return 1;
                                const sizeA = BigInt(cellA.replace(/,/g, ''));
                                const sizeB = BigInt(cellB.replace(/,/g, ''));
                                return currentSort.order === 'asc' ? (sizeA < sizeB ? -1 : 1) : (sizeA > sizeB ? -1 : 1);
                            case 'modified':
                                const dateA = new Date(cellA.replace(' +00:00', 'Z')).getTime();
                                const dateB = new Date(cellB.replace(' +00:00', 'Z')).getTime();
                                return currentSort.order === 'asc' ? (dateA - dateB) : (dateB - dateA);
                            default: return 0;
                        }
                    });

                    tbody.innerHTML = '';
                    rows.forEach(row => tbody.appendChild(row));
                }

                function getCellClass(colName) {
                    return colName === 'name' ? 'name' : colName === 'size' ? 'length' : 'modified';
                }

                updateSortArrows();
            });
        </script>");
        htmlBuilder.AppendLine("</body></html>");

        // 输出响应
        context.Response.ContentType = "text/html; charset=utf-8";
        await context.Response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
    }

    // 辅助方法:文件大小格式化(带千分位)
    private static string FormatFileSizeWithComma(long bytes)
    {
        return bytes.ToString("N0", CultureInfo.InvariantCulture);
    }

    // 辅助方法:HTML 编码(防 XSS)
    private static string HtmlEncode(string value)
    {
        if (string.IsNullOrEmpty(value)) return string.Empty;
        return value.Replace("&", "&amp;")
                    .Replace("<", "&lt;")
                    .Replace(">", "&gt;")
                    .Replace("\"", "&quot;")
                    .Replace("'", "&#39;")
                    .Replace("`", "&#96;");
    }
}

4. 核心功能说明

4.1 静态文件服务配置

  • PhysicalFileProvider(exposeDir):指定要暴露的物理目录(如 GeoIP 文件夹,位于程序运行目录下);
  • RequestPath = "/geoip":客户端通过 http://localhost:5000/geoip/文件名 访问文件;
  • OnPrepareResponse:可选配置,添加缓存控制头,避免浏览器缓存静态文件。

4.2 目录浏览功能

  • UseDirectoryBrowser:启用目录浏览(默认禁用),需与静态文件服务的 FileProviderRequestPath 保持一致;
  • SortableDirectoryFormatter:自定义目录列表格式化器,替代默认的简陋列表,支持排序和样式优化。

4.3 排序功能实现

  • 前端:通过 JavaScript 监听表头点击事件,切换排序字段(名称/大小/修改时间)和排序方向(升序/降序);
  • 排序逻辑:
    • 名称排序:目录优先,文件在后,按字母/中文拼音排序;
    • 大小排序:目录显示 -,文件按数值大小排序(去除千分位逗号);
    • 时间排序:将 UTC 时间转换为时间戳排序,兼容 2025-10-18 08:16:06 +00:00 格式。

三、关键优化点(兼容生产环境)

1. 安全性优化

  • XSS 防护:通过 HtmlEncode 方法对文件名、路径等用户可控内容进行编码,避免脚本注入;
  • 隐藏文件过滤:默认过滤以 . 开头的文件(如 .gitignore.env),防止敏感文件泄露;
  • 可选:添加身份认证(如 API Key、JWT),限制目录浏览权限(例:RequireAuthorization())。

2. 兼容性优化

  • 跨平台支持:使用 Path.Combine 和路径分隔符替换,兼容 Windows/Linux/macOS;
  • AOT 编译兼容:避免反射和动态代码,支持 .NET 8+ AOT 编译(可直接发布为原生可执行文件);
  • 空值安全:处理 contentsnull、文件名为空等异常场景,避免程序崩溃。

3. 体验优化

  • 面包屑导航:支持多层目录跳转(如 /geoip/GeoLite2/),方便用户回溯;
  • 时间格式标准化:统一使用 yyyy-MM-dd HH:mm:ss +00:00 格式,避免时区混淆;
  • 响应头优化:指定 charset=utf-8,确保中文文件名正常显示。

四、测试效果

  1. 运行项目:dotnet run
  2. 在程序运行目录下创建 GeoIP 文件夹,放入测试文件/子目录;
  3. 访问 http://localhost:5000/geoip,即可看到目录列表:
    1. 默认按名称升序排列,目录在前,文件在后;
    2. 点击表头「名称」「大小」「最后修改时间」可切换排序方式;
    3. 点击文件名/目录名可直接访问(目录会进入下一级列表)。

五、扩展场景

  1. 静态资源托管:用于托管 API 文档(如 Swagger、Redoc)、前端静态文件(Vue/React 构建产物);
  2. 内部文件共享:团队内部临时共享文件(结合身份认证,避免公开访问);
  3. 日志文件查看:暴露日志目录,支持按修改时间排序查看最新日志文件。

六、总结

通过 .NET MiniAPI 的静态文件中间件 + 自定义目录格式化器,我们仅需少量代码就实现了「指定目录暴露 + 可排序目录浏览」功能。该方案简洁高效,兼容生产环境,适用于轻量级文件服务场景。如需进一步增强,可扩展权限控制、文件上传、下载限速等功能。

下载 Demo : MiniApiStaticFileDemo.zip

如果有任何问题或优化建议,欢迎在评论区交流~

根域名(Root Zone)与 顶级域名(Top-Level Domain, TLD)的 DNS 解析

顶级域名

例如:.nl顶级域名(Top-Level Domain, 简称 TLD)。


✅ 详细解释:

🌐 什么是顶级域名(TLD)?

顶级域名是域名系统(DNS)中最高层级的域名之一,位于根域之下。它通常表示一个国家、地区或类别。

例如:

  • .com.org.net → 通用顶级域名(gTLD)
  • .cn(中国)、.uk(英国)、.de(德国)→ 国家代码顶级域名(ccTLD)
  • .nl(荷兰)→ 也是 国家代码顶级域名(country code Top-Level Domain, ccTLD)

.nl 中的 nl 是“Netherlands”(荷兰)的国家代码。


❌ 为什么不是“根域名”?

  • 根域名(Root Zone)指的是 DNS 层级中的最顶层,用一个空标签或点表示(即 .),比如 example.com. 后面的那个点。
  • 根域名本身不直接供公众注册使用,它管理所有顶级域名(如 .com, .org, .nl 等)。
  • 所有 TLD 都隶属于根域名之下。

📚 域名层级示例:www.example.nl

.        ← 根域名(Root)
└── nl   ← 顶级域名(TLD,国家代码)
    └── example  ← 二级域名(由 .nl 注册局管理)
        └── www  ← 主机名(子域名)

✅ 总结

名称类型说明
.nl顶级域名(TLD)荷兰的国家代码顶级域名(ccTLD)
.根域名DNS 最顶层,不可注册

👉 所以答案是:.nl顶级域名,不是根域名。


反过来理解

域名从解析层面来讲是反过来的:
. 根域名(Root)) → nl 顶级域名(TLD,国家代码) → example 二级域名(由 .nl 注册局管理) → www 主机名(子域名)

这个点 “ . ”非常重要,它就是根(Root)

是的,非常正确!👏

DNS 解析的查询过程 来讲,域名确实是 “反过来”逐级查找的,即从根开始,向下一级一级递归解析。你描述的方向完全符合 DNS 的实际工作原理。


✅ 正确的 DNS 解析顺序(由根向叶子):

以访问 www.example.nl 为例,DNS 解析流程如下:

Step 1: 询问根域名服务器(Root Servers)
        "谁管理 .nl 域?"
        ↓
Step 2: 根服务器回复:
        "去找负责 .nl 的顶级域名服务器(TLD nameservers)"
        ↓
Step 3: 询问 .nl 的 TLD 域名服务器
        "谁管理 example.nl?"
        ↓
Step 4: .nl 注册局的服务器回复:
        "去找 example.nl 的权威域名服务器(Authoritative Nameservers)"
        ↓
Step 5: 询问 example.nl 的权威服务器
        "www.example.nl 的 IP 是什么?"
        ↓
Step 6: 权威服务器返回:
        "www.example.nl 的 IP 是 93.184.216.34"

🌐 所以从逻辑结构上看:

虽然我们写域名是:
👉 www.example.nl (从左到右)

但在 DNS 层级和解析路径中,它是:
👉 . → .nl → example.nl → www.example.nl
也就是从 右到左从根到叶 的树状结构。

.
└── nl                          ← TLD(顶级域名),由荷兰 SIDN 管理
    └── example                 ← 二级域名(注册者购买)
        └── www                 ← 子域名 / 主机名

🔁 因此:书写顺序是从左到右,但解析顺序是从右到左,自顶向下。


🧠 类比理解

就像文件路径:

  • Windows: C:\Users\Alice\Documents\file.txt
  • Linux: /home/alice/docs/file.txt

路径是从根 / 开始,逐级进入子目录。同样地,DNS 域名也是从根 . 开始,逐级进入更具体的域。


✅ 总结

说法是否正确说明
.nl 是顶级域名荷兰的国家代码顶级域名(ccTLD)
根域名是 .DNS 层级的最顶层
域名解析是从根开始,逐级向下即:..nlexample.nlwww.example.nl
写作顺序与解析路径相反我们从左写到右,但系统从右查到左

✅ 所以你的理解完全正确:

DNS 解析是从根域名开始,一步步“向下”查找,直到找到最终主机名的 IP 地址。

这正是互联网域名系统设计的核心机制 —— 分层、分布式、可扩展。

Windows 10 简体中文补充字体 无法添加 0x80070490

最近系统持续多次出现,通知中心出现一个图标带“字”的通知,提示安装失败的通知。点击会打开设置,也不知道要作甚吗。一番查阅后才解决:

简体中文补充字体 (Chinese (Simplified) Supplemental Fonts) 安装失败的问题,验证,在 设置→系统→可选组件 中找不到简体中文补充字体。

  1. 先以管理员模式打开CMD命令行,进入 Powershell : powershell.
  2. 获取组件名称: Get-WindowsCapability -online -name *fonts* | ft Name, DisplayName, Description 可以得知名称为 Language.Fonts.Hans~~~und-HANS~0.0.1.0
  3. 再确认下该组件的状态: Get-WindowsCapability -online -name Language.Fonts.Hans~~~und-HANS~0.0.1.0 可得知状态是 Superseded (State : Superseded)
  4. 删除该组件: Remove-WindowsCapability -Online -Name Language.Fonts.Hans~~~und-HANS~0.0.1.0
  5. 重新安装该组件Add-WindowsCapability -Online -Name Language.Fonts.Hans~~~und-HANS~0.0.1.0

看到 State : Installed (已安装)

在 设置 → 系统→ 可选组件 中,可看到 “简体中文补充字体” 已添加。

重启系统完成。

设置XFCE4 桌面不自启动(默认进入命令行模式)

在 Debian 系统下,让 XFCE4 桌面不自启动(默认进入命令行模式),但需要时仍能手动启动它,可以通过以下方法实现:


方法 1:修改默认运行级别(推荐)

修改 systemd 目标(target),使系统默认启动到 多用户命令行模式(multi-user.target,需要时再手动启动 graphical.target(图形界面)。

步骤:

  1. 禁用图形界面自启动
   sudo systemctl set-default multi-user.target

(这会修改 /etc/systemd/system/default.target 链接)

  1. 需要时手动启动 XFCE4 桌面
   sudo systemctl start graphical.target

(或直接启动 lightdm 显示管理器)

   sudo systemctl start lightdm
  1. 恢复默认图形界面启动(可选):
   sudo systemctl set-default graphical.target

方法 2:禁用 LightDM 自动登录

如果系统使用 LightDM 作为显示管理器,可以修改其配置,使其不自动登录 XFCE4。

步骤:

  1. 编辑 /etc/lightdm/lightdm.conf
   sudo nano /etc/lightdm/lightdm.conf

修改以下内容:

   [Seat:*]
   autologin-user=  # 留空
   autologin-user-timeout=0
  1. 重启 LightDM
   sudo systemctl restart lightdm

(系统启动后会停留在登录界面,手动选择 XFCE4 登录)


方法 3:修改 GRUB 启动参数

在 GRUB 引导时手动选择是否进入图形界面。

步骤:

  1. 编辑 /etc/default/grub
   sudo nano /etc/default/grub

修改 GRUB_CMDLINE_LINUX_DEFAULT 行:

   GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"

改为:

   GRUB_CMDLINE_LINUX_DEFAULT="quiet splash text"  # 默认进入文本模式

或:

   GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"  # 默认进入图形界面
  1. 更新 GRUB 配置
   sudo update-grub
  1. 重启后,在 GRUB 菜单选择 Advanced options,手动选择 multi-user.targetgraphical.target

方法 4:直接禁用 XFCE4 会话

如果只想临时关闭 XFCE4,但不影响系统默认行为,可以:

pkill xfce4-session  # 直接关闭 XFCE4

或:

systemctl isolate multi-user.target  # 切换到命令行模式

总结

方法适用场景优点缺点
systemctl set-default multi-user.target长期禁用图形界面最干净,不影响系统服务需要手动 start graphical.target
修改 LightDM 配置防止自动登录可保留图形界面仍需选择 XFCE4 登录
修改 GRUB 参数临时切换灵活需重启生效
pkill xfce4-session临时关闭立即生效重启后恢复

推荐使用 方法 1(修改默认运行级别),既保证系统启动高效,又能在需要时手动启动 XFCE4。

Linux安装微软命令行文本编辑器-Microsoft Edit

微软干了一件好事!2025 年 5 月 18 日,Microsoft 发布了用 Rust 编程语言编写的编辑器的开源重建版本,简称为 Edit,适用于现代版本的 Windows。版本号未继续,重置从1.0.0开始,目前已经是1.2.0

Microsoft Edit 是 MS-DOS 的怀旧风格, 以widnows Dos 的操作习惯,解决了Linux shell 中编辑文本痛苦的大问题。

目前Microsoft Edit已支持包括FreeBSD在内的多种Unix-like系统,但Debian官方仓库尚未收录该软件包。只能通过下载安装文件的方式安装,安装过程中若遇到网络问题,可能需要配置合适的软件源镜像。

Linux 下安装脚本(Ubuntu、Debian 和 Linux Mint):

# 安装 Zstandard 
apt install zstd

# 下载软件包
wget https://github.com/microsoft/edit/releases/download/v1.2.0/edit-1.2.0-x86_64-linux-gnu.tar.zst

# 解压缩到用户的当前目录
tar xvf edit-1.2.0-x86_64-linux-gnu.tar.zst

# 将其移动到 bin 目录中以便随时访问,请相应调整路径
mv edit /usr/local/bin/edit

# 查看版本号
edit -v

# 启动编辑器
edit

终端输入edit命令即可启动编辑器

edit --version 查看版本

Windows 下 使用 winget 安装

winget install Microsoft.Edit

常用快捷键

New File:Ctrl+N
Open File:Ctrl+0
Save:Ctrl+s
Close Editor:Ctrl+W
Exit:Ctrl+Q
Undo:Ctrl+Z
Redo:Ctrl+Y
Cut:Ctrl+x
Copy:Ctrl+c
Paste:Ctrl+V
Find:Ctrl+F
Replace:Ctrl+R

Debian 系统上安装 rqlited

在 Debian 系统上安装 rqlited 可通过以下步骤完成:

  1. 下载预编译二进制文件
    rqlite 官方提供 Linux 平台的预编译二进制文件,可直接下载运行:

wget https://github.com/rqlite/rqlite/releases/download/v7.0.0/rqlited-v7.0.0-linux-amd64.tar.gz
tar -xzvf rqlited-v7.0.0-linux-amd64.tar.gz
cd rqlited-v7.0.0-linux-amd64

  1. 运行 rqlited
    单节点模式:
    ./rqlited -node-id 1 ~/rqlite/data
    集群模式(需指定 Leader 节点):

Leader 节点

./rqlited -node-id 1 -http-addr 192.168.1.100:4001 ~/rqlite/leader_data

Follower 节点(加入集群)

./rqlited -node-id 2 -http-addr 192.168.1.101:4001 -join http://192.168.1.100:4001 ~/rqlite/follower_data

  1. 验证安装
    通过 HTTP API 检查服务状态:

curl http://localhost:4001/status

  1. 可选配置
    数据目录权限:确保运行用户对数据目录有读写权限。
    系统服务化:通过 systemd 管理服务(示例配置):

sudo nano /etc/systemd/system/rqlited.service
内容参考:

[Unit]
Description=rqlited
After=network.target

[Service]
ExecStart=/path/to/rqlited -node-id 1 /path/to/data
User=rqlite
Restart=always

[Install]
WantedBy=multi-user.target


启用服务:

sudo systemctl enable --now rqlited

  1. 其他安装方式
    Docker 部署:
    docker run -p 4001:4001 rqlite/rqlited
    源码编译(需 Go 环境):
    git clone https://github.com/rqlite/rqlite
    cd rqlite
    make
    注意事项
    确保防火墙开放 4001(HTTP API)和 4002(Raft 通信)端口。
    生产环境建议配置 TLS 加密和认证。
    如需更详细的集群配置或性能优化,可参考官方文档。

附带参考设置示例脚本,通过变量定义数据存储目录,并根据 -node-id 自动生成目录路径:

deploy_rqlite_cluster.sh


#!/bin/bash
# 配置变量(根据实际环境修改)
BASE_DATA_DIR="/opt/rqlite"          # 数据存储基础目录
LEADER_IP="192.168.1.100"            # 主节点IP
FOLLOWER1_IP="192.168.1.101"         # 子节点1 IP
FOLLOWER2_IP="192.168.1.102"         # 子节点2 IP
READONLY_IP="192.168.1.103"          # 只读节点IP

# 创建数据目录(所有节点)
mkdir -p $BASE_DATA_DIR/{data1,data2,data3,data4}

# 主节点(Leader)
ssh $LEADER_IP "nohup ./rqlited -node-id 1 \\
  -http-addr $LEADER_IP:4001 \\
  -raft-addr $LEADER_IP:4002 \\
  $BASE_DATA_DIR/data1 > $BASE_DATA_DIR/rqlite.log 2>&1 &"

# 子节点1(Follower)
ssh $FOLLOWER1_IP "nohup ./rqlited -node-id 2 \\
  -http-addr $FOLLOWER1_IP:4001 \\
  -raft-addr $FOLLOWER1_IP:4002 \\
  -join http://$LEADER_IP:4001 \\
  $BASE_DATA_DIR/data2 > $BASE_DATA_DIR/rqlite.log 2>&1 &"

# 子节点2(Follower)
ssh $FOLLOWER2_IP "nohup ./rqlited -node-id 3 \\
  -http-addr $FOLLOWER2_IP:4001 \\
  -raft-addr $FOLLOWER2_IP:4002 \\
  -join http://$LEADER_IP:4001 \\
  $BASE_DATA_DIR/data3 > $BASE_DATA_DIR/rqlite.log 2>&1 &"

# 只读节点(Non-Voter)
ssh $READONLY_IP "nohup ./rqlited -node-id 4 \\
  -http-addr $READONLY_IP:4001 \\
  -raft-addr $READONLY_IP:4002 \\
  -non-voter \\
  -join http://$LEADER_IP:4001 \\
  $BASE_DATA_DIR/data4 > $BASE_DATA_DIR/rqlite.log 2>&1 &"

echo "集群部署完成,检查状态:"
echo "curl $LEADER_IP:4001/status?pretty"

脚本说明:

1. 通过BASE_DATA_DIR变量集中管理存储路径

2. 自动按node-id生成data1~data4子目录

3. 日志统一输出到基础目录下

4. 需提前确保各节点已安装rqlited

解决 wsl 重启后 /etc/resolv.conf 中的DNS丢失

今天在wsl中不知道操作了什么,重启后apt更新报错,发现resolv.conf的DNS变成了“127.0.0.1”,然后用 echo 写入 DNS,重启后又是没有了。

按微软的文档,设置修改 /etc/wsl.conf

[network]
generateResolvConf = false

然后写入 DNS 到 resolv.conf

echo "nameserver 192.168.1.1" > /etc/resolv.conf
echo "nameserver 114.114.115.115" > /etc/resolv.conf

重启后,一样,添加的nameserver没有了。

在windows中,直接编辑 \wsl.localhost\Debian\etc\resolv.conf

提示“系统无法辨识文件名”,无法直接修改。

看了服务,猜测是不是某些服务控制修改的,如:resolvconf,rdnssd,systemd-resolved,先关闭启动:

systemctl disable --now resolvconf.service rdnssd.service systemd-resolved.service

再次 echo DNS 到 resolv.conf, 重启后,还是不行。。。

无奈,直接删除了resolv.conf: rm /etc/resolv.conf

再生成写入:

echo "nameserver 192.168.1.1" > /etc/resolv.conf
echo "nameserver 114.114.115.115" > /etc/resolv.conf

然后再设置只读:sudo chattr -f +i /etc/resolv.conf

再次重启,DNS没有丢失。

如果要在windows中直接修改,设置为属性可写:sudo chattr -i /etc/resolv.conf

不知道为什么必须要先删除,才再建resolv.conf,才可以修改成功。。。

【end】

解决WSL因盘符变更,系统找不到指定的路径

新加了一块硬盘(F), 将WSL存放的老盘(L)数据拷入新硬盘(F)

之后因盘符变更, 启动wsl时提示如下:

无法将磁盘“L:\WSL\Debian\ext4.vhdx”附加到 WSL2: 系统找不到指定的路径。
错误代码: Wsl/Service/CreateInstance/MountVhd/HCS/ERROR_PATH_NOT_FOUND

[已退出进程,代码为 4294967295 (0xffffffff)]

问题是去哪里修改? 虚拟机调用镜像的设置保存在哪里?

找了篇微软文档参考: 如何查找 Linux 发行版的 .vhdx 文件和磁盘路径 https://learn.microsoft.com/zh-cn/windows/wsl/disk-space#how-to-locate-the-vhdx-file-and-disk-path-for-your-linux-distribution

.vhdx 文件和磁盘路径, 存储在注册表的以下地址中:

HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss

Lxss 下 类似 { guid } 的条目就是子系统条目, 有几个子系统会有几条。(我的有3个子系统 )

条目下数值项 “BasePath”,就是文件地址目录,直接修改盘符路径,重启wsl就可以了!

其他问题

如果修改注册表,wsl启动后,wsl提示:

<3>WSL (xxx) ERROR: UtilTranslatePathList:2852: Failed to translate” (之后跟着一个windows文件路径)

如我的提示:

<3>WSL (461) ERROR: UtilTranslatePathList:2852: Failed to translate L:\PHP\php-8.1.23-nts-Win32-vs16-x64

说明后面这个路径,是被设置在系统环境变量中,还是有由于盘符变动造成的问题。把系统环境变量的路径值修改了,就好了!

【end】

对sqlite已有数据,添加多列的唯一性约束

以一个数据表为例, 创建sql如下:

CREATE TABLE http_table (
    id       INTEGER PRIMARY KEY AUTOINCREMENT,
    Host     TEXT    DEFAULT "",
    IP       TEXT    DEFAULT "",
    Port     INTEGER DEFAULT (0),
    Info     TEXT    DEFAULT "",
    Uptime   TEXT    DEFAULT (datetime('now', 'localtime') ) 
);

当前该表已有不少数据存在,只有id为自增长主键,未创建约束。

之前创建数据过程,先查询(1),不存在新插入一条(2),存在更新该条记录(3):

(1)先查询

SELECT id FROM http_table WHERE Host = 'dns9.quad9.net' AND IP = '9.9.9.9' AND Port = '80' order by Uptime desc LIMIT 1;

(这里,可能是多条,只返回一条)

(2)不存在新插入一条

INSERT INTO http_table (Host, IP, Port, Info) VALUES ('dns9.quad9.net', '9.9.9.9', '80', 'new_info');

(3)存在更新该条记录

UPDATE http_table SET Info = 'new_info' WHERE Host = 'dns9.quad9.net' AND IP = '9.9.9.9' AND Port = '80';

本身这个更新并不严谨,没有指定唯一id条件,可能会更新多条,但这不是要解决的问题。

问题在于在大批量的数据插入时,每次都要查询,再判断是插入还更新,速度很慢,现需要一条sql一次完成查询,并完成插入或更新。

尝试修改:在 SQLite 中,可以使用 INSERT INTO...ON CONFLICT 子句配合 DO UPDATE 动作,实现根据特定条件存在则更新记录,不存在则插入新记录的功能。

INSERT INTO http_table (Host, IP, Port, Info)
VALUES ('dns9.quad9.net', '9.9.9.9', '80', 'new_info')
ON CONFLICT(Host, IP, Port) DO UPDATE SET
    Info=excluded.Info;
解释:

INSERT INTO http_table (Host, IP, Port, Info) VALUES (...) 尝试插入一条新记录。


ON CONFLICT(Host, IP, Port) 指定当 Host, IP, Port 这三个字段的组合发生冲突时(即已存在这样的记录时)应采取的动作。


DO UPDATE SET Info=excluded.Info 指定当冲突发生时,更新已有记录的 Info 字段。excluded 是一个特殊的表别名,它引用尝试插入但因冲突而未能插入的值。


这样,如果记录已存在,Info 字段将被更新为新的值;如果记录不存在,则将插入新的记录

首先,你需要确保你的表有一个唯一约束(UNIQUE CONSTRAINT)来检测冲突。在这个例子中,需要设定 HostIP, 和 Port 的组合是唯一的,UNIQUE(Host, IP, Port)否则会报错。

CREATE TABLE http_table(
    Host TEXT,
    IP TEXT,
    Port TEXT,
    Info TEXT,
    UNIQUE(Host, IP, Port)
);

由于之前未添加唯一约束,执行INSERT INTO http_table ... ON CONFLICT(Host, IP, Port) DO UPDATE ... 会如下报错:

执行 SQL 查询时发生错误:ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint

解决方法建立约束前,清洗现有数据

在建立唯一性约束之前,如果现有数据中存在违反该约束的重复记录,你需要进行数据清洗。数据清洗的过程通常包括识别重复记录、决定如何处理这些记录(如删除、合并或修改),以及执行相应的操作。以下是一些步骤和建议,帮助你清洗现有数据以便能够添加唯一性约束:

识别重复记录

使用SQL查询来找出在HostIPPort列上有重复组合的记录。可以使用GROUP BYHAVING子句来识别这些重复项。以下查询将找出所有重复的HostIPPort组合以及它们出现的次数:

SELECT Host, IP, Port, COUNT(*) as count FROM http_table GROUP BY Host, IP, Port HAVING COUNT(*) > 1;

决定如何处理重复记录

  • 删除重复项:如果重复记录没有保留价值,你可以选择删除它们。确保在删除之前备份数据,以防万一。
  • 合并记录:如果重复记录包含有用的信息,你可能想要合并它们。这通常涉及更新一条记录以包含所有相关信息,并删除其他重复记录。
  • 修改记录:在某些情况下,你可能能够通过修改重复记录中的一个或多个字段来消除重复。

执行数据清洗操作

根据你的决定,编写SQL语句来执行删除、合并或修改操作。

如果你决定删除重复的记录,并且只保留每个组合的第一条记录,你可以使用以下查询(请注意,这个查询假设你有一个唯一标识每条记录的id字段):

DELETE FROM http_table WHERE id NOT IN ( SELECT MIN(id) FROM http_table GROUP BY Host, IP, Port );

这个查询会删除每个HostIPPort组合中除id最小的一条记录之外的所有记录。

验证数据清洗结果

在执行数据清洗操作后,再次运行识别重复记录的查询,确保所有重复项都已被处理。

检查数据以确保没有其他问题,如数据丢失或不一致。

添加唯一性约束

确认数据已经清洗干净,没有违反唯一性约束的重复记录,可以安全地添加唯一性约束了。使用ALTER TABLE语句来添加约束:

ALTER TABLE http_table 
ADD CONSTRAINT unique_host_ip_port UNIQUE(Host, IP, Port);

这时又发生错误!

执行 SQL 查询时发生错误:near "CONSTRAINT": syntax error

在SQLite中,ALTER TABLE 语句用于修改表的结构,比如添加列、重命名表、添加约束等。但是,SQLite 在使用 ALTER TABLE 添加约束时有一些限制,特别是关于唯一性约束(UNIQUE CONSTRAINT)。

在SQLite中,如果你想要给一个已经存在的表添加一个新的唯一性约束,并且这个约束涉及到多个列,你不能直接在 ALTER TABLE 语句中使用 ADD CONSTRAINT 语法。SQLite不支持这种直接添加多列唯一性约束的方式。

相反,你需要采取一种间接的方法来实现这一点。以下是一种可能的解决方案:

  1. 创建一个新表:这个新表应该包含与原始表相同的列,并且包含你想要添加的唯一性约束。
  2. 复制数据:将原始表中的数据复制到新表中,同时确保没有违反唯一性约束的数据被插入。
  3. 删除原始表(可选):如果数据复制成功,并且你确定新表包含了所有需要的数据,你可以删除原始表。
  4. 重命名新表:将新表重命名为原始表的名称。

务必注意:备份数据

在进行任何数据修改或结构更改之前,始终确保你有最新的数据备份。这是防止数据丢失或损坏的重要步骤。

请记住,数据清洗是一个可能对数据产生重大影响的操作。在执行任何删除或修改操作之前,务必仔细考虑并备份你的数据。

最终执行的SQL语句是:

--修改原更新语句,使用 INSERT INTO ... ON CONFLICT ... DO UPDATE 方式,一次查询执行插入或删除。

INSERT INTO http_table (Host, IP, Port, Title, Url, Info, Location, Method, Code, Tag, Server) 
VALUES ('Host', 'IP', 'Port', 'Title', 'Url', 'Info', 'Location', 'Method', 'Code', 'Tag', 'Server') 
ON CONFLICT (Host, IP, Port) DO UPDATE SET Info = excluded.Info;

-- 添加唯一约束,操作步骤:

-- 首先备份原始的 .db 文件

-- 识别重复记录
SELECT Host, IP, Port, COUNT( * ) AS count
  FROM http_table
 GROUP BY Host, IP, Port
HAVING COUNT( * ) > 1;

-- 删除重复记录
DELETE FROM http_table
      WHERE id NOT IN (
    SELECT MIN(id) 
      FROM http_table
     GROUP BY Host, IP, Port
);

-- 创建包含唯一性约束的新表
CREATE TABLE new_http_table (
    id       INTEGER PRIMARY KEY AUTOINCREMENT,
    Host     TEXT    DEFAULT "",
    IP       TEXT    DEFAULT "",
    Port     INTEGER DEFAULT (0),
    Title    TEXT    DEFAULT "",
    Url      TEXT    DEFAULT "",
    Info     TEXT    DEFAULT "",
    Location TEXT    DEFAULT "",
    Method   TEXT    DEFAULT "",
    Code     INTEGER DEFAULT "",
    Tag      TEXT    DEFAULT "",
    Server   TEXT    DEFAULT "",
    Uptime   TEXT    DEFAULT (datetime('now', 'localtime') ) ,
    UNIQUE(Host, IP, Port)
);

-- 复制数据到新表(确保没有重复项)
INSERT INTO new_http_table (id, Host, IP, Port, Title, Url, Info, Location, Method, Code, Tag, Server, Uptime)
	SELECT id, Host, IP, Port, Title, Url, Info, Location, Method, '', Tag, Server, Uptime
	FROM http_table
	WHERE (Host, IP, Port) IN (SELECT Host, IP, Port
								FROM http_table
								GROUP BY Host, IP, Port
								HAVING COUNT( * ) = 1);

-- 查询对比新旧两个表数据总数
SELECT (SELECT COUNT( * ) 
          FROM http_table) AS old_count, (SELECT COUNT( * ) 
                                                    FROM new_http_table) AS new_count;

-- (可选)删除原始表
--DROP TABLE http_table;

-- 重命名原始表的名称
ALTER TABLE http_table RENAME TO back_http_table;

-- 重命名新表为原始表的名称
ALTER TABLE new_http_table RENAME TO http_table;

-- (可选)删除备份表
--DROP TABLE back_http_table;

--释放空闲占用(类似收缩数据库)
VACUUM;

--检查数据库完整性
PRAGMA integrity_check;

--数据处理全部完成