高性能IP查询服务端开发日志

之前使用IP信息查询的服务也不少了,观察了不少的接口,大部分返回的数据都是json格式的数据,于是我就在想,我能不能也自己开发一个类似的查询工具玩一下?
说干就干,在想清楚了软件的架构设计后,我决定开工。

架构设计与编程语言选型

在架构设计方面,我打算进行前后端彻底解耦,前端为静态网页,后端为纯API程序。

  • 对于前端,我计划利用JS来从API获取信息并更新页面
  • 对于后端,就做最纯粹的API服务,根据查询请求响应JSON字符串

在亲眼见证过PHP,Python这类动态解释型语言的效率噩梦后,我打算使用GO语言进行开发,理由很简单:

  • GO是编译型语言,运行效率比解释型语言要高得多。
  • GO天生就适合开发后端,抗并发能力强。
  • 相比于C语言,GO语言要更适合上手。

构建前端

不得不说,就目前的情况,Gemini仍然是前端之神,我直接让Gemini根据我的想法设计了一套网页,还是相当不错的:
01.png
代码如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>猫娘IP地址查询</title>
    <!-- 引入 Tailwind CSS 进行快速样式构建 -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        /* 自定义可爱波点纹理背景 */
        body {
            background-color: #fff0f5; /* 极淡的粉色底色 */
            background-image: 
                radial-gradient(#ffb6c1 15%, transparent 16%), 
                radial-gradient(#ffb6c1 15%, transparent 16%);
            background-size: 40px 40px;
            background-position: 0 0, 20px 20px;
            font-family: 'Nunito', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 1rem;
        }

        /* 隐藏滚动条但保留滚动功能(适配部分长列表情况) */
        ::-webkit-scrollbar {
            width: 8px;
        }
        ::-webkit-scrollbar-track {
            background: #fff0f5; 
        }
        ::-webkit-scrollbar-thumb {
            background: #ffb6c1; 
            border-radius: 10px;
        }
        ::-webkit-scrollbar-thumb:hover {
            background: #ff8da1; 
        }

        /* 动画效果 */
        @keyframes float {
            0% { transform: translateY(0px); }
            50% { transform: translateY(-10px); }
            100% { transform: translateY(0px); }
        }
        .float-animation {
            animation: float 3s ease-in-out infinite;
        }
    </style>
</head>
<body class="text-gray-700 w-full flex flex-col items-center">

    <!-- 全局包裹容器,控制最大宽度 -->
    <div class="w-full max-w-2xl relative pt-8 md:pt-16 px-4 pb-12">
        
        <!-- 装饰性猫耳 (直接悬浮在最外层) -->
        <div class="absolute top-4 left-4 md:left-0 text-5xl float-animation z-10" style="animation-delay: 0s;">🐱</div>
        <div class="absolute top-4 right-4 md:right-0 text-5xl float-animation z-10" style="animation-delay: 1.5s;">🐾</div>

        <!-- 头部:标题与当前IP (直接放在背景上) -->
        <header class="text-center mb-8 mt-2 relative z-10">
            <h1 class="text-4xl md:text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-pink-500 to-rose-400 tracking-wide drop-shadow-sm mb-4">
                猫娘IP地址查询
            </h1>
            <p class="text-pink-600 font-medium text-lg bg-white/70 backdrop-blur-sm inline-block px-6 py-2 rounded-full border border-pink-200 shadow-sm">
                您的 IP 地址为 <span id="current-ip" class="font-bold text-pink-700 ml-1">正在获取喵...</span>
            </p>
        </header>

        <!-- 主体结构 -->
        <main>
            <!-- 搜索输入区域 (直接放在背景上) -->
            <div class="flex flex-col md:flex-row items-center justify-center mb-10 w-full group relative z-10">
                <div class="relative w-full flex shadow-sm rounded-full">
                    <span class="absolute left-6 top-1/2 transform -translate-y-1/2 text-pink-400 text-xl">🔍</span>
                    <input 
                        type="text" 
                        id="ip-input"
                        placeholder="请输入要查询的 IP 地址喵~" 
                        class="w-full bg-white/90 backdrop-blur-sm border-2 border-pink-200 text-pink-700 placeholder-pink-300 px-14 py-4 rounded-full md:rounded-r-none focus:outline-none focus:border-pink-400 focus:bg-white transition-all duration-300"
                    >
                </div>
                <button 
                    id="search-btn"
                    class="w-full md:w-auto mt-4 md:mt-0 bg-gradient-to-r from-pink-400 to-rose-400 hover:from-pink-500 hover:to-rose-500 text-white font-bold text-lg px-8 py-4 rounded-full md:rounded-l-none md:rounded-r-full shadow-md hover:shadow-pink-300/50 transform hover:-translate-y-0.5 transition-all duration-300 whitespace-nowrap active:scale-95"
                >
                    查询喵 🐾
                </button>
            </div>

            <!-- 结果列表卡片 (独立卡片,竖向列表) -->
            <div class="bg-white/90 backdrop-blur-sm border-4 border-pink-200 rounded-[2.5rem] p-6 md:p-10 w-full shadow-[0_10px_40px_rgba(255,182,193,0.5)]">
                <div id="results-container" class="opacity-100 transition-opacity duration-500">
                    <ul class="flex flex-col divide-y-2 divide-pink-100/60">
                        <!-- 列表项 1: 国家 -->
                        <li class="flex items-center justify-between py-4 px-2 hover:bg-pink-50 rounded-2xl transition-colors group">
                            <div class="flex items-center gap-4">
                                <span class="text-2xl group-hover:scale-110 transition-transform bg-pink-100/50 p-2 rounded-full">🌍</span>
                                <span class="text-pink-500 font-bold tracking-wider">国家</span>
                            </div>
                            <span id="res-country" class="text-gray-700 font-semibold text-lg">-</span>
                        </li>
                        <!-- 列表项 2: 省份 -->
                        <li class="flex items-center justify-between py-4 px-2 hover:bg-pink-50 rounded-2xl transition-colors group">
                            <div class="flex items-center gap-4">
                                <span class="text-2xl group-hover:scale-110 transition-transform bg-pink-100/50 p-2 rounded-full">🗺️</span>
                                <span class="text-pink-500 font-bold tracking-wider">省份</span>
                            </div>
                            <span id="res-province" class="text-gray-700 font-semibold text-lg">-</span>
                        </li>
                        <!-- 列表项 3: 城市 -->
                        <li class="flex items-center justify-between py-4 px-2 hover:bg-pink-50 rounded-2xl transition-colors group">
                            <div class="flex items-center gap-4">
                                <span class="text-2xl group-hover:scale-110 transition-transform bg-pink-100/50 p-2 rounded-full">🏙️</span>
                                <span class="text-pink-500 font-bold tracking-wider">城市</span>
                            </div>
                            <span id="res-city" class="text-gray-700 font-semibold text-lg">-</span>
                        </li>
                        <!-- 列表项 4: ISP -->
                        <li class="flex items-center justify-between py-4 px-2 hover:bg-pink-50 rounded-2xl transition-colors group">
                            <div class="flex items-center gap-4">
                                <span class="text-2xl group-hover:scale-110 transition-transform bg-pink-100/50 p-2 rounded-full">🏢</span>
                                <span class="text-pink-500 font-bold tracking-wider">ISP 运营商</span>
                            </div>
                            <span id="res-isp" class="text-gray-700 font-semibold text-lg">-</span>
                        </li>
                        <!-- 列表项 5: 经纬度 -->
                        <li class="flex items-center justify-between py-4 px-2 hover:bg-pink-50 rounded-2xl transition-colors group">
                            <div class="flex items-center gap-4">
                                <span class="text-2xl group-hover:scale-110 transition-transform bg-pink-100/50 p-2 rounded-full">🧭</span>
                                <span class="text-pink-500 font-bold tracking-wider">经纬度</span>
                            </div>
                            <span id="res-latlon" class="text-gray-700 font-semibold text-lg">-</span>
                        </li>
                    </ul>
                </div>
                
                <!-- API 提示按钮 -->
                <div class="mt-6 flex justify-end relative z-10 border-t-2 border-dashed border-pink-100/60 pt-4">
                    <button id="api-info-btn" class="text-sm text-pink-400 hover:text-pink-600 font-medium flex items-center gap-1 transition-colors group">
                        <span class="group-hover:animate-bounce">💡</span> 开发者 API 说明
                    </button>
                </div>
            </div>
        </main>
    </div>

    <!-- API 说明弹窗 (Modal) -->
    <div id="api-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center p-4">
        <!-- 黑色半透明背景遮罩 -->
        <div id="api-modal-overlay" class="absolute inset-0 bg-black/30 backdrop-blur-sm opacity-0 transition-opacity duration-300"></div>
        
        <!-- 弹窗主体 -->
        <div id="api-modal-content" class="bg-white border-4 border-pink-200 rounded-[2rem] p-6 md:p-8 max-w-lg w-full shadow-2xl relative z-10 opacity-0 scale-95 transition-all duration-300">
            <div class="flex justify-between items-center mb-6">
                <h2 class="text-2xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-rose-400">
                    🛠️ API 调用说明
                </h2>
                <button id="close-modal-top" class="text-pink-300 hover:text-pink-500 text-3xl transition-colors leading-none">&times;</button>
            </div>
            
            <div class="text-gray-600 text-sm space-y-4">
                <p>您可以通过发送 <code class="bg-pink-50 text-pink-600 px-1 py-0.5 rounded font-bold">GET</code> 请求到我们的接口来获取 IP 详细信息喵:</p>
                
                <div class="bg-pink-50 p-4 rounded-xl border border-pink-100 font-mono text-xs overflow-x-auto text-pink-700 shadow-inner">
                    GET https://api.maoniang.example/v1/ip?address=<span class="text-rose-500">{ip}</span>
                </div>
                
                <p class="font-bold text-gray-700 mt-4">📦 返回数据格式示例 (JSON):</p>
                
                <pre class="bg-slate-800 text-pink-200 p-4 rounded-xl text-xs overflow-x-auto font-mono shadow-inner leading-relaxed">
{
  "code": 200,
  "msg": "success",
  "data": {
    "country": "美国",
    "province": "加利福尼亚州",
    "city": "圣克拉拉",
    "isp": "阿里云",
    "latitude": "37.355701",
    "longitude": "-121.955002"
  }
}</pre>
            </div>
            
            <div class="mt-8 text-center">
                <button id="close-modal-bottom" class="bg-pink-100 hover:bg-pink-200 text-pink-600 font-bold py-2.5 px-8 rounded-full transition-colors active:scale-95 shadow-sm">
                    我知道了喵 🐾
                </button>
            </div>
        </div>
    </div>

    <!-- 交互逻辑 -->
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const currentIpSpan = document.getElementById('current-ip');
            const searchBtn = document.getElementById('search-btn');
            const ipInput = document.getElementById('ip-input');
            
            // 结果节点
            const resCountry = document.getElementById('res-country');
            const resProvince = document.getElementById('res-province');
            const resCity = document.getElementById('res-city');
            const resIsp = document.getElementById('res-isp');
            const resLatlon = document.getElementById('res-latlon');

            // 弹窗相关节点
            const apiModal = document.getElementById('api-modal');
            const apiModalOverlay = document.getElementById('api-modal-overlay');
            const apiModalContent = document.getElementById('api-modal-content');
            const apiInfoBtn = document.getElementById('api-info-btn');
            const closeModalTop = document.getElementById('close-modal-top');
            const closeModalBottom = document.getElementById('close-modal-bottom');

            // 1. 获取用户当前IP (使用免费的 ipify API)
            fetch('https://api.ipify.org?format=json')
                .then(response => response.json())
                .then(data => {
                    currentIpSpan.textContent = data.ip;
                })
                .catch(error => {
                    currentIpSpan.textContent = "获取失败喵 T_T";
                    console.error('获取IP失败:', error);
                });

            // ================= 弹窗控制逻辑 =================
            const openModal = () => {
                apiModal.classList.remove('hidden');
                // 强制重绘以触发动画
                void apiModal.offsetWidth;
                apiModalOverlay.classList.remove('opacity-0');
                apiModalOverlay.classList.add('opacity-100');
                apiModalContent.classList.remove('opacity-0', 'scale-95');
                apiModalContent.classList.add('opacity-100', 'scale-100');
            };

            const closeModal = () => {
                apiModalOverlay.classList.remove('opacity-100');
                apiModalOverlay.classList.add('opacity-0');
                apiModalContent.classList.remove('opacity-100', 'scale-100');
                apiModalContent.classList.add('opacity-0', 'scale-95');
                
                // 等待动画结束后隐藏元素
                setTimeout(() => {
                    apiModal.classList.add('hidden');
                }, 300); // 对应 duration-300
            };

            apiInfoBtn.addEventListener('click', openModal);
            closeModalTop.addEventListener('click', closeModal);
            closeModalBottom.addEventListener('click', closeModal);
            apiModalOverlay.addEventListener('click', closeModal); // 点击遮罩层也可关闭

            // 2. 点击查询按钮的逻辑
            searchBtn.addEventListener('click', () => {
                const ipToSearch = ipInput.value.trim();
                
                if (!ipToSearch) {
                    alert('请输入要查询的 IP 地址喵!');
                    return;
                }

                // 添加加载动画效果
                searchBtn.innerHTML = '查询中... 🐾';
                searchBtn.classList.add('opacity-80', 'cursor-not-allowed');
                
                setTimeout(() => {
                    // 适配你提供的 JSON 格式模拟返回数据
                    const mockData = {
                        "country": "美国",
                        "province": "加利福尼亚州",
                        "city": "圣克拉拉",
                        "isp": "阿里云",
                        "latitude": "37.355701",
                        "longitude": "-121.955002"
                    };

                    // 更新 DOM 数据
                    resCountry.textContent = mockData.country;
                    resProvince.textContent = mockData.province;
                    resCity.textContent = mockData.city;
                    resIsp.textContent = mockData.isp;
                    resLatlon.textContent = `${mockData.latitude}, ${mockData.longitude}`;

                    // 恢复按钮状态
                    searchBtn.innerHTML = '查询喵 🐾';
                    searchBtn.classList.remove('opacity-80', 'cursor-not-allowed');
                    
                    // 小彩蛋互动
                    ipInput.value = '';
                    ipInput.placeholder = '查询成功了喵!继续输入吧~';

                }, 600); // 模拟网络延迟
            });

            // 支持回车查询
            ipInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    searchBtn.click();
                }
            });
        });
    </script>
