高性能IP查询服务端开发日志
之前使用IP信息查询的服务也不少了,观察了不少的接口,大部分返回的数据都是json格式的数据,于是我就在想,我能不能也自己开发一个类似的查询工具玩一下?
说干就干,在想清楚了软件的架构设计后,我决定开工。
架构设计与编程语言选型
在架构设计方面,我打算进行前后端彻底解耦,前端为静态网页,后端为纯API程序。
- 对于前端,我计划利用JS来从API获取信息并更新页面
- 对于后端,就做最纯粹的API服务,根据查询请求响应JSON字符串
在亲眼见证过PHP,Python这类动态解释型语言的效率噩梦后,我打算使用GO语言进行开发,理由很简单:
- GO是编译型语言,运行效率比解释型语言要高得多。
- GO天生就适合开发后端,抗并发能力强。
- 相比于C语言,GO语言要更适合上手。
构建前端
不得不说,就目前的情况,Gemini仍然是前端之神,我直接让Gemini根据我的想法设计了一套网页,还是相当不错的:
代码如下:
<!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">×</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后续还有界面的美化,目前确定是这样了:
变可爱了,有没有?
后端设计
因为项目的定位是轻量,所以我不打算依赖外部的数据库。再加上IP查询这个业务场景几乎全是读操作,所以使用SQLite是没问题的,读取这方面没有瓶颈。
为了实现高效的 IP 查询,数据库表结构的设计至关重要。核心思路是将文本格式的 IP 地址转换为无符号 32 位整数(uint32),这样不仅运算效率高,也便于进行范围比较。
数据库表(例如 ip_info)主要包含三个字段:
network_start(INTEGER): 存储该 IP 段的起始地址,以uint32格式保存。这是查询的核心,我们会根据这个字段建立索引(CREATE INDEX idx_network_start ON ip_info (network_start);),以确保查询性能。network_end(INTEGER): 存储该 IP 段的结束地址,同样以uint32格式保存。用于在查找到候选记录后,进行二次验证,确保目标 IP 确实落在该网段内。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









































































































































































































