假如按人口比例平均分配IP

IP地址,要是按人口比例来分,是不是才算真公平?之前有人问过中国有没有完整的/8段IP,查资料发现不仅没有,实际分配量和人口占比比起来还差不少。顺着这个思路,我干脆把美国的情况也一起扒了扒,算完一组数据,还挺有感触的,今天就把这些思考整理成日志记下来。

先跟不熟悉的朋友科普下基础概念:咱们说的/8段,就是常说的A类IP地址,每个这样的段里固定有16777216个IP(大概1677万个),这是IPv4时代里很大的一个地址块了。而全球IPv4地址总量看着多,有42.95亿个,但扣掉私网、组播、环回这些不能公网使用的保留地址后,实际能分配的公网IP也就39.2亿个左右,这个数据是查了APNIC的IPv4枯竭报告得出来的,算是行业里比较公认的估算值。

这里先放一张我整理的常见/8段归属表,很多人容易误解这些段属于中国,其实不然:

前缀实际管理方中国利用率说明
14.0.0.0/8APNIC96%亚太区共享,含日韩等国地址
59.0.0.0/8APNIC100%区域分配,非中国独有,含澳新机构使用
101.0.0.0/8APNIC89%中国占比高,但含APNIC保留及其他亚太用户
114.0.0.0/8APNIC61%中国电信为主,但含亚太其他运营商子网
183.0.0.0/8APNIC98%中国电信使用,仍有部分地址分配给亚太其他实体

计算按人口比例中国应得的 IPv4 地址数及对应 / 8 段数量,需基于全球 IPv4 总可分配地址数权威人口统计数据进行量化计算,核心逻辑是「中国人口占全球比例 × 全球可分配 IPv4 总量」,再换算为 / 8 段(每段固定含 16,777,216 个地址)。以下是完整计算过程、数据来源及结论:

计算

1. 全球 IPv4 总可分配地址数

IPv4 地址空间总量为 2³² = 4,294,967,296 个(约 42.95 亿),但需扣除 IANA 保留地址段(不可用于公网分配),主要包括:

  • 私网地址:10.0.0.0/8(1677 万)、172.16.0.0/12(1048 万)、192.168.0.0/16(6.55 万)
  • 组播地址:224.0.0.0/4(2684 万)
  • 环回地址:127.0.0.0/8(1677 万)
  • 其他保留段(如测试地址、未分配段)

实际可分配公网 IPv4 总量:约 3,920,000,000 个(39.2 亿)(行业通用估算值,误差 ±1%,来源:APNIC《IPv4 地址枯竭报告》)。

2. 人口数据(2023 年权威统计)

  • 中国人口:14.12 亿(国家统计局 2023 年常住人口数据,含港澳台)
  • 全球人口:79.54 亿(联合国《世界人口展望 2022》修订版)
  • 中国人口占全球比例:14.12亿 ÷ 79.54亿 ≈ 17.75%

理论分配量计算

1. 应得 IPv4 地址总数

按人口比例分配公式:中国应得地址数 = 全球可分配IPv4总量 × 中国人口占比代入数据:39.2亿 × 17.75% ≈ 6.96亿(精确值:3920000000 × 0.1775 = 695800000)

2. 对应 / 8 段数量

每个 / 8 段(A 类地址)的地址数固定为 2²⁴ = 16,777,216 个(约 1677.72 万)。应得 / 8 段数量 = 应得地址总数 ÷ 单个 / 8 段地址数代入数据:695,800,000 ÷ 16,777,216 ≈ 41.47 个

简化场景(不扣除保留地址)

若直接按 IPv4 总量(42.95 亿)计算(忽略保留段):42.95亿 × 17.75% ≈ 7.62亿对应 / 8 段:762,000,000 ÷ 16,777,216 ≈ 45.42 个

对比再看美国:现有IP量有多“富余”?

沿用上述核心算法公式,计算美国的相关数据。既然聊到公平性,肯定要看看现在IP持有量最多的美国。先找现有数据,不同平台统计略有差异,IPIP.NET显示美国已分配IPv4约14.54亿个,DICloak的数据则是15.18亿个,我取个中间值,按14.8亿个来算比较客观。

再算美国的理论应得量。美国2024年官方统计人口大概3.38亿,占全球人口的比例就是3.38亿÷79.54亿≈4.25%。按这个比例算,美国应得的IP数量是39.2亿×4.25%≈1.67亿个,对应/8段的话,就是1.67亿÷1677万≈9.96个,差不多10个完整的/8段。

这一对比就很直观了:美国实际持有的14.8亿个IP,是理论应得量1.67亿的8.8倍;现有/8段数量(查IPinfo的地址段列表,美国独占或主要使用的/8段就有几十个,远超10个)更是远远超出人口比例对应的份额。说句实在的,这就是早期互联网发展的“先发优势”,1990年代前欧美就占了全球60%的/8段,美国单国就握了大概150个,相当于把大量IP资源提前锁在了自己手里。

三、一组对比表,看清差距本质

为了更清晰,我把核心数据整理成了表格,一眼就能看出差异:

国家人口占全球比例理论应得IP数对应/8段数量实际已分配IP数实际与理论比值
中国17.75%约6.96亿约41个约3.49亿0.56(56%)
美国4.25%约1.67亿约10个约14.8亿8.8(880%)
计算场景中国应得 IPv4 地址数对应完整 / 8 段数量剩余地址数(约)
扣除保留段(实际可分配)6.96 亿41 个0.47×1677 万≈808 万
不扣除保留段(理论总量)7.62 亿45 个0.42×1677 万≈704 万

与实际分配的差距

根据 APNIC 最新数据(2024Q4),中国实际分配的 IPv4 地址约 3.9 亿(仅占全球可分配总量的 9.95%),仅为理论应得量(6.96 亿)的 56%,对应 / 8 段仅约 23 个(3.9 亿 ÷1677 万≈23.25),远低于人口比例对应的 41 个。

数据来源与引用说明

  1. 数据来源可追溯
  2. 公式合理性:人口比例分配是「资源公平分配」的理论假设,核心是「每人获得平等的地址配额」,未考虑经济发展、互联网渗透率等实际因素,但能直观反映分配公平性。
  3. 误差说明
    • 保留地址扣除量存在 ±1% 误差(不同机构统计口径略有差异),但不影响核心结论(41-45 个 / 8 段)。
    • 人口数据为 2023 年静态值,若按 2025 年预测(中国 14.05 亿、全球 81.2 亿),比例约 17.3%,应得 / 8 段约 40-44 个,差异极小。
  4. 实际分配的历史原因:IPv4 分配采用「先到先得」机制,1990 年代前欧美国家已占据全球约 60% 的 / 8 段(仅美国就持有约 150 个 / 8 段),中国因互联网起步较晚(1994 年接入国际互联网),错失大段分配机会。
  5. IPv6 的解决方案:IPv6 地址空间(2¹²⁸)无需按人口比例分配,中国已获得 APNIC 分配的大量 IPv6 /32 段(每个 / 32 含 2⁹⁶个地址,远超全球人口需求),目前 IPv6 活跃用户数已超 8 亿,成为全球最大 IPv6 网络。

排查 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 解析顺序(由根向叶子):

以访问 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】