</body>
</html>

但是仔细观察,你会发现使用了实时构建的 Tailwind CSS ,其实这样做对于一个静态页面不太友好,于是我让他按照 Tailwind CSS 的样式,原生帮我写了一版样式。因为处理后代码变多了,我就不在这里放出来了。最后静态文件就三个:

[chocola@Neko-X99 static]$ tree 
.
├── favicon.png
├── index.html
└── main.css

1 directory, 3 files

后续还有界面的美化,目前确定是这样了:
02.png
变可爱了,有没有?

后端设计

因为项目的定位是轻量,所以我不打算依赖外部的数据库。再加上IP查询这个业务场景几乎全是读操作,所以使用SQLite是没问题的,读取这方面没有瓶颈。
为了实现高效的 IP 查询,数据库表结构的设计至关重要。核心思路是将文本格式的 IP 地址转换为无符号 32 位整数(uint32),这样不仅运算效率高,也便于进行范围比较。
数据库表(例如 ip_info)主要包含三个字段:

  1. network_start (INTEGER): 存储该 IP 段的起始地址,以 uint32 格式保存。这是查询的核心,我们会根据这个字段建立索引CREATE INDEX idx_network_start ON ip_info (network_start);),以确保查询性能。
  2. network_end (INTEGER): 存储该 IP 段的结束地址,同样以 uint32 格式保存。用于在查找到候选记录后,进行二次验证,确保目标 IP 确实落在该网段内。
  3. ip_info_json (TEXT): 将该 IP 段对应的地理位置、运营商等详细信息,序列化成 JSON 字符串后存储在此字段。这种方案灵活度高,可以方便地增减返回的字段,而无需修改表结构。

查询时,我们首先将待查询的 IP 转换为整数,然后利用 WHERE network_start <= ? ORDER BY network_start DESC LIMIT 1 这样的 SQL 语句,借助索引快速定位到最接近且小于等于目标 IP 的那条记录。接着,再用 network_end 字段验证目标 IP 是否在该记录的范围内,从而得出最终结果。这种“先定位,后校验”的方式,配合索引,能够将查询耗时稳定在微秒级别。

测试环境:

  • CPU:i5-2410M
  • 内存:4GB DDR3 1600MHz
  • 硬盘:希捷 320G SATA2 机械硬盘
  • Nginx:模拟真实环境,开启SSL进行反代

第一版代码

package main

import (
    "database/sql"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "net"
    "net/http"
    "strings"

    _ "github.com/mattn/go-sqlite3"
)

var db *sql.DB

// 定义完全匹配前端需求的数据结构
type IPInfo struct {
    IP        string `json:"ip"`
    Country   string `json:"country"`
    Province  string `json:"province"`
    City      string `json:"city"`
    ISP       string `json:"isp"`
    Latitude  string `json:"latitude"`
    Longitude string `json:"longitude"`
}

// 定义标准的 API 响应格式
type APIResponse struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data"` // 用 interface{} 以便在出错时返回 nil
}

// 辅助函数:获取客户端真实 IP
func getClientIP(r *http.Request) string {
    ip := r.Header.Get("X-Real-IP")
    if ip == "" {
        ip = r.Header.Get("X-Forwarded-For")
    }
    if ip == "" {
        ip, _, _ = net.SplitHostPort(r.RemoteAddr)
    }
    // X-Forwarded-For 可能是逗号分隔的多个IP,取第一个真实的
    ip = strings.Split(ip, ",")[0]
    // 清理可能包含的空格,提升健壮性
    return strings.TrimSpace(ip)
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Access-Control-Allow-Origin", "*")

    // 提取查询参数并去除两端空白字符
    queryIPStr := strings.TrimSpace(r.URL.Query().Get("ip"))

    targetIP := queryIPStr
    if targetIP == "" {
        targetIP = getClientIP(r)
    }

    // 【安全防线 1】:严格限制为合法的 IPv4 格式。任何注入代码都会在这里被直接拦截。
    parsedIP := net.ParseIP(targetIP)
    if parsedIP == nil || parsedIP.To4() == nil {
        json.NewEncoder(w).Encode(APIResponse{Code: 400, Msg: "非法的 IPv4 地址喵!", Data: nil})
        return
    }

    // 【安全防线 2】:转为 uint32 整型。彻底杜绝字符型注入。
    ipInt := uint32(parsedIP.To4()[0])<<24 | uint32(parsedIP.To4()[1])<<16 | uint32(parsedIP.To4()[2])<<8 | uint32(parsedIP.To4()[3])

    var infoJSON string
    var networkEnd uint32 // 🌟 记得提前声明这个变量喵

    // 【性能优化核心】:利用索引极速向下查找最近的一条记录
    err := db.QueryRow(`
    SELECT network_end, ip_info_json
    FROM ip_info
    WHERE network_start <= ?
    ORDER BY network_start DESC
    LIMIT 1`, ipInt).Scan(&networkEnd, &infoJSON)

    if err != nil {
        if err == sql.ErrNoRows {
            json.NewEncoder(w).Encode(APIResponse{Code: 404, Msg: "数据库里没有找到这个 IP 喵~", Data: nil})
        } else {
            // 避免将底层的数据库错误直接暴露给前端(防止信息泄露)
            log.Printf("数据库查询错误: %v\n", err)
            json.NewEncoder(w).Encode(APIResponse{Code: 500, Msg: "数据库查询出错了喵", Data: nil})
        }
        return
    }

    // 【关键逻辑校验】:虽然找到了最近的起始 IP,但必须确认目标 IP 是否在这个网段的覆盖范围内
    if ipInt > networkEnd {
        json.NewEncoder(w).Encode(APIResponse{Code: 404, Msg: "数据库里没有找到这个 IP 喵~", Data: nil})
        return
    }

    var rawData map[string]string
    if err := json.Unmarshal([]byte(infoJSON), &rawData); err != nil {
        log.Printf("JSON 解析错误: %v\n", err)
        json.NewEncoder(w).Encode(APIResponse{Code: 500, Msg: "数据解析失败喵", Data: nil})
        return
    }

    result := IPInfo{
        IP:        targetIP,
        Country:   rawData["country"],
        Province:  rawData["province"],
        City:      rawData["city"],
        ISP:       rawData["isp"],
        Latitude:  rawData["latitude"],
        Longitude: rawData["longitude"],
    }

    json.NewEncoder(w).Encode(APIResponse{
        Code: 200,
        Msg:  "success",
        Data: result,
    })
}

func main() {
    // ================= 定义启动参数 =================
    // flag.String("参数名", "默认值", "说明文字")
    dbPath := flag.String("db", "ip_info.db", "SQLite 数据库文件路径")
    port := flag.String("port", "8080", "API 服务监听端口")

    // 解析命令行参数
    flag.Parse()

    var err error
    // 使用参数指定的数据库路径
    db, err = sql.Open("sqlite3", *dbPath)
    if err != nil {
        log.Fatal("无法打开数据库: ", err)
    }
    defer db.Close()

    http.HandleFunc("/ipinfo", apiHandler)

    // 拼接监听地址
    addr := fmt.Sprintf(":%s", *port)
    fmt.Printf("猫娘纯净 API 服务启动于 %s 喵... 🐾\n", addr)
    fmt.Printf("当前使用的数据库文件: %s\n", *dbPath)

    log.Fatal(http.ListenAndServe(addr, nil))
}

测试下来直接就踩了一个大坑:因为没有设置索引,数据库查询效率非常低下,以至于到了难以置信的慢。
通过这个命令:

CREATE INDEX idx_network_start ON ip_info (network_start);

建立好索引后,查询速度直接起飞。从原来每秒只能处理10个请求飙升至每秒可以处理5000+个请求,性能直接翻了500倍!太夸张了!

第二版代码

虽然第一版代码的性能已经可以做到很优秀,但是我还是希望尝试进一步优化,那就是——数据库存入内存!而而且我不但要存入内存,还要将其转化为Go原生的数据存储格式,进一步优化性能。

package main

import (
    "database/sql"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "net"
    "net/http"
    "sort"
    "strings"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

var db *sql.DB

// --- 全局配置与缓存 ---
var (
    ipCache         []IPRule // 全量内存切片
    useMemoryCache  bool     // 内存模式开关
    enableDetailLog bool     // 详细日志开关
)

type IPRule struct {
    NetworkStart uint32
    NetworkEnd   uint32
    Info         IPInfo
}

type IPInfo struct {
    IP        string `json:"ip"`
    Country   string `json:"country"`
    Province  string `json:"province"`
    City      string `json:"city"`
    ISP       string `json:"isp"`
    Latitude  string `json:"latitude"`
    Longitude string `json:"longitude"`
}

type APIResponse struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data"`
}

// 辅助函数:获取客户端真实 IP
func getClientIP(r *http.Request) string {
    ip := r.Header.Get("X-Real-IP")
    if ip == "" {
        ip = r.Header.Get("X-Forwarded-For")
    }
    if ip == "" {
        ip, _, _ = net.SplitHostPort(r.RemoteAddr)
    }
    ip = strings.Split(ip, ",")[0]
    return strings.TrimSpace(ip)
}

// 统一处理响应和日志打印
func sendJSONResponse(w http.ResponseWriter, clientIP, targetIP string, resp APIResponse) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.Header().Set("Access-Control-Allow-Origin", "*")

    // 将结构体序列化为 JSON 字节
    respBytes, err := json.Marshal(resp)
    if err != nil {
        log.Printf("响应序列化失败: %v", err)
        http.Error(w, `{"code":500,"msg":"系统内部错误","data":null}`, http.StatusInternalServerError)
        return
    }

    // 如果开启了日志,就在这里统一输出
    if enableDetailLog {
        log.Printf("[访问日志] 来源IP: %-15s | 查询IP: %-15s | 结果: %s", clientIP, targetIP, string(respBytes))
    }

    // 写入响应
    w.Write(respBytes)
}

func loadDataToMemory() error {
    log.Println("正在将数据库载入内存,请稍候喵...")
    startTime := time.Now()

    rows, err := db.Query(`SELECT network_start, network_end, ip_info_json FROM ip_info ORDER BY network_start ASC`)
    if err != nil {
        return err
    }
    defer rows.Close()

    for rows.Next() {
        var start, end uint32
        var infoJSON string
        if err := rows.Scan(&start, &end, &infoJSON); err != nil {
            return err
        }

        var rawData map[string]string
        if err := json.Unmarshal([]byte(infoJSON), &rawData); err != nil {
            continue
        }

        info := IPInfo{
            Country:   rawData["country"],
            Province:  rawData["province"],
            City:      rawData["city"],
            ISP:       rawData["isp"],
            Latitude:  rawData["latitude"],
            Longitude: rawData["longitude"],
        }

        ipCache = append(ipCache, IPRule{
            NetworkStart: start,
            NetworkEnd:   end,
            Info:         info,
        })
    }

    log.Printf("载入完成!共加载了 %d 条规则,耗时 %v 喵!", len(ipCache), time.Since(startTime))
    return nil
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    clientIP := getClientIP(r) // 提取实际访问者的 IP,用于日志记录

    queryIPStr := strings.TrimSpace(r.URL.Query().Get("ip"))
    targetIP := queryIPStr
    if targetIP == "" {
        targetIP = clientIP
    }

    parsedIP := net.ParseIP(targetIP)
    if parsedIP == nil || parsedIP.To4() == nil {
        sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 400, Msg: "非法的 IPv4 地址喵!", Data: nil})
        return
    }

    ipInt := uint32(parsedIP.To4()[0])<<24 | uint32(parsedIP.To4()[1])<<16 | uint32(parsedIP.To4()[2])<<8 | uint32(parsedIP.To4()[3])

    if useMemoryCache {
        idx := sort.Search(len(ipCache), func(i int) bool {
            return ipCache[i].NetworkStart > ipInt
        })

        if idx > 0 {
            rule := ipCache[idx-1]
            if ipInt <= rule.NetworkEnd {
                result := rule.Info
                result.IP = targetIP
                sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 200, Msg: "success", Data: result})
                return
            }
        }

        sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 404, Msg: "内存库里没有找到这个 IP 喵~", Data: nil})

    } else {
        var infoJSON string
        var networkEnd uint32

        err := db.QueryRow(`
        SELECT network_end, ip_info_json
        FROM ip_info
        WHERE network_start <= ?
        ORDER BY network_start DESC
        LIMIT 1`, ipInt).Scan(&networkEnd, &infoJSON)

        if err != nil {
            if err == sql.ErrNoRows {
                sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 404, Msg: "数据库里没有找到这个 IP 喵~", Data: nil})
            } else {
                log.Printf("数据库查询错误: %v\n", err)
                sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 500, Msg: "数据库查询出错了喵", Data: nil})
            }
            return
        }

        if ipInt > networkEnd {
            sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 404, Msg: "数据库里没有找到这个 IP 喵~", Data: nil})
            return
        }

        var rawData map[string]string
        if err := json.Unmarshal([]byte(infoJSON), &rawData); err != nil {
            log.Printf("JSON 解析错误: %v\n", err)
            sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 500, Msg: "数据解析失败喵", Data: nil})
            return
        }

        result := IPInfo{
            IP:        targetIP,
            Country:   rawData["country"],
            Province:  rawData["province"],
            City:      rawData["city"],
            ISP:       rawData["isp"],
            Latitude:  rawData["latitude"],
            Longitude: rawData["longitude"],
        }

        sendJSONResponse(w, clientIP, targetIP, APIResponse{Code: 200, Msg: "success", Data: result})
    }
}

func main() {
    dbPath := flag.String("db", "ip_info.db", "SQLite 数据库文件路径")
    port := flag.String("port", "8080", "API 服务监听端口")
    memFlag := flag.Bool("mem", false, "是否开启全量内存模式(内存换取极致性能喵~)")
    logFlag := flag.Bool("log", false, "是否开启详细访问日志输出")

    flag.Parse()
    useMemoryCache = *memFlag
    enableDetailLog = *logFlag // 赋值给全局变量

    var err error
    db, err = sql.Open("sqlite3", *dbPath)
    if err != nil {
        log.Fatal("无法打开数据库: ", err)
    }
    defer db.Close()

    if useMemoryCache {
        if err := loadDataToMemory(); err != nil {
            log.Fatal("致命错误:无法将数据加载到内存喵: ", err)
        }
    }

    http.HandleFunc("/ipinfo", apiHandler)

    addr := fmt.Sprintf(":%s", *port)
    fmt.Printf("猫娘 API 服务启动于 %s 喵...\n", addr)
    fmt.Printf("当前数据库文件: %s\n", *dbPath)

    if useMemoryCache {
        fmt.Println("当前运行模式: [极致性能] 全量内存 + 二分查找")
    } else {
        fmt.Println("当前运行模式: [省内存] SQLite 实时查询 (可加 -mem=true 提速)")
    }

    if enableDetailLog {
        fmt.Println("详细访问日志: 已开启 (将在控制台打印每次请求细节)")
    } else {
        fmt.Println("详细访问日志: 未开启 (可加 -log=true 开启)")
    }

    log.Fatal(http.ListenAndServe(addr, nil))
}

另外,我还加上了日志输出的功能,方便进行API调用的监控,可以防范滥用。
经过测试,在把数据库存进内存后,可以抗的并发请求从 5000+ 请求/秒 提升到了 7000+ 请求/秒,提升了40%!
不过我也观察到,此时Nginx的CPU占用率明显上升,大约占据了40%+的CPU,而Go程序本身的CPU占用率则从50%左右降低到了35%!不过有点奇怪的是,大约有20%的CPU占用率不知道是被什么程序占据了,BTOP没有显示出来。
其实在第一版代码的时候,Nginx的CPU占用已经挺高了,请求数量提升后更高。
值得注意的是,两个版本的服务端进行极限压力测试的时候,整机的CPU占用率几乎都是100%。不知道各位对这个性能表现感觉如何?

分析:“消失”的20% CPU占用去哪了?

在测试中,我观察到一个有趣的现象:Nginx 占用了约 40%,Go 程序占用了约 35%,加起来只有 75% 左右,但整机 CPU 却已经打满(100%)。剩下的 20% 去哪了?难道被猫娘偷吃了吗?

其实,这并非 BUG,而是高性能网络服务的典型特征——内核态开销(System CPU)

当 QPS 达到 7000+ 时,服务器每秒需要处理数万个网络数据包的接收、发送、上下文切换以及系统调用。这些操作并不发生在 Nginx 或 Go 程序的“用户态”代码中,而是直接由 Linux 内核 接管处理:

  • 网络协议栈处理:TCP/IP 包的解析、校验、重组。
  • 软中断(SoftIRQs):网卡中断后的数据搬运。
  • 线程调度:在数千个并发连接间快速切换 CPU 时间片。

结论:这“消失”的 20% 实际上是被 操作系统内核 消耗掉了,用于支撑如此高频率的网络吞吐。这也侧面证明了我们的程序优化已经非常到位,瓶颈不再在于应用层代码,而在于底层的网络 IO 处理能力。如果再想提升,可能就需要考虑内核参数调优(如调整 TCP 缓冲区、开启 RSS 多队列网卡等)或者升级更强的单核 CPU 了喵!

提升程序的兼容性

因为我们的项目依赖了go-sqlite3,这要求必须开启CGO进行编译。如果在一般的Linux发行版(如Ubuntu、CentOS)上直接编译,默认会动态链接系统的C语言标准库(glibc)。这必然会导致把程序放到不同年代、不同发行版的服务器上运行时,出现“找不到glibc版本”等兼容性报错。
为了达到“一次编译,到处运行”的完美兼容性,我们需要进行纯静态链接打包。但是,在普通发行版上强行静态编译 glibc 是非常痛苦且容易踩坑的。因此,我们需要借助musl这个极其精简的C标准库。
使用musl的典型代表就是Alpine Linux(这也是老朋友了,我的云服务器基本上都是这个发行版)。我们可以非常优雅地在 Alpine 环境下进行全静态编译操作:
先安装必要的 C 语言编译工具链:

apk add go gcc musl-dev

进入代码目录后,初始化模块并拉取依赖:

go mod init github.com/Chocola-X/NekoIPinfo
go mod tidy

最后,使用“静态编译参数”进行编译:

CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags="-linkmode external -extldflags -static -s -w" -o neko-ip-api main.go

这样编译出来的二进制文件内部已经打包了所有需要的底层库。你把它丢到绝大部分 x86_64 架构的 Linux 发行版上,都可以做到直接开箱即用!

结尾

项目已经开源到了Github:NekoIPinfo
并且我也自己搭建了查询服务,感兴趣的可以来玩玩:NekoIPinfo Demo