Compare commits
564 Commits
fr_xdb_ipv
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
2955431edb | |
|
|
c0d85cc71d | |
|
|
d5e3616d8e | |
|
|
cd40e3a1d5 | |
|
|
ee19ac95ca | |
|
|
a99989343e | |
|
|
324bfdf35d | |
|
|
f4d81dd3dd | |
|
|
ba96deaecf | |
|
|
c4eb1a14ce | |
|
|
dbef88e337 | |
|
|
30ed0cf028 | |
|
|
d06690f651 | |
|
|
daf6749c34 | |
|
|
e50ead2cba | |
|
|
b1eb00e25e | |
|
|
fac3f73193 | |
|
|
d1dda93abd | |
|
|
44013251b6 | |
|
|
546ce589a9 | |
|
|
507b7bddc1 | |
|
|
25513cb246 | |
|
|
445eb4cb8e | |
|
|
a1667d1182 | |
|
|
d189535bb8 | |
|
|
06d54d8084 | |
|
|
8849cc9130 | |
|
|
c911ca1290 | |
|
|
241831a6f0 | |
|
|
2f0dc5f33f | |
|
|
c1db56c369 | |
|
|
211c374ee2 | |
|
|
a3b8d817ee | |
|
|
b7bb22589f | |
|
|
1068bf05a3 | |
|
|
fc46254325 | |
|
|
7b320846c9 | |
|
|
e360f3d031 | |
|
|
e7c240a8d9 | |
|
|
d638015592 | |
|
|
3eb65a2657 | |
|
|
fac3d0376a | |
|
|
a8b9d542c6 | |
|
|
82a73fa60d | |
|
|
8b79b69524 | |
|
|
c5fb71facb | |
|
|
62e1f98497 | |
|
|
2af5ee176c | |
|
|
0ce74b8873 | |
|
|
f4fcd5e900 | |
|
|
072b6fed24 | |
|
|
f939363a19 | |
|
|
353524325a | |
|
|
59cf547107 | |
|
|
3147144a7f | |
|
|
d4299a1d1b | |
|
|
18ecd7ae85 | |
|
|
5fbdef93cd | |
|
|
a90191e8d8 | |
|
|
593e028677 | |
|
|
72c657c378 | |
|
|
19a8816827 | |
|
|
787bd7ae07 | |
|
|
5b10989ac2 | |
|
|
90e827633a | |
|
|
4dd0a1e006 | |
|
|
77134c4315 | |
|
|
fedf5aaf03 | |
|
|
d28a10b0c1 | |
|
|
aab908c923 | |
|
|
c314ef54ca | |
|
|
69d267d85d | |
|
|
a603a75ab7 | |
|
|
d730db816a | |
|
|
d36edc6e15 | |
|
|
1d9ffabfeb | |
|
|
03cc433717 | |
|
|
3562b7af8e | |
|
|
165746ba38 | |
|
|
79243eb0ba | |
|
|
be71b92f20 | |
|
|
d380827871 | |
|
|
1a08f5e568 | |
|
|
36c9de10bb | |
|
|
61ef15cff0 | |
|
|
d6444e740f | |
|
|
e3094c8ef2 | |
|
|
5244ebad08 | |
|
|
4d391acac2 | |
|
|
5cb17713a7 | |
|
|
e1f0d5126a | |
|
|
d9fc17727e | |
|
|
dabe7798f6 | |
|
|
8f841e6237 | |
|
|
d7b60f41e4 | |
|
|
83d6fafaa0 | |
|
|
02f6b31894 | |
|
|
97e647234a | |
|
|
1209b72452 | |
|
|
62aa42e05e | |
|
|
e76f995ceb | |
|
|
edb29aea3b | |
|
|
8a7b088216 | |
|
|
897ee5c0a9 | |
|
|
1eba8ece3b | |
|
|
007cb0808a | |
|
|
7d6ea00303 | |
|
|
e183607da5 | |
|
|
b0466348da | |
|
|
04e51e2182 | |
|
|
c63b1dc5db | |
|
|
1a29562c2d | |
|
|
29c7048f93 | |
|
|
fa16a4a45d | |
|
|
8adec5a3c8 | |
|
|
51e194a105 | |
|
|
b965ab8be9 | |
|
|
a720267842 | |
|
|
112dfb99b5 | |
|
|
ca665cfd2e | |
|
|
0ad8031754 | |
|
|
fb0efface4 | |
|
|
e373f92312 | |
|
|
3d21a8247c | |
|
|
d5e10e1383 | |
|
|
1a8d01d967 | |
|
|
e87f580510 | |
|
|
962335954a | |
|
|
2c01e01d2c | |
|
|
79886f77e7 | |
|
|
132221b494 | |
|
|
aef4f91348 | |
|
|
9d4b165409 | |
|
|
4b55d14fca | |
|
|
b030661865 | |
|
|
537e6f9306 | |
|
|
aaad26244d | |
|
|
a31a966546 | |
|
|
832e3fd77d | |
|
|
ef51b901ee | |
|
|
e8242a24ea | |
|
|
30e5f2732d | |
|
|
1c6d299efa | |
|
|
dce0bb582f | |
|
|
b3115ba7aa | |
|
|
81885a16cf | |
|
|
2b61de20d6 | |
|
|
7a1970729a | |
|
|
5a53add68b | |
|
|
c9e8acaffc | |
|
|
ce92aabc11 | |
|
|
3f0480be1f | |
|
|
a694cfa8d2 | |
|
|
8a44c98f99 | |
|
|
cab8fbc4ab | |
|
|
7d11be2ce7 | |
|
|
8302525ae8 | |
|
|
e926501591 | |
|
|
87418d879a | |
|
|
16fa5572f2 | |
|
|
39c297ad04 | |
|
|
6081603bd8 | |
|
|
2861abcb83 | |
|
|
daf54bf877 | |
|
|
6c98b8faf9 | |
|
|
1dc0003661 | |
|
|
c4a3a2e1f8 | |
|
|
0e828bba0d | |
|
|
b0ab8a0b2f | |
|
|
9caef15329 | |
|
|
572fda5910 | |
|
|
67d709ab39 | |
|
|
076f3918fa | |
|
|
9411b7f8c2 | |
|
|
75c375d146 | |
|
|
5037816f41 | |
|
|
aafee317c8 | |
|
|
839d5f59a5 | |
|
|
d4180c0f57 | |
|
|
8da3f55092 | |
|
|
7ff7e5f61f | |
|
|
44fd27f027 | |
|
|
3539f5e4e3 | |
|
|
7bb61ec59c | |
|
|
89852a70f0 | |
|
|
d427b4dcd8 | |
|
|
ad4253dbb0 | |
|
|
e6e3087144 | |
|
|
ad0cfd6bd9 | |
|
|
0cf4cf8e26 | |
|
|
ba2c9ec119 | |
|
|
91f281d53c | |
|
|
05158e0a2e | |
|
|
384567f184 | |
|
|
937c0772ec | |
|
|
ebc6253dfe | |
|
|
969a5f0e03 | |
|
|
c2bb78aecd | |
|
|
e139731d9e | |
|
|
96717ab155 | |
|
|
0520f299ee | |
|
|
bbe2dddecf | |
|
|
14753ffc29 | |
|
|
3127ac0cdc | |
|
|
108ef78a51 | |
|
|
f2c988287c | |
|
|
7bab822a49 | |
|
|
1df8b55789 | |
|
|
68cd1053d2 | |
|
|
398149f17e | |
|
|
4c569581b8 | |
|
|
3d876f895e | |
|
|
0863e172eb | |
|
|
437a2c424a | |
|
|
2cb7863112 | |
|
|
249ea17178 | |
|
|
697eabc33b | |
|
|
930924bb34 | |
|
|
7839a335ed | |
|
|
91816b108b | |
|
|
31dabf8c95 | |
|
|
7628ba32c2 | |
|
|
4577e9a17b | |
|
|
f302c68012 | |
|
|
fe2dfc1088 | |
|
|
93f7652594 | |
|
|
0d152d91a5 | |
|
|
351be412e6 | |
|
|
bd4a2d6bd1 | |
|
|
bf76adad4e | |
|
|
02e10db478 | |
|
|
ad08637d21 | |
|
|
245bbaa366 | |
|
|
56606de2f6 | |
|
|
de6089e321 | |
|
|
37f5e4475c | |
|
|
1f850dcde4 | |
|
|
7f9e6b8586 | |
|
|
b0de60f6cb | |
|
|
f12b9765b3 | |
|
|
8c20994b12 | |
|
|
897af4532e | |
|
|
9669585216 | |
|
|
e84ea900d4 | |
|
|
450fce589c | |
|
|
9b0bedc045 | |
|
|
6059d63504 | |
|
|
e11a390afc | |
|
|
ff75ddbcfd | |
|
|
ea297041dd | |
|
|
c68cc0097c | |
|
|
c2a0a55005 | |
|
|
b52555c5ec | |
|
|
cb9ed02bc6 | |
|
|
d4b8f9f841 | |
|
|
7f454276a1 | |
|
|
f80830f666 | |
|
|
2fbf899140 | |
|
|
72912301ee | |
|
|
6531f3970f | |
|
|
b44654230f | |
|
|
1b9fb0c95c | |
|
|
242bff6208 | |
|
|
a1c536ba98 | |
|
|
2454568203 | |
|
|
48dce3545e | |
|
|
dde56752af | |
|
|
d9ce5da85f | |
|
|
dae949b8ce | |
|
|
54628265b3 | |
|
|
9d67e3fcad | |
|
|
bbccbfa451 | |
|
|
1f8bf73a0f | |
|
|
620646e8e0 | |
|
|
1adc5643aa | |
|
|
316355a72e | |
|
|
e73321a860 | |
|
|
ab22a5e03c | |
|
|
ace0c7f300 | |
|
|
2a8e3444d4 | |
|
|
4c9ef35a9a | |
|
|
540831e6a9 | |
|
|
a220db2345 | |
|
|
619b6645a5 | |
|
|
40c6c2c1bd | |
|
|
89bb8cb709 | |
|
|
e64864aa40 | |
|
|
c78ba4827d | |
|
|
a3d1ba5d30 | |
|
|
000e19d419 | |
|
|
b7f2ffa285 | |
|
|
12c94cf0b6 | |
|
|
3f84cd41c1 | |
|
|
9669237830 | |
|
|
8cecf2f3aa | |
|
|
ab16071797 | |
|
|
4a4c8de636 | |
|
|
942f3d9dbd | |
|
|
a383e4dd17 | |
|
|
b5ae94b36d | |
|
|
efbbb576ee | |
|
|
5c5c0ab160 | |
|
|
7f2d1ae789 | |
|
|
a91c8e9747 | |
|
|
f8a154aeda | |
|
|
a8087e06b7 | |
|
|
317a900077 | |
|
|
096d68ea77 | |
|
|
23ae923cff | |
|
|
c0f0efdb95 | |
|
|
b3c2d40588 | |
|
|
7612e679fb | |
|
|
5b8b47df5e | |
|
|
0b3635b3c3 | |
|
|
33f8bf766d | |
|
|
0b3d794738 | |
|
|
613501fc00 | |
|
|
e95452c1c8 | |
|
|
3ad925406b | |
|
|
6e4af36dc9 | |
|
|
d1151accde | |
|
|
2c9ba9c56c | |
|
|
85144e1c40 | |
|
|
426038b3fe | |
|
|
2c7b170637 | |
|
|
14d2332483 | |
|
|
ad84da7d3c | |
|
|
9ecca28fc2 | |
|
|
4ea2e7360d | |
|
|
edc2559489 | |
|
|
7bd81fecbf | |
|
|
6684d0a928 | |
|
|
30fa54a83f | |
|
|
ca7fc93953 | |
|
|
db50f7b0da | |
|
|
5cfd5f8242 | |
|
|
87b69e6b5f | |
|
|
002c482db3 | |
|
|
75d944eaf7 | |
|
|
92cc3fd7cb | |
|
|
f1d0e1bee0 | |
|
|
d7d06e2185 | |
|
|
55a493fd8e | |
|
|
f03861162f | |
|
|
cbf8f9bfa8 | |
|
|
3aa732d1ce | |
|
|
9ef1fc34ac | |
|
|
62ab160572 | |
|
|
87666aa353 | |
|
|
a2778f9b98 | |
|
|
af82b7e18e | |
|
|
a6cb5161ff | |
|
|
d4d211c50b | |
|
|
31cd078455 | |
|
|
499e091761 | |
|
|
4f39707beb | |
|
|
7fab19c46f | |
|
|
bd30b77d54 | |
|
|
cd78ff2797 | |
|
|
8692309c5b | |
|
|
aa5437c637 | |
|
|
8573f2f04e | |
|
|
6eee95967b | |
|
|
ef7ac3c088 | |
|
|
4b7172766c | |
|
|
dc64c27abd | |
|
|
1da81ffc02 | |
|
|
ae94b2dac3 | |
|
|
f8f01ecd13 | |
|
|
db95055a56 | |
|
|
32a755ac3f | |
|
|
784f336646 | |
|
|
732d4eb945 | |
|
|
e753437afc | |
|
|
ab0c39b7cd | |
|
|
db3b9b22ae | |
|
|
91e542de38 | |
|
|
bec072cd04 | |
|
|
bf0ded1171 | |
|
|
c4113e5eae | |
|
|
731269ebad | |
|
|
9797b595ea | |
|
|
3ea717043c | |
|
|
42b9cebb9d | |
|
|
9e2ca6bd98 | |
|
|
0422ed3f4f | |
|
|
7f5844ca94 | |
|
|
cc97bb80a2 | |
|
|
d4d5525392 | |
|
|
6c653f85cd | |
|
|
47067dbe73 | |
|
|
f6213aaaa4 | |
|
|
3a3abdd01c | |
|
|
bbf5df5601 | |
|
|
0f2260d116 | |
|
|
572aa7b76a | |
|
|
580d1bb431 | |
|
|
029a5578aa | |
|
|
501c3c086d | |
|
|
08d3ef69b0 | |
|
|
4ebfa61179 | |
|
|
8a7a899c64 | |
|
|
f7ad4309f8 | |
|
|
d56230518a | |
|
|
2a77acf1c0 | |
|
|
d5d0c15c8d | |
|
|
b3498197a8 | |
|
|
d10c57b598 | |
|
|
7050da1a68 | |
|
|
bb3c845c81 | |
|
|
ca6c0506e6 | |
|
|
79cc6c1b0d | |
|
|
bd69a255fd | |
|
|
8f6effd8eb | |
|
|
d503206e14 | |
|
|
b3383680f9 | |
|
|
567224ca60 | |
|
|
541420b8df | |
|
|
6cb1da2794 | |
|
|
ec7cb6d3ed | |
|
|
a30d922225 | |
|
|
a1b2c21767 | |
|
|
1e31b4b143 | |
|
|
baa9f62d6b | |
|
|
3b4a229918 | |
|
|
d362231ae8 | |
|
|
1ad01ff98b | |
|
|
5deb6d6553 | |
|
|
444b2036d1 | |
|
|
35e505bd74 | |
|
|
2fe8032d8b | |
|
|
69eb94dcd8 | |
|
|
0ad3172f2e | |
|
|
2ac7655f62 | |
|
|
6aab24fb88 | |
|
|
b94123fee3 | |
|
|
0caff12306 | |
|
|
6ad1e8e2e2 | |
|
|
9e979efd7b | |
|
|
f594838784 | |
|
|
24dcb24e24 | |
|
|
ddb2b4ff1d | |
|
|
2662fb05c2 | |
|
|
7e4d37b1db | |
|
|
d1715c75f6 | |
|
|
c82d2e6117 | |
|
|
ffb632c3d0 | |
|
|
11f890f3a4 | |
|
|
87b6cd5247 | |
|
|
61dc2af93d | |
|
|
413ccd32ce | |
|
|
e3b87326d2 | |
|
|
5a51b75411 | |
|
|
a2b76d38a9 | |
|
|
8c7c83cf7f | |
|
|
fbadc7cca0 | |
|
|
f03d33e5fe | |
|
|
c809c072bc | |
|
|
5c1522fce4 | |
|
|
a3eedc5438 | |
|
|
4f8cdb05c9 | |
|
|
008bbbe4d7 | |
|
|
d35a7b9120 | |
|
|
4d71b47e05 | |
|
|
40ea8a267f | |
|
|
2dcccf22a3 | |
|
|
e073e26f52 | |
|
|
ed42fd0fba | |
|
|
9959117d92 | |
|
|
3cab1bae2e | |
|
|
5a8d7c4152 | |
|
|
4507c2f706 | |
|
|
ad78dbe547 | |
|
|
e924a7c17d | |
|
|
6c463282d0 | |
|
|
bb19c7110a | |
|
|
5fce4eaa01 | |
|
|
01bd749d4a | |
|
|
3693eb47a8 | |
|
|
e39ac2d6d1 | |
|
|
d97967d565 | |
|
|
8650273a85 | |
|
|
ccf7f323c2 | |
|
|
a930a17e39 | |
|
|
0a456283e6 | |
|
|
d6345bc015 | |
|
|
086250c666 | |
|
|
54a160e230 | |
|
|
2d71241a3b | |
|
|
8a254e3e83 | |
|
|
95f5fcfcdb | |
|
|
2cd59657e1 | |
|
|
1f1ad5aaf6 | |
|
|
e887c01615 | |
|
|
df6004b42e | |
|
|
05c7c17af0 | |
|
|
a6c0a66a36 | |
|
|
a137813509 | |
|
|
0179edc3a8 | |
|
|
4ff67f168c | |
|
|
cb3329390a | |
|
|
93c5088591 | |
|
|
1ac2113afc | |
|
|
139972dfb8 | |
|
|
379ab6ce73 | |
|
|
d3592eb7cd | |
|
|
b6410e16d0 | |
|
|
d7f4ef5a07 | |
|
|
7a4bd4ed2b | |
|
|
a6931ce31b | |
|
|
32e005e1e6 | |
|
|
b5b00aeb87 | |
|
|
b16fe21506 | |
|
|
931fb9e67b | |
|
|
7576daa6cc | |
|
|
75893f0331 | |
|
|
355c0d8673 | |
|
|
815b1fb54b | |
|
|
29fa8e25ab | |
|
|
13324d1575 | |
|
|
46f56f0eca | |
|
|
2fe0d1f59e | |
|
|
b01253e6b3 | |
|
|
6712ee9504 | |
|
|
5fa69300ad | |
|
|
7e599e05a0 | |
|
|
a36a85558f | |
|
|
48c2b395bc | |
|
|
dff3084b93 | |
|
|
8fbdf82797 | |
|
|
73c8b20d3a | |
|
|
69b9f062e4 | |
|
|
1575e581cd | |
|
|
4cff9eb6ee | |
|
|
0fd0bc59d0 | |
|
|
c64837db67 | |
|
|
eb80b89c0e | |
|
|
9a14e32736 | |
|
|
4c1b0362c8 | |
|
|
5d11d6c0fb | |
|
|
7bbe7d681d | |
|
|
95c129c7d1 | |
|
|
716326f4bc | |
|
|
d1e9b6e3bc | |
|
|
ae344d20aa | |
|
|
537077279a | |
|
|
a91fdd38bc | |
|
|
9af9c927fe | |
|
|
f226ef1f79 | |
|
|
e8b6049584 | |
|
|
6db9654c7d | |
|
|
c6c0ee954f | |
|
|
4911bdcd41 | |
|
|
23b2446736 | |
|
|
6727987960 | |
|
|
629f509c6f | |
|
|
23b3c3bc9b | |
|
|
1cf2e419e1 | |
|
|
697b46bb2e | |
|
|
83fd149c59 | |
|
|
87d53b5018 | |
|
|
f0a9462041 | |
|
|
6a729e40a5 | |
|
|
fdec88605f |
|
|
@ -1,2 +0,0 @@
|
||||||
data/ipv6_source.txt filter=lfs diff=lfs merge=lfs -text
|
|
||||||
data/ip2region_v6.xdb filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
*.log
|
*.log
|
||||||
*.la
|
*.la
|
||||||
*.so
|
*.so
|
||||||
*.xdb
|
|
||||||
*.iml
|
*.iml
|
||||||
META-INF/
|
META-INF/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
@ -27,20 +26,20 @@ META-INF/
|
||||||
# vim swp file #
|
# vim swp file #
|
||||||
*.swp
|
*.swp
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
# binding
|
# binding
|
||||||
/v1.0//binding/java/classes/
|
|
||||||
/v1.0//binding/java/doc/
|
|
||||||
/v1.0//binding/java/target/
|
|
||||||
/v1.0//binding/java/*.jar
|
|
||||||
/binding/java/classes/
|
/binding/java/classes/
|
||||||
/binding/java/doc/
|
/binding/java/doc/
|
||||||
/binding/java/target/
|
/binding/java/target/
|
||||||
/binding/java/*.jar
|
/binding/java/*.jar
|
||||||
|
|
||||||
|
# python
|
||||||
|
/**/__pycache__
|
||||||
|
|
||||||
# clang
|
# clang
|
||||||
/binding/c/xdb_searcher
|
/binding/c/xdb_searcher
|
||||||
/binding/c/util_test
|
/binding/c/test_util
|
||||||
/binding/c/cmake-build-debug
|
/binding/c/cmake-build-debug
|
||||||
|
|
||||||
# lua/luc_c
|
# lua/luc_c
|
||||||
|
|
@ -58,24 +57,25 @@ target
|
||||||
|
|
||||||
# VS ignore cases
|
# VS ignore cases
|
||||||
/**/*.sln
|
/**/*.sln
|
||||||
/v1.0/binding/c#/**/.vs/
|
|
||||||
/v1.0/binding/c#/**/packages
|
|
||||||
/v1.0/binding/c#/**/bin
|
|
||||||
/v1.0/binding/c#/**/obj
|
|
||||||
/binding/c#/**/.vs/
|
/binding/c#/**/.vs/
|
||||||
/binding/c#/**/packages
|
/binding/c#/**/packages
|
||||||
/binding/c#/**/bin
|
/binding/c#/**/bin
|
||||||
/binding/c#/**/obj
|
/binding/c#/**/obj
|
||||||
|
|
||||||
# Nodejs
|
# Nodejs
|
||||||
/v1.0/binding/nodejs/tests/unitTests/__snapshots__
|
|
||||||
/v1.0/binding/nodejs/coverage
|
|
||||||
/binding/nodejs/tests/unitTests/__snapshots__
|
/binding/nodejs/tests/unitTests/__snapshots__
|
||||||
/binding/nodejs/coverage
|
/binding/nodejs/coverage
|
||||||
/binding/nodejs/node_modules
|
/binding/nodejs/node_modules
|
||||||
/binding/nodejs/.nyc_output
|
/binding/nodejs/.nyc_output
|
||||||
/binging/nodejs/package-lock.json
|
/binging/nodejs/package-lock.json
|
||||||
|
|
||||||
|
# Javascript
|
||||||
|
/binding/javascript/tests/unitTests/__snapshots__
|
||||||
|
/binding/javascript/coverage
|
||||||
|
/binding/javascript/node_modules
|
||||||
|
/binding/javascript/.nyc_output
|
||||||
|
/binding/javascript/package-lock.json
|
||||||
|
|
||||||
# maker
|
# maker
|
||||||
## golang
|
## golang
|
||||||
/maker/golang/dbmaker
|
/maker/golang/dbmaker
|
||||||
|
|
@ -89,3 +89,6 @@ target
|
||||||
#vscode
|
#vscode
|
||||||
.vscode
|
.vscode
|
||||||
build
|
build
|
||||||
|
|
||||||
|
# git worktrees
|
||||||
|
.worktrees/
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
|
ip2region is dual-licensed under the Apache License 2.0 and the MIT License.
|
||||||
|
|
||||||
|
You may use, distribute, and modify this software under either license,
|
||||||
|
at your option. The full text of both licenses appears below.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
http://www.apache.org/licenses/
|
http://www.apache.org/licenses/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
|
# ip2region
|
||||||
|
|
||||||
|
[ip2region](https://ip2region.net) - is an offline IP address localization library and IP localization data management framework. It supports both `IPv4` and `IPv6` with query efficiency at the 10-microsecond level. It provides `xdb` data generation and query client implementations for many mainstream programming languages.
|
||||||
|
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
### 1. Offline Localization Library
|
||||||
|
|
||||||
|
The project itself provides raw data for both IPv4 (`data/ipv4_source.txt`) and IPv6 (`data/ipv6_source.txt`), along with corresponding xdb files (`data/ip2region_v4.xdb` and `data/ip2region_v6.xdb`) to achieve city-level query localization. The field format is: `Country|Province|City|ISP|iso-alpha2-code`. Localization information for China is entirely in Chinese, while regional information for non-China areas is entirely in English.
|
||||||
|
|
||||||
|
### 2. Data Management Framework
|
||||||
|
|
||||||
|
`xdb` supports hundreds of millions of IP data segment rows. Region information supports full customization. The region information of the built-in data is fixed in the format: `Country|Province|City|ISP|iso-alpha2-Code`. You can append data for specific business needs to the region, such as: GPS information/International Standard Regional Codes/Zip codes, etc. In other words, you can fully use ip2region to manage your own IP localization data.
|
||||||
|
|
||||||
|
### 3. Data Deduplication and Compression
|
||||||
|
|
||||||
|
The `xdb` format generation program automatically processes the input raw data, checks and completes the merging of adjacent IP segments, and performs deduplication and compression of identical regional information.
|
||||||
|
|
||||||
|
### 4. High-Speed Query Response
|
||||||
|
|
||||||
|
Even for queries based entirely on the `xdb` file, the single query response time is at the 10-microsecond level. Memory-accelerated queries can be enabled through the following two methods:
|
||||||
|
|
||||||
|
1. `vIndex` Index Caching: Uses a fixed `512KiB` of memory to cache vector index data, reducing one disk IO operation and maintaining average query efficiency within 100 microseconds.
|
||||||
|
2. Entire `xdb` File Caching: Loads the entire `xdb` file into memory. Memory usage is equal to the `xdb` file size. There is no disk IO operation, maintaining 10-microsecond level query efficiency.
|
||||||
|
|
||||||
|
### 5. Unified Query Interface
|
||||||
|
|
||||||
|
`xdb` provides version-compatible query implementations. A unified API can simultaneously provide queries for both IPv4 and IPv6 data and return unified data.
|
||||||
|
|
||||||
|
|
||||||
|
# `xdb` Query
|
||||||
|
|
||||||
|
For API introductions, usage documentation, and test programs, please refer to the README introduction under the corresponding `searcher` query client. All query binding implementations are as follows:
|
||||||
|
|
||||||
|
| Language | Description | IPv4 Support | IPv6 Support |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Golang](binding/golang/README.md) | golang query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [PHP](binding/php/README.md) | php query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Java](binding/java/README.md) | java query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C](binding/c/README.md) | C[std=c99] query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Lua_c](binding/lua_c/README.md) | lua c extension query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Lua](binding/lua/README.md) | lua query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Rust](binding/rust/README.md) | rust query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Python](binding/python/README.md) | python query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Javascript](binding/javascript/README.md) | javascript query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Csharp](binding/csharp) | csharp query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Erlang](binding/erlang/README.md) | erlang query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Nginx](binding/nginx) | nginx extension query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C++](binding/cpp/README.md) | C++ query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Cangjie](binding/cangjie/README.md) | Cangjie query client | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
The following toolchain implementations are contributed by community developers via third-party repositories:
|
||||||
|
|
||||||
|
| Language | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| [ip2region-composer](https://github.com/zoujingli/ip2region) | php composer management client |
|
||||||
|
| [ip2region-ts](https://github.com/Steven-Qiang/ts-ip2region2) | node.js addon management client |
|
||||||
|
| [ruby-ip2region](https://github.com/jicheng1014/ruby-ip2region) | ruby xdb query client implementation |
|
||||||
|
| [Ip2regionTool](https://github.com/orestonce/Ip2regionTool) | ip2region data conversion tool |
|
||||||
|
|
||||||
|
|
||||||
|
# `xdb` Generation
|
||||||
|
|
||||||
|
For API introductions, usage documentation, and test programs, please refer to the README documents under the following `maker` generation programs:
|
||||||
|
|
||||||
|
| Language | Description | IPv4 Support | IPv6 Support |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Golang](maker/golang/README.md) | golang xdb generation program | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Java](maker/java/README.md) | java xdb generation program | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Python](maker/python/README.md) | python xdb generation program | :white_check_mark: | :x: |
|
||||||
|
| [Csharp](maker/csharp/README.md) | csharp xdb generation program | :white_check_mark: | :x: |
|
||||||
|
| [Rust](maker/rust/README.md) | rust xdb generation program | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C++](maker/cpp) | C++ xdb generation program | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
||||||
|
# `xdb` Update
|
||||||
|
|
||||||
|
The core of the ip2region project lies in **researching the design and implementation of IP data storage and fast querying**. The raw data `./data/ipv4_source.txt` and `./data/ipv6_source.txt` included in the project are updated irregularly. For scenarios with high requirements for data accuracy and update frequency, it is recommended to purchase commercial offline data from the [Ip2Region Community](https://ip2region.net/products/offline) or third-party vendors. You can try to update the data yourself using the following methods:
|
||||||
|
|
||||||
|
### Manual Editing and Updating
|
||||||
|
|
||||||
|
You can modify the data yourself based on the raw IP data provided by ip2region in `./data/ipv4_source.txt` and `./data/ipv6_source.txt` using the editing tools provided by ip2region. Currently, the data sources include:
|
||||||
|
|
||||||
|
1. Data provided by the ip2region community (please refer to the official account at the bottom for community notifications)
|
||||||
|
2. Project Issues tagged with `[Data_Updates]`
|
||||||
|
3. Other custom data: e.g., data provided by customers, data obtained through GPS and WIFI positioning, or legal and compliant data from other platforms.
|
||||||
|
|
||||||
|
For instructions on using the raw IP data editing tools, please refer to the README documents under the following `maker` generation programs:
|
||||||
|
|
||||||
|
| Language | Description | IPv4 Support | IPv6 Support |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Golang](maker/golang/README.md#xdb-data-editing) | golang IP raw data editor | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C++](maker/cpp/README.md) | C++ IP raw data editor | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
### Detection Automatic Update
|
||||||
|
|
||||||
|
If you want to update data via your own API or data source, you can refer to the update algorithm based on the "Detection Algorithm" shared in the following videos to write your own update program:
|
||||||
|
|
||||||
|
1. [Data Update Implementation Video Sharing - part1](https://www.bilibili.com/video/BV1934y1E7Q5/)
|
||||||
|
2. [Data Update Implementation Video Sharing - part2](https://www.bilibili.com/video/BV1pF411j7Aw/)
|
||||||
|
|
||||||
|
|
||||||
|
# Official Community
|
||||||
|
|
||||||
|
The Ip2Region official community was officially launched on `2025/06/12`. On one hand, it provides stable [commercial offline data](https://ip2region.net/products/offline) services. On the other hand, it facilitates the strengthening of the IP toolchain and data services outside the core code, such as [usage documentation](https://ip2region.net/doc/), [query testing](https://ip2region.net/search/demo), and data correction. For more information and services regarding the community, please visit the [Ip2Region Official Community](https://ip2region.net/).
|
||||||
|
|
||||||
|
|
||||||
|
# Related Remarks
|
||||||
|
|
||||||
|
### 1. xdb Technical Documents:
|
||||||
|
|
||||||
|
1. xdb Data Structure Analysis: ["ip2region xdb - Data Structure Description"](https://ip2region.net/doc/xdb/structure)
|
||||||
|
2. xdb Query Process Analysis: ["ip2region xdb - Query Process Description"](https://ip2region.net/doc/xdb/search)
|
||||||
|
3. xdb Generation Process Analysis: ["ip2region xdb - Generation Process Description"](https://ip2region.net/doc/xdb/generate)
|
||||||
|
4. xdb File Generation Tutorial: ["ip2region xdb - File Generation Tutorial"](https://ip2region.net/doc/data/xdb_make)
|
||||||
|
5. xdb Concurrent Safety Query: ["ip2region xdb - Concurrent Safety Query"](https://ip2region.net/doc/xdb/concurrent)
|
||||||
|
6. xdb Data Update Method: ["ip2region Data Update and Use of xdb Data Editor"](https://mp.weixin.qq.com/s/cZH5qIn4E5rQFy6N32RCzA)
|
||||||
|
|
||||||
|
### 3. Technical Information Blogs
|
||||||
|
|
||||||
|
1. WeChat Official Account - lionsoul-org, the author's active technical sharing channel
|
||||||
|
2. [Ip2Region Official Community](https://ip2region.net)
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
|
# ip2region
|
||||||
|
|
||||||
|
[ip2region](https://ip2region.net) - 是一个离线IP地址定位库和IP定位数据管理框架,同时支持 `IPv4` 和 `IPv6` ,10微秒级别的查询效率,提供了众多主流编程语言的 `xdb` 数据生成和查询客户端实现。
|
||||||
|
|
||||||
|
|
||||||
|
# 项目特性
|
||||||
|
|
||||||
|
### 1、离线定位库
|
||||||
|
|
||||||
|
项目本身同时了提供了一份 IPv4 (`data/ipv4_source.txt`) 和 IPv6 (`data/ipv6_source.txt`) 的原始数据和对应的 xdb 文件(`data/ip2region_v4.xdb` 和 `data/ip2region_v6.xdb`) 用于实现精确到城市的查询定位功能,字段格式为:`国家|省份|城市|ISP|iso-alpha2-code(国家两字母简称)`,中国的定位信息全部为中文,非中国地区的地域信息全部为英文。
|
||||||
|
|
||||||
|
### 2、数据管理框架
|
||||||
|
|
||||||
|
`xdb` 支持亿级别的 IP 数据段行数,region 信息支持完全自定义,自带数据的 region 信息固定了格式为:`国家|省份|城市|ISP|iso-alpha2-Code`,你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。
|
||||||
|
|
||||||
|
### 3、数据去重和压缩
|
||||||
|
|
||||||
|
`xdb` 格式生成程序会自动处理输入的原始数据,检查并且完成相连 IP 段的的合并以及相同地域信息的去重和压缩。
|
||||||
|
|
||||||
|
### 4、极速查询响应
|
||||||
|
|
||||||
|
即使是完全基于 `xdb` 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:
|
||||||
|
|
||||||
|
1. `vIndex` 索引缓存 :使用固定的 `512KiB` 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在100微秒之内。
|
||||||
|
2. `xdb` 整个文件缓存:将整个 `xdb` 文件全部加载到内存,内存占用等同于 `xdb` 文件大小,无磁盘 IO 操作,保持10微秒级别的查询效率。
|
||||||
|
|
||||||
|
### 5、统一的查询接口
|
||||||
|
|
||||||
|
`xdb` 提供了版本兼容的查询实现,一个统一的 API 可以同时提供对 IPv4 和 IPv6 数据的查询并且返回统一的数据。
|
||||||
|
|
||||||
|
|
||||||
|
# `xdb` 查询
|
||||||
|
|
||||||
|
API 介绍,使用文档和测试程序请参考对应 `searcher` 查询客户端下的 README 介绍,全部查询 binding 实现情况如下:
|
||||||
|
| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Golang](binding/golang/README_zh.md) | golang 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [PHP](binding/php/README_zh.md) | php 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Java](binding/java/README_zh.md) | java 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C](binding/c/README_zh.md) | C[std=c99] 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Lua_c](binding/lua_c/README_zh.md) | lua c 扩展查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Lua](binding/lua/README_zh.md) | lua 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Rust](binding/rust/README_zh.md) | rust 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Python](binding/python/README_zh.md) | python 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Javascript](binding/javascript/README_zh.md) | javascript 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Csharp](binding/csharp) | csharp 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Erlang](binding/erlang/README_zh.md) | erlang 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Nginx](binding/nginx) | nginx 扩展查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C++](binding/cpp/README_zh.md) | C++ 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Cangjie](binding/cangjie/README.md) | 仓颉 查询客户端 | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
||||||
|
以下工具链实现由社区开发者通过第三方仓库贡献:
|
||||||
|
| 编程语言 | 描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| [ip2region-composer](https://github.com/zoujingli/ip2region) | php composer 管理客户端 |
|
||||||
|
| [ip2region-ts](https://github.com/Steven-Qiang/ts-ip2region2) | node.js addon 管理客户端|
|
||||||
|
| [ruby-ip2region](https://github.com/jicheng1014/ruby-ip2region) | ruby xdb 查询客户端实现 |
|
||||||
|
| [Ip2regionTool](https://github.com/orestonce/Ip2regionTool) | ip2region 数据转换工具 |
|
||||||
|
|
||||||
|
|
||||||
|
# `xdb` 生成
|
||||||
|
|
||||||
|
API 介绍,使用文档和测试程序请参考如下 `maker` 生成程序下的 README 文档:
|
||||||
|
|
||||||
|
| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Golang](maker/golang/README_zh.md) | golang xdb 生成程序 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Java](maker/java/README_zh.md) | java xdb 生成程序 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [Python](maker/python/README_zh.md) | python xdb 生成程序 | :white_check_mark: | :x: |
|
||||||
|
| [Csharp](maker/csharp/README_zh.md) | csharp xdb 生成程序 | :white_check_mark: | :x: |
|
||||||
|
| [Rust](maker/rust/README_zh.md) | rust xdb 生成程序 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C++](maker/cpp) | C++ xdb 生成程序 | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
||||||
|
# `xdb` 更新
|
||||||
|
|
||||||
|
ip2region 项目的核心在于 <b>研究 IP 数据的存储和快速查询的设计和实现</b>, 项目自带的 `./data/ipv4_source.txt` 和 `./data/ipv6_source.txt` 原始数据不定期更新,对于数据精度和更新频率要求很高的使用场景建议到 [Ip2Region社区](https://ip2region.net/products/offline) 或者第三方购买商用离线数据,你可以使用如下几种方式来尝试自己更新数据:
|
||||||
|
|
||||||
|
### 手动编辑更新
|
||||||
|
你可以基于 ip2region 自带的 `./data/ipv4_source.txt` 和 `./data/ipv6_source.txt` 原始 IP 数据用 ip2region 提供的编辑工具来自己修改,目前数据源有如下几种方式:
|
||||||
|
1. ip2region 社区提供的数据(请参考地底部的公众号关注社区通知)
|
||||||
|
2. ip2region Github/Gitee 中带有 `[数据源补充]` 标签的 Issue
|
||||||
|
3. 其他自定义数据:例如客户提供的数据,或者通过 GPS 和 WIFI 定位得到的数据,或者来自其他平台的合法合规的数据
|
||||||
|
|
||||||
|
原始 IP 数据编辑工具使用方法请参考如下的 `maker` 生成程序下的 README 文档:
|
||||||
|
| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Golang](maker/golang/README_zh.md#xdb-数据编辑) | golang IP 原始数据编辑器 | :white_check_mark: | :white_check_mark: |
|
||||||
|
| [C++](maker/cpp) | C++ IP 原始数据编辑器 | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
||||||
|
### 检测自动更新
|
||||||
|
如果你想通过你自己的 API 或数据源来更新数据,你可以参考以下视频分享的 `基于检测算法` 的更新算法来自己编写一个更新程序:
|
||||||
|
1. [数据更新实现视频分享 - part1](https://www.bilibili.com/video/BV1934y1E7Q5/)
|
||||||
|
2. [数据更新实现视频分享 - part2](https://www.bilibili.com/video/BV1pF411j7Aw/)
|
||||||
|
|
||||||
|
|
||||||
|
# 官方社区
|
||||||
|
Ip2Region 官方社区正式上线于 `2025/06/12` 日,一方面提供了稳定的 [商用离线数据](https://ip2region.net/products/offline) 服务,另一方面便于在核心代码外强化 IP 工具链和数据服务,例如 [使用文档](https://ip2region.net/doc/),[查询测试](https://ip2region.net/search/demo),数据纠错等,更多关于社区的信息和服务请访问 [Ip2Region 官方社区](https://ip2region.net/)。
|
||||||
|
|
||||||
|
|
||||||
|
# 相关备注
|
||||||
|
|
||||||
|
### 1、xdb 技术文档:
|
||||||
|
1. xdb 数据结构分析:[“ip2region xdb-数据结构描述“](https://ip2region.net/doc/xdb/structure)
|
||||||
|
2. xdb 查询过程分析:[“ip2region xdb-查询过程描述”](https://ip2region.net/doc/xdb/search)
|
||||||
|
3. xdb 生成过程分析:[“ip2region xdb-生成过程描述”](https://ip2region.net/doc/xdb/generate)
|
||||||
|
4. xdb 文件生成教程:[“ip2region xdb-文件生成教程”](https://ip2region.net/doc/data/xdb_make)
|
||||||
|
5. xdb 并发安全查询:[“ip2region xdb-并发安全查询”](https://ip2region.net/doc/xdb/concurrent)
|
||||||
|
6. xdb 数据更新方法:[“ip2region 数据更新和 xdb 数据编辑器的使用”](https://mp.weixin.qq.com/s/cZH5qIn4E5rQFy6N32RCzA)
|
||||||
|
|
||||||
|
### 3、技术信息博客
|
||||||
|
1. 微信公众号 - lionsoul-org,作者活跃的技术分享渠道
|
||||||
|
2. [Ip2Region 官方社区](https://ip2region.net)
|
||||||
115
ReadMe.md
115
ReadMe.md
|
|
@ -1,115 +0,0 @@
|
||||||
# Ip2region 是什么
|
|
||||||
|
|
||||||
[ip2region](https://ip2region.net) - 是一个离线IP地址定位库和IP定位数据管理框架,同时支持`IPv4`和`IPv6`,10微秒级别的查询效率,提供了众多主流编程语言的 `xdb` 数据生成和查询客户端实现。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Ip2region 特性
|
|
||||||
|
|
||||||
### 1、离线定位库
|
|
||||||
|
|
||||||
项目本身同时了提供了一份 IPv4 和 IPv6 的原始数据和对应的 xdb 文件(`ip2region_v4.xdb` 和 `ip2region_v6.xdb`) 用于实现精确到城市的的查询定位功能。
|
|
||||||
|
|
||||||
### 2、数据管理框架
|
|
||||||
|
|
||||||
`xdb` 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:`国家|省份|城市|ISP`,缺省的地域信息默认是0。
|
|
||||||
region 信息支持完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。
|
|
||||||
|
|
||||||
### 3、数据去重和压缩
|
|
||||||
|
|
||||||
`xdb` 格式生成程序会自动处理输入的原始数据,检查并且完成相连 IP 段的的合并以及相同地域信息的去重和压缩。
|
|
||||||
|
|
||||||
### 4、极速查询响应
|
|
||||||
|
|
||||||
即使是完全基于 `xdb` 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:
|
|
||||||
|
|
||||||
1. `vIndex` 索引缓存 :使用固定的 `512KiB` 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在100微秒之内。
|
|
||||||
2. `xdb` 整个文件缓存:将整个 `xdb` 文件全部加载到内存,内存占用等同于 `xdb` 文件大小,无磁盘 IO 操作,保持10微秒级别的查询效率。
|
|
||||||
|
|
||||||
### 5、统一的查询接口
|
|
||||||
|
|
||||||
`xdb` 提供了版本兼容的查询实现,一个统一的 API 可以同时提供对 IPv4 和 IPv6 数据的查询并且返回统一的数据。
|
|
||||||
|
|
||||||
# `xdb` 数据查询
|
|
||||||
|
|
||||||
API 介绍,使用文档和测试程序请参考对应 `searcher` 查询客户端下的 ReadMe 介绍,全部查询 binding 实现情况如下:
|
|
||||||
| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 | 贡献者 |
|
|
||||||
|:---------------------------------|:----------------------------|:-------------------|:------------------ |:--------------------------------------------------|
|
|
||||||
| [Golang](binding/golang) | golang xdb 查询客户端 | :white_check_mark: | :white_check_mark: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [PHP](binding/php) | php xdb 查询客户端 | :white_check_mark: | :soon: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [Java](binding/java) | java xdb 查询客户端 | :white_check_mark: | :soon: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [Lua](binding/lua) | lua xdb 查询客户端 | :white_check_mark: | :soon: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [ANSI C](binding/c) | ANSC c xdb 查询客户端 | :white_check_mark: | :soon: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [Lua_c](binding/lua_c) | lua c 扩展 xdb 查询客户端 | :white_check_mark: | :soon: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [Rust](binding/rust) | rust xdb 查询客户端 | :white_check_mark: | :x: | [gongzhengyang](https://github.com/gongzhengyang) |
|
|
||||||
| [Python](binding/python) | python xdb 查询客户端 | :white_check_mark: | :x: | [厉害的花花](https://github.com/luckydog6132) |
|
|
||||||
| [Nodejs](binding/nodejs) | nodejs xdb 查询客户端 | :white_check_mark: | :x: | [Wu Jian Ping](https://github.com/wujjpp) |
|
|
||||||
| [Csharp](binding/csharp) | csharp xdb 查询客户端 | :white_check_mark: | :x: | [Alen Lee](https://github.com/malus2077) |
|
|
||||||
| [Erlang](binding/erlang) | erlang xdb 查询客户端 | :white_check_mark: | :x: | [malou](https://github.com/malou996) |
|
|
||||||
| [Nginx](binding/nginx) | nginx 扩展 xdb 查询客户端 | :white_check_mark: | :x: | [Wu Jian Ping](https://github.com/wujjpp) |
|
|
||||||
| [C++](binding/cpp) | C++ xdb 查询客户端 | :white_check_mark: | :x: | [Yunbin Liu](https://github.com/liuyunbin) |
|
|
||||||
| [Typescript](binding/typescript) | Typescript xdb 查询客户端 | :white_check_mark: | :x: | [Alan Lee](https://github.com/malus2077) |
|
|
||||||
|
|
||||||
|
|
||||||
以下工具链实现由社区开发者通过第三方仓库贡献:
|
|
||||||
| 编程语言 | 描述 | 贡献者 |
|
|
||||||
|:--------------------------------------------------------------- |:------------------------|:-----------------------------------------------|
|
|
||||||
| [ruby-ip2region](https://github.com/jicheng1014/ruby-ip2region) | ruby xdb 查询客户端实现 | [jicheng1014](https://github.com/jicheng1014) |
|
|
||||||
| [Ip2regionTool](https://github.com/orestonce/Ip2regionTool) | ip2region 数据转换工具 | [orestonce](https://github.com/orestonce) |
|
|
||||||
|
|
||||||
|
|
||||||
# `xdb` 数据生成
|
|
||||||
|
|
||||||
API 介绍,使用文档和测试程序请参考如下 `maker` 生成程序下的 ReadMe 文档:
|
|
||||||
|
|
||||||
| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 | 贡献者 |
|
|
||||||
| :---------------------- | :------------------- |:-------------------|:-------------------| :----------------------------------------- |
|
|
||||||
| [Golang](maker/golang) | golang xdb 生成程序 | :white_check_mark: | :white_check_mark: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [Java](maker/java) | java xdb 生成程序 | :white_check_mark: | :soon: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [Python](maker/python) | python xdb 生成程序 | :white_check_mark: | :x: | [leolin49](https://github.com/leolin49) |
|
|
||||||
| [Csharp](maker/csharp) | csharp xdb 生成程序 | :white_check_mark: | :x: | [Alan Lee](https://github.com/malus2077) |
|
|
||||||
| [Rust](maker/rust) | rust xdb 生成程序 | :white_check_mark: | :x: | [KevinWang](https://github.com/KevinWL) |
|
|
||||||
| [C++](maker/cpp) | C++ xdb 生成程序 | :white_check_mark: | :x: | [Yunbin Liu](https://github.com/liuyunbin) |
|
|
||||||
|
|
||||||
|
|
||||||
# `xdb` 数据更新
|
|
||||||
|
|
||||||
ip2region 项目的核心在于 <b>研究 IP 数据的存储和快速查询的设计和实现</b>, 项目自带的 `./data/ipv4_source.txt` 和 `./data/ipv6_source.txt` 原始数据不会再提供更新,对于数据精度和更新频率要求很高的使用场景建议到 [Ip2Region社区](https://ip2region.net/products/offline) 或者第三方购买商用离线数据,你可以使用如下几种方式来尝试自己更新数据:
|
|
||||||
|
|
||||||
### 手动编辑更新
|
|
||||||
你可以基于 ip2region 自带的 `./data/ipv4_source.txt` 和 `./data/ipv6_source.txt` 原始 IP 数据用 ip2region 提供的编辑工具来自己修改,目前数据源有如下几种方式:
|
|
||||||
1. ip2region 社区提供的数据(请参考地底部的公众号关注社区通知)
|
|
||||||
2. ip2region Github/Gitee 中带有 `[数据源补充]` 标签的 Issue
|
|
||||||
3. 其他自定义数据:例如客户提供的数据,或者通过 GPS 和 WIFI 定位得到的数据,或者来自其他平台的合法合规的数据
|
|
||||||
|
|
||||||
原始 IP 数据编辑工具使用方法请参考如下的 `maker` 生成程序下的 ReadMe 文档:
|
|
||||||
| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 | 贡献者 |
|
|
||||||
|:------------------------------------|:-------------------------|:-------------------|:-------------------|:-------------------------------------------|
|
|
||||||
| [Golang](maker/golang#xdb-数据编辑) | golang IP 原始数据编辑器 | :white_check_mark: | :white_check_mark: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [Java](maker/java#xdb-数据编辑) | java IP 原始数据编辑器 | :white_check_mark: | :soon: | [Lion](https://github.com/lionsoul2014) |
|
|
||||||
| [C++](maker/cpp#xdb-数据编辑) | C++ IP 原始数据编辑器 | :white_check_mark: | :x: | [Yunbin Liu](https://github.com/liuyunbin) |
|
|
||||||
|
|
||||||
|
|
||||||
### 检测自动更新
|
|
||||||
如果你想通过你自己的 API 或数据源来更新数据,你可以参考以下视频分享的 `基于检测算法` 的更新算法来自己编写一个更新程序:
|
|
||||||
1. [数据更新实现视频分享 - part1](https://www.bilibili.com/video/BV1934y1E7Q5/)
|
|
||||||
2. [数据更新实现视频分享 - part2](https://www.bilibili.com/video/BV1pF411j7Aw/)
|
|
||||||
|
|
||||||
# 官方社区
|
|
||||||
Ip2Region 官方社区正式上线于 `2025/06/12` 日,一方面提供了稳定的 [商用离线数据](https://ip2region.net/products/offline) 服务,另一方面便于在核心代码外强化 IP 工具链和数据服务,例如 [使用文档](https://ip2region.net/doc/),[查询测试](https://ip2region.net/search/demo),数据纠错等,更多关于社区的信息和服务请访问 [Ip2Region 官方社区](https://ip2region.net/)。
|
|
||||||
|
|
||||||
# 相关备注
|
|
||||||
|
|
||||||
### 1、并发查询必读
|
|
||||||
xdb 整个缓存的查询都 <b>是</b> 并发安全的,基于文件的查询都 <b>不是</b> 并发安全的实现,不同进程/线程/协程需要通过创建不同的查询对象来安全使用,并发量很大的情况下,基于文件查询的方式可能会导致打开文件数过多的错误,请修改内核的最大允许打开文件数(fs.file-max=一个更高的值),或者将整个xdb加载到内存进行安全并发使用。
|
|
||||||
|
|
||||||
### 2、技术资源分享
|
|
||||||
1. xdb 数据结构分析:[“ip2region xdb-数据结构描述“](https://ip2region.net/doc/xdb/ipv4_structure)
|
|
||||||
2. xdb 查询过程分析:[“ip2region xdb-查询过程描述”](https://ip2region.net/doc/xdb/ipv4_search)
|
|
||||||
3. xdb 生成过程分析:[“ip2region xdb-生成过程描述”](https://ip2region.net/doc/xdb/ipv4_generate)
|
|
||||||
4. xdb 文件生成教程:[“ip2region xdb-文件生成教程”](https://ip2region.net/doc/data/ipv4_xdb_make)
|
|
||||||
5. xdb 数据更新方法:[“ip2region 数据更新和 xdb 数据编辑器的使用”](https://mp.weixin.qq.com/s/cZH5qIn4E5rQFy6N32RCzA)
|
|
||||||
|
|
||||||
### 3、技术信息博客
|
|
||||||
1. [Ip2Region 官方社区](https://ip2region.net)
|
|
||||||
2. 请先关注微信公众号 lionsoul-org (狮子的魂)
|
|
||||||
|
|
@ -1,24 +1,27 @@
|
||||||
all: xdb_searcher util_test
|
all: xdb_searcher test_util
|
||||||
|
|
||||||
xdb_searcher: xdb_searcher.h xdb_searcher.c main.c
|
xdb_searcher: xdb_api.h xdb_util.c xdb_searcher.c main.c
|
||||||
gcc -O2 -I./ xdb_searcher.c main.c -o xdb_searcher
|
gcc -std=c99 -Wall -O2 -I./ xdb_util.c xdb_searcher.c main.c -o xdb_searcher
|
||||||
|
|
||||||
util_test: xdb_searcher.h xdb_searcher.c util_test.c
|
test_util: xdb_api.h xdb_util.c test_util.c
|
||||||
gcc -O2 -I./ xdb_searcher.c util_test.c -o util_test
|
gcc -std=c99 -Wall -O2 -I./ xdb_util.c test_util.c -o test_util
|
||||||
|
|
||||||
xdb_searcher.o: xdb_searcher.c
|
xdb_searcher.o: xdb_searcher.c
|
||||||
gcc -c -o xdb_searcher.o xdb_searcher.c
|
gcc -std=c99 -Wall -c xdb_searcher.c
|
||||||
|
|
||||||
xdb_searcher_lib: xdb_searcher.o
|
xdb_util.o: xdb_util.c
|
||||||
|
gcc -std=c99 -Wall -c xdb_util.c
|
||||||
|
|
||||||
|
xdb_searcher_lib: xdb_util.o xdb_searcher.o
|
||||||
mkdir -p build/lib
|
mkdir -p build/lib
|
||||||
mkdir -p build/include
|
mkdir -p build/include
|
||||||
ar -rc build/lib/libxdb_searcher.a `find . -name *.o`
|
ar -rc build/lib/libxdb.a `find . -name "*.o"`
|
||||||
cp xdb_searcher.h build/include
|
cp xdb_api.h build/include
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find ./ -name \*.o | xargs rm -f
|
find ./ -name \*.o | xargs rm -f
|
||||||
find ./ -name util_test | xargs rm -f
|
find ./ -name test_util | xargs rm -f
|
||||||
find ./ -name xdb_searcher | xargs rm -f
|
find ./ -name xdb_searcher | xargs rm -f
|
||||||
rm -rf build
|
rm -rf build
|
||||||
|
|
||||||
.PHONY: all clean xdb_searcher util_test
|
.PHONY: all clean xdb_searcher test_util
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
|
# ip2region c Query Client
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
### About Query API
|
||||||
|
|
||||||
|
The prototype of the Query API is as follows:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Query via string IP
|
||||||
|
int xdb_search_by_string(xdb_searcher_t *, const string_ip_t *, xdb_region_buffer_t *);
|
||||||
|
// Query via binary IP returned by xdb_parse_ip
|
||||||
|
int xdb_search(xdb_searcher_t *, const bytes_ip_t *, int, xdb_region_buffer_t *);
|
||||||
|
```
|
||||||
|
|
||||||
|
If the query fails, a non-`0` error code will be returned. If the query is successful, the `region` information string can be obtained from `xdb_region_buffer_t`. If the input IP cannot be found, `xdb_region_buffer_t` will receive an empty string `""`.
|
||||||
|
|
||||||
|
### About IPv4 and IPv6
|
||||||
|
|
||||||
|
This xdb query client implementation supports both IPv4 and IPv6 queries. The usage is as follows:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "xdb_api.h";
|
||||||
|
|
||||||
|
// For IPv4: Set xdb path to the v4 xdb file, specify IP version as IPv4
|
||||||
|
const char *db_path = "../../data/ip2region_v4.xdb"; // or your ipv4 xdb path
|
||||||
|
xdb_version_t *version = XDB_IPv4;
|
||||||
|
|
||||||
|
// For IPv6: Set xdb path to the v6 xdb file, specify IP version as IPv6
|
||||||
|
const char *db_path = "../../data/ip2region_v6.xdb"; // or your ipv6 xdb path
|
||||||
|
xdb_version_t *version = XDB_IPv6;
|
||||||
|
|
||||||
|
// The IP version of the xdb specified by db_path must be consistent with the version, otherwise an error will occur during query execution
|
||||||
|
// Note: The following demonstration directly uses db_path and version variables
|
||||||
|
```
|
||||||
|
|
||||||
|
### XDB File Verification
|
||||||
|
|
||||||
|
It is recommended that you proactively verify the applicability of the xdb file, as some future new features may cause the current Searcher version to be incompatible with the xdb file you are using. Verification can avoid unpredictable errors during runtime. You do not need to verify every time; for example, verify when the service starts or manually call a command to confirm version matching. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "xdb_api.h";
|
||||||
|
|
||||||
|
int errcode = xdb_verify(db_path);
|
||||||
|
if ($err != 0) {
|
||||||
|
// Applicability verification failed!!!
|
||||||
|
// The current query client implementation is not suitable for querying the xdb file specified by db_path.
|
||||||
|
// You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation compatible with db_path.
|
||||||
|
printf("failed to verify xdb file `%s`, errcode: %d\n", db_path, errcode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verification passed, the current Searcher can be safely used for query operations on the xdb pointed to by dbPath
|
||||||
|
```
|
||||||
|
|
||||||
|
### File-Based Query
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "xdb_api.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
xdb_searcher_t searcher;
|
||||||
|
char region_buffer[512] = {'\0'};
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
|
||||||
|
// Initialize region_buffer_t using region_buffer from stack space
|
||||||
|
int err = xdb_region_buffer_init(®ion, region_buffer, sizeof(region_buffer));
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the region buffer with errcode=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize winsock when the service starts; no need to call repeatedly, only needed on Windows systems
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Initialize xdb query object from db_path.
|
||||||
|
// @Note: Use the db_path and version described above to create the searcher
|
||||||
|
err = xdb_new_with_file_only(version, &searcher, db_path);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to create xdb searcher from `%s` with errno=%d\n", db_path, err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Call search API to query, both IPv4 and IPv6 are supported.
|
||||||
|
const char *ip_string = "1.2.3.4";
|
||||||
|
// ip_string = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; // IPv6
|
||||||
|
|
||||||
|
long cost_time = 0, s_time = xdb_now();
|
||||||
|
err = xdb_search_by_string(&searcher, ip_string, ®ion);
|
||||||
|
cost_time = (int) (xdb_now() - s_time);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed search(%s) with errno=%d\n", ip_string, err);
|
||||||
|
} else {
|
||||||
|
printf("{region: %s, took: %d μs}", region.value, cost_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up memory resources for region info; must be called after every search
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
|
||||||
|
// Note: For concurrent use, each thread needs to define and initialize its own searcher query object independently.
|
||||||
|
|
||||||
|
// 3. Close xdb searcher
|
||||||
|
xdb_close(&searcher);
|
||||||
|
xdb_clean_winsock(); // Call on Windows
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching `VectorIndex`
|
||||||
|
|
||||||
|
We can pre-load VectorIndex data from the xdb file and cache it globally. Using the global VectorIndex cache every time a Searcher object is created can reduce a fixed IO operation, thereby accelerating queries and reducing IO pressure.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "xdb_api.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
xdb_vector_index_t *v_index;
|
||||||
|
xdb_searcher_t searcher;
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
|
||||||
|
// Initialize region_buffer with NULL to let it manage memory allocation automatically
|
||||||
|
int err = xdb_region_buffer_init(®ion, NULL, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the region buffer with errcode=%d\n", err);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize winsock when the service starts; no need to call repeatedly, only needed on Windows systems
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Load VectorIndex from the db_path described above.
|
||||||
|
// Obtain v_index to create a global cache for subsequent repeated use.
|
||||||
|
// Note: v_index does not need to be loaded every time; it is recommended to load it once at service startup as a global resource.
|
||||||
|
v_index = xdb_load_vector_index_from_file(db_path);
|
||||||
|
if (v_index == NULL) {
|
||||||
|
printf("failed to load vector index from `%s`\n", db_path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Use the global VectorIndex variable to create an xdb searcher with VectorIndex cache.
|
||||||
|
// @Note: Use the db_path and version described above to create the searcher
|
||||||
|
err = xdb_new_with_vector_index(version, &searcher, db_path, v_index);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to create vector index cached searcher with errcode=%d\n", err);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3. Call search API to query, both IPv4 and IPv6 are supported
|
||||||
|
const char *ip_string = "1.2.3.4";
|
||||||
|
// ip_string = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; // IPv6
|
||||||
|
|
||||||
|
long cost_time = 0, s_time = xdb_now();
|
||||||
|
err = xdb_search_by_string(&searcher, ip_string, ®ion);
|
||||||
|
cost_time = (int) (xdb_now() - s_time);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed search(%s) with errno=%d\n", ip_string, err);
|
||||||
|
} else {
|
||||||
|
printf("{region: %s, took: %d μs}", region.value, cost_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up memory resources for region info; must be called after every search
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
|
||||||
|
|
||||||
|
// Note: For concurrent use, each thread needs to define and initialize its own searcher query object independently.
|
||||||
|
|
||||||
|
// 4. Close xdb searcher; if the service is being shut down, the memory for v_index also needs to be freed.
|
||||||
|
xdb_close(&searcher);
|
||||||
|
xdb_close_vector_index(v_index);
|
||||||
|
xdb_clean_winsock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching the Entire `xdb` File
|
||||||
|
|
||||||
|
We can also pre-load the entire xdb file into memory and then create a query object based on this data to achieve fully memory-based queries, similar to the previous memory search.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "xdb_api.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
xdb_content_t *c_buffer;
|
||||||
|
xdb_searcher_t searcher;
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
|
||||||
|
// Initialize region_buffer with NULL to let it manage memory allocation automatically
|
||||||
|
int err = xdb_region_buffer_init(®ion, NULL, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the region buffer with errcode=%d\n", err);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize winsock when the service starts; no need to call repeatedly, only needed on Windows systems
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Load the entire xdb data from the db_path described above.
|
||||||
|
c_buffer = xdb_load_content_from_file(db_path);
|
||||||
|
if (v_index == NULL) {
|
||||||
|
printf("failed to load xdb content from `%s`\n", db_path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Use the global c_buffer variable to create a fully memory-based xdb query object.
|
||||||
|
// @Note: Use the version described above to create the searcher.
|
||||||
|
err = xdb_new_with_buffer(version, &searcher, c_buffer);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to create content cached searcher with errcode=%d\n", err);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Call search API to query, both IPv4 and IPv6 are supported
|
||||||
|
const char *ip_string = "1.2.3.4";
|
||||||
|
// ip_string = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; // IPv6
|
||||||
|
|
||||||
|
long cost_time = 0, s_time = xdb_now();
|
||||||
|
err = xdb_search_by_string(&searcher, ip_string, ®ion);
|
||||||
|
cost_time = (int) (xdb_now() - s_time);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed search(%s) with errno=%d\n", ip_string, err);
|
||||||
|
} else {
|
||||||
|
printf("{region: %s, took: %d μs}", region.value, cost_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up memory resources for region info; must be called after every search
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
|
||||||
|
|
||||||
|
// Note: For concurrent use, xdb query objects created this way can be safely used for concurrency.
|
||||||
|
// It is recommended to create them when the service starts and then use them safely in parallel until the service shuts down.
|
||||||
|
|
||||||
|
// 4. Close xdb searcher; memory for c_buffer needs to be freed when shutting down the service.
|
||||||
|
xdb_close(&searcher);
|
||||||
|
xdb_close_content(c_buffer);
|
||||||
|
xdb_clean_winsock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### About Storage of Location Information
|
||||||
|
|
||||||
|
In older implementations, search-related functions relied on a specified `region_buffer` memory to store location information, which had significant limitations.
|
||||||
|
The new implementation provides an `xdb_region_buffer_t` object to manage these memory allocations. You can still specify a fixed `region_buffer` to create memory management for the region; this is suitable when the maximum length of your location information is known, which helps reduce memory fragmentation during runtime. If the length of the location information is uncertain or if your program is not suited for pre-allocating a block of memory, you can initialize `xdb_region_buffer_t` by specifying `NULL`. In this case, the object will automatically manage memory allocation, making it suitable for storing location information of any length, though this approach will certainly increase memory fragmentation over long-term operation.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 1. Create region_buffer by specifying a memory block
|
||||||
|
char buffer[512];
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
int err = xdb_region_buffer_init(®ion, buffer, sizeof(buffer));
|
||||||
|
if (err != 0) {
|
||||||
|
// Initialization failed
|
||||||
|
printf("failed to init region buffer width errcode=%d", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create region_buffer by specifying NULL to let it allocate memory as needed automatically
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
int err = xdb_region_buffer_init(®ion, NULL, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
// Initialization failed
|
||||||
|
printf("failed to init region buffer width errcode=%d", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Note: After each query call, you must manually call the function to free memory.
|
||||||
|
// The search function will report an error if used with uncleaned region info.
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
```
|
||||||
|
|
||||||
|
# Compiling the Test Program
|
||||||
|
|
||||||
|
Compile and obtain the `xdb_searcher` executable as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# cd to the c binding root directory
|
||||||
|
➜ c git:(master) ✗ make
|
||||||
|
gcc -std=c99 -Wall -O2 -I./ xdb_util.c xdb_searcher.c main.c -o xdb_searcher
|
||||||
|
gcc -std=c99 -Wall -O2 -I./ xdb_util.c test_util.c -o test_util
|
||||||
|
```
|
||||||
|
|
||||||
|
# Query Testing
|
||||||
|
|
||||||
|
Test queries against xdb via the `xdb_searcher search` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher search
|
||||||
|
./xdb_searcher search [command options]
|
||||||
|
options:
|
||||||
|
--db string ip2region binary xdb file path
|
||||||
|
--cache-policy string cache policy: file/vectorIndex/content
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: performing IPv4 query testing using the default data/ip2region_v4.xdb:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v4.xdb
|
||||||
|
ip2region xdb searcher test program
|
||||||
|
source xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)
|
||||||
|
type 'quit' to exit
|
||||||
|
ip2region>> 1.2.3.4
|
||||||
|
{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 39 μs}
|
||||||
|
ip2region>> 120.229.45.2
|
||||||
|
{region: 中国|广东省|深圳市|移动|CN, io_count: 3, took: 13 μs}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: performing IPv6 query testing using the default data/ip2region_v6.xdb:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v6.xdb
|
||||||
|
ip2region xdb searcher test program
|
||||||
|
source xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)
|
||||||
|
type 'quit' to exit
|
||||||
|
ip2region>> ::
|
||||||
|
{region: , io_count: 1, took: 38 μs}
|
||||||
|
ip2region>> 2604:bc80:8001:11a4:ffff:ffff:ffff:ffff
|
||||||
|
{region: United States|Florida|Miami|velia.net Internetdienste GmbH|US, io_count: 14, took: 76 μs}
|
||||||
|
ip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e
|
||||||
|
{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 42 μs}
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter an IP to perform a query; enter `quit` to exit the test program. You can also set `cache-policy` to file/vectorIndex/content respectively to test the efficiency of the three different cache implementations.
|
||||||
|
|
||||||
|
# bench Testing
|
||||||
|
|
||||||
|
Perform bench testing via the `xdb_searcher bench` command. This ensures there are no errors in the query program and the `xdb` file, while also providing average query performance through a large number of queries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher bench
|
||||||
|
./xdb_searcher bench [command options]
|
||||||
|
options:
|
||||||
|
--db string ip2region binary xdb file path
|
||||||
|
--src string source ip text file path
|
||||||
|
--cache-policy string cache policy: file/vectorIndex/content
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: performing IPv4 bench testing via the default data/ip2region_v4.xdb and data/ipv4_source.txt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt
|
||||||
|
Bench finished, {cache_policy: vectorIndex, total: 1367686, took: 7.640s, cost: 5 μs/op}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: performing IPv6 bench testing via the default data/ip2region_v6.xdb and data/ipv6_source.txt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt
|
||||||
|
Bench finished, {cache_policy: vectorIndex, total: 34159862, took: 857.750s, cost: 24 μs/op}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can set the `cache-policy` parameter to test the efficiency of different cache mechanisms (file/vectorIndex/content). @Note: Please ensure that the `src` file used for benching is the same source file used to generate the corresponding `xdb` file.
|
||||||
|
|
@ -0,0 +1,357 @@
|
||||||
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
|
# ip2region c 查询客户端
|
||||||
|
|
||||||
|
|
||||||
|
# 使用方式
|
||||||
|
|
||||||
|
### 关于查询 API
|
||||||
|
查询 API 的原型如下:
|
||||||
|
```c
|
||||||
|
// 通过字符串 IP 进行查询
|
||||||
|
int xdb_search_by_string(xdb_searcher_t *, const string_ip_t *, xdb_region_buffer_t *);
|
||||||
|
// 通过 xdb_parse_ip 返回的二进制 IP 进行查询
|
||||||
|
int xdb_search(xdb_searcher_t *, const bytes_ip_t *, int, xdb_region_buffer_t *);
|
||||||
|
```
|
||||||
|
如果查询失败将会返回非 `0` 的错误代码,如果查询成功 xdb_region_buffer_t 可以获取到字符串的 `region` 信息,如果输入的 IP 找不到相关的信息,xdb_region_buffer_t 将会得到一个空的字符串 `""`。
|
||||||
|
|
||||||
|
### 关于 IPv4 和 IPv6
|
||||||
|
该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询,使用方式如下:
|
||||||
|
```c
|
||||||
|
#include "xdb_api.h";
|
||||||
|
|
||||||
|
// 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件,IP版本指定为 IPv4
|
||||||
|
const char *db_path = "../../data/ip2region_v4.xdb"; // 或者你的 ipv4 xdb 的路径
|
||||||
|
xdb_version_t *version = XDB_IPv4;
|
||||||
|
|
||||||
|
// 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件,IP版本指定为 IPv6
|
||||||
|
const char *db_path = "../../data/ip2region_v6.xdb"; // 或者你的 ipv6 xdb 路径
|
||||||
|
xdb_version_t *version = XDB_IPv6;
|
||||||
|
|
||||||
|
// db_path 指定的 xdb 的 IP 版本必须和 version 指定的一致,不然查询执行的时候会报错
|
||||||
|
// 备注:以下演示直接使用 db_path 和 version 变量
|
||||||
|
```
|
||||||
|
|
||||||
|
### XDB 文件验证
|
||||||
|
建议您主动去验证 xdb 文件的适用性,因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件,验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证,例如在服务启动的时候,或者手动调用命令验证确认版本匹配即可,不要在每次创建的 Searcher 的时候运行验证,这样会影响查询的响应速度,尤其是高并发的使用场景。
|
||||||
|
```c
|
||||||
|
#include "xdb_api.h";
|
||||||
|
|
||||||
|
int errcode = xdb_verify(db_path);
|
||||||
|
if ($err != 0) {
|
||||||
|
// 适用性验证失败!!!
|
||||||
|
// 当前查询客户端实现不适用于 db_path 指定的 xdb 文件的查询.
|
||||||
|
// 应该停止启动服务,使用合适的 xdb 文件或者升级到适合 db_path 的 Searcher 实现。
|
||||||
|
printf("failed to verify xdb file `%s`, errcode: %d\n", db_path, errcode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通过,当前使用的 Searcher 可以安全的用于对 dbPath 指向的 xdb 的查询操作
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完全基于文件的查询
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "xdb_api.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
xdb_searcher_t searcher;
|
||||||
|
char region_buffer[512] = {'\0'};
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
|
||||||
|
// 使用栈空间的 region_buffer 初始化 region_buffer_t
|
||||||
|
int err = xdb_region_buffer_init(®ion, region_buffer, sizeof(region_buffer));
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the region buffer with errcode=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在服务启动的时候初始化 winsock,不需要重复调用,只需要在 windows 系统下调用
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1、从 db_path 初始化 xdb 查询对象.
|
||||||
|
// @Note: 使用顶部描述的 db_path 和 version 来创建 searcher
|
||||||
|
err = xdb_new_with_file_only(version, &searcher, db_path);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to create xdb searcher from `%s` with errno=%d\n", db_path, err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2、调用 search API 查询,IPv4 和 IPv6 都支持.
|
||||||
|
const char *ip_string = "1.2.3.4";
|
||||||
|
// ip_string = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; // IPv6
|
||||||
|
|
||||||
|
long cost_time = 0, s_time = xdb_now();
|
||||||
|
err = xdb_search_by_string(&searcher, ip_string, ®ion);
|
||||||
|
cost_time = (int) (xdb_now() - s_time);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed search(%s) with errno=%d\n", ip_string, err);
|
||||||
|
} else {
|
||||||
|
printf("{region: %s, took: %d μs}", region.value, cost_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 region 信息的内存资源,每次 search 之后都得调用
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
|
||||||
|
// 备注:并发使用,每一个线程需要单独定义并且初始化一个 searcher 查询对象。
|
||||||
|
|
||||||
|
// 3、关闭 xdb 查询器
|
||||||
|
xdb_close(&searcher);
|
||||||
|
xdb_clean_winsock(); // windows 下调用
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存 `VectorIndex` 索引
|
||||||
|
|
||||||
|
我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "xdb_api.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
xdb_vector_index_t *v_index;
|
||||||
|
xdb_searcher_t searcher;
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
|
||||||
|
// 使用 NULL 初始化 region_buffer,让其自动管理内存的分配
|
||||||
|
int err = xdb_region_buffer_init(®ion, NULL, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the region buffer with errcode=%d\n", err);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在服务启动的时候初始化 winsock,不需要重复调用,只需要在 windows 系统下调用
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1、从顶部描述的 db_path 加载 VectorIndex 索引。
|
||||||
|
// 得到 v_index 做成全局缓存,便于后续反复使用。
|
||||||
|
// 注意:v_index 不需要每次都加载,建议在服务启动的时候加载一次,然后做成全局资源。
|
||||||
|
v_index = xdb_load_vector_index_from_file(db_path);
|
||||||
|
if (v_index == NULL) {
|
||||||
|
printf("failed to load vector index from `%s`\n", db_path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2、使用全局的 VectorIndex 变量创建带 VectorIndex 缓存的 xdb 查询对象.
|
||||||
|
// @Note: 使用顶部描述的 db_path 和 version 来创建 searcher
|
||||||
|
err = xdb_new_with_vector_index(version, &searcher, db_path, v_index);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to create vector index cached searcher with errcode=%d\n", err);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3、调用 search API 查询,IPv4 和 IPv6 都支持
|
||||||
|
const char *ip_string = "1.2.3.4";
|
||||||
|
// ip_string = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; // IPv6
|
||||||
|
|
||||||
|
long cost_time = 0, s_time = xdb_now();
|
||||||
|
err = xdb_search_by_string(&searcher, ip_string, ®ion);
|
||||||
|
cost_time = (int) (xdb_now() - s_time);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed search(%s) with errno=%d\n", ip_string, err);
|
||||||
|
} else {
|
||||||
|
printf("{region: %s, took: %d μs}", region.value, cost_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 region 信息的内存资源,每次 search 之后都得调用
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
|
||||||
|
|
||||||
|
// 备注:并发使用,每一个线程需要单独定义并且初始化一个 searcher 查询对象。
|
||||||
|
|
||||||
|
// 4、关闭 xdb 查询器,如果是要关闭服务,也需要释放 v_index 的内存。
|
||||||
|
xdb_close(&searcher);
|
||||||
|
xdb_close_vector_index(v_index);
|
||||||
|
xdb_clean_winsock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存整个 `xdb` 文件
|
||||||
|
|
||||||
|
我们也可以预先加载整个 xdb 文件到内存,然后基于这个数据创建查询对象来实现完全基于内存的查询,类似之前的 memory search。
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "xdb_api.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
xdb_content_t *c_buffer;
|
||||||
|
xdb_searcher_t searcher;
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
|
||||||
|
// 使用 NULL 初始化 region_buffer,让其自动管理内存的分配
|
||||||
|
int err = xdb_region_buffer_init(®ion, NULL, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the region buffer with errcode=%d\n", err);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 在服务启动的时候初始化 winsock,不需要重复调用,只需要在 windows 系统下调用
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1、从 顶部描述的 db_path 加载整个 xdb 的数据。
|
||||||
|
c_buffer = xdb_load_content_from_file(db_path);
|
||||||
|
if (v_index == NULL) {
|
||||||
|
printf("failed to load xdb content from `%s`\n", db_path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2、使用全局的 c_buffer 变量创建一个完全基于内存的 xdb 查询对象.
|
||||||
|
// @Note: 使用顶部描述的 version 来创建 searcher.
|
||||||
|
err = xdb_new_with_buffer(version, &searcher, c_buffer);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to create content cached searcher with errcode=%d\n", err);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3、调用 search API 查询,IPv4 和 IPv6 都支持
|
||||||
|
const char *ip_string = "1.2.3.4";
|
||||||
|
// ip_string = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; // IPv6
|
||||||
|
|
||||||
|
long cost_time = 0, s_time = xdb_now();
|
||||||
|
err = xdb_search_by_string(&searcher, ip_string, ®ion);
|
||||||
|
cost_time = (int) (xdb_now() - s_time);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed search(%s) with errno=%d\n", ip_string, err);
|
||||||
|
} else {
|
||||||
|
printf("{region: %s, took: %d μs}", region.value, cost_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 region 信息的内存资源,每次 search 之后都得调用
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
|
||||||
|
|
||||||
|
// 备注:并发使用,使用这种方式创建的 xdb 查询对象可以安全用于并发。
|
||||||
|
// 建议在服务启动的时候创建好,然后一直安全并发使用,直到服务关闭。
|
||||||
|
|
||||||
|
// 4、关闭 xdb 查询器,关闭服务的时候需要释放 c_buffer 的内存。
|
||||||
|
xdb_close(&searcher);
|
||||||
|
xdb_close_content(c_buffer);
|
||||||
|
xdb_clean_winsock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关于定位信息的存储
|
||||||
|
在旧版本的实现中,search相关的函数都是依靠指定一个 `region_buffer` 内存来用于存储地域信息,这种方式还是有很大的局限性。
|
||||||
|
新的实现提供了一个 `xdb_region_buffer_t` 对象来管理这些内存的分配,你依然可以指定一个固定的 `region_buffer` 来创建 region 的内存管理,这个情况适合当你的地域信息的最大长度是可知的,这种方式可以减少运行过程中内存的碎片堆积。如果地域信息的长度不确定或者你的程序不适合提前分配一块内存来管理,你可以通过指定 NULL 的方式来初始化 `xdb_region_buffer_t`,这样对象会自动管理内存的分配,也适合任意长度的地域信息的存储,不过这种方式在长期的运行过程中肯定会增加内存碎片的堆积。
|
||||||
|
```c
|
||||||
|
// 1, 通过指定一块内存来创建 region_buffer
|
||||||
|
char buffer[512];
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
int err = xdb_region_buffer_init(®ion, buffer, sizeof(buffer));
|
||||||
|
if (err != 0) {
|
||||||
|
// 初始化失败
|
||||||
|
printf("failed to init region buffer width errcode=%d", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2,通过指定 NULL 来创建 region_buffer,让其自动按需分配内存
|
||||||
|
xdb_region_buffer_t region;
|
||||||
|
int err = xdb_region_buffer_init(®ion, NULL, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
// 初始化失败
|
||||||
|
printf("failed to init region buffer width errcode=%d", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 备注:在每次调用 search 完成 IP 定位信息的查询后,你需要手动调用函数来释放内存 .
|
||||||
|
// search 函数使用未经清理的 region 信息会报错。
|
||||||
|
xdb_region_buffer_free(®ion);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# 测试程序编译
|
||||||
|
|
||||||
|
通过如下方式编译得到 xdb_searcher 可执行程序:
|
||||||
|
```bash
|
||||||
|
# cd 到 c binding 根目录
|
||||||
|
➜ c git:(master) ✗ make
|
||||||
|
gcc -std=c99 -Wall -O2 -I./ xdb_util.c xdb_searcher.c main.c -o xdb_searcher
|
||||||
|
gcc -std=c99 -Wall -O2 -I./ xdb_util.c test_util.c -o test_util
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# 查询测试
|
||||||
|
|
||||||
|
通过 `xdb_searcher search` 命令来测试对 xdb 的查询:
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher search
|
||||||
|
./xdb_searcher search [command options]
|
||||||
|
options:
|
||||||
|
--db string ip2region binary xdb file path
|
||||||
|
--cache-policy string cache policy: file/vectorIndex/content
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:使用默认的 data/ip2region_v4.xdb 进行 IPv4 查询测试:
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v4.xdb
|
||||||
|
ip2region xdb searcher test program
|
||||||
|
source xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)
|
||||||
|
type 'quit' to exit
|
||||||
|
ip2region>> 1.2.3.4
|
||||||
|
{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 39 μs}
|
||||||
|
ip2region>> 120.229.45.2
|
||||||
|
{region: 中国|广东省|深圳市|移动|CN, io_count: 3, took: 13 μs}
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:使用默认的 data/ip2region_v6.xdb 进行 IPv6 查询测试:
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v6.xdb
|
||||||
|
ip2region xdb searcher test program
|
||||||
|
source xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)
|
||||||
|
type 'quit' to exit
|
||||||
|
ip2region>> ::
|
||||||
|
{region: , io_count: 1, took: 38 μs}
|
||||||
|
ip2region>> 2604:bc80:8001:11a4:ffff:ffff:ffff:ffff
|
||||||
|
{region: United States|Florida|Miami|velia.net Internetdienste GmbH|US, io_count: 14, took: 76 μs}
|
||||||
|
ip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e
|
||||||
|
{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 42 μs}
|
||||||
|
```
|
||||||
|
|
||||||
|
输入 ip 即可进行查询,输入 quit 即可退出测试程序。也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同的缓存实现的效率。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# bench 测试
|
||||||
|
|
||||||
|
通过 `xdb_searcher bench` 命令来进行 bench 测试,一方面确保查询程序和 `xdb` 文件没有错误,另一方面可以通过大量的查询得到平均的查询性能:
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher bench
|
||||||
|
./xdb_searcher bench [command options]
|
||||||
|
options:
|
||||||
|
--db string ip2region binary xdb file path
|
||||||
|
--src string source ip text file path
|
||||||
|
--cache-policy string cache policy: file/vectorIndex/content
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 来进行 IPv4 的 bench 测试:
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt
|
||||||
|
Bench finished, {cache_policy: vectorIndex, total: 1367686, took: 7.640s, cost: 5 μs/op}
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 来进行 IPv6 的 bench 测试:
|
||||||
|
```bash
|
||||||
|
➜ c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt
|
||||||
|
Bench finished, {cache_policy: vectorIndex, total: 34159862, took: 857.750s, cost: 24 μs/op}
|
||||||
|
```
|
||||||
|
|
||||||
|
可以设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 不同缓存实现机制的效率。 @Note:请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件相同的源文件。
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
# ip2region xdb c 查询客户端实现
|
|
||||||
|
|
||||||
|
|
||||||
# 使用方式
|
|
||||||
|
|
||||||
### 完全基于文件的查询
|
|
||||||
|
|
||||||
```c
|
|
||||||
#include <stdio.h>
|
|
||||||
#include "xdb_searcher.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
char *db_path = "ip2region.xdb file path";
|
|
||||||
xdb_searcher_t searcher;
|
|
||||||
char region_buffer[256], ip_buffer[16], *ip = "1.2.3.4";
|
|
||||||
long s_time;
|
|
||||||
|
|
||||||
// 1、从 db_path 初始化 xdb 查询对象
|
|
||||||
int err = xdb_new_with_file_only(&searcher, db_path);
|
|
||||||
if (err != 0) {
|
|
||||||
printf("failed to create xdb searcher from `%s` with errno=%d\n", db_path, err);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2、调用 search API 查询
|
|
||||||
// 得到的 region 信息会存储到 region_buffer 里面,如果你自定义了数据,请确保给足 buffer 的空间。
|
|
||||||
s_time = xdb_now();
|
|
||||||
err = xdb_search_by_string(&searcher, ip, region_buffer, sizeof(region_buffer));
|
|
||||||
if (err != 0) {
|
|
||||||
printf("failed search(%s) with errno=%d\n", ip, err);
|
|
||||||
} else {
|
|
||||||
printf("{region: %s, took: %d μs}", region_buffer, (int)(xdb_now() - s_time));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 备注:并发使用,没一个线程需要单独定义并且初始化一个 searcher 查询对象。
|
|
||||||
|
|
||||||
// 3、关闭 xdb 查询器
|
|
||||||
xdb_close(&searcher);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 缓存 `VectorIndex` 索引
|
|
||||||
|
|
||||||
我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。
|
|
||||||
```c
|
|
||||||
#include <stdio.h>
|
|
||||||
#include "xdb_searcher.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
char *db_path = "ip2region.xdb file path";
|
|
||||||
xdb_vector_index_t *v_index;
|
|
||||||
xdb_searcher_t searcher;
|
|
||||||
char region_buffer[256], ip_buffer[16], *ip = "1.2.3.4";
|
|
||||||
long s_time;
|
|
||||||
|
|
||||||
// 1、从 db_path 加载 VectorIndex 索引。
|
|
||||||
// 得到 v_index 做成全局缓存,便于后续反复使用。
|
|
||||||
// 注意:v_index 不需要每次都加载,建议在服务启动的时候加载一次,然后做成全局资源。
|
|
||||||
v_index = xdb_load_vector_index_from_file(db_path);
|
|
||||||
if (v_index == NULL) {
|
|
||||||
printf("failed to load vector index from `%s`\n", db_path);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2、使用全局的 VectorIndex 变量创建带 VectorIndex 缓存的 xdb 查询对象
|
|
||||||
int err = xdb_new_with_vector_index(&searcher, db_path, v_index);
|
|
||||||
if (err != 0) {
|
|
||||||
printf("failed to create vector index cached searcher with errcode=%d\n", err);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3、调用 search API 查询
|
|
||||||
// 得到的 region 信息会存储到 region_buffer 里面,如果你自定义了数据,请确保给足 buffer 的空间。
|
|
||||||
s_time = xdb_now();
|
|
||||||
err = xdb_search_by_string(&searcher, ip, region_buffer, sizeof(region_buffer));
|
|
||||||
if (err != 0) {
|
|
||||||
printf("failed search(%s) with errno=%d\n", ip, err);
|
|
||||||
} else {
|
|
||||||
printf("{region: %s, took: %d μs}", region_buffer, (int)(xdb_now() - s_time));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 备注:并发使用,没一个线程需要单独定义并且初始化一个 searcher 查询对象。
|
|
||||||
|
|
||||||
// 4、关闭 xdb 查询器,如果是要关闭服务,也需要释放 v_index 的内存。
|
|
||||||
xdb_close(&searcher);
|
|
||||||
xdb_close_vector_index(v_index);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 缓存整个 `xdb` 数据
|
|
||||||
|
|
||||||
我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。
|
|
||||||
```c
|
|
||||||
#include <stdio.h>
|
|
||||||
#include "xdb_searcher.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
char *db_path = "ip2region.xdb file path";
|
|
||||||
xdb_content_t *c_buffer;
|
|
||||||
xdb_searcher_t searcher;
|
|
||||||
char region_buffer[256], ip_buffer[16], *ip = "1.2.3.4";
|
|
||||||
long s_time;
|
|
||||||
|
|
||||||
// 1、从 db_path 加载整个 xdb 的数据。
|
|
||||||
c_buffer = xdb_load_content_from_file(db_path);
|
|
||||||
if (v_index == NULL) {
|
|
||||||
printf("failed to load xdb content from `%s`\n", db_path);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2、使用全局的 c_buffer 变量创建一个完全基于内存的 xdb 查询对象
|
|
||||||
err = xdb_new_with_buffer(&searcher, c_buffer);
|
|
||||||
if (err != 0) {
|
|
||||||
printf("failed to create content cached searcher with errcode=%d\n", err);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3、调用 search API 查询
|
|
||||||
// 得到的 region 信息会存储到 region_buffer 里面,如果你自定义了数据,请确保给足 buffer 的空间。
|
|
||||||
s_time = xdb_now();
|
|
||||||
err = xdb_search_by_string(&searcher, ip, region_buffer, sizeof(region_buffer));
|
|
||||||
if (err != 0) {
|
|
||||||
printf("failed search(%s) with errno=%d\n", ip, err);
|
|
||||||
} else {
|
|
||||||
printf("{region: %s, took: %d μs}", region_buffer, (int)(xdb_now() - s_time));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 备注:并发使用,使用这种方式创建的 xdb 查询对象可以安全用于并发。
|
|
||||||
// 建议在服务启动的时候创建好,然后一直安全并发使用,直到服务关闭。
|
|
||||||
|
|
||||||
// 4、关闭 xdb 查询器,关闭服务的时候需要释放 c_buffer 的内存。
|
|
||||||
xdb_close(&searcher);
|
|
||||||
xdb_close_content(c_buffer);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# 测试程序编译
|
|
||||||
|
|
||||||
通过如下方式编译得到 xdb_searcher 可执行程序:
|
|
||||||
```bash
|
|
||||||
# cd 到 c binding 根目录
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# 查询测试
|
|
||||||
|
|
||||||
通过 `xdb_searcher search` 命令来测试对 ip2region.xdb 的查询:
|
|
||||||
```bash
|
|
||||||
➜ c git:(c_binding) ✗ ./xdb_searcher search
|
|
||||||
./xdb_searcher search [command options]
|
|
||||||
options:
|
|
||||||
--db string ip2region binary xdb file path
|
|
||||||
--cache-policy string cache policy: file/vectorIndex/content
|
|
||||||
```
|
|
||||||
|
|
||||||
例如:使用默认的 data/ip2region.xdb 进行查询测试:
|
|
||||||
```bash
|
|
||||||
➜ c git:(c_binding) ✗ ./xdb_searcher search --db=../../data/ip2region.xdb --cache-policy=vectorIndex
|
|
||||||
ip2region xdb searcher test program, cache_policy: vectorIndex
|
|
||||||
type 'quit' to exit
|
|
||||||
ip2region>> 1.2.3.4
|
|
||||||
{region: 美国|0|华盛顿|0|谷歌, io_count: 7, took: 13 μs}
|
|
||||||
ip2region>>
|
|
||||||
```
|
|
||||||
|
|
||||||
输入 ip 即可进行查询,输入 quit 即可退出测试程序。也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同的缓存实现的效率。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# bench 测试
|
|
||||||
|
|
||||||
通过 `xdb_searcher bench` 命令来进行 bench 测试,一方面确保查询程序和 `xdb` 文件没有错误,另一方面可以通过大量的查询得到评价的查询性能:
|
|
||||||
```bash
|
|
||||||
➜ c git:(c_binding) ✗ ./xdb_searcher bench
|
|
||||||
./xdb_searcher bench [command options]
|
|
||||||
options:
|
|
||||||
--db string ip2region binary xdb file path
|
|
||||||
--src string source ip text file path
|
|
||||||
--cache-policy string cache policy: file/vectorIndex/content
|
|
||||||
```
|
|
||||||
|
|
||||||
例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 来进行 bench 测试:
|
|
||||||
```bash
|
|
||||||
➜ c git:(c_binding) ✗ ./xdb_searcher bench --db=../../data/ip2region.xdb --src=../../data/ip.merge.txt --cache-policy=vectorIndex
|
|
||||||
Bench finished, {cache_policy: vectorIndex, total: 3417955, took: 4.233s, cost: 1 μs/op}
|
|
||||||
```
|
|
||||||
|
|
||||||
可以设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 不同缓存实现机制的效率。 @Note:请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件相同的源文件。
|
|
||||||
181
binding/c/main.c
181
binding/c/main.c
|
|
@ -7,56 +7,112 @@
|
||||||
// @Date 2022/06/28
|
// @Date 2022/06/28
|
||||||
|
|
||||||
#include "stdio.h"
|
#include "stdio.h"
|
||||||
#include "xdb_searcher.h"
|
#include "xdb_api.h"
|
||||||
|
|
||||||
struct searcher_test_entry {
|
struct searcher_test_entry {
|
||||||
xdb_searcher_t searcher;
|
xdb_searcher_t searcher;
|
||||||
xdb_vector_index_t *v_index;
|
xdb_vector_index_t *v_index;
|
||||||
xdb_content_t *c_buffer;
|
xdb_content_t *c_buffer;
|
||||||
|
|
||||||
|
// xdb region buffer
|
||||||
|
// char region_buffer[256];
|
||||||
|
xdb_region_buffer_t region;
|
||||||
};
|
};
|
||||||
typedef struct searcher_test_entry searcher_test_t;
|
typedef struct searcher_test_entry searcher_test_t;
|
||||||
|
|
||||||
int init_searcher_test(searcher_test_t *test, char *db_path, char *cache_policy) {
|
int init_searcher_test(searcher_test_t *test, char *db_path, char *cache_policy) {
|
||||||
int err;
|
int err, errcode = 0;
|
||||||
|
FILE *handle = fopen(db_path, "rb");
|
||||||
|
if (handle == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto detect the version from the xdb header
|
||||||
|
xdb_header_t *header = xdb_load_header(handle);
|
||||||
|
if (header == NULL) {
|
||||||
|
printf("failed to load header from `%s`\n", db_path);
|
||||||
|
errcode = 1;
|
||||||
|
goto defer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the current xdb
|
||||||
|
err = xdb_verify_from_header(handle, header);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to verify xdb file `%s` with errno=%d\n", db_path, err);
|
||||||
|
errcode = 2;
|
||||||
|
goto defer;
|
||||||
|
}
|
||||||
|
|
||||||
|
xdb_version_t *version = xdb_version_from_header(header);
|
||||||
|
if (version == NULL) {
|
||||||
|
printf("failed to load version from header\n");
|
||||||
|
errcode = 3;
|
||||||
|
goto defer;
|
||||||
|
}
|
||||||
|
|
||||||
test->v_index = NULL;
|
test->v_index = NULL;
|
||||||
test->c_buffer = NULL;
|
test->c_buffer = NULL;
|
||||||
|
|
||||||
if (strcmp(cache_policy, "file") == 0) {
|
if (strcmp(cache_policy, "file") == 0) {
|
||||||
err = xdb_new_with_file_only(&test->searcher, db_path);
|
err = xdb_new_with_file_only(version, &test->searcher, db_path);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
printf("failed to create searcher with errcode=%d\n", err);
|
printf("failed to create searcher with errcode=%d\n", err);
|
||||||
return 1;
|
errcode = 4;
|
||||||
|
goto defer;
|
||||||
}
|
}
|
||||||
} else if (strcmp(cache_policy, "vectorIndex") == 0) {
|
} else if (strcmp(cache_policy, "vectorIndex") == 0) {
|
||||||
test->v_index = xdb_load_vector_index_from_file(db_path);
|
test->v_index = xdb_load_vector_index_from_file(db_path);
|
||||||
if (test->v_index == NULL) {
|
if (test->v_index == NULL) {
|
||||||
printf("failed to load vector index from `%s`\n", db_path);
|
printf("failed to load vector index from `%s`\n", db_path);
|
||||||
return 2;
|
errcode = 4;
|
||||||
|
goto defer;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = xdb_new_with_vector_index(&test->searcher, db_path, test->v_index);
|
err = xdb_new_with_vector_index(version, &test->searcher, db_path, test->v_index);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
printf("failed to create vector index cached searcher with errcode=%d\n", err);
|
printf("failed to create vector index cached searcher with errcode=%d\n", err);
|
||||||
return 3;
|
errcode = 5;
|
||||||
|
goto defer;
|
||||||
}
|
}
|
||||||
} else if (strcmp(cache_policy, "content") == 0) {
|
} else if (strcmp(cache_policy, "content") == 0) {
|
||||||
test->c_buffer = xdb_load_content_from_file(db_path);
|
test->c_buffer = xdb_load_content_from_file(db_path);
|
||||||
if (test->c_buffer == NULL) {
|
if (test->c_buffer == NULL) {
|
||||||
printf("failed to load xdb content from `%s`\n", db_path);
|
printf("failed to load xdb content from `%s`\n", db_path);
|
||||||
return 4;
|
errcode = 4;
|
||||||
|
goto defer;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = xdb_new_with_buffer(&test->searcher, test->c_buffer);
|
err = xdb_new_with_buffer(version, &test->searcher, test->c_buffer);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
printf("failed to create content cached searcher with errcode=%d\n", err);
|
printf("failed to create content cached searcher with errcode=%d\n", err);
|
||||||
return 5;
|
errcode = 5;
|
||||||
|
goto defer;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
printf("invalid cache policy `%s`, options: file/vectorIndex/content\n", cache_policy);
|
printf("invalid cache policy `%s`, options: file/vectorIndex/content\n", cache_policy);
|
||||||
return 6;
|
errcode = 6;
|
||||||
|
goto defer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
// init the region buffer
|
||||||
|
// err = xdb_region_buffer_init(&test->region, test->region_buffer, sizeof(test->region_buffer));
|
||||||
|
err = xdb_region_buffer_init(&test->region, NULL, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the region buffer with err=%d\n", err);
|
||||||
|
errcode = 7;
|
||||||
|
goto defer;
|
||||||
|
}
|
||||||
|
|
||||||
|
defer:
|
||||||
|
if (header != NULL) {
|
||||||
|
xdb_free_header(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle != NULL) {
|
||||||
|
fclose(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
void destroy_searcher_test(searcher_test_t *test) {
|
void destroy_searcher_test(searcher_test_t *test) {
|
||||||
|
|
@ -64,13 +120,13 @@ void destroy_searcher_test(searcher_test_t *test) {
|
||||||
|
|
||||||
// check and free the vector index
|
// check and free the vector index
|
||||||
if (test->v_index != NULL) {
|
if (test->v_index != NULL) {
|
||||||
xdb_close_vector_index(test->v_index);
|
xdb_free_vector_index(test->v_index);
|
||||||
test->v_index = NULL;
|
test->v_index = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check and free the content buffer
|
// check and free the content buffer
|
||||||
if (test->c_buffer != NULL) {
|
if (test->c_buffer != NULL) {
|
||||||
xdb_close_content(test->c_buffer);
|
xdb_free_content(test->c_buffer);
|
||||||
test->c_buffer = NULL;
|
test->c_buffer = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,8 +163,12 @@ void test_search(int argc, char *argv[]) {
|
||||||
|
|
||||||
// for search
|
// for search
|
||||||
long s_time, c_time;
|
long s_time, c_time;
|
||||||
unsigned int ip;
|
char line[512] = {'\0'};
|
||||||
char line[512] = {'\0'}, region[512] = {'\0'};
|
|
||||||
|
// ip parse
|
||||||
|
xdb_version_t *version;
|
||||||
|
bytes_ip_t ip_bytes[16] = {'\0'};
|
||||||
|
|
||||||
searcher_test_t test;
|
searcher_test_t test;
|
||||||
|
|
||||||
for (i = 2; i < argc; i++) {
|
for (i = 2; i < argc; i++) {
|
||||||
|
|
@ -152,6 +212,13 @@ void test_search(int argc, char *argv[]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init the win sock
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// printf("db_file=%s, cache_policy=%s\n", db_file, cache_policy);
|
// printf("db_file=%s, cache_policy=%s\n", db_file, cache_policy);
|
||||||
err = init_searcher_test(&test, db_file, cache_policy);
|
err = init_searcher_test(&test, db_file, cache_policy);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
|
|
@ -159,8 +226,9 @@ void test_search(int argc, char *argv[]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("ip2region xdb searcher test program, "
|
printf("ip2region xdb searcher test program\n"
|
||||||
"cache_policy: %s\ntype 'quit' to exit\n", cache_policy);
|
"source xdb: %s (%s, %s)\n"
|
||||||
|
"type 'quit' to exit\n", db_file, xdb_get_version(&test.searcher)->name, cache_policy);
|
||||||
while ( 1 ) {
|
while ( 1 ) {
|
||||||
printf("ip2region>> ");
|
printf("ip2region>> ");
|
||||||
get_line(stdin, line);
|
get_line(stdin, line);
|
||||||
|
|
@ -168,26 +236,31 @@ void test_search(int argc, char *argv[]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcasecmp( line, "quit") == 0 ) {
|
if (strcmp(line, "quit") == 0 ) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xdb_check_ip(line, &ip) != 0) {
|
version = xdb_parse_ip(line, ip_bytes, sizeof(ip_bytes));
|
||||||
|
if (version == NULL) {
|
||||||
printf("invalid ip address `%s`\n", line);
|
printf("invalid ip address `%s`\n", line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_time = xdb_now();
|
s_time = xdb_now();
|
||||||
err = xdb_search(&test.searcher, ip, region, sizeof(region));
|
err = xdb_search(&test.searcher, ip_bytes, version->bytes, &test.region);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
printf("{err: %d, io_count: %d}\n", err, xdb_get_io_count(&test.searcher));
|
printf("{err: %d, io_count: %d}\n", err, xdb_get_io_count(&test.searcher));
|
||||||
} else {
|
} else {
|
||||||
c_time = xdb_now() - s_time;
|
c_time = xdb_now() - s_time;
|
||||||
printf("{region: %s, io_count: %d, took: %ld μs}\n", region, xdb_get_io_count(&test.searcher), c_time);
|
printf("{region: %s, io_count: %d, took: %ld μs}\n", test.region.value, xdb_get_io_count(&test.searcher), c_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// free the region
|
||||||
|
xdb_region_buffer_free(&test.region);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy_searcher_test(&test);
|
destroy_searcher_test(&test);
|
||||||
|
xdb_clean_winsock();
|
||||||
printf("searcher test program exited, thanks for trying\n");
|
printf("searcher test program exited, thanks for trying\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,13 +270,18 @@ void test_bench(int argc, char *argv[]) {
|
||||||
char db_file[256] = {'\0'}, src_file[256] = {'\0'}, cache_policy[16] = {"vectorIndex"};
|
char db_file[256] = {'\0'}, src_file[256] = {'\0'}, cache_policy[16] = {"vectorIndex"};
|
||||||
|
|
||||||
FILE *handle;
|
FILE *handle;
|
||||||
char line[1024] = {'\0'}, sip_str[16] = {'\0'}, eip_str[16] = {'\0'};
|
char line[1024] = {'\0'}, sip_str[INET6_ADDRSTRLEN+1] = {'\0'}, eip_str[INET6_ADDRSTRLEN+1] = {'\0'};
|
||||||
char src_region[512] = {'\0'}, region_buffer[512] = {'\0'};
|
char src_region[512] = {'\0'};
|
||||||
unsigned int sip, eip, mip, ip_list[5];
|
|
||||||
int count = 0, took;
|
int count = 0, took;
|
||||||
long s_time, t_time, c_time = 0;
|
long s_time, t_time, c_time = 0;
|
||||||
searcher_test_t test;
|
|
||||||
|
|
||||||
|
// ip parse
|
||||||
|
xdb_version_t *s_version, *e_version;
|
||||||
|
bytes_ip_t sip_bytes[16] = {'\0'}, eip_bytes[16] = {'\0'};
|
||||||
|
string_ip_t ip_string[INET6_ADDRSTRLEN] = {'\0'};
|
||||||
|
bytes_ip_t *ip_list[2];
|
||||||
|
|
||||||
|
searcher_test_t test;
|
||||||
for (i = 2; i < argc; i++) {
|
for (i = 2; i < argc; i++) {
|
||||||
r = argv[i];
|
r = argv[i];
|
||||||
if (strlen(r) < 5) {
|
if (strlen(r) < 5) {
|
||||||
|
|
@ -246,6 +324,13 @@ void test_bench(int argc, char *argv[]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init the win sock
|
||||||
|
err = xdb_init_winsock();
|
||||||
|
if (err != 0) {
|
||||||
|
printf("failed to init the winsock with errno=%d\n", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// printf("db_file=%s, src_file=%s, cache_policy=%s\n", db_file, src_file, cache_policy);
|
// printf("db_file=%s, src_file=%s, cache_policy=%s\n", db_file, src_file, cache_policy);
|
||||||
s_time = xdb_now();
|
s_time = xdb_now();
|
||||||
err = init_searcher_test(&test, db_file, cache_policy);
|
err = init_searcher_test(&test, db_file, cache_policy);
|
||||||
|
|
@ -262,57 +347,63 @@ void test_bench(int argc, char *argv[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
while(fgets(line, sizeof(line), handle) != NULL) {
|
while(fgets(line, sizeof(line), handle) != NULL) {
|
||||||
n = sscanf(line, "%15[^|]|%15[^|]|%511[^\n]", sip_str, eip_str, src_region);
|
n = sscanf(line, "%46[^|]|%46[^|]|%511[^\n]", sip_str, eip_str, src_region);
|
||||||
if (n != 3) {
|
if (n != 3) {
|
||||||
printf("invalid ip segment line `%s`\n", line);
|
printf("invalid ip segment line `%s`\n", line);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xdb_check_ip(sip_str, &sip) != 0) {
|
s_version = xdb_parse_ip(sip_str, sip_bytes, sizeof(sip_bytes));
|
||||||
|
if (s_version == NULL) {
|
||||||
printf("invalid start ip `%s`\n", sip_str);
|
printf("invalid start ip `%s`\n", sip_str);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xdb_check_ip(eip_str, &eip) != 0) {
|
e_version = xdb_parse_ip(eip_str, eip_bytes, sizeof(eip_bytes));
|
||||||
|
if (e_version == NULL) {
|
||||||
printf("invalid end ip `%s`\n", sip_str);
|
printf("invalid end ip `%s`\n", sip_str);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sip > eip) {
|
if (s_version->id != e_version->id) {
|
||||||
|
printf("start ip and end ip version not match for line `%s`\n", line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xdb_ip_sub_compare(sip_bytes, s_version->bytes, (string_ip_t *) eip_bytes, 0) > 0) {
|
||||||
printf("start ip(%s) should not be greater than end ip(%s)\n", sip_str, eip_str);
|
printf("start ip(%s) should not be greater than end ip(%s)\n", sip_str, eip_str);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mip = xdb_mip(sip, eip);
|
ip_list[0] = sip_bytes;
|
||||||
ip_list[0] = sip;
|
ip_list[1] = eip_bytes;
|
||||||
ip_list[1] = xdb_mip(sip, mip);
|
for (i = 0; i < 2; i++) {
|
||||||
ip_list[2] = mip;
|
|
||||||
ip_list[3] = xdb_mip(mip, eip);
|
|
||||||
ip_list[4] = eip;
|
|
||||||
for (i = 0; i < 5; i++) {
|
|
||||||
t_time = xdb_now();
|
t_time = xdb_now();
|
||||||
err = xdb_search(&test.searcher, ip_list[i], region_buffer, sizeof(region_buffer));
|
err = xdb_search(&test.searcher, ip_list[i], s_version->bytes, &test.region);
|
||||||
|
c_time += xdb_now() - t_time;
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
xdb_long2ip(ip_list[i], sip_str);
|
xdb_ip_to_string(ip_list[i], s_version->bytes, ip_string, sizeof(ip_string));
|
||||||
printf("failed to search ip `%s` with errno=%d\n", sip_str, err);
|
printf("failed to search ip `%s` with errno=%d\n", ip_string, err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
c_time += xdb_now() - t_time;
|
|
||||||
|
|
||||||
// check the region info
|
// check the region info
|
||||||
if (strcmp(region_buffer, src_region) != 0) {
|
if (strcmp(test.region.value, src_region) != 0) {
|
||||||
xdb_long2ip(ip_list[i], sip_str);
|
xdb_ip_to_string(ip_list[i], s_version->bytes, ip_string, sizeof(ip_string));
|
||||||
printf("failed to search(%s) with (%s != %s)\n", sip_str, region_buffer, src_region);
|
printf("failed to search(%s) with (%s != %s)\n", ip_string, test.region.value, src_region);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// free the region buffer
|
||||||
|
xdb_region_buffer_free(&test.region);
|
||||||
|
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
took = xdb_now() - s_time;
|
took = xdb_now() - s_time;
|
||||||
destroy_searcher_test(&test);
|
destroy_searcher_test(&test);
|
||||||
|
xdb_clean_winsock();
|
||||||
fclose(handle);
|
fclose(handle);
|
||||||
printf("Bench finished, {cache_policy: %s, total: %d, took: %.3fs, cost: %d μs/op}\n",
|
printf("Bench finished, {cache_policy: %s, total: %d, took: %.3fs, cost: %d μs/op}\n",
|
||||||
cache_policy, count, took/1e6, count == 0 ? 0 : (int)(c_time/count));
|
cache_policy, count, took/1e6, count == 0 ? 0 : (int)(c_time/count));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
// Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// @Author Lion <chenxin619315@gmail.com>
|
||||||
|
// @Date 2022/06/27
|
||||||
|
|
||||||
|
#include "stdio.h"
|
||||||
|
#include "xdb_api.h"
|
||||||
|
|
||||||
|
typedef void (* test_func_ptr) ();
|
||||||
|
struct test_func_entry {
|
||||||
|
char *name;
|
||||||
|
test_func_ptr func;
|
||||||
|
};
|
||||||
|
typedef struct test_func_entry test_func_t;
|
||||||
|
|
||||||
|
void test_load_header() {
|
||||||
|
xdb_header_t *header = xdb_load_header_from_file("../../data/ip2region_v4.xdb");
|
||||||
|
if (header == NULL) {
|
||||||
|
printf("failed to load header");
|
||||||
|
} else {
|
||||||
|
printf("header loaded: {\n"
|
||||||
|
" version: %d, \n"
|
||||||
|
" index_policy: %d, \n"
|
||||||
|
" created_at: %u, \n"
|
||||||
|
" start_index_ptr: %d, \n"
|
||||||
|
" end_index_ptr: %d\n"
|
||||||
|
" ip_version: %d\n"
|
||||||
|
" runtime_ptr_bytes: %d\n"
|
||||||
|
" length: %d\n"
|
||||||
|
"}\n",
|
||||||
|
header->version, header->index_policy, header->created_at,
|
||||||
|
header->start_index_ptr, header->end_index_ptr,
|
||||||
|
header->ip_version, header->runtime_ptr_bytes, header->length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
xdb_free_header(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_load_vector_index() {
|
||||||
|
xdb_vector_index_t *v_index = xdb_load_vector_index_from_file("../../data/ip2region_v4.xdb");
|
||||||
|
if (v_index == NULL) {
|
||||||
|
printf("failed to load vector index from file\n");
|
||||||
|
} else {
|
||||||
|
printf("vector index loaded from file, length=%d\n", v_index->length);
|
||||||
|
}
|
||||||
|
|
||||||
|
xdb_free_vector_index(v_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_load_content() {
|
||||||
|
xdb_content_t *content = xdb_load_content_from_file("../../data/ip2region_v4.xdb");
|
||||||
|
if (content == NULL) {
|
||||||
|
printf("failed to load content from file\n");
|
||||||
|
} else {
|
||||||
|
printf("content loaded from file, length=%d\n", content->length);
|
||||||
|
}
|
||||||
|
|
||||||
|
xdb_free_content(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_parse_ip() {
|
||||||
|
const char *ip_list[] = {
|
||||||
|
"1.0.0.0", "58.251.30.115", "192.168.1.100",
|
||||||
|
"::", "2c0f:fff0::", "2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "240e:982:e617:ffff:ffff:ffff:ffff:ffff",
|
||||||
|
"219.xx.xx.11", "::xx:ffff",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
int errcode;
|
||||||
|
xdb_version_t *version;
|
||||||
|
bytes_ip_t ip_bytes[16] = {'\0'};
|
||||||
|
string_ip_t ip_string[INET6_ADDRSTRLEN] = {'\0'};
|
||||||
|
|
||||||
|
// init the sock env (for windows)
|
||||||
|
if ((errcode = xdb_init_winsock()) != 0) {
|
||||||
|
printf("failed to init the winsock");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
if (ip_list[i] == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
version = xdb_parse_ip(ip_list[i], ip_bytes, sizeof(ip_bytes));
|
||||||
|
if (version == NULL) {
|
||||||
|
printf("failed to parse ip `%s`\n", ip_list[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
xdb_ip_to_string(ip_bytes, version->bytes, ip_string, sizeof(ip_string));
|
||||||
|
printf("ip: %s (version=v%d), toString: %s\n", ip_list[i], version->id, ip_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up the winsock
|
||||||
|
xdb_clean_winsock();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ip_pair {
|
||||||
|
const char *sip;
|
||||||
|
const char *eip;
|
||||||
|
};
|
||||||
|
void test_ip_compare() {
|
||||||
|
struct ip_pair ip_pair_list[] = {
|
||||||
|
{"1.0.0.0", "1.0.0.1"},
|
||||||
|
{"192.168.1.101", "192.168.1.90"},
|
||||||
|
{"219.133.111.87", "114.114.114.114"},
|
||||||
|
{"1.0.4.0", "1.0.1.0"},
|
||||||
|
{"1.0.4.0", "1.0.3.255"},
|
||||||
|
{"2000::", "2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff"},
|
||||||
|
{"2001:4:112::", "2001:4:112:ffff:ffff:ffff:ffff:ffff"},
|
||||||
|
{"ffff::", "2001:4:ffff:ffff:ffff:ffff:ffff:ffff"},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ip_pair *pair_ptr = NULL;
|
||||||
|
bytes_ip_t sip_bytes[16] = {'\0'};
|
||||||
|
bytes_ip_t eip_bytes[16] = {'\0'};
|
||||||
|
xdb_version_t *s_version, *e_version;
|
||||||
|
int errcode;
|
||||||
|
|
||||||
|
// init the sock env (for windows)
|
||||||
|
if ((errcode = xdb_init_winsock()) != 0) {
|
||||||
|
printf("failed to init the winsock");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; ;i++) {
|
||||||
|
pair_ptr = &ip_pair_list[i];
|
||||||
|
if (pair_ptr->sip == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_version = xdb_parse_ip(pair_ptr->sip, sip_bytes, sizeof(sip_bytes));
|
||||||
|
if (s_version == NULL) {
|
||||||
|
printf("failed to parse sip `%s`", pair_ptr->sip);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
e_version = xdb_parse_ip(pair_ptr->eip, eip_bytes, sizeof(eip_bytes));
|
||||||
|
if (e_version == NULL) {
|
||||||
|
printf("failed to parse eip `%s`", pair_ptr->eip);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_version->id != e_version->id) {
|
||||||
|
printf("sip and eip version not match `%s` != `%s`\n", s_version->name, e_version->name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(
|
||||||
|
"ip_sub_compare(%s, %s): %d\n",
|
||||||
|
pair_ptr->sip, pair_ptr->eip,
|
||||||
|
xdb_ip_sub_compare(sip_bytes, s_version->bytes, (string_ip_t *) eip_bytes, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up the winsock
|
||||||
|
xdb_clean_winsock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// please register your function heare
|
||||||
|
static test_func_t _test_function_list[] = {
|
||||||
|
// xdb buffer
|
||||||
|
{"test_load_header", test_load_header},
|
||||||
|
{"test_load_vector_index", test_load_vector_index},
|
||||||
|
{"test_load_content", test_load_content},
|
||||||
|
|
||||||
|
// ip utils
|
||||||
|
{"test_parse_ip", test_parse_ip},
|
||||||
|
{"test_ip_compare", test_ip_compare},
|
||||||
|
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
// valgrind --tool=memcheck --leak-check=full ./a.out
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int i;
|
||||||
|
char *name;
|
||||||
|
|
||||||
|
// check and call the function
|
||||||
|
if (argc < 2) {
|
||||||
|
printf("please specified the function name to call\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
name = argv[1];
|
||||||
|
test_func_ptr func = NULL;
|
||||||
|
for (i = 0; ; i++) {
|
||||||
|
if (_test_function_list[i].name == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(name, _test_function_list[i].name) == 0) {
|
||||||
|
func = _test_function_list[i].func;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (func == NULL) {
|
||||||
|
printf("can't find test function `%s`\n", name);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the function
|
||||||
|
func();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
// Copyright 2022 The Ip2Region Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// @Author Lion <chenxin619315@gmail.com>
|
|
||||||
// @Date 2022/06/27
|
|
||||||
|
|
||||||
#include "stdio.h"
|
|
||||||
#include "xdb_searcher.h"
|
|
||||||
|
|
||||||
void test_check_ip() {
|
|
||||||
char *ip_list[] = {
|
|
||||||
"1.2.3.4", "192.168.2.3", "120.24.78.129", "255.255.255.0",
|
|
||||||
"256.7.12.9", "12.56.78.320", "32.12.45.192", "222.221.220.219",
|
|
||||||
"192.168.1.101 ", "132.96.12.98a", "x23.12.2.12"
|
|
||||||
};
|
|
||||||
|
|
||||||
int errcode, i;
|
|
||||||
unsigned int ip;
|
|
||||||
char ip_buff[16] = {'\0'};
|
|
||||||
for (i = 0; i < 11; i++) {
|
|
||||||
errcode = xdb_check_ip(ip_list[i], &ip);
|
|
||||||
if (errcode != 0) {
|
|
||||||
printf("invalid ip address `%s`\n", ip_list[i]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_long2ip(ip, ip_buff);
|
|
||||||
printf("long(%-15s)=%-10u, long2ip(%-10u)=%-15s", ip_list[i], ip, ip, ip_buff);
|
|
||||||
if (strcmp(ip_list[i], ip_buff) != 0) {
|
|
||||||
printf(" --[Failed]\n");
|
|
||||||
} else {
|
|
||||||
printf(" --[Ok]\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_load_header() {
|
|
||||||
xdb_header_t *header = xdb_load_header_from_file("../../data/ip2region.xdb");
|
|
||||||
if (header == NULL) {
|
|
||||||
printf("failed to load header");
|
|
||||||
} else {
|
|
||||||
printf("header loaded: {\n"
|
|
||||||
" version: %d, \n"
|
|
||||||
" index_policy: %d, \n"
|
|
||||||
" created_at: %u, \n"
|
|
||||||
" start_index_ptr: %d, \n"
|
|
||||||
" end_index_ptr: %d\n"
|
|
||||||
" length: %d\n"
|
|
||||||
"}\n",
|
|
||||||
header->version, header->index_policy, header->created_at,
|
|
||||||
header->start_index_ptr, header->end_index_ptr, header->length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_close_header(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_load_vector_index() {
|
|
||||||
xdb_vector_index_t *v_index = xdb_load_vector_index_from_file("../../data/ip2region.xdb");
|
|
||||||
if (v_index == NULL) {
|
|
||||||
printf("failed to load vector index from file\n");
|
|
||||||
} else {
|
|
||||||
printf("vector index loaded from file, length=%d\n", v_index->length);
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_close_vector_index(v_index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_load_content() {
|
|
||||||
xdb_content_t *content = xdb_load_content_from_file("../../data/ip2region.xdb");
|
|
||||||
if (content == NULL) {
|
|
||||||
printf("failed to load content from file\n");
|
|
||||||
} else {
|
|
||||||
printf("content loaded from file, length=%d\n", content->length);
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_close_content(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// valgrind --tool=memcheck --leak-check=full ./a.out
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
printf("test check ip ... \n");
|
|
||||||
test_check_ip();
|
|
||||||
printf("|--done\n\n");
|
|
||||||
|
|
||||||
printf("test load header ... \n");
|
|
||||||
test_load_header();
|
|
||||||
printf("|--done\n\n");
|
|
||||||
|
|
||||||
printf("test load vector index ... \n");
|
|
||||||
test_load_vector_index();
|
|
||||||
printf("|--done\n\n");
|
|
||||||
|
|
||||||
printf("test load content ... \n");
|
|
||||||
test_load_content();
|
|
||||||
printf("|--done\n\n");
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
// Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// @Author Lion <chenxin619315@gmail.com>
|
||||||
|
// @Date 2022/06/27
|
||||||
|
|
||||||
|
#ifndef C_IP2REGION_XDB_H
|
||||||
|
#define C_IP2REGION_XDB_H
|
||||||
|
|
||||||
|
// @Note:
|
||||||
|
// this define must be put before any header include
|
||||||
|
// force the LFS for ftell
|
||||||
|
#define _FILE_OFFSET_BITS 64
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#if ( defined(WIN32) || defined(_WIN32) || defined(__WINDOWS_) || defined(WINNT) )
|
||||||
|
# define XDB_PUBLIC(type) extern __declspec(dllexport) type
|
||||||
|
# define XDB_PRIVATE(type) static type
|
||||||
|
# define XDB_WINDOWS
|
||||||
|
#include <windows.h>
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
|
#elif (defined(linux) || defined(_UNIX) || defined(__APPLE__) || defined(unix) || defined(__unix) || defined(__unix__) || defined(__linux__) || defined(linux) || defined(__linux))
|
||||||
|
# define XDB_PUBLIC(type) extern type
|
||||||
|
# define XDB_PRIVATE(type) static inline type
|
||||||
|
# define XDB_LINUX
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#else
|
||||||
|
# define XDB_PUBLIC(type) type
|
||||||
|
# define XDB_PRIVATE(type) static type
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define xdb_calloc( _blocks, _bytes ) calloc( _blocks, _bytes )
|
||||||
|
#define xdb_malloc( _bytes ) malloc( _bytes )
|
||||||
|
#define xdb_free( _ptr ) free( _ptr )
|
||||||
|
|
||||||
|
// public constants define
|
||||||
|
#define xdb_structure_20 2
|
||||||
|
#define xdb_structure_30 3
|
||||||
|
#define xdb_header_info_length 256
|
||||||
|
#define xdb_vector_index_rows 256
|
||||||
|
#define xdb_vector_index_cols 256
|
||||||
|
#define xdb_vector_index_size 8
|
||||||
|
#define xdb_v4_index_size 14 // 4 + 4 + 2 + 4
|
||||||
|
#define xdb_v6_index_size 38 // 16 + 16 + 2 + 4
|
||||||
|
|
||||||
|
// --- ip version info
|
||||||
|
#define xdb_ipv4_id 4
|
||||||
|
#define xdb_ipv6_id 6
|
||||||
|
#define xdb_ipv4_bytes 4
|
||||||
|
#define xdb_ipv6_bytes 16
|
||||||
|
// cache of vector_index_row × vector_index_rows × vector_index_size
|
||||||
|
#define xdb_vector_index_length 524288
|
||||||
|
|
||||||
|
// --- xdb buffer functions
|
||||||
|
|
||||||
|
// use the following buffer struct to wrap the binary buffer data
|
||||||
|
// since the buffer data could not be operated with the string API.
|
||||||
|
struct xdb_header {
|
||||||
|
unsigned short version;
|
||||||
|
unsigned short index_policy;
|
||||||
|
unsigned int created_at;
|
||||||
|
unsigned int start_index_ptr;
|
||||||
|
unsigned int end_index_ptr;
|
||||||
|
|
||||||
|
// since 3.0+ with IPv6 supporting
|
||||||
|
unsigned short ip_version;
|
||||||
|
unsigned short runtime_ptr_bytes;
|
||||||
|
|
||||||
|
// the original buffer
|
||||||
|
unsigned int length;
|
||||||
|
char buffer[xdb_header_info_length];
|
||||||
|
};
|
||||||
|
typedef struct xdb_header xdb_header_t;
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_header_t *) xdb_load_header(FILE *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_header_t *) xdb_load_header_from_file(const char *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_free_header(void *);
|
||||||
|
|
||||||
|
|
||||||
|
// --- vector index buffer
|
||||||
|
struct xdb_vector_index {
|
||||||
|
unsigned int length;
|
||||||
|
char buffer[xdb_vector_index_length];
|
||||||
|
};
|
||||||
|
typedef struct xdb_vector_index xdb_vector_index_t;
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index(FILE *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index_from_file(const char *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_free_vector_index(void *);
|
||||||
|
|
||||||
|
|
||||||
|
// --- content buffer
|
||||||
|
struct xdb_content {
|
||||||
|
unsigned int length;
|
||||||
|
char *buffer;
|
||||||
|
};
|
||||||
|
typedef struct xdb_content xdb_content_t;
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_content_t *) xdb_load_content(FILE *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_content_t *) xdb_load_content_from_file(const char *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_free_content(void *);
|
||||||
|
|
||||||
|
// --- xdb verify
|
||||||
|
|
||||||
|
// Verify if the current Searcher could be used to search the specified xdb file.
|
||||||
|
// Why do we need this check ?
|
||||||
|
// The future features of the xdb impl may cause the current searcher not able to work properly.
|
||||||
|
//
|
||||||
|
// @Note: You Just need to check this ONCE when the service starts
|
||||||
|
// Or use another process (eg, A command) to check once Just to confirm the suitability.
|
||||||
|
XDB_PUBLIC(int) xdb_verify(FILE *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_verify_from_header(FILE *handle, xdb_header_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_verify_from_file(const char *);
|
||||||
|
|
||||||
|
// --- End xdb buffer
|
||||||
|
|
||||||
|
|
||||||
|
// types type define
|
||||||
|
typedef char string_ip_t;
|
||||||
|
typedef unsigned char bytes_ip_t;
|
||||||
|
|
||||||
|
// --- ip version
|
||||||
|
#define XDB_IPv4 (xdb_version_v4())
|
||||||
|
#define XDB_IPv6 (xdb_version_v6())
|
||||||
|
typedef int (* ip_compare_fn_t) (const bytes_ip_t *, int, const char *, int);
|
||||||
|
struct xdb_ip_version_entry {
|
||||||
|
int id; // version id
|
||||||
|
char *name; // version name
|
||||||
|
int bytes; // ip bytes number
|
||||||
|
int segment_index_size; // segment index size in bytes
|
||||||
|
|
||||||
|
// function to compare two ips
|
||||||
|
ip_compare_fn_t ip_compare;
|
||||||
|
};
|
||||||
|
typedef struct xdb_ip_version_entry xdb_version_t;
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_v4();
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_v6();
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_version_is_v4(const xdb_version_t *);
|
||||||
|
XDB_PUBLIC(int) xdb_version_is_v6(const xdb_version_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_from_name(char *);
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_from_header(xdb_header_t *);
|
||||||
|
|
||||||
|
// --- END ip version
|
||||||
|
|
||||||
|
// --- xdb util functions
|
||||||
|
|
||||||
|
// to compatiable with the windows
|
||||||
|
// returns: 0 for ok and -1 for failed
|
||||||
|
XDB_PUBLIC(int) xdb_init_winsock();
|
||||||
|
XDB_PUBLIC(void) xdb_clean_winsock();
|
||||||
|
|
||||||
|
// get the current time in microseconds
|
||||||
|
XDB_PUBLIC(long) xdb_now();
|
||||||
|
|
||||||
|
// get unsigned long (4bytes) from a specified buffer start from the specified offset with little-endian
|
||||||
|
XDB_PUBLIC(unsigned int) xdb_le_get_uint32(const char *, int);
|
||||||
|
|
||||||
|
// get unsigned short (2bytes) from a specified buffer start from the specified offset with little-endian
|
||||||
|
XDB_PUBLIC(int) xdb_le_get_uint16(const char *, int);
|
||||||
|
|
||||||
|
|
||||||
|
// parse the specified IP address to byte array.
|
||||||
|
// returns: xdb_version_t for valid ipv4 / ipv6, or NULL for failed
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_parse_ip(const string_ip_t *, bytes_ip_t *, size_t);
|
||||||
|
|
||||||
|
// parse the specified IPv4 address to byte array
|
||||||
|
// returns: xdb_version_t for valid ipv4, or NULL for failed
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_parse_v4_ip(const string_ip_t *, bytes_ip_t *, size_t);
|
||||||
|
|
||||||
|
// parse the specified IPv6 address to byte array
|
||||||
|
// returns: xdb_version_t for valid ipv6, or NULL for failed
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_parse_v6_ip(const string_ip_t *, bytes_ip_t *, size_t);
|
||||||
|
|
||||||
|
// convert a specified ip bytes to humen-readable string.
|
||||||
|
// returns: 0 for success or -1 for failed.
|
||||||
|
XDB_PUBLIC(int) xdb_ip_to_string(const bytes_ip_t *, int, char *, size_t);
|
||||||
|
|
||||||
|
// ipv4 bytes to string
|
||||||
|
XDB_PUBLIC(int) xdb_v4_ip_to_string(const bytes_ip_t *, char *, size_t);
|
||||||
|
|
||||||
|
// ipv6 bytes to string
|
||||||
|
XDB_PUBLIC(int) xdb_v6_ip_to_string(const bytes_ip_t *, char *, size_t);
|
||||||
|
|
||||||
|
// compare the specified ip bytes with another ip bytes in the specified buff from offset.
|
||||||
|
// ip args must be the return value from #xdb_parse_ip.
|
||||||
|
// returns: -1 if ip1 < ip2, 1 if ip1 > ip2 or 0
|
||||||
|
XDB_PUBLIC(int) xdb_ip_sub_compare(const bytes_ip_t *, int, const char *, int);
|
||||||
|
|
||||||
|
// large file seek and tell
|
||||||
|
XDB_PUBLIC(int) xdb_fseek(FILE *, long long, int);
|
||||||
|
|
||||||
|
XDB_PUBLIC(long long) xdb_ftell(FILE *);
|
||||||
|
|
||||||
|
// --- END xdb utils
|
||||||
|
|
||||||
|
|
||||||
|
// --- xdb searcher api
|
||||||
|
|
||||||
|
// xdb region info structure
|
||||||
|
#define xdb_region_buffer_wrapper 1
|
||||||
|
#define xdb_region_buffer_auto 2
|
||||||
|
struct xdb_region_buffer_entry {
|
||||||
|
int type; // buffer type
|
||||||
|
char *value; // region value
|
||||||
|
size_t length; // buffer length
|
||||||
|
};
|
||||||
|
typedef struct xdb_region_buffer_entry xdb_region_buffer_t;
|
||||||
|
|
||||||
|
// wrapper the region from a local stack buffer.
|
||||||
|
// returns: 0 for succeed or failed
|
||||||
|
XDB_PUBLIC(int) xdb_region_buffer_init(xdb_region_buffer_t *, char *, size_t);
|
||||||
|
|
||||||
|
// do the buffer alloc.
|
||||||
|
// returns: 0 for ok or failed
|
||||||
|
XDB_PUBLIC(int) xdb_region_buffer_alloc(xdb_region_buffer_t *, int);
|
||||||
|
|
||||||
|
// empty alloc - empty string
|
||||||
|
// returns: 0 - always
|
||||||
|
XDB_PUBLIC(int) xdb_region_buffer_empty(xdb_region_buffer_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_region_buffer_free(xdb_region_buffer_t *);
|
||||||
|
|
||||||
|
// xdb searcher structure
|
||||||
|
struct xdb_searcher_entry {
|
||||||
|
// ip version
|
||||||
|
xdb_version_t *version;
|
||||||
|
|
||||||
|
// xdb file handle
|
||||||
|
FILE *handle;
|
||||||
|
|
||||||
|
// header info
|
||||||
|
const char *header;
|
||||||
|
int io_count;
|
||||||
|
|
||||||
|
// vector index buffer cache.
|
||||||
|
// preload the vector index will reduce the number of IO operations
|
||||||
|
// thus speedup the search process.
|
||||||
|
const xdb_vector_index_t *v_index;
|
||||||
|
|
||||||
|
// content buffer.
|
||||||
|
// cache the whole xdb content.
|
||||||
|
const xdb_content_t *content;
|
||||||
|
};
|
||||||
|
typedef struct xdb_searcher_entry xdb_searcher_t;
|
||||||
|
|
||||||
|
// xdb searcher new api define
|
||||||
|
XDB_PUBLIC(int) xdb_new_with_file_only(xdb_version_t *, xdb_searcher_t *, const char *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_new_with_vector_index(xdb_version_t *, xdb_searcher_t *, const char *, const xdb_vector_index_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_new_with_buffer(xdb_version_t *, xdb_searcher_t *, const xdb_content_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_close(void *);
|
||||||
|
|
||||||
|
// xdb searcher search api define
|
||||||
|
XDB_PUBLIC(int) xdb_search_by_string(xdb_searcher_t *, const string_ip_t *, xdb_region_buffer_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_search(xdb_searcher_t *, const bytes_ip_t *, int, xdb_region_buffer_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_get_version(xdb_searcher_t *);
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_get_io_count(xdb_searcher_t *);
|
||||||
|
|
||||||
|
// --- END xdb searcher api
|
||||||
|
|
||||||
|
#endif // C_IP2REGION_XDB_H
|
||||||
|
|
@ -6,42 +6,104 @@
|
||||||
// @Author Lion <chenxin619315@gmail.com>
|
// @Author Lion <chenxin619315@gmail.com>
|
||||||
// @Date 2022/06/27
|
// @Date 2022/06/27
|
||||||
|
|
||||||
#include "xdb_searcher.h"
|
#include "xdb_api.h"
|
||||||
|
|
||||||
// for Linux
|
// --- region buffer
|
||||||
#ifdef XDB_LINUX
|
XDB_PUBLIC(int) xdb_region_buffer_init(xdb_region_buffer_t *region, char *buffer, size_t length) {
|
||||||
#include "sys/time.h"
|
if (buffer == NULL) {
|
||||||
#endif
|
region->type = xdb_region_buffer_auto;
|
||||||
|
region->length = 0;
|
||||||
// @Note: since 2023/10/13 to compatible with the windows system
|
} else if (length <= 0) {
|
||||||
#ifdef XDB_WINDOWS
|
return 1;
|
||||||
#include <windows.h>
|
} else {
|
||||||
XDB_PRIVATE(int) gettimeofday(struct timeval* tp, void* tzp) {
|
region->type = xdb_region_buffer_wrapper;
|
||||||
time_t clock;
|
region->length = length;
|
||||||
struct tm tm;
|
memset(buffer, 0x00, length); // zero-fill the buffer
|
||||||
SYSTEMTIME wtm;
|
|
||||||
GetLocalTime(&wtm);
|
|
||||||
tm.tm_year = wtm.wYear - 1900;
|
|
||||||
tm.tm_mon = wtm.wMonth - 1;
|
|
||||||
tm.tm_mday = wtm.wDay;
|
|
||||||
tm.tm_hour = wtm.wHour;
|
|
||||||
tm.tm_min = wtm.wMinute;
|
|
||||||
tm.tm_sec = wtm.wSecond;
|
|
||||||
tm.tm_isdst = -1;
|
|
||||||
clock = mktime(&tm);
|
|
||||||
tp->tv_sec = clock;
|
|
||||||
tp->tv_usec = wtm.wMilliseconds * 1000;
|
|
||||||
return (0);
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
|
region->value = buffer;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_region_buffer_alloc(xdb_region_buffer_t *region, int length) {
|
||||||
|
if (length <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no allocation supports for the buffer wapper
|
||||||
|
if (region->type == xdb_region_buffer_wrapper) {
|
||||||
|
if (length >= region->length) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
region->value[length] = '\0';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that the value were freed
|
||||||
|
// by calling #xdb_region_buffer_free
|
||||||
|
if (region->value != NULL) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *ptr = (char *) xdb_malloc(length + 1);
|
||||||
|
if (ptr == NULL) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr[length] = '\0'; // NULL-end
|
||||||
|
region->value = ptr;
|
||||||
|
region->length = length;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixed internal empty string ptr
|
||||||
|
static char * _empty_region_string = "\0";
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_region_buffer_empty(xdb_region_buffer_t *region) {
|
||||||
|
// no allocation supports for the buffer wapper
|
||||||
|
if (region->type == xdb_region_buffer_wrapper) {
|
||||||
|
region->value[0] = '\0';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that the value were freed
|
||||||
|
// by calling #xdb_region_buffer_free
|
||||||
|
if (region->value != NULL) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
region->value = _empty_region_string;
|
||||||
|
region->length = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_region_buffer_free(xdb_region_buffer_t *region) {
|
||||||
|
if (region->type == xdb_region_buffer_auto) {
|
||||||
|
// empty string interception
|
||||||
|
if (region->length == 0
|
||||||
|
|| region->value == _empty_region_string) {
|
||||||
|
// do nothing for empty string
|
||||||
|
} else {
|
||||||
|
xdb_free(region->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the value
|
||||||
|
region->value = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- END region buffer
|
||||||
|
|
||||||
// internal function prototype define
|
// internal function prototype define
|
||||||
XDB_PRIVATE(int) read(xdb_searcher_t *, long offset, char *, size_t length);
|
XDB_PRIVATE(int) read(xdb_searcher_t *, long offset, char *, size_t length);
|
||||||
|
|
||||||
XDB_PRIVATE(int) xdb_new_base(xdb_searcher_t *xdb, const char *db_path, const xdb_vector_index_t *v_index, const xdb_content_t *c_buffer) {
|
XDB_PRIVATE(int) xdb_new_base(xdb_version_t *version, xdb_searcher_t *xdb, const char *db_path, const xdb_vector_index_t *v_index, const xdb_content_t *c_buffer) {
|
||||||
memset(xdb, 0x00, sizeof(xdb_searcher_t));
|
memset(xdb, 0x00, sizeof(xdb_searcher_t));
|
||||||
|
|
||||||
|
// set the version
|
||||||
|
xdb->version = version;
|
||||||
|
|
||||||
// check the content buffer first
|
// check the content buffer first
|
||||||
if (c_buffer != NULL) {
|
if (c_buffer != NULL) {
|
||||||
xdb->v_index = NULL;
|
xdb->v_index = NULL;
|
||||||
|
|
@ -62,16 +124,16 @@ XDB_PRIVATE(int) xdb_new_base(xdb_searcher_t *xdb, const char *db_path, const xd
|
||||||
}
|
}
|
||||||
|
|
||||||
// xdb searcher new api define
|
// xdb searcher new api define
|
||||||
XDB_PUBLIC(int) xdb_new_with_file_only(xdb_searcher_t *xdb, const char *db_path) {
|
XDB_PUBLIC(int) xdb_new_with_file_only(xdb_version_t *version, xdb_searcher_t *xdb, const char *db_path) {
|
||||||
return xdb_new_base(xdb, db_path, NULL, NULL);
|
return xdb_new_base(version, xdb, db_path, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_new_with_vector_index(xdb_searcher_t *xdb, const char *db_path, const xdb_vector_index_t *v_index) {
|
XDB_PUBLIC(int) xdb_new_with_vector_index(xdb_version_t *version, xdb_searcher_t *xdb, const char *db_path, const xdb_vector_index_t *v_index) {
|
||||||
return xdb_new_base(xdb, db_path, v_index, NULL);
|
return xdb_new_base(version, xdb, db_path, v_index, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_new_with_buffer(xdb_searcher_t *xdb, const xdb_content_t *c_buffer) {
|
XDB_PUBLIC(int) xdb_new_with_buffer(xdb_version_t *version, xdb_searcher_t *xdb, const xdb_content_t *c_buffer) {
|
||||||
return xdb_new_base(xdb, NULL, NULL, c_buffer);
|
return xdb_new_base(version, xdb, NULL, NULL, c_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close(void *ptr) {
|
XDB_PUBLIC(void) xdb_close(void *ptr) {
|
||||||
|
|
@ -84,93 +146,109 @@ XDB_PUBLIC(void) xdb_close(void *ptr) {
|
||||||
|
|
||||||
// --- xdb searcher search api define
|
// --- xdb searcher search api define
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_search_by_string(xdb_searcher_t *xdb, const char *str_ip, char *region_buffer, size_t length) {
|
XDB_PUBLIC(int) xdb_search_by_string(xdb_searcher_t *xdb, const string_ip_t *ip_string, xdb_region_buffer_t *region) {
|
||||||
unsigned int ip = 0;
|
bytes_ip_t ip_bytes[16] = {'\0'};
|
||||||
int errcode = xdb_check_ip(str_ip, &ip);
|
xdb_version_t *version = xdb_parse_ip(ip_string, ip_bytes, sizeof(ip_bytes));
|
||||||
if (errcode != 0) {
|
if (version == NULL) {
|
||||||
return 10 + errcode;
|
return 10;
|
||||||
} else {
|
} else {
|
||||||
return xdb_search(xdb, ip, region_buffer, length);
|
return xdb_search(xdb, ip_bytes, version->bytes, region);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_search(xdb_searcher_t *xdb, unsigned int ip, char *region_buffer, size_t length) {
|
XDB_PUBLIC(int) xdb_search(xdb_searcher_t *xdb, const bytes_ip_t *ip_bytes, int ip_len, xdb_region_buffer_t *region) {
|
||||||
int il0, il1, idx, err, l, h, m, data_len;
|
int il0, il1, idx, err, bytes, d_bytes;
|
||||||
unsigned int s_ptr, e_ptr, p, sip, eip, data_ptr;
|
register int seg_index_size, l, h, m, p;
|
||||||
char vector_buffer[xdb_vector_index_size], segment_buffer[xdb_segment_index_size];
|
unsigned int s_ptr, e_ptr, data_ptr, data_len;
|
||||||
|
char vector_buffer[xdb_vector_index_size];
|
||||||
|
char segment_buffer[xdb_v6_index_size];
|
||||||
|
|
||||||
// reset the io counter
|
// ip version check
|
||||||
|
if (ip_len != xdb->version->bytes) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// some resets
|
||||||
|
err = 0;
|
||||||
|
data_len = 0;
|
||||||
|
bytes = xdb->version->bytes;
|
||||||
|
d_bytes = xdb->version->bytes << 1;
|
||||||
xdb->io_count = 0;
|
xdb->io_count = 0;
|
||||||
|
|
||||||
// locate the segment index block based on the vector index
|
// locate the segment index block based on the vector index
|
||||||
il0 = ((int) (ip >> 24)) & 0xFF;
|
il0 = (int) (ip_bytes[0]);
|
||||||
il1 = ((int) (ip >> 16)) & 0xFF;
|
il1 = (int) (ip_bytes[1]);
|
||||||
idx = il0 * xdb_vector_index_cols * xdb_vector_index_size + il1 * xdb_vector_index_size;
|
idx = il0 * xdb_vector_index_cols * xdb_vector_index_size + il1 * xdb_vector_index_size;
|
||||||
if (xdb->v_index != NULL) {
|
if (xdb->v_index != NULL) {
|
||||||
s_ptr = xdb_get_uint(xdb->v_index->buffer, idx);
|
s_ptr = xdb_le_get_uint32(xdb->v_index->buffer, idx);
|
||||||
e_ptr = xdb_get_uint(xdb->v_index->buffer, idx + 4);
|
e_ptr = xdb_le_get_uint32(xdb->v_index->buffer, idx + 4);
|
||||||
} else if (xdb->content != NULL) {
|
} else if (xdb->content != NULL) {
|
||||||
s_ptr = xdb_get_uint(xdb->content->buffer, xdb_header_info_length + idx);
|
s_ptr = xdb_le_get_uint32(xdb->content->buffer, xdb_header_info_length + idx);
|
||||||
e_ptr = xdb_get_uint(xdb->content->buffer, xdb_header_info_length + idx + 4);
|
e_ptr = xdb_le_get_uint32(xdb->content->buffer, xdb_header_info_length + idx + 4);
|
||||||
} else {
|
} else {
|
||||||
err = read(xdb, xdb_header_info_length + idx, vector_buffer, sizeof(vector_buffer));
|
err = read(xdb, xdb_header_info_length + idx, vector_buffer, sizeof(vector_buffer));
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
return 10 + err;
|
return 10 + err;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_ptr = xdb_get_uint(vector_buffer, 0);
|
s_ptr = xdb_le_get_uint32(vector_buffer, 0);
|
||||||
e_ptr = xdb_get_uint(vector_buffer, 4);
|
e_ptr = xdb_le_get_uint32(vector_buffer, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// printf("s_ptr=%u, e_ptr=%u\n", s_ptr, e_ptr);
|
// printf("s_ptr=%u, e_ptr=%u\n", s_ptr, e_ptr);
|
||||||
|
// @Note: ptr validate, zero ptr means source data missing
|
||||||
|
// so we could just stop here and return an empty string.
|
||||||
|
if (s_ptr == 0 || e_ptr == 0) {
|
||||||
|
xdb_region_buffer_empty(region);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
// binary search to get the final region info
|
// binary search to get the final region info
|
||||||
|
// segment_buffer = xdb_malloc(seg_index_size);
|
||||||
|
seg_index_size = xdb->version->segment_index_size;
|
||||||
data_len = 0, data_ptr = 0;
|
data_len = 0, data_ptr = 0;
|
||||||
l = 0, h = ((int) (e_ptr - s_ptr)) / xdb_segment_index_size;
|
l = 0, h = ((int) (e_ptr - s_ptr)) / seg_index_size;
|
||||||
while (l <= h) {
|
while (l <= h) {
|
||||||
m = (l + h) >> 1;
|
m = (l + h) >> 1;
|
||||||
p = s_ptr + m * xdb_segment_index_size;
|
p = s_ptr + m * seg_index_size;
|
||||||
|
|
||||||
// read the segment index item
|
// read the segment index item
|
||||||
err = read(xdb, p, segment_buffer, sizeof(segment_buffer));
|
err = read(xdb, p, segment_buffer, seg_index_size);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
return 20 + err;
|
return 20 + err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode the data fields as needed
|
// decode the data fields as needed
|
||||||
sip = xdb_get_uint(segment_buffer, 0);
|
if (xdb->version->ip_compare(ip_bytes, bytes, segment_buffer, 0) < 0) {
|
||||||
if (ip < sip) {
|
|
||||||
h = m - 1;
|
h = m - 1;
|
||||||
} else {
|
} else if (xdb->version->ip_compare(ip_bytes, bytes, segment_buffer, bytes) > 0) {
|
||||||
eip = xdb_get_uint(segment_buffer, 4);
|
|
||||||
if (ip > eip) {
|
|
||||||
l = m + 1;
|
l = m + 1;
|
||||||
} else {
|
} else {
|
||||||
data_len = xdb_get_ushort(segment_buffer, 8);
|
data_len = xdb_le_get_uint16(segment_buffer, d_bytes);
|
||||||
data_ptr = xdb_get_uint(segment_buffer, 10);
|
data_ptr = xdb_le_get_uint32(segment_buffer, d_bytes + 2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// printf("data_len=%u, data_ptr=%u\n", data_len, data_ptr);
|
// printf("data_len=%u, data_ptr=%u\n", data_len, data_ptr);
|
||||||
if (data_len == 0) {
|
if (data_len == 0) {
|
||||||
region_buffer[0] = '\0';
|
// return 100;
|
||||||
return 0;
|
xdb_region_buffer_empty(region);
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// buffer length checking
|
// buffer alloc checking
|
||||||
if (data_len >= (int) length) {
|
err = xdb_region_buffer_alloc(region, data_len);
|
||||||
return 1;
|
if (err != 0) {
|
||||||
|
return 100 + err;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = read(xdb, data_ptr, region_buffer, data_len);
|
err = read(xdb, data_ptr, region->value, data_len);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
return 30 + err;
|
return 30 + err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto append a NULL-end
|
return err;
|
||||||
region_buffer[data_len] = '\0';
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
XDB_PRIVATE(int) read(xdb_searcher_t *xdb, long offset, char *buffer, size_t length) {
|
XDB_PRIVATE(int) read(xdb_searcher_t *xdb, long offset, char *buffer, size_t length) {
|
||||||
|
|
@ -193,237 +271,10 @@ XDB_PRIVATE(int) read(xdb_searcher_t *xdb, long offset, char *buffer, size_t len
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_get_version(xdb_searcher_t *xdb) {
|
||||||
|
return xdb->version;
|
||||||
|
}
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_get_io_count(xdb_searcher_t *xdb) {
|
XDB_PUBLIC(int) xdb_get_io_count(xdb_searcher_t *xdb) {
|
||||||
return xdb->io_count;
|
return xdb->io_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- buffer load util functions
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_header_t *) xdb_load_header(FILE *handle) {
|
|
||||||
xdb_header_t *header;
|
|
||||||
unsigned int size = xdb_header_info_length;
|
|
||||||
|
|
||||||
// entry alloc
|
|
||||||
header = (xdb_header_t *) xdb_malloc(sizeof(xdb_header_t));
|
|
||||||
if (header == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fseek(handle, 0, SEEK_SET) == -1) {
|
|
||||||
xdb_free(header);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fread(header->buffer, 1,size, handle) != size) {
|
|
||||||
xdb_free(header);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill the fields
|
|
||||||
header->length = size;
|
|
||||||
header->version = (unsigned short) xdb_get_ushort(header->buffer, 0);
|
|
||||||
header->index_policy = (unsigned short) xdb_get_ushort(header->buffer, 2);
|
|
||||||
header->created_at = xdb_get_uint(header->buffer, 4);
|
|
||||||
header->start_index_ptr = xdb_get_uint(header->buffer, 8);
|
|
||||||
header->end_index_ptr = xdb_get_uint(header->buffer,12);
|
|
||||||
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_header_t *) xdb_load_header_from_file(const char *db_path) {
|
|
||||||
xdb_header_t *header;
|
|
||||||
FILE *handle = fopen(db_path, "rb");
|
|
||||||
if (handle == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
header = xdb_load_header(handle);
|
|
||||||
fclose(handle);
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close_header(void *ptr) {
|
|
||||||
xdb_header_t *header = (xdb_header_t *) ptr;
|
|
||||||
if (header->length > 0) {
|
|
||||||
header->length = 0;
|
|
||||||
xdb_free(header);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- vector index
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index(FILE *handle) {
|
|
||||||
xdb_vector_index_t *v_index;
|
|
||||||
unsigned int size = xdb_vector_index_length;
|
|
||||||
|
|
||||||
// seek to the vector index offset
|
|
||||||
if (fseek(handle, xdb_header_info_length, SEEK_SET) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the buffer read
|
|
||||||
v_index = (xdb_vector_index_t *) xdb_malloc(sizeof(xdb_vector_index_t));
|
|
||||||
if (v_index == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
v_index->length = size;
|
|
||||||
if (fread(v_index->buffer, 1, size, handle) != size) {
|
|
||||||
xdb_free(v_index);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return v_index;
|
|
||||||
}
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index_from_file(const char *db_path) {
|
|
||||||
xdb_vector_index_t *v_index;
|
|
||||||
FILE *handle = fopen(db_path, "rb");
|
|
||||||
if (handle == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
v_index = xdb_load_vector_index(handle);
|
|
||||||
fclose(handle);
|
|
||||||
return v_index;
|
|
||||||
}
|
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close_vector_index(void *ptr) {
|
|
||||||
xdb_vector_index_t *v_index = (xdb_vector_index_t *) ptr;
|
|
||||||
if (v_index->length > 0) {
|
|
||||||
v_index->length = 0;
|
|
||||||
xdb_free(v_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- content buffer
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_content_t *) xdb_load_content(FILE *handle) {
|
|
||||||
unsigned int size;
|
|
||||||
xdb_content_t *content;
|
|
||||||
|
|
||||||
// determine the file size
|
|
||||||
if (fseek(handle, 0, SEEK_END) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
size = (unsigned int) ftell(handle);
|
|
||||||
if (fseek(handle, 0, SEEK_SET) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the file read
|
|
||||||
content = (xdb_content_t *) xdb_malloc(sizeof(xdb_content_t));
|
|
||||||
if (content == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the buffer alloc
|
|
||||||
content->buffer = (char *) xdb_malloc(size);
|
|
||||||
if (content->buffer == NULL) {
|
|
||||||
xdb_free(content);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the content into the buffer
|
|
||||||
content->length = size;
|
|
||||||
if (fread(content->buffer, 1, size, handle) != size) {
|
|
||||||
xdb_free(content);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_content_t *) xdb_load_content_from_file(const char *db_path) {
|
|
||||||
xdb_content_t *content;
|
|
||||||
FILE *handle = fopen(db_path, "rb");
|
|
||||||
if (handle == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
content = xdb_load_content(handle);
|
|
||||||
fclose(handle);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close_content(void *ptr) {
|
|
||||||
xdb_content_t *content = (xdb_content_t *) ptr;
|
|
||||||
if (content->length > 0) {
|
|
||||||
content->length = 0;
|
|
||||||
xdb_free(content->buffer);
|
|
||||||
content->buffer = NULL;
|
|
||||||
xdb_free(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- End
|
|
||||||
|
|
||||||
// get unsigned long (4bytes) from a specified buffer start from the specified offset
|
|
||||||
XDB_PUBLIC(unsigned int) xdb_get_uint(const char *buffer, int offset) {
|
|
||||||
return (
|
|
||||||
((buffer[offset ]) & 0x000000FF) |
|
|
||||||
((buffer[offset+1] << 8) & 0x0000FF00) |
|
|
||||||
((buffer[offset+2] << 16) & 0x00FF0000) |
|
|
||||||
((buffer[offset+3] << 24) & 0xFF000000)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get unsigned short (2bytes) from a specified buffer start from the specified offset
|
|
||||||
XDB_PUBLIC(int) xdb_get_ushort(const char *buffer, int offset) {
|
|
||||||
return (
|
|
||||||
((buffer[offset ]) & 0x000000FF) |
|
|
||||||
((buffer[offset+1] << 8) & 0x0000FF00)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// string ip to unsigned int
|
|
||||||
static int shiftIndex[4] = {24, 16, 8, 0};
|
|
||||||
XDB_PUBLIC(int) xdb_check_ip(const char *src_ip, unsigned int *dst_ip) {
|
|
||||||
char c;
|
|
||||||
int i, n, ip = 0;
|
|
||||||
const char *ptr = src_ip;
|
|
||||||
for (i = 0; i < 4; i++) {
|
|
||||||
n = 0;
|
|
||||||
while (1) {
|
|
||||||
c = *ptr;
|
|
||||||
ptr++;
|
|
||||||
if (c >= '0' && c <= '9') {
|
|
||||||
n *= 10;
|
|
||||||
n += c - '0';
|
|
||||||
} else if ((i < 3 && c == '.') || i == 3) {
|
|
||||||
// stopping at the '.' but ignore the tailing chars
|
|
||||||
// after the 3rd one (auto clean the tailing none-integer ?).
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n > 0xFF) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
ip |= (n << shiftIndex[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
*dst_ip = ip;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsigned int ip to string ip
|
|
||||||
XDB_PUBLIC(void) xdb_long2ip(unsigned int ip, char *buffer) {
|
|
||||||
sprintf(buffer, "%d.%d.%d.%d", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the middle ip of a and b
|
|
||||||
XDB_PUBLIC(unsigned int) xdb_mip(unsigned long a, unsigned long b) {
|
|
||||||
return (unsigned int) ((a + b) >> 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
XDB_PUBLIC(long) xdb_now() {
|
|
||||||
struct timeval c_time;
|
|
||||||
gettimeofday(&c_time, NULL);
|
|
||||||
return c_time.tv_sec * (int)1e6 + c_time.tv_usec;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
// Copyright 2022 The Ip2Region Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// @Author Lion <chenxin619315@gmail.com>
|
|
||||||
// @Date 2022/06/27
|
|
||||||
|
|
||||||
#ifndef C_XDB_SEARCHER_H
|
|
||||||
#define C_XDB_SEARCHER_H
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#if ( defined(WIN32) || defined(_WIN32) || defined(__WINDOWS_) || defined(WINNT) )
|
|
||||||
# define XDB_PUBLIC(type) extern __declspec(dllexport) type
|
|
||||||
# define XDB_PRIVATE(type) static type
|
|
||||||
# define XDB_WINDOWS
|
|
||||||
#elif ( defined(linux) || defined(_UNIX) || defined(__APPLE__) )
|
|
||||||
# define XDB_PUBLIC(type) extern type
|
|
||||||
# define XDB_PRIVATE(type) static inline type
|
|
||||||
# define XDB_LINUX
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define xdb_calloc( _blocks, _bytes ) calloc( _blocks, _bytes )
|
|
||||||
#define xdb_malloc( _bytes ) malloc( _bytes )
|
|
||||||
#define xdb_free( _ptr ) free( _ptr )
|
|
||||||
|
|
||||||
// public constants define
|
|
||||||
#define xdb_header_info_length 256
|
|
||||||
#define xdb_vector_index_rows 256
|
|
||||||
#define xdb_vector_index_cols 256
|
|
||||||
#define xdb_vector_index_size 8
|
|
||||||
#define xdb_segment_index_size 14
|
|
||||||
|
|
||||||
// cache of vector_index_row × vector_index_rows × vector_index_size
|
|
||||||
#define xdb_vector_index_length 524288
|
|
||||||
|
|
||||||
|
|
||||||
// --- buffer load util functions
|
|
||||||
|
|
||||||
// use the following buffer struct to wrap the binary buffer data
|
|
||||||
// since the buffer data could not be operated with the string API.
|
|
||||||
struct xdb_header {
|
|
||||||
unsigned short version;
|
|
||||||
unsigned short index_policy;
|
|
||||||
unsigned int created_at;
|
|
||||||
unsigned int start_index_ptr;
|
|
||||||
unsigned int end_index_ptr;
|
|
||||||
|
|
||||||
// the original buffer
|
|
||||||
unsigned int length;
|
|
||||||
char buffer[xdb_header_info_length];
|
|
||||||
};
|
|
||||||
typedef struct xdb_header xdb_header_t;
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_header_t *) xdb_load_header(FILE *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_header_t *) xdb_load_header_from_file(const char *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close_header(void *);
|
|
||||||
|
|
||||||
|
|
||||||
// --- vector index buffer
|
|
||||||
struct xdb_vector_index {
|
|
||||||
unsigned int length;
|
|
||||||
char buffer[xdb_vector_index_length];
|
|
||||||
};
|
|
||||||
typedef struct xdb_vector_index xdb_vector_index_t;
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index(FILE *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index_from_file(const char *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close_vector_index(void *);
|
|
||||||
|
|
||||||
|
|
||||||
// --- content buffer
|
|
||||||
struct xdb_content {
|
|
||||||
unsigned int length;
|
|
||||||
char *buffer;
|
|
||||||
};
|
|
||||||
typedef struct xdb_content xdb_content_t;
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_content_t *) xdb_load_content(FILE *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(xdb_content_t *) xdb_load_content_from_file(const char *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close_content(void *);
|
|
||||||
|
|
||||||
// --- End buffer load
|
|
||||||
|
|
||||||
// xdb searcher structure
|
|
||||||
struct xdb_searcher_entry {
|
|
||||||
FILE *handle;
|
|
||||||
|
|
||||||
// header info
|
|
||||||
const char *header;
|
|
||||||
int io_count;
|
|
||||||
|
|
||||||
// vector index buffer cache.
|
|
||||||
// preload the vector index will reduce the number of IO operations
|
|
||||||
// thus speedup the search process.
|
|
||||||
const xdb_vector_index_t *v_index;
|
|
||||||
|
|
||||||
// content buffer.
|
|
||||||
// cache the whole xdb content.
|
|
||||||
const xdb_content_t *content;
|
|
||||||
};
|
|
||||||
typedef struct xdb_searcher_entry xdb_searcher_t;
|
|
||||||
|
|
||||||
// xdb searcher new api define
|
|
||||||
XDB_PUBLIC(int) xdb_new_with_file_only(xdb_searcher_t *, const char *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_new_with_vector_index(xdb_searcher_t *, const char *, const xdb_vector_index_t *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_new_with_buffer(xdb_searcher_t *, const xdb_content_t *);
|
|
||||||
|
|
||||||
XDB_PUBLIC(void) xdb_close(void *);
|
|
||||||
|
|
||||||
// xdb searcher search api define
|
|
||||||
XDB_PUBLIC(int) xdb_search_by_string(xdb_searcher_t *, const char *, char *, size_t);
|
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_search(xdb_searcher_t *, unsigned int, char *, size_t);
|
|
||||||
|
|
||||||
XDB_PUBLIC(int) xdb_get_io_count(xdb_searcher_t *);
|
|
||||||
|
|
||||||
|
|
||||||
// get unsigned long (4bytes) from a specified buffer start from the specified offset with little-endian
|
|
||||||
XDB_PUBLIC(unsigned int) xdb_get_uint(const char *, int);
|
|
||||||
|
|
||||||
// get unsigned short (2bytes) from a specified buffer start from the specified offset with little-endian
|
|
||||||
XDB_PUBLIC(int) xdb_get_ushort(const char *, int);
|
|
||||||
|
|
||||||
// check the specified string ip and convert it to an unsigned int
|
|
||||||
XDB_PUBLIC(int) xdb_check_ip(const char *, unsigned int *);
|
|
||||||
|
|
||||||
// unsigned int ip to string ip
|
|
||||||
XDB_PUBLIC(void) xdb_long2ip(unsigned int, char *);
|
|
||||||
|
|
||||||
// get the middle ip of a and b
|
|
||||||
XDB_PUBLIC(unsigned int) xdb_mip(unsigned long, unsigned long);
|
|
||||||
|
|
||||||
// get the current time in microseconds
|
|
||||||
XDB_PUBLIC(long) xdb_now();
|
|
||||||
|
|
||||||
|
|
||||||
#endif //C_XDB_SEARCHER_H
|
|
||||||
|
|
@ -0,0 +1,525 @@
|
||||||
|
// Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// @Author Lion <chenxin619315@gmail.com>
|
||||||
|
// @Date 2022/06/27
|
||||||
|
|
||||||
|
#include "xdb_api.h"
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
// for Linux
|
||||||
|
#ifdef XDB_LINUX
|
||||||
|
#include "sys/time.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef XDB_WINDOWS
|
||||||
|
#include <time.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// @Note: since 2023/10/13 to compatible with the windows system
|
||||||
|
#ifdef XDB_WINDOWS
|
||||||
|
static int winsock_initialized = 0;
|
||||||
|
XDB_PUBLIC(int) xdb_init_winsock() {
|
||||||
|
if (winsock_initialized == 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WSADATA wsaData;
|
||||||
|
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
winsock_initialized = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_clean_winsock() {
|
||||||
|
if (winsock_initialized == 1) {
|
||||||
|
WSACleanup();
|
||||||
|
winsock_initialized = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PRIVATE(int) gettimeofday(struct timeval* tp, void* tzp) {
|
||||||
|
time_t clock;
|
||||||
|
struct tm tm;
|
||||||
|
SYSTEMTIME wtm;
|
||||||
|
GetLocalTime(&wtm);
|
||||||
|
tm.tm_year = wtm.wYear - 1900;
|
||||||
|
tm.tm_mon = wtm.wMonth - 1;
|
||||||
|
tm.tm_mday = wtm.wDay;
|
||||||
|
tm.tm_hour = wtm.wHour;
|
||||||
|
tm.tm_min = wtm.wMinute;
|
||||||
|
tm.tm_sec = wtm.wSecond;
|
||||||
|
tm.tm_isdst = -1;
|
||||||
|
clock = mktime(&tm);
|
||||||
|
tp->tv_sec = clock;
|
||||||
|
tp->tv_usec = wtm.wMilliseconds * 1000;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
XDB_PUBLIC(int) xdb_init_winsock() {return 0;}
|
||||||
|
XDB_PUBLIC(void) xdb_clean_winsock() {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --- xdb buffer function implementations
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_header_t *) xdb_load_header(FILE *handle) {
|
||||||
|
xdb_header_t *header;
|
||||||
|
unsigned int size = xdb_header_info_length;
|
||||||
|
|
||||||
|
// entry alloc
|
||||||
|
header = (xdb_header_t *) xdb_malloc(sizeof(xdb_header_t));
|
||||||
|
if (header == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek(handle, 0, SEEK_SET) == -1) {
|
||||||
|
xdb_free(header);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fread(header->buffer, 1,size, handle) != size) {
|
||||||
|
xdb_free(header);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill the fields
|
||||||
|
header->length = size;
|
||||||
|
header->version = (unsigned short) xdb_le_get_uint16(header->buffer, 0);
|
||||||
|
header->index_policy = (unsigned short) xdb_le_get_uint16(header->buffer, 2);
|
||||||
|
header->created_at = xdb_le_get_uint32(header->buffer, 4);
|
||||||
|
header->start_index_ptr = xdb_le_get_uint32(header->buffer, 8);
|
||||||
|
header->end_index_ptr = xdb_le_get_uint32(header->buffer,12);
|
||||||
|
|
||||||
|
// since IPv6 supporting
|
||||||
|
header->ip_version = xdb_le_get_uint16(header->buffer, 16);
|
||||||
|
header->runtime_ptr_bytes = xdb_le_get_uint16(header->buffer, 18);
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_header_t *) xdb_load_header_from_file(const char *db_path) {
|
||||||
|
xdb_header_t *header;
|
||||||
|
FILE *handle = fopen(db_path, "rb");
|
||||||
|
if (handle == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
header = xdb_load_header(handle);
|
||||||
|
fclose(handle);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_free_header(void *ptr) {
|
||||||
|
xdb_header_t *header = (xdb_header_t *) ptr;
|
||||||
|
if (header->length > 0) {
|
||||||
|
header->length = 0;
|
||||||
|
xdb_free(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- vector index
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index(FILE *handle) {
|
||||||
|
xdb_vector_index_t *v_index;
|
||||||
|
unsigned int size = xdb_vector_index_length;
|
||||||
|
|
||||||
|
// seek to the vector index offset
|
||||||
|
if (fseek(handle, xdb_header_info_length, SEEK_SET) == -1) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the buffer read
|
||||||
|
v_index = (xdb_vector_index_t *) xdb_malloc(sizeof(xdb_vector_index_t));
|
||||||
|
if (v_index == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
v_index->length = size;
|
||||||
|
if (fread(v_index->buffer, 1, size, handle) != size) {
|
||||||
|
xdb_free(v_index);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return v_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index_from_file(const char *db_path) {
|
||||||
|
xdb_vector_index_t *v_index;
|
||||||
|
FILE *handle = fopen(db_path, "rb");
|
||||||
|
if (handle == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
v_index = xdb_load_vector_index(handle);
|
||||||
|
fclose(handle);
|
||||||
|
return v_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_free_vector_index(void *ptr) {
|
||||||
|
xdb_vector_index_t *v_index = (xdb_vector_index_t *) ptr;
|
||||||
|
if (v_index->length > 0) {
|
||||||
|
v_index->length = 0;
|
||||||
|
xdb_free(v_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- content buffer
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_content_t *) xdb_load_content(FILE *handle) {
|
||||||
|
unsigned int size;
|
||||||
|
xdb_content_t *content;
|
||||||
|
|
||||||
|
// determine the file size
|
||||||
|
if (fseek(handle, 0, SEEK_END) == -1) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = (unsigned int) ftell(handle);
|
||||||
|
if (fseek(handle, 0, SEEK_SET) == -1) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the file read
|
||||||
|
content = (xdb_content_t *) xdb_malloc(sizeof(xdb_content_t));
|
||||||
|
if (content == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the buffer alloc
|
||||||
|
content->buffer = (char *) xdb_malloc(size);
|
||||||
|
if (content->buffer == NULL) {
|
||||||
|
xdb_free(content);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the content into the buffer
|
||||||
|
content->length = size;
|
||||||
|
if (fread(content->buffer, 1, size, handle) != size) {
|
||||||
|
xdb_free(content);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_content_t *) xdb_load_content_from_file(const char *db_path) {
|
||||||
|
xdb_content_t *content;
|
||||||
|
FILE *handle = fopen(db_path, "rb");
|
||||||
|
if (handle == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = xdb_load_content(handle);
|
||||||
|
fclose(handle);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(void) xdb_free_content(void *ptr) {
|
||||||
|
xdb_content_t *content = (xdb_content_t *) ptr;
|
||||||
|
if (content->length > 0) {
|
||||||
|
content->length = 0;
|
||||||
|
xdb_free(content->buffer);
|
||||||
|
content->buffer = NULL;
|
||||||
|
xdb_free(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_verify_from_header(FILE *handle, xdb_header_t *header) {
|
||||||
|
unsigned int runtime_ptr_bytes = 0; // runtime ptr bytes
|
||||||
|
if (header->version == xdb_structure_20) {
|
||||||
|
runtime_ptr_bytes = 4;
|
||||||
|
} else if (header->version == xdb_structure_30) {
|
||||||
|
runtime_ptr_bytes = header->runtime_ptr_bytes;
|
||||||
|
} else {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1, confirm the xdb file size.
|
||||||
|
// to ensure that the maximum file pointer does not overflow.
|
||||||
|
int err = fseek(handle, 0L, SEEK_END);
|
||||||
|
if (err != 0) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
long long fileBytes = xdb_ftell(handle);
|
||||||
|
long long maxFilePtr = (1LL << (runtime_ptr_bytes * 8)) - 1;
|
||||||
|
// printf("fileBytes: %lld, maxFilePtr: %lld\n", fileBytes, maxFilePtr);
|
||||||
|
if (fileBytes > maxFilePtr) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_verify(FILE *handle) {
|
||||||
|
xdb_header_t *header = xdb_load_header(handle);
|
||||||
|
if (header == NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int errcode = xdb_verify_from_header(handle, header);
|
||||||
|
if (errcode != 0) {
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// what next ?
|
||||||
|
done:
|
||||||
|
xdb_free_header(header);
|
||||||
|
return errcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_verify_from_file(const char *db_path) {
|
||||||
|
FILE *handle = fopen(db_path, "rb");
|
||||||
|
if (handle == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = xdb_verify(handle);
|
||||||
|
fclose(handle);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- End content buffer
|
||||||
|
|
||||||
|
|
||||||
|
// --- ip version
|
||||||
|
|
||||||
|
// ip compare for IPv4
|
||||||
|
// ip1 - with Big endian byte order parsed from an input
|
||||||
|
// ip2 - with Little endian byte order read from the xdb index.
|
||||||
|
// to compatiable with the Little Endian encoded IPv4 on xdb 2.0.
|
||||||
|
XDB_PRIVATE(int) _ipv4_sub_compare(const bytes_ip_t *ip_bytes, int bytes, const char *buffer, int offset) {
|
||||||
|
register int i0, i1;
|
||||||
|
for (int i = 0, j = offset + bytes - 1; i < bytes; i++, j--) {
|
||||||
|
i0 = ip_bytes[i];
|
||||||
|
i1 = buffer[j] & 0xFF;
|
||||||
|
if (i0 > i1) {
|
||||||
|
return 1;
|
||||||
|
} else if (i0 < i1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static xdb_version_t _ip_version_list[] = {
|
||||||
|
// 14 = 4 + 4 + 2 + 4
|
||||||
|
{xdb_ipv4_id, "IPv4", xdb_ipv4_bytes, xdb_v4_index_size, _ipv4_sub_compare},
|
||||||
|
|
||||||
|
// 38 = 16 + 16 + 2 + 4
|
||||||
|
{xdb_ipv6_id, "IPv6", xdb_ipv6_bytes, xdb_v6_index_size, xdb_ip_sub_compare},
|
||||||
|
|
||||||
|
// END
|
||||||
|
{0, NULL, 0, 0, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_v4() {
|
||||||
|
return &_ip_version_list[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_v6() {
|
||||||
|
return &_ip_version_list[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_version_is_v4(const xdb_version_t *version) {
|
||||||
|
return version->id == xdb_ipv4_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_version_is_v6(const xdb_version_t *version) {
|
||||||
|
return version->id == xdb_ipv6_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_from_name(char *name) {
|
||||||
|
// to upper case the name
|
||||||
|
for (int i = 0; name[i] != '\0'; i++) {
|
||||||
|
name[i] = toupper((unsigned char) name[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(name, "V4") == 0 || strcmp(name, "IPV4") == 0) {
|
||||||
|
return xdb_version_v4();
|
||||||
|
} else if (strcmp(name, "V6") == 0 || strcmp(name, "IPV6") == 0) {
|
||||||
|
return xdb_version_v6();
|
||||||
|
} else {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_version_from_header(xdb_header_t *header) {
|
||||||
|
// Old structure with ONLY IPv4 supports
|
||||||
|
if (header->version == xdb_structure_20) {
|
||||||
|
return xdb_version_v4();
|
||||||
|
}
|
||||||
|
|
||||||
|
// structure 3.0 with IPv6 supporting
|
||||||
|
if (header->version != xdb_structure_30) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->ip_version == xdb_ipv4_id) {
|
||||||
|
return xdb_version_v4();
|
||||||
|
} else if (header->ip_version == xdb_ipv6_id) {
|
||||||
|
return xdb_version_v6();
|
||||||
|
} else {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- END ip version
|
||||||
|
|
||||||
|
XDB_PUBLIC(long) xdb_now() {
|
||||||
|
struct timeval c_time;
|
||||||
|
gettimeofday(&c_time, NULL);
|
||||||
|
return c_time.tv_sec * (int)1e6 + c_time.tv_usec;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(unsigned int) xdb_le_get_uint32(const char *buffer, int offset) {
|
||||||
|
return (
|
||||||
|
((buffer[offset ]) & 0x000000FF) |
|
||||||
|
((buffer[offset+1] << 8) & 0x0000FF00) |
|
||||||
|
((buffer[offset+2] << 16) & 0x00FF0000) |
|
||||||
|
((buffer[offset+3] << 24) & 0xFF000000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_le_get_uint16(const char *buffer, int offset) {
|
||||||
|
return (
|
||||||
|
((buffer[offset ]) & 0x000000FF) |
|
||||||
|
((buffer[offset+1] << 8) & 0x0000FF00)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_parse_ip(const string_ip_t *ip_string, bytes_ip_t *buffer, size_t length) {
|
||||||
|
char *d_ptr = strchr(ip_string, '.');
|
||||||
|
char *c_ptr = strchr(ip_string, ':');
|
||||||
|
// version check
|
||||||
|
if (d_ptr != NULL && c_ptr == NULL) {
|
||||||
|
return xdb_parse_v4_ip(ip_string, buffer, length);
|
||||||
|
} else if (c_ptr != NULL) {
|
||||||
|
return xdb_parse_v6_ip(ip_string, buffer, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_parse_v4_ip(const string_ip_t *ip_string, bytes_ip_t *buffer, size_t length) {
|
||||||
|
struct in_addr addr;
|
||||||
|
|
||||||
|
// buffer length checking
|
||||||
|
if (length < xdb_ipv4_bytes) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inet_pton(AF_INET, ip_string, &addr) != 1) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode the address to buffer with big endian byte bufffer.
|
||||||
|
buffer[0] = (addr.s_addr) & 0xFF;
|
||||||
|
buffer[1] = (addr.s_addr >> 8) & 0xFF;
|
||||||
|
buffer[2] = (addr.s_addr >> 16) & 0xFF;
|
||||||
|
buffer[3] = (addr.s_addr >> 24) & 0xFF;
|
||||||
|
return XDB_IPv4;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(xdb_version_t *) xdb_parse_v6_ip(const string_ip_t *ip_string, bytes_ip_t *buffer, size_t length) {
|
||||||
|
struct in6_addr addr;
|
||||||
|
|
||||||
|
// buffer length checking
|
||||||
|
if (length < xdb_ipv6_bytes) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inet_pton(AF_INET6, ip_string, &addr) != 1) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(buffer, addr.s6_addr, xdb_ipv6_bytes);
|
||||||
|
return XDB_IPv6;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_ip_to_string(const bytes_ip_t *ip_bytes, int bytes, char *ip_string, size_t length) {
|
||||||
|
if (bytes == xdb_ipv4_bytes) {
|
||||||
|
return xdb_v4_ip_to_string(ip_bytes, ip_string, length);
|
||||||
|
} else if (bytes == xdb_ipv6_bytes) {
|
||||||
|
return xdb_v6_ip_to_string(ip_bytes, ip_string, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_v4_ip_to_string(const bytes_ip_t *ip_bytes, char *ip_string, size_t length) {
|
||||||
|
if (!ip_bytes || !ip_string || length == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// buffer length checking
|
||||||
|
if (length < INET_ADDRSTRLEN) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inet_ntop(AF_INET, ip_bytes, ip_string, length) == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_v6_ip_to_string(const bytes_ip_t *ip_bytes, char *ip_string, size_t length) {
|
||||||
|
if (!ip_bytes || !ip_string || length == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length < INET6_ADDRSTRLEN) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inet_ntop(AF_INET6, ip_bytes, ip_string, length) == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_ip_sub_compare(const bytes_ip_t *ip1, int bytes, const char *buffer, int offset) {
|
||||||
|
register int i, i1, i2;
|
||||||
|
for (i = 0; i < bytes; i++) {
|
||||||
|
i1 = ip1[i];
|
||||||
|
i2 = buffer[offset + i] & 0xFF;
|
||||||
|
if (i1 > i2) {
|
||||||
|
return 1;
|
||||||
|
} else if (i1 < i2) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(int) xdb_fseek(FILE *handle, long long offset, int whence) {
|
||||||
|
// we may have to use the large file solution later
|
||||||
|
// #if defined(XDB_LINUX)
|
||||||
|
// return fseeko(handle, (off_t) offset, whence);
|
||||||
|
// #elif defined(XDB_WINDOWS)
|
||||||
|
// return _fseeki64(handle, (__int64) offset, whence)
|
||||||
|
// #else
|
||||||
|
// return fseek(handle, (long) offset, whence);
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return fseek(handle, (long) offset, whence);
|
||||||
|
}
|
||||||
|
|
||||||
|
XDB_PUBLIC(long long) xdb_ftell(FILE *handle) {
|
||||||
|
// we may have to use the large file solution later
|
||||||
|
// #if defined(XDB_LINUX)
|
||||||
|
// return (long long) ftello(handle);
|
||||||
|
// #elif defined(XDB_WINDOWS)
|
||||||
|
// return (long long) _ftelli64(handle);
|
||||||
|
// #else
|
||||||
|
// // report error ?
|
||||||
|
// return (long long) ftell(handle);
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return (long long) ftell(handle);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# build cache
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# dependency lock
|
||||||
|
cjpm.lock
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
# ip2region Cangjie Query Client
|
||||||
|
|
||||||
|
This implementation supports both `IPv4` and `IPv6`, and provides a `thread-safe` service layer.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
binding/cangjie/
|
||||||
|
├── src/
|
||||||
|
│ ├── xdb/ # Low-level xdb query engine
|
||||||
|
│ │ ├── searcher.cj # Searcher (file-only / vector-index / content-buff modes)
|
||||||
|
│ │ ├── util.cj # IP parsing/comparison, byte readers
|
||||||
|
│ │ ├── header.cj # xdb header + version parsing
|
||||||
|
│ │ ├── version.cj # IPv4/IPv6 version definitions
|
||||||
|
│ │ ├── *_test.cj # Unit tests (24 cases)
|
||||||
|
│ ├── service/ # High-level thread-safe service layer
|
||||||
|
│ │ ├── config.cj # Config (cache policy, pool size, pre-loaded data)
|
||||||
|
│ │ ├── searcher_pool.cj # SearcherPool (Semaphore + ConcurrentLinkedQueue)
|
||||||
|
│ │ ├── ip2region.cj # Ip2Region unified API
|
||||||
|
│ │ ├── *_test.cj # Unit tests (17 cases)
|
||||||
|
├── example/ # Executable CLI demo (separate project)
|
||||||
|
│ ├── cjpm.toml
|
||||||
|
│ └── src/main.cj # 5 usage demos + benchmarks
|
||||||
|
├── cjpm.toml # Static library project
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Two API layers:**
|
||||||
|
|
||||||
|
- **`ip2region.xdb`** — Low-level Searcher. Not thread-safe (mutable `ioCount` + `File` seek/read state). Supports three cache modes.
|
||||||
|
- **`ip2region.service`** — High-level `Ip2Region` class wrapping `SearcherPool`. Thread-safe, handles v4/v6 dispatch automatically.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd binding/cangjie
|
||||||
|
cjpm build # static library
|
||||||
|
cd example
|
||||||
|
cjpm build # example executable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
41 tests across both packages (24 xdb + 17 service):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd binding/cangjie
|
||||||
|
cjpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Policies
|
||||||
|
|
||||||
|
| Policy | Memory | Speed | Thread-safe* |
|
||||||
|
|--------|--------|-------|-------------|
|
||||||
|
| `FileOnly` (0) | ~0 MB | ~21 µs/op (v4) | Via SearcherPool |
|
||||||
|
| `VectorIndex` (1) | ~4 MB | ~20 µs/op (v4) | Via SearcherPool |
|
||||||
|
| `ContentBuff` (2) | ~full xdb | ~5 µs/op (v4) | Via SearcherPool |
|
||||||
|
|
||||||
|
\* Low-level `Searcher` is NOT thread-safe. The service layer (`SearcherPool` / `Ip2Region`) provides thread safety by pooling searchers (FileOnly/VectorIndex) or sharing a read-only buffer (ContentBuff).
|
||||||
|
|
||||||
|
## Benchmark Results
|
||||||
|
|
||||||
|
Benchmarked on Windows 11, AMD Ryzen 7, 487k IPv4 records / 638k IPv6 records:
|
||||||
|
|
||||||
|
### IPv4 (`ip2region_v4.xdb`, 487,167 records)
|
||||||
|
|
||||||
|
| Cache Policy | Total | Time | Avg |
|
||||||
|
|-------------|-------|------|-----|
|
||||||
|
| ContentBuff | 487,167 | 3,059 ms | **5 µs/op** |
|
||||||
|
| VectorIndex | 487,167 | 10,413 ms | **20 µs/op** |
|
||||||
|
| FileOnly | 487,167 | 11,271 ms | **21 µs/op** |
|
||||||
|
|
||||||
|
### IPv6 (`ip2region_v6.xdb`, 638,953 records)
|
||||||
|
|
||||||
|
| Cache Policy | Total | Time | Avg |
|
||||||
|
|-------------|-------|------|-----|
|
||||||
|
| ContentBuff | 638,953 | 37,080 ms | **56 µs/op** |
|
||||||
|
| VectorIndex | 638,953 | 38,443 ms | **58 µs/op** |
|
||||||
|
| FileOnly | 638,953 | 19,148 ms | **29 µs/op** |
|
||||||
|
|
||||||
|
Run your own:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd example
|
||||||
|
cjpm run -- bench --db ../../../data/ip2region_v4.xdb --src ../../../data/ipv4_source.txt --cache-policy content
|
||||||
|
cjpm run -- bench --db ../../../data/ip2region_v6.xdb --src ../../../data/ipv6_source.txt --cache-policy file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Low-level API (`ip2region.xdb`)
|
||||||
|
|
||||||
|
```cangjie
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
// Content buffer mode (fastest)
|
||||||
|
let content = File.readFrom(Path("ip2region_v4.xdb"))
|
||||||
|
let header = newHeaderFromBytes(content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let searcher = Searcher(version, content, "ip2region_v4.xdb")
|
||||||
|
let region = searcher.search(parseIP("220.181.108.183"))
|
||||||
|
searcher.close()
|
||||||
|
|
||||||
|
// Vector index mode
|
||||||
|
let vIndex = loadVectorIndex(content)
|
||||||
|
let searcher2 = Searcher(version, "ip2region_v4.xdb", vIndex)
|
||||||
|
searcher2.close()
|
||||||
|
|
||||||
|
// File only mode
|
||||||
|
let searcher3 = Searcher(version, "ip2region_v4.xdb")
|
||||||
|
searcher3.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### High-level API (`ip2region.service`)
|
||||||
|
|
||||||
|
```cangjie
|
||||||
|
import ip2region.service.*
|
||||||
|
|
||||||
|
// Via factory (VectorIndex, 20 searchers)
|
||||||
|
let region = newIp2Region("ip2region_v4.xdb", "ip2region_v6.xdb")
|
||||||
|
let result = region.search("220.181.108.183")
|
||||||
|
region.close()
|
||||||
|
|
||||||
|
// Custom config
|
||||||
|
let v4Cfg = Config(ContentBuff, IPv4, "ip2region_v4.xdb", 10)
|
||||||
|
let v6Cfg = Config(VectorIndex, IPv6, "ip2region_v6.xdb", 20)
|
||||||
|
let svc = Ip2Region(v4Cfg, v6Cfg)
|
||||||
|
let result = svc.search("2408:8266:100:1000::")
|
||||||
|
svc.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Single-version API
|
||||||
|
|
||||||
|
```cangjie
|
||||||
|
import ip2region.service.*
|
||||||
|
|
||||||
|
// IPv4 only — Ip2Region(config, IPv4)
|
||||||
|
let v4Cfg = Config(ContentBuff, IPv4, "ip2region_v4.xdb", 10)
|
||||||
|
let v4 = Ip2Region(v4Cfg, IPv4)
|
||||||
|
let r4 = v4.search("220.181.108.183")
|
||||||
|
v4.close()
|
||||||
|
|
||||||
|
// IPv6 only — Ip2Region(config, IPv6)
|
||||||
|
let v6Cfg = Config(ContentBuff, IPv6, "ip2region_v6.xdb", 10)
|
||||||
|
let v6 = Ip2Region(v6Cfg, IPv6)
|
||||||
|
let r6 = v6.search("2408:8266:100:1000::")
|
||||||
|
v6.close()
|
||||||
|
|
||||||
|
// Factory functions
|
||||||
|
let v4Only = newIp2RegionV4(v4Cfg)
|
||||||
|
let v6Only = newIp2RegionV6(v6Cfg)
|
||||||
|
v4Only.close(); v6Only.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd example
|
||||||
|
cjpm run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
- **`xdb.Searcher`** — NOT thread-safe. Keep one per thread or use a pool.
|
||||||
|
- **`service.SearcherPool`** — Thread-safe. Pre-allocates N searchers, uses `Semaphore` for backpressure and `ConcurrentLinkedQueue` for storage. `borrow()` blocks until a searcher is available.
|
||||||
|
- **`service.Ip2Region`** — Thread-safe. Delegates to pool or shared in-mem searcher based on cache policy. ContentBuff mode shares a single Searcher (Array<Byte> is read-only), FileOnly/VectorIndex use the pool.
|
||||||
|
- **`service.Config`** — Immutable after construction. Safe to share.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Cangjie SDK 1.1.0+
|
||||||
|
- `cjpm` build tool
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
cjc-version = "1.1.0"
|
||||||
|
name = "ip2region"
|
||||||
|
organization = ""
|
||||||
|
description = "ip2region xdb searcher library for Cangjie"
|
||||||
|
version = "1.0.0"
|
||||||
|
target-dir = ""
|
||||||
|
script-dir = ""
|
||||||
|
output-type = "static"
|
||||||
|
compile-option = "-Woff unused"
|
||||||
|
override-compile-option = ""
|
||||||
|
link-option = ""
|
||||||
|
package-configuration = {}
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
cjc-version = "1.1.0"
|
||||||
|
name = "ip2regionExample"
|
||||||
|
version = "1.0.0"
|
||||||
|
output-type = "executable"
|
||||||
|
compile-option = "-Woff unused"
|
||||||
|
description = "ip2region Cangjie CLI example"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ip2region = { path = ".." }
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
package ip2regionExample
|
||||||
|
|
||||||
|
import std.fs.*
|
||||||
|
import std.time.*
|
||||||
|
import std.collection.*
|
||||||
|
import ip2region.xdb.*
|
||||||
|
import ip2region.service.*
|
||||||
|
|
||||||
|
// ip2region Cangjie 使用示例
|
||||||
|
//
|
||||||
|
// 构建: cjpm build
|
||||||
|
// 运行: cjpm run
|
||||||
|
//
|
||||||
|
// 默认从 ../../../data/ 查找 xdb 文件,
|
||||||
|
// 也可通过命令行参数指定路径:
|
||||||
|
// cjpm run -- --v4-db <path> --v6-db <path>
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
println("ip2region Cangjie 使用示例")
|
||||||
|
println("用法: cjpm run -- [options]")
|
||||||
|
println("选项:")
|
||||||
|
println(" --v4-db <path> IPv4 xdb 文件路径 (默认: ../../../data/ip2region_v4.xdb)")
|
||||||
|
println(" --v6-db <path> IPv6 xdb 文件路径 (默认: ../../../data/ip2region_v6.xdb)")
|
||||||
|
println(" --help 显示此帮助")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 方式一: 使用底层 Searcher API (ip2region.xdb)
|
||||||
|
// 可自行选择缓存策略,单线程使用
|
||||||
|
// ============================================================
|
||||||
|
func demoXdbSearcher(v4Path: String, v6Path: String) {
|
||||||
|
println("\n=== 方式一: 底层 Searcher API ===")
|
||||||
|
|
||||||
|
// 1. 读取 xdb 文件到内存
|
||||||
|
let v4Content = File.readFrom(Path(v4Path))
|
||||||
|
|
||||||
|
// 2. 解析文件头,获取版本信息
|
||||||
|
let v4Header = newHeaderFromBytes(v4Content)
|
||||||
|
let v4Version = versionFromHeader(v4Header)
|
||||||
|
|
||||||
|
// 3. 选择缓存策略创建 Searcher
|
||||||
|
// 支持三种模式: ContentBuff(最快), VectorIndex(均衡), FileOnly(省内存)
|
||||||
|
let vIndex = loadVectorIndex(v4Content)
|
||||||
|
let searcher = Searcher(v4Version, v4Path, vIndex) // VectorIndex 模式
|
||||||
|
|
||||||
|
// 4. 查询 IP
|
||||||
|
let ip = parseIP("220.181.108.183")
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
println(" IP: 220.181.108.183")
|
||||||
|
println(" 结果: ${region}")
|
||||||
|
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 方式二: 使用 Ip2Region 服务 API (ip2region.service)
|
||||||
|
// 自动管理连接池,支持多线程安全访问
|
||||||
|
// ============================================================
|
||||||
|
func demoIp2Region(v4Path: String, v6Path: String) {
|
||||||
|
println("\n=== 方式二: Ip2Region 服务 API ===")
|
||||||
|
|
||||||
|
// 1. 创建配置 (缓存策略 + 连接池大小)
|
||||||
|
let v4Cfg = Config(VectorIndex, IPv4, v4Path, 10)
|
||||||
|
let v6Cfg = Config(VectorIndex, IPv6, v6Path, 10)
|
||||||
|
|
||||||
|
// 2. 创建服务 (同时支持 IPv4 + IPv6)
|
||||||
|
let region = Ip2Region(v4Cfg, v6Cfg)
|
||||||
|
|
||||||
|
// 3. 查询 IPv4
|
||||||
|
let r1 = region.search("220.181.108.183")
|
||||||
|
println(" IPv4: 220.181.108.183 -> ${r1}")
|
||||||
|
|
||||||
|
// 4. 查询 IPv6
|
||||||
|
let r2 = region.search("2408:8266:100:1000::")
|
||||||
|
println(" IPv6: 2408:8266:100:1000:: -> ${r2}")
|
||||||
|
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 方式三: 单版本服务 (仅 IPv4 或 仅 IPv6)
|
||||||
|
// 使用 init(config, ipVersion) 构造函数
|
||||||
|
// ============================================================
|
||||||
|
func demoSingleVersion(v4Path: String, v6Path: String) {
|
||||||
|
println("\n=== 方式三: 单版本服务 ===")
|
||||||
|
|
||||||
|
// IPv4 only
|
||||||
|
let v4Cfg = Config(ContentBuff, IPv4, v4Path, 3)
|
||||||
|
let v4Region = Ip2Region(v4Cfg, IPv4)
|
||||||
|
let r4 = v4Region.search("220.181.108.183")
|
||||||
|
println(" IPv4 only: 220.181.108.183 -> ${r4}")
|
||||||
|
v4Region.close()
|
||||||
|
|
||||||
|
// IPv6 only
|
||||||
|
let v6Cfg = Config(ContentBuff, IPv6, v6Path, 3)
|
||||||
|
let v6Region = Ip2Region(v6Cfg, IPv6)
|
||||||
|
let r6 = v6Region.search("2408:8266:100:1000::")
|
||||||
|
println(" IPv6 only: 2408:8266:100:1000:: -> ${r6}")
|
||||||
|
v6Region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 方式四: 使用简便工厂函数 (ip2region.service)
|
||||||
|
// 一行创建,默认 VectorIndex + 20 连接池
|
||||||
|
// ============================================================
|
||||||
|
func demoFactory(v4Path: String, v6Path: String) {
|
||||||
|
println("\n=== 方式四: 简便工厂函数 ===")
|
||||||
|
|
||||||
|
let region = newIp2Region(v4Path, v6Path)
|
||||||
|
let result = region.search("220.181.108.183")
|
||||||
|
println(" 220.181.108.183 -> ${result}")
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 性能测试: 测试三种缓存策略的查询速度
|
||||||
|
// ============================================================
|
||||||
|
func bench(v4Path: String) {
|
||||||
|
println("\n=== 性能测试 (IPv4, 487k 条记录) ===")
|
||||||
|
|
||||||
|
let content = File.readFrom(Path(v4Path))
|
||||||
|
let header = newHeaderFromBytes(content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let vIndex = loadVectorIndex(content)
|
||||||
|
|
||||||
|
// 从数据文件读取测试 IP 列表
|
||||||
|
let srcPath = v4Path.replace("ip2region_v4.xdb", "ipv4_source.txt")
|
||||||
|
let srcContent = File.readFrom(Path(srcPath))
|
||||||
|
let lines = String.fromUtf8(srcContent).split("\n")
|
||||||
|
|
||||||
|
var ipList = ArrayList<Array<Byte>>()
|
||||||
|
for (line in lines) {
|
||||||
|
let trimmed = line.trimAsciiStart().trimAsciiEnd()
|
||||||
|
if (trimmed.size == 0) { continue }
|
||||||
|
let parts = trimmed.split("|")
|
||||||
|
if (parts.size < 3) { continue }
|
||||||
|
try { ipList.add(parseIP(parts[0])) } catch (_) { continue }
|
||||||
|
}
|
||||||
|
println(" 加载了 ${ipList.size} 个 IP")
|
||||||
|
|
||||||
|
// 测试 ContentBuff 模式
|
||||||
|
println("\n [ContentBuff 模式]")
|
||||||
|
let s1 = Searcher(version, content, v4Path)
|
||||||
|
benchSearcher(s1, ipList)
|
||||||
|
|
||||||
|
// 测试 VectorIndex 模式
|
||||||
|
println(" [VectorIndex 模式]")
|
||||||
|
let s2 = Searcher(version, v4Path, vIndex)
|
||||||
|
benchSearcher(s2, ipList)
|
||||||
|
|
||||||
|
// 测试 FileOnly 模式
|
||||||
|
println(" [FileOnly 模式]")
|
||||||
|
let s3 = Searcher(version, v4Path)
|
||||||
|
benchSearcher(s3, ipList)
|
||||||
|
|
||||||
|
s1.close(); s2.close(); s3.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchSearcher(searcher: Searcher, ipList: ArrayList<Array<Byte>>) {
|
||||||
|
let count = ipList.size
|
||||||
|
let start = MonoTime.now()
|
||||||
|
for (i in 0..count) {
|
||||||
|
let _ = searcher.search(ipList[i])
|
||||||
|
}
|
||||||
|
let elapsed = MonoTime.now() - start
|
||||||
|
let avg = elapsed.toMicroseconds() / count
|
||||||
|
println(" 总量: ${count}, 耗时: ${elapsed.toMilliseconds()} ms, 平均: ${avg} us/op")
|
||||||
|
}
|
||||||
|
|
||||||
|
main(args: Array<String>): Int64 {
|
||||||
|
var v4Path = "../../../data/ip2region_v4.xdb"
|
||||||
|
var v6Path = "../../../data/ip2region_v6.xdb"
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (i < args.size) {
|
||||||
|
if (args[i] == "--v4-db" && i + 1 < args.size) {
|
||||||
|
v4Path = args[i + 1]; i = i + 2
|
||||||
|
} else if (args[i] == "--v6-db" && i + 1 < args.size) {
|
||||||
|
v6Path = args[i + 1]; i = i + 2
|
||||||
|
} else if (args[i] == "--help") {
|
||||||
|
printHelp(); return 0
|
||||||
|
} else { i = i + 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
println("ip2region Cangjie 使用示例")
|
||||||
|
println("xdb 路径: ${v4Path}")
|
||||||
|
|
||||||
|
demoXdbSearcher(v4Path, v6Path)
|
||||||
|
demoIp2Region(v4Path, v6Path)
|
||||||
|
demoSingleVersion(v4Path, v6Path)
|
||||||
|
demoFactory(v4Path, v6Path)
|
||||||
|
bench(v4Path)
|
||||||
|
|
||||||
|
println("\n完成!")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
package ip2region
|
||||||
|
|
||||||
|
import std.fs.*
|
||||||
|
import std.time.*
|
||||||
|
import std.env.*
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
println("ip2region xdb searcher")
|
||||||
|
println("Usage: ip2region [command] [command options]")
|
||||||
|
println("Command:")
|
||||||
|
println(" search search input test")
|
||||||
|
println(" bench search bench test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSearch(args: Array<String>) {
|
||||||
|
var v4DbPath = "../../data/ip2region_v4.xdb"
|
||||||
|
var v6DbPath = "../../data/ip2region_v6.xdb"
|
||||||
|
var v4CachePolicy = "vectorIndex"
|
||||||
|
var v6CachePolicy = "vectorIndex"
|
||||||
|
var showHelp = false
|
||||||
|
|
||||||
|
var i = 1 // skip command name
|
||||||
|
while (i < args.size) {
|
||||||
|
let arg = args[i]
|
||||||
|
if (arg == "--v4-db" && i + 1 < args.size) {
|
||||||
|
v4DbPath = args[i + 1]
|
||||||
|
i = i + 2
|
||||||
|
} else if (arg == "--v6-db" && i + 1 < args.size) {
|
||||||
|
v6DbPath = args[i + 1]
|
||||||
|
i = i + 2
|
||||||
|
} else if (arg == "--v4-cache-policy" && i + 1 < args.size) {
|
||||||
|
v4CachePolicy = args[i + 1]
|
||||||
|
i = i + 2
|
||||||
|
} else if (arg == "--v6-cache-policy" && i + 1 < args.size) {
|
||||||
|
v6CachePolicy = args[i + 1]
|
||||||
|
i = i + 2
|
||||||
|
} else if (arg == "--help") {
|
||||||
|
showHelp = true
|
||||||
|
i = i + 1
|
||||||
|
} else {
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showHelp) {
|
||||||
|
println("ip2region search [command options]")
|
||||||
|
println("options:")
|
||||||
|
println(" --v4-db <path> ip2region v4 binary xdb file path")
|
||||||
|
println(" --v4-cache-policy <mode> v4 cache policy: file/vectorIndex/content")
|
||||||
|
println(" --v6-db <path> ip2region v6 binary xdb file path")
|
||||||
|
println(" --v6-cache-policy <mode> v6 cache policy: file/vectorIndex/content")
|
||||||
|
println(" --help print this help")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load v4 xdb
|
||||||
|
print("Loading v4 xdb ... ")
|
||||||
|
let v4Content = File.readFrom(Path(v4DbPath))
|
||||||
|
let v4Header = newHeaderFromBytes(v4Content)
|
||||||
|
let v4Version = versionFromHeader(v4Header)
|
||||||
|
let v4Searcher = createSearcher(v4Version, v4Content, v4DbPath, v4CachePolicy)
|
||||||
|
println("done")
|
||||||
|
|
||||||
|
// Load v6 xdb
|
||||||
|
print("Loading v6 xdb ... ")
|
||||||
|
let v6Content = File.readFrom(Path(v6DbPath))
|
||||||
|
let v6Header = newHeaderFromBytes(v6Content)
|
||||||
|
let v6Version = versionFromHeader(v6Header)
|
||||||
|
let v6Searcher = createSearcher(v6Version, v6Content, v6DbPath, v6CachePolicy)
|
||||||
|
println("done")
|
||||||
|
|
||||||
|
println("ip2region search service test program")
|
||||||
|
println("type 'quit' to exit")
|
||||||
|
|
||||||
|
let reader = getStdIn()
|
||||||
|
while (true) {
|
||||||
|
print("ip2region>> ")
|
||||||
|
let lineOpt = reader.readln()
|
||||||
|
if (lineOpt == None) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let line = lineOpt.getOrThrow()
|
||||||
|
if (line == "quit" || line == "exit") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (line.size == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = MonoTime.now()
|
||||||
|
let region = searchIP(line, v4Searcher, v6Searcher)
|
||||||
|
let elapsed = MonoTime.now() - start
|
||||||
|
println("${line} -> ${region} (took: ${elapsed.toMicroseconds()} us)")
|
||||||
|
}
|
||||||
|
|
||||||
|
v4Searcher.close()
|
||||||
|
v6Searcher.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBench(args: Array<String>) {
|
||||||
|
var dbFile = ""
|
||||||
|
var srcFile = ""
|
||||||
|
var cachePolicy = "content"
|
||||||
|
|
||||||
|
var i = 1 // skip command name
|
||||||
|
while (i < args.size) {
|
||||||
|
let arg = args[i]
|
||||||
|
if (arg == "--db" && i + 1 < args.size) {
|
||||||
|
dbFile = args[i + 1]
|
||||||
|
i = i + 2
|
||||||
|
} else if (arg == "--src" && i + 1 < args.size) {
|
||||||
|
srcFile = args[i + 1]
|
||||||
|
i = i + 2
|
||||||
|
} else if (arg == "--cache-policy" && i + 1 < args.size) {
|
||||||
|
cachePolicy = args[i + 1]
|
||||||
|
i = i + 2
|
||||||
|
} else {
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbFile == "" || srcFile == "") {
|
||||||
|
println("ip2region bench [command options]")
|
||||||
|
println("options:")
|
||||||
|
println(" --db <path> ip2region binary xdb file path")
|
||||||
|
println(" --src <path> source ip text file path")
|
||||||
|
println(" --cache-policy <mode> cache policy: file/vectorIndex/content")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Loading xdb from: ${dbFile}")
|
||||||
|
let content = File.readFrom(Path(dbFile))
|
||||||
|
let header = newHeaderFromBytes(content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let searcher = createSearcher(version, content, dbFile, cachePolicy)
|
||||||
|
|
||||||
|
println("Loading source data from: ${srcFile}")
|
||||||
|
let srcContent = File.readFrom(Path(srcFile))
|
||||||
|
let srcStr = String.fromUtf8(srcContent)
|
||||||
|
let lines = srcStr.split("\n")
|
||||||
|
|
||||||
|
var count: Int64 = 0
|
||||||
|
var totalCost: Int64 = 0
|
||||||
|
let tStart = MonoTime.now()
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
let trimmed = line.trimAsciiStart().trimAsciiEnd()
|
||||||
|
if (trimmed.size == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = trimmed.split("|")
|
||||||
|
if (parts.size < 3) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let ipBytes = parseIP(parts[0])
|
||||||
|
let t0 = MonoTime.now()
|
||||||
|
let _ = searcher.search(ipBytes)
|
||||||
|
let t1 = MonoTime.now()
|
||||||
|
totalCost = totalCost + (t1 - t0).toNanoseconds()
|
||||||
|
count = count + 1
|
||||||
|
|
||||||
|
if (count % 10000 == 0) {
|
||||||
|
print(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tEnd = MonoTime.now()
|
||||||
|
let totalTime = tEnd - tStart
|
||||||
|
|
||||||
|
println("")
|
||||||
|
println("Bench finished:")
|
||||||
|
println(" cachePolicy: ${cachePolicy}")
|
||||||
|
println(" total: ${count}")
|
||||||
|
println(" took: ${totalTime.toMilliseconds()} ms")
|
||||||
|
if (count > 0) {
|
||||||
|
println(" avg: ${totalCost / count / 1000} us/op")
|
||||||
|
}
|
||||||
|
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSearcher(version: Version, content: Array<Byte>, dbPath: String, cachePolicy: String): Searcher {
|
||||||
|
if (cachePolicy == "content") {
|
||||||
|
println("Using content buffer mode")
|
||||||
|
return Searcher(version, content, dbPath)
|
||||||
|
} else if (cachePolicy == "vectorIndex") {
|
||||||
|
println("Using vector index mode")
|
||||||
|
let vIndex = loadVectorIndex(content)
|
||||||
|
return Searcher(version, dbPath, vIndex)
|
||||||
|
} else {
|
||||||
|
println("Using file only mode")
|
||||||
|
return Searcher(version, dbPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchIP(ipStr: String, v4Searcher: Searcher, v6Searcher: Searcher): String {
|
||||||
|
let ipBytes = parseIP(ipStr)
|
||||||
|
if (ipBytes.size == 4) {
|
||||||
|
return v4Searcher.search(ipBytes)
|
||||||
|
} else if (ipBytes.size == 16) {
|
||||||
|
return v6Searcher.search(ipBytes)
|
||||||
|
}
|
||||||
|
return "Invalid IP"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(args: Array<String>): Int64 {
|
||||||
|
if (args.size == 0) {
|
||||||
|
printHelp()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = args[0]
|
||||||
|
if (cmd == "search") {
|
||||||
|
runSearch(args)
|
||||||
|
} else if (cmd == "bench") {
|
||||||
|
runBench(args)
|
||||||
|
} else {
|
||||||
|
printHelp()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package ip2region.service
|
||||||
|
|
||||||
|
import std.fs.*
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
// Cache policy constants - match Searcher modes
|
||||||
|
public let FileOnly: Int64 = 0
|
||||||
|
public let VectorIndex: Int64 = 1
|
||||||
|
public let ContentBuff: Int64 = 2
|
||||||
|
|
||||||
|
// Config holds xdb file configuration and pre-loaded data for creating Searcher instances
|
||||||
|
public class Config {
|
||||||
|
public let cachePolicy: Int64
|
||||||
|
public let ipVersion: Version
|
||||||
|
public let xdbPath: String
|
||||||
|
public let header: Header
|
||||||
|
public let vIndex: Array<Byte>
|
||||||
|
public let cBuffer: Array<Byte>
|
||||||
|
public let searchers: Int64
|
||||||
|
|
||||||
|
public init(cachePolicy: Int64, ipVersion: Version, xdbPath: String, searchers: Int64) {
|
||||||
|
if (searchers <= 0) {
|
||||||
|
throw Exception("searchers must be > 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachePolicy = cachePolicy
|
||||||
|
this.ipVersion = ipVersion
|
||||||
|
this.xdbPath = xdbPath
|
||||||
|
this.searchers = searchers
|
||||||
|
|
||||||
|
let content = File.readFrom(Path(xdbPath))
|
||||||
|
this.header = newHeaderFromBytes(content)
|
||||||
|
|
||||||
|
// Verify IP version matches
|
||||||
|
let detectedVersion = versionFromHeader(this.header)
|
||||||
|
if (detectedVersion.id != ipVersion.id) {
|
||||||
|
throw Exception("xdb file IP version mismatch: expected ${ipVersion.name}, got ${detectedVersion.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachePolicy == VectorIndex) {
|
||||||
|
this.vIndex = loadVectorIndex(content)
|
||||||
|
this.cBuffer = Array<Byte>(0, repeat: 0)
|
||||||
|
} else if (cachePolicy == ContentBuff) {
|
||||||
|
this.vIndex = Array<Byte>(0, repeat: 0)
|
||||||
|
this.cBuffer = content
|
||||||
|
} else {
|
||||||
|
this.vIndex = Array<Byte>(0, repeat: 0)
|
||||||
|
this.cBuffer = Array<Byte>(0, repeat: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(cachePolicy: Int64, ipVersion: Version, xdbPath: String) {
|
||||||
|
this(cachePolicy, ipVersion, xdbPath, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static factories for IPv4 and IPv6
|
||||||
|
public func newV4Config(cachePolicy: Int64, xdbPath: String, searchers: Int64): Config {
|
||||||
|
return Config(cachePolicy, IPv4, xdbPath, searchers)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func newV6Config(cachePolicy: Int64, xdbPath: String, searchers: Int64): Config {
|
||||||
|
return Config(cachePolicy, IPv6, xdbPath, searchers)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func newV4Config(cachePolicy: Int64, xdbPath: String): Config {
|
||||||
|
return Config(cachePolicy, IPv4, xdbPath, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func newV6Config(cachePolicy: Int64, xdbPath: String): Config {
|
||||||
|
return Config(cachePolicy, IPv6, xdbPath, 20)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
package ip2region.service
|
||||||
|
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
// Ip2Region is the high-level API for searching IP addresses.
|
||||||
|
// It supports both IPv4 and IPv6 with thread-safe access via SearcherPool.
|
||||||
|
public class Ip2Region {
|
||||||
|
let v4Pool: SearcherPool
|
||||||
|
let v6Pool: SearcherPool
|
||||||
|
let v4InMemSearcher: Searcher
|
||||||
|
let v6InMemSearcher: Searcher
|
||||||
|
let hasV4: Bool
|
||||||
|
let hasV6: Bool
|
||||||
|
let v4IsInMem: Bool
|
||||||
|
let v6IsInMem: Bool
|
||||||
|
|
||||||
|
// Create Ip2Region with both v4 and v6 configurations.
|
||||||
|
public init(v4Config: Config, v6Config: Config) {
|
||||||
|
// v4 setup
|
||||||
|
if (v4Config.cachePolicy == ContentBuff) {
|
||||||
|
this.hasV4 = true
|
||||||
|
this.v4IsInMem = true
|
||||||
|
this.v4InMemSearcher = Searcher(v4Config.ipVersion, v4Config.cBuffer, v4Config.xdbPath)
|
||||||
|
this.v4Pool = SearcherPool(v4Config)
|
||||||
|
} else {
|
||||||
|
this.hasV4 = true
|
||||||
|
this.v4IsInMem = false
|
||||||
|
this.v4InMemSearcher = Searcher(v4Config.ipVersion, Array<Byte>(0, repeat: 0), v4Config.xdbPath)
|
||||||
|
this.v4Pool = SearcherPool(v4Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// v6 setup
|
||||||
|
if (v6Config.cachePolicy == ContentBuff) {
|
||||||
|
this.hasV6 = true
|
||||||
|
this.v6IsInMem = true
|
||||||
|
this.v6InMemSearcher = Searcher(v6Config.ipVersion, v6Config.cBuffer, v6Config.xdbPath)
|
||||||
|
this.v6Pool = SearcherPool(v6Config)
|
||||||
|
} else {
|
||||||
|
this.hasV6 = true
|
||||||
|
this.v6IsInMem = false
|
||||||
|
this.v6InMemSearcher = Searcher(v6Config.ipVersion, Array<Byte>(0, repeat: 0), v6Config.xdbPath)
|
||||||
|
this.v6Pool = SearcherPool(v6Config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-version: provide Config and IP version.
|
||||||
|
// Examples:
|
||||||
|
// Ip2Region(v4Config, IPv4) — IPv4 only
|
||||||
|
// Ip2Region(v6Config, IPv6) — IPv6 only
|
||||||
|
public init(config: Config, ipVersion: Version) {
|
||||||
|
if (ipVersion.id == IPv4VersionNo) {
|
||||||
|
this.hasV4 = true
|
||||||
|
this.v4IsInMem = (config.cachePolicy == ContentBuff)
|
||||||
|
if (config.cachePolicy == ContentBuff) {
|
||||||
|
this.v4InMemSearcher = Searcher(config.ipVersion, config.cBuffer, config.xdbPath)
|
||||||
|
} else {
|
||||||
|
this.v4InMemSearcher = Searcher(config.ipVersion, Array<Byte>(0, repeat: 0), config.xdbPath)
|
||||||
|
}
|
||||||
|
this.v4Pool = SearcherPool(config)
|
||||||
|
// IPv6 disabled
|
||||||
|
this.hasV6 = false
|
||||||
|
this.v6IsInMem = false
|
||||||
|
this.v6InMemSearcher = Searcher(IPv4, Array<Byte>(0, repeat: 0), config.xdbPath)
|
||||||
|
this.v6Pool = SearcherPool(Config(ContentBuff, IPv4, config.xdbPath, 1))
|
||||||
|
} else {
|
||||||
|
this.hasV6 = true
|
||||||
|
this.v6IsInMem = (config.cachePolicy == ContentBuff)
|
||||||
|
if (config.cachePolicy == ContentBuff) {
|
||||||
|
this.v6InMemSearcher = Searcher(config.ipVersion, config.cBuffer, config.xdbPath)
|
||||||
|
} else {
|
||||||
|
this.v6InMemSearcher = Searcher(config.ipVersion, Array<Byte>(0, repeat: 0), config.xdbPath)
|
||||||
|
}
|
||||||
|
this.v6Pool = SearcherPool(config)
|
||||||
|
// IPv4 disabled
|
||||||
|
this.hasV4 = false
|
||||||
|
this.v4IsInMem = false
|
||||||
|
this.v4InMemSearcher = Searcher(IPv6, Array<Byte>(0, repeat: 0), config.xdbPath)
|
||||||
|
this.v4Pool = SearcherPool(Config(ContentBuff, IPv6, config.xdbPath, 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search an IP string and return the region info.
|
||||||
|
public func search(ipStr: String): String {
|
||||||
|
try {
|
||||||
|
let ipBytes = parseIP(ipStr)
|
||||||
|
|
||||||
|
if (ipBytes.size == 4) {
|
||||||
|
return this.searchV4(ipBytes)
|
||||||
|
} else if (ipBytes.size == 16) {
|
||||||
|
return this.searchV6(ipBytes)
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
return "Invalid IP"
|
||||||
|
}
|
||||||
|
return "Invalid IP"
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchV4(ip: Array<Byte>): String {
|
||||||
|
if (!this.hasV4) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (this.v4IsInMem) {
|
||||||
|
return this.v4InMemSearcher.search(ip)
|
||||||
|
}
|
||||||
|
let searcher = this.v4Pool.borrow()
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
this.v4Pool.return_(searcher)
|
||||||
|
return region
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchV6(ip: Array<Byte>): String {
|
||||||
|
if (!this.hasV6) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (this.v6IsInMem) {
|
||||||
|
return this.v6InMemSearcher.search(ip)
|
||||||
|
}
|
||||||
|
let searcher = this.v6Pool.borrow()
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
this.v6Pool.return_(searcher)
|
||||||
|
return region
|
||||||
|
}
|
||||||
|
|
||||||
|
// close releases all resources.
|
||||||
|
public func close() {
|
||||||
|
if (this.hasV4) {
|
||||||
|
this.v4Pool.close()
|
||||||
|
}
|
||||||
|
if (this.hasV6) {
|
||||||
|
this.v6Pool.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeTimeout releases all resources with the specified timeout.
|
||||||
|
public func closeTimeout(timeout: Duration) {
|
||||||
|
if (this.hasV4) {
|
||||||
|
this.v4Pool.closeTimeout(timeout)
|
||||||
|
}
|
||||||
|
if (this.hasV6) {
|
||||||
|
this.v6Pool.closeTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an Ip2Region instance using xdb file paths.
|
||||||
|
// Uses VectorIndex cache policy with 20 searchers by default.
|
||||||
|
public func newIp2Region(v4XdbPath: String, v6XdbPath: String): Ip2Region {
|
||||||
|
let v4Config = Config(VectorIndex, IPv4, v4XdbPath, 20)
|
||||||
|
let v6Config = Config(VectorIndex, IPv6, v6XdbPath, 20)
|
||||||
|
return Ip2Region(v4Config, v6Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an IPv4-only Ip2Region service.
|
||||||
|
public func newIp2RegionV4(config: Config): Ip2Region {
|
||||||
|
return Ip2Region(config, IPv4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an IPv6-only Ip2Region service.
|
||||||
|
public func newIp2RegionV6(config: Config): Ip2Region {
|
||||||
|
return Ip2Region(config, IPv6)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
package ip2region.service
|
||||||
|
|
||||||
|
import std.sync.*
|
||||||
|
import std.core.*
|
||||||
|
import std.time.*
|
||||||
|
import std.collection.concurrent.*
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
// SearcherPool is a thread-safe pool of xdb.Searcher instances.
|
||||||
|
// Uses Semaphore for backpressure and ConcurrentLinkedQueue for storage.
|
||||||
|
public class SearcherPool {
|
||||||
|
let config: Config
|
||||||
|
let queue: ConcurrentLinkedQueue<Searcher>
|
||||||
|
let semaphore: Semaphore
|
||||||
|
let closing: AtomicBool
|
||||||
|
let loanCnt: AtomicInt64
|
||||||
|
|
||||||
|
public init(config: Config) {
|
||||||
|
this.config = config
|
||||||
|
this.queue = ConcurrentLinkedQueue<Searcher>()
|
||||||
|
this.semaphore = Semaphore(config.searchers)
|
||||||
|
this.closing = AtomicBool(false)
|
||||||
|
this.loanCnt = AtomicInt64(0)
|
||||||
|
|
||||||
|
let poolSize = config.searchers
|
||||||
|
for (_ in 0..poolSize) {
|
||||||
|
let searcher = createPoolSearcher(config)
|
||||||
|
this.queue.add(searcher)
|
||||||
|
this.semaphore.release(amount: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// borrow takes a Searcher from the pool, blocking until one is available.
|
||||||
|
// Throws if the pool is closing or closed.
|
||||||
|
public func borrow(): Searcher {
|
||||||
|
while (!this.closing.load()) {
|
||||||
|
if (this.semaphore.tryAcquire(amount: 1)) {
|
||||||
|
let opt = this.queue.remove()
|
||||||
|
if (opt.isNone()) {
|
||||||
|
this.semaphore.release(amount: 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let s = opt.getOrThrow()
|
||||||
|
this.loanCnt.fetchAdd(1)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
sleep(Duration.millisecond * 5)
|
||||||
|
}
|
||||||
|
throw Exception("SearcherPool is closing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// return_ returns a Searcher to the pool, or closes it if the pool is shutting down.
|
||||||
|
public func return_(searcher: Searcher) {
|
||||||
|
if (this.closing.load()) {
|
||||||
|
searcher.close()
|
||||||
|
} else {
|
||||||
|
this.queue.add(searcher)
|
||||||
|
this.semaphore.release(amount: 1)
|
||||||
|
}
|
||||||
|
this.loanCnt.fetchSub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// close closes the pool with a default 10 second timeout.
|
||||||
|
public func close() {
|
||||||
|
this.closeTimeout(Duration.second * 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeTimeout closes the pool with the specified timeout.
|
||||||
|
public func closeTimeout(timeout: Duration) {
|
||||||
|
this.closing.store(true)
|
||||||
|
|
||||||
|
// Drain the queue: acquire all permits and close searchers
|
||||||
|
while (true) {
|
||||||
|
if (!this.semaphore.tryAcquire(amount: 1)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let opt = this.queue.remove()
|
||||||
|
if (opt.isSome()) {
|
||||||
|
opt.getOrThrow().close()
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for outstanding loans to return (with timeout)
|
||||||
|
let deadline = MonoTime.now() + timeout
|
||||||
|
while (this.loanCnt.load() > 0) {
|
||||||
|
if (MonoTime.now() >= deadline) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sleep(Duration.millisecond * 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getLoanCount(): Int64 {
|
||||||
|
return this.loanCnt.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create a Searcher based on Config's cache policy
|
||||||
|
func createPoolSearcher(config: Config): Searcher {
|
||||||
|
if (config.cachePolicy == ContentBuff) {
|
||||||
|
return Searcher(config.ipVersion, config.cBuffer, config.xdbPath)
|
||||||
|
} else if (config.cachePolicy == VectorIndex) {
|
||||||
|
return Searcher(config.ipVersion, config.xdbPath, config.vIndex)
|
||||||
|
} else {
|
||||||
|
return Searcher(config.ipVersion, config.xdbPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package ip2region.tests.service
|
||||||
|
|
||||||
|
import ip2region.xdb.*
|
||||||
|
import ip2region.service.*
|
||||||
|
|
||||||
|
@Test
|
||||||
|
class ConfigV4Tests {
|
||||||
|
@TestCase
|
||||||
|
func testCreateFileOnly() {
|
||||||
|
let cfg = Config(FileOnly, IPv4, "../../data/ip2region_v4.xdb", 5)
|
||||||
|
if (cfg.cachePolicy != FileOnly) { throw Exception("wrong cache policy") }
|
||||||
|
if (cfg.searchers != 5) { throw Exception("wrong searchers count") }
|
||||||
|
if (cfg.ipVersion.id != IPv4VersionNo) { throw Exception("wrong version") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testCreateVectorIndex() {
|
||||||
|
let cfg = Config(VectorIndex, IPv4, "../../data/ip2region_v4.xdb", 10)
|
||||||
|
if (cfg.vIndex.size == 0) { throw Exception("vIndex should be loaded") }
|
||||||
|
let expectedSize = VectorIndexRows * VectorIndexCols * VectorIndexSize
|
||||||
|
if (cfg.vIndex.size != expectedSize) {
|
||||||
|
throw Exception("vIndex size mismatch: ${cfg.vIndex.size} vs ${expectedSize}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testCreateContentBuff() {
|
||||||
|
let cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 3)
|
||||||
|
if (cfg.cBuffer.size == 0) { throw Exception("cBuffer should be loaded") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testFactoryV4() {
|
||||||
|
let cfg = newV4Config(VectorIndex, "../../data/ip2region_v4.xdb", 8)
|
||||||
|
if (cfg.ipVersion.id != IPv4VersionNo) { throw Exception("should be IPv4") }
|
||||||
|
if (cfg.searchers != 8) { throw Exception("wrong searchers") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testFactoryV6() {
|
||||||
|
let cfg = newV6Config(VectorIndex, "../../data/ip2region_v6.xdb", 8)
|
||||||
|
if (cfg.ipVersion.id != IPv6VersionNo) { throw Exception("should be IPv6") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testFactoryDefaultSearchers() {
|
||||||
|
let cfg = newV4Config(ContentBuff, "../../data/ip2region_v4.xdb")
|
||||||
|
if (cfg.searchers != 20) { throw Exception("default searchers should be 20") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package ip2region.tests.service
|
||||||
|
|
||||||
|
import ip2region.xdb.*
|
||||||
|
import ip2region.service.*
|
||||||
|
|
||||||
|
@Test
|
||||||
|
class Ip2RegionBasicTests {
|
||||||
|
@TestCase
|
||||||
|
func testSearchWithContentBuff() {
|
||||||
|
let v4Cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 1)
|
||||||
|
let v6Cfg = Config(ContentBuff, IPv6, "../../data/ip2region_v6.xdb", 1)
|
||||||
|
let region = Ip2Region(v4Cfg, v6Cfg)
|
||||||
|
let r1 = region.search("220.181.108.183")
|
||||||
|
if (r1.size == 0) {
|
||||||
|
throw Exception("v4 search should return result")
|
||||||
|
}
|
||||||
|
let r2 = region.search("2408:8266:100:1000::")
|
||||||
|
if (r2.size == 0) {
|
||||||
|
throw Exception("v6 search should return result")
|
||||||
|
}
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testSearchWithVectorIndex() {
|
||||||
|
let v4Cfg = Config(VectorIndex, IPv4, "../../data/ip2region_v4.xdb", 5)
|
||||||
|
let v6Cfg = Config(VectorIndex, IPv6, "../../data/ip2region_v6.xdb", 5)
|
||||||
|
let region = Ip2Region(v4Cfg, v6Cfg)
|
||||||
|
let r = region.search("220.181.108.183")
|
||||||
|
if (r.size == 0) {
|
||||||
|
throw Exception("v4 search should return result")
|
||||||
|
}
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testInvalidIP() {
|
||||||
|
let v4Cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 1)
|
||||||
|
let v6Cfg = Config(ContentBuff, IPv6, "../../data/ip2region_v6.xdb", 1)
|
||||||
|
let region = Ip2Region(v4Cfg, v6Cfg)
|
||||||
|
let r = region.search("invalid")
|
||||||
|
if (r != "Invalid IP") {
|
||||||
|
throw Exception("expected 'Invalid IP', got '${r}'")
|
||||||
|
}
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testClose() {
|
||||||
|
let v4Cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 1)
|
||||||
|
let v6Cfg = Config(ContentBuff, IPv6, "../../data/ip2region_v6.xdb", 1)
|
||||||
|
let region = Ip2Region(v4Cfg, v6Cfg)
|
||||||
|
region.search("220.181.108.183")
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testV4OnlyInit() {
|
||||||
|
let cfg = Config(VectorIndex, IPv4, "../../data/ip2region_v4.xdb", 3)
|
||||||
|
let region = Ip2Region(cfg, IPv4)
|
||||||
|
let r = region.search("220.181.108.183")
|
||||||
|
if (r.size == 0) {
|
||||||
|
throw Exception("v4-only region should find v4 IP")
|
||||||
|
}
|
||||||
|
let r6 = region.search("2408:8266:100:1000::")
|
||||||
|
if (r6.size != 0) {
|
||||||
|
throw Exception("v4-only region should not find v6 IP")
|
||||||
|
}
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testV6OnlyInit() {
|
||||||
|
let cfg = Config(VectorIndex, IPv6, "../../data/ip2region_v6.xdb", 3)
|
||||||
|
let region = Ip2Region(cfg, IPv6)
|
||||||
|
let r = region.search("2408:8266:100:1000::")
|
||||||
|
if (r.size == 0) {
|
||||||
|
throw Exception("v6-only region should find v6 IP")
|
||||||
|
}
|
||||||
|
let r4 = region.search("220.181.108.183")
|
||||||
|
if (r4.size != 0) {
|
||||||
|
throw Exception("v6-only region should not find v4 IP")
|
||||||
|
}
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testNewV4Factory() {
|
||||||
|
let cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 2)
|
||||||
|
let region = newIp2RegionV4(cfg)
|
||||||
|
let r = region.search("220.181.108.183")
|
||||||
|
if (r.size == 0) {
|
||||||
|
throw Exception("newIp2RegionV4 should work")
|
||||||
|
}
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testNewV6Factory() {
|
||||||
|
let cfg = Config(ContentBuff, IPv6, "../../data/ip2region_v6.xdb", 2)
|
||||||
|
let region = newIp2RegionV6(cfg)
|
||||||
|
let r = region.search("2408:8266:100:1000::")
|
||||||
|
if (r.size == 0) {
|
||||||
|
throw Exception("newIp2RegionV6 should work")
|
||||||
|
}
|
||||||
|
region.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package ip2region.tests.service
|
||||||
|
|
||||||
|
import ip2region.xdb.*
|
||||||
|
import ip2region.service.*
|
||||||
|
|
||||||
|
@Test
|
||||||
|
class SearcherPoolTests {
|
||||||
|
@TestCase
|
||||||
|
func testBorrowAndReturn() {
|
||||||
|
let cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 3)
|
||||||
|
let pool = SearcherPool(cfg)
|
||||||
|
let s = pool.borrow()
|
||||||
|
if (pool.getLoanCount() != 1) {
|
||||||
|
throw Exception("loan count should be 1, got ${pool.getLoanCount()}")
|
||||||
|
}
|
||||||
|
pool.return_(s)
|
||||||
|
if (pool.getLoanCount() != 0) {
|
||||||
|
throw Exception("loan count should be 0 after return")
|
||||||
|
}
|
||||||
|
pool.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testBorrowAll() {
|
||||||
|
let cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 5)
|
||||||
|
let pool = SearcherPool(cfg)
|
||||||
|
let s1 = pool.borrow()
|
||||||
|
let s2 = pool.borrow()
|
||||||
|
let s3 = pool.borrow()
|
||||||
|
let s4 = pool.borrow()
|
||||||
|
let s5 = pool.borrow()
|
||||||
|
if (pool.getLoanCount() != 5) {
|
||||||
|
throw Exception("should have 5 loans, got ${pool.getLoanCount()}")
|
||||||
|
}
|
||||||
|
pool.return_(s5)
|
||||||
|
pool.return_(s4)
|
||||||
|
pool.return_(s3)
|
||||||
|
pool.return_(s2)
|
||||||
|
pool.return_(s1)
|
||||||
|
if (pool.getLoanCount() != 0) {
|
||||||
|
throw Exception("all should be returned, got ${pool.getLoanCount()}")
|
||||||
|
}
|
||||||
|
pool.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testClosePool() {
|
||||||
|
let cfg = Config(ContentBuff, IPv4, "../../data/ip2region_v4.xdb", 2)
|
||||||
|
let pool = SearcherPool(cfg)
|
||||||
|
let s = pool.borrow()
|
||||||
|
pool.return_(s)
|
||||||
|
pool.close()
|
||||||
|
pool.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package ip2region.tests
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package ip2region.tests.xdb
|
||||||
|
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
@Test
|
||||||
|
class HeaderTests {
|
||||||
|
func makeHeaderBytes(): Array<Byte> {
|
||||||
|
let buf = Array<Byte>(256, repeat: 0)
|
||||||
|
var v: Byte
|
||||||
|
v = 3; buf[0] = v; v = 0; buf[1] = v
|
||||||
|
v = 1; buf[2] = v; v = 0; buf[3] = v
|
||||||
|
v = 0xA0; buf[4] = v; v = 0x86; buf[5] = v; v = 0x01; buf[6] = v; v = 0x00; buf[7] = v
|
||||||
|
v = 0xE8; buf[8] = v; v = 0x03; buf[9] = v; v = 0x00; buf[10] = v; v = 0x00; buf[11] = v
|
||||||
|
v = 0xD0; buf[12] = v; v = 0x07; buf[13] = v; v = 0x00; buf[14] = v; v = 0x00; buf[15] = v
|
||||||
|
v = 4; buf[16] = v; v = 0; buf[17] = v
|
||||||
|
v = 0; buf[18] = v; v = 0; buf[19] = v
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testNewHeaderFromBytes() {
|
||||||
|
let buf = makeHeaderBytes()
|
||||||
|
let h = newHeaderFromBytes(buf)
|
||||||
|
if (h.version != UInt16(3)) {
|
||||||
|
throw Exception("expected version 3, got ${h.version}")
|
||||||
|
}
|
||||||
|
if (h.ipVersion != UInt16(4)) {
|
||||||
|
throw Exception("expected ipVersion 4, got ${h.ipVersion}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testHeaderToString() {
|
||||||
|
let buf = makeHeaderBytes()
|
||||||
|
let h = newHeaderFromBytes(buf)
|
||||||
|
let s = h.toString()
|
||||||
|
if (s.size == 0) {
|
||||||
|
throw Exception("toString should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testVersionFromHeaderV4() {
|
||||||
|
let buf = makeHeaderBytes()
|
||||||
|
let h = newHeaderFromBytes(buf)
|
||||||
|
let v = versionFromHeader(h)
|
||||||
|
if (v.id != IPv4VersionNo) {
|
||||||
|
throw Exception("expected IPv4, got ${v.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testVersionFromHeaderV6() {
|
||||||
|
let buf = makeHeaderBytes()
|
||||||
|
var v: Byte = 6; buf[16] = v
|
||||||
|
let h = newHeaderFromBytes(buf)
|
||||||
|
let ver = versionFromHeader(h)
|
||||||
|
if (ver.id != IPv6VersionNo) {
|
||||||
|
throw Exception("expected IPv6, got ${ver.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testVersionFromHeader20() {
|
||||||
|
let buf = makeHeaderBytes()
|
||||||
|
var v: Byte = 2; buf[0] = v
|
||||||
|
v = 0; buf[16] = v
|
||||||
|
let h = newHeaderFromBytes(buf)
|
||||||
|
let ver = versionFromHeader(h)
|
||||||
|
if (ver.id != IPv4VersionNo) {
|
||||||
|
throw Exception("expected IPv4 for v2.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testHeaderConstants() {
|
||||||
|
if (HeaderInfoLength != 256) { throw Exception("expected 256") }
|
||||||
|
if (VectorIndexRows != 256) { throw Exception("expected 256") }
|
||||||
|
if (VectorIndexSize != 8) { throw Exception("expected 8") }
|
||||||
|
if (IPv4SegmentIndexSize != 14) { throw Exception("expected 14") }
|
||||||
|
if (IPv6SegmentIndexSize != 38) { throw Exception("expected 38") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package ip2region.tests.xdb
|
||||||
|
|
||||||
|
import std.fs.*
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
@Test
|
||||||
|
class SearcherV4Tests {
|
||||||
|
var v4Content: Array<Byte> = Array<Byte>(0, repeat: 0)
|
||||||
|
var initialized: Bool = false
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
func setup() {
|
||||||
|
this.v4Content = File.readFrom(Path("../../data/ip2region_v4.xdb"))
|
||||||
|
this.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testSearchContentMode() {
|
||||||
|
if (!this.initialized) { return }
|
||||||
|
let header = newHeaderFromBytes(this.v4Content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let searcher = Searcher(version, this.v4Content, "../../data/ip2region_v4.xdb")
|
||||||
|
let ip = parseIP("220.181.108.183")
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
if (region.size == 0) {
|
||||||
|
throw Exception("should find region for known IP")
|
||||||
|
}
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testSearchVectorIndexMode() {
|
||||||
|
if (!this.initialized) { return }
|
||||||
|
let header = newHeaderFromBytes(this.v4Content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let vIndex = loadVectorIndex(this.v4Content)
|
||||||
|
let searcher = Searcher(version, "../../data/ip2region_v4.xdb", vIndex)
|
||||||
|
let ip = parseIP("220.181.108.183")
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
if (region.size == 0) {
|
||||||
|
throw Exception("should find region for known IP")
|
||||||
|
}
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testSearchFileMode() {
|
||||||
|
if (!this.initialized) { return }
|
||||||
|
let header = newHeaderFromBytes(this.v4Content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let searcher = Searcher(version, "../../data/ip2region_v4.xdb")
|
||||||
|
let ip = parseIP("220.181.108.183")
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
if (region.size == 0) {
|
||||||
|
throw Exception("should find region for known IP")
|
||||||
|
}
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testAllModesSameResult() {
|
||||||
|
if (!this.initialized) { return }
|
||||||
|
let header = newHeaderFromBytes(this.v4Content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let vIndex = loadVectorIndex(this.v4Content)
|
||||||
|
let s1 = Searcher(version, this.v4Content, "../../data/ip2region_v4.xdb")
|
||||||
|
let s2 = Searcher(version, "../../data/ip2region_v4.xdb", vIndex)
|
||||||
|
let s3 = Searcher(version, "../../data/ip2region_v4.xdb")
|
||||||
|
let ip = parseIP("1.2.3.4")
|
||||||
|
let r1 = s1.search(ip)
|
||||||
|
let r2 = s2.search(ip)
|
||||||
|
let r3 = s3.search(ip)
|
||||||
|
s1.close(); s2.close(); s3.close()
|
||||||
|
if (r1 != r2 || r2 != r3) {
|
||||||
|
throw Exception("all modes should return same result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testSearchByString() {
|
||||||
|
if (!this.initialized) { return }
|
||||||
|
let header = newHeaderFromBytes(this.v4Content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let searcher = Searcher(version, this.v4Content, "../../data/ip2region_v4.xdb")
|
||||||
|
let region = searcher.searchByString("220.181.108.183")
|
||||||
|
if (region.size == 0) {
|
||||||
|
throw Exception("searchByString should find region")
|
||||||
|
}
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
class SearcherV6Tests {
|
||||||
|
var v6Content: Array<Byte> = Array<Byte>(0, repeat: 0)
|
||||||
|
var initialized: Bool = false
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
func setup() {
|
||||||
|
this.v6Content = File.readFrom(Path("../../data/ip2region_v6.xdb"))
|
||||||
|
this.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testSearchV6ContentMode() {
|
||||||
|
if (!this.initialized) { return }
|
||||||
|
let header = newHeaderFromBytes(this.v6Content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let searcher = Searcher(version, this.v6Content, "../../data/ip2region_v6.xdb")
|
||||||
|
let ip = parseIP("2408:8266:100:1000::")
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
if (region.size == 0) {
|
||||||
|
throw Exception("should find region for known IPv6")
|
||||||
|
}
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testSearchV6VectorIndexMode() {
|
||||||
|
if (!this.initialized) { return }
|
||||||
|
let header = newHeaderFromBytes(this.v6Content)
|
||||||
|
let version = versionFromHeader(header)
|
||||||
|
let vIndex = loadVectorIndex(this.v6Content)
|
||||||
|
let searcher = Searcher(version, "../../data/ip2region_v6.xdb", vIndex)
|
||||||
|
let ip = parseIP("2408:8266:100:1000::")
|
||||||
|
let region = searcher.search(ip)
|
||||||
|
if (region.size == 0) {
|
||||||
|
throw Exception("should find region for known IPv6")
|
||||||
|
}
|
||||||
|
searcher.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
package ip2region.tests.xdb
|
||||||
|
|
||||||
|
import ip2region.xdb.*
|
||||||
|
|
||||||
|
@Test
|
||||||
|
class UtilTests {
|
||||||
|
@TestCase
|
||||||
|
func testParseIPv4() {
|
||||||
|
let ip = parseIP("1.2.3.4")
|
||||||
|
if (ip.size != 4) {
|
||||||
|
throw Exception("expected 4 bytes, got ${ip.size}")
|
||||||
|
}
|
||||||
|
if (Int64(ip[0]) != 1 || Int64(ip[1]) != 2 || Int64(ip[2]) != 3 || Int64(ip[3]) != 4) {
|
||||||
|
throw Exception("IPv4 parse mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testParseIPv6() {
|
||||||
|
let ip = parseIP("::1")
|
||||||
|
if (ip.size != 16) {
|
||||||
|
throw Exception("expected 16 bytes for IPv6, got ${ip.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testIPCompareEqual() {
|
||||||
|
let a = Array<Byte>(4, repeat: 0)
|
||||||
|
var v: Byte = 1; a[0] = v; v = 2; a[1] = v; v = 3; a[2] = v; v = 4; a[3] = v
|
||||||
|
let b = Array<Byte>(4, repeat: 0)
|
||||||
|
v = 1; b[0] = v; v = 2; b[1] = v; v = 3; b[2] = v; v = 4; b[3] = v
|
||||||
|
let r = ipCompare(a, b)
|
||||||
|
if (r != 0) {
|
||||||
|
throw Exception("expected 0, got ${r}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testIPCompareLess() {
|
||||||
|
let a = Array<Byte>(4, repeat: 0)
|
||||||
|
var v: Byte = 1; a[0] = v; v = 2; a[1] = v; v = 3; a[2] = v; v = 4; a[3] = v
|
||||||
|
let b = Array<Byte>(4, repeat: 0)
|
||||||
|
v = 1; b[0] = v; v = 2; b[1] = v; v = 3; b[2] = v; v = 5; b[3] = v
|
||||||
|
let r = ipCompare(a, b)
|
||||||
|
if (r != -1) {
|
||||||
|
throw Exception("expected -1, got ${r}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testIPCompareGreater() {
|
||||||
|
let a = Array<Byte>(4, repeat: 0)
|
||||||
|
var v: Byte = 1; a[0] = v; v = 2; a[1] = v; v = 3; a[2] = v; v = 5; a[3] = v
|
||||||
|
let b = Array<Byte>(4, repeat: 0)
|
||||||
|
v = 1; b[0] = v; v = 2; b[1] = v; v = 3; b[2] = v; v = 4; b[3] = v
|
||||||
|
let r = ipCompare(a, b)
|
||||||
|
if (r != 1) {
|
||||||
|
throw Exception("expected 1, got ${r}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testReadLEUint32() {
|
||||||
|
let buf = Array<Byte>(4, repeat: 0)
|
||||||
|
var v: Byte = 0x78; buf[0] = v; v = 0x56; buf[1] = v; v = 0x34; buf[2] = v; v = 0x12; buf[3] = v
|
||||||
|
let val = readLEUint32(buf, 0)
|
||||||
|
let expected: Int64 = 0x12345678
|
||||||
|
if (val != expected) {
|
||||||
|
throw Exception("expected 0x12345678, got ${val}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testReadLEUint16() {
|
||||||
|
let buf = Array<Byte>(2, repeat: 0)
|
||||||
|
var v: Byte = 0x34; buf[0] = v; v = 0x12; buf[1] = v
|
||||||
|
let val = readLEUint16(buf, 0)
|
||||||
|
if (val != 0x1234) {
|
||||||
|
throw Exception("expected 0x1234, got ${val}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testIpToString() {
|
||||||
|
let ip = Array<Byte>(4, repeat: 0)
|
||||||
|
var v: Byte = 127; ip[0] = v; v = 0; ip[1] = v; v = 0; ip[2] = v; v = 1; ip[3] = v
|
||||||
|
let s = ipToString(ip)
|
||||||
|
if (s != "127.0.0.1") {
|
||||||
|
throw Exception("expected 127.0.0.1, got ${s}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testCompareIPv4() {
|
||||||
|
let ip = Array<Byte>(4, repeat: 0)
|
||||||
|
var v: Byte = 1; ip[0] = v; v = 2; ip[1] = v; v = 3; ip[2] = v; v = 4; ip[3] = v
|
||||||
|
let entry = Array<Byte>(8, repeat: 0)
|
||||||
|
v = 4; entry[0] = v; v = 3; entry[1] = v; v = 2; entry[2] = v; v = 1; entry[3] = v
|
||||||
|
v = 8; entry[4] = v; v = 7; entry[5] = v; v = 6; entry[6] = v; v = 5; entry[7] = v
|
||||||
|
let r = compareIPv4(ip, entry, 0)
|
||||||
|
if (r != 0) {
|
||||||
|
throw Exception("expected equal, got ${r}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testCompareIPv6() {
|
||||||
|
let ip = Array<Byte>(16, repeat: 0)
|
||||||
|
var v: Byte = 0x20; ip[0] = v; v = 0x01; ip[1] = v; v = 0x0d; ip[2] = v; v = 0xb8; ip[3] = v
|
||||||
|
let entry = Array<Byte>(16, repeat: 0)
|
||||||
|
v = 0x20; entry[0] = v; v = 0x01; entry[1] = v; v = 0x0d; entry[2] = v; v = 0xb8; entry[3] = v
|
||||||
|
let r = compareIPv6(ip, entry, 0)
|
||||||
|
if (r != 0) {
|
||||||
|
throw Exception("expected equal, got ${r}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestCase
|
||||||
|
func testLoadVectorIndex() {
|
||||||
|
let viSize = VectorIndexRows * VectorIndexCols * VectorIndexSize
|
||||||
|
let cBuff = Array<Byte>(HeaderInfoLength + viSize, repeat: 0)
|
||||||
|
var v: Byte = 0x42
|
||||||
|
cBuff[HeaderInfoLength] = v
|
||||||
|
let vi = loadVectorIndex(cBuff)
|
||||||
|
if (vi.size != viSize) {
|
||||||
|
throw Exception("expected size ${viSize}, got ${vi.size}")
|
||||||
|
}
|
||||||
|
let firstVal = Int64(vi[0])
|
||||||
|
if (firstVal != 0x42) {
|
||||||
|
throw Exception("expected first byte 0x42, got ${firstVal}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package ip2region.xdb
|
||||||
|
|
||||||
|
// xdb file format constants
|
||||||
|
public let HeaderInfoLength: Int64 = 256
|
||||||
|
public let VectorIndexRows: Int64 = 256
|
||||||
|
public let VectorIndexCols: Int64 = 256
|
||||||
|
public let VectorIndexSize: Int64 = 8
|
||||||
|
public let IPv4SegmentIndexSize: Int64 = 14 // 4 + 4 + 2 + 4
|
||||||
|
public let IPv6SegmentIndexSize: Int64 = 38 // 16 + 16 + 2 + 4
|
||||||
|
|
||||||
|
public let Structure20: Int64 = 2
|
||||||
|
public let Structure30: Int64 = 3
|
||||||
|
|
||||||
|
// xdb header parsed from the first 256 bytes of the xdb file
|
||||||
|
public class Header {
|
||||||
|
public let version: UInt16
|
||||||
|
public let indexPolicy: UInt16
|
||||||
|
public let createdAt: UInt32
|
||||||
|
public let startIndexPtr: UInt32
|
||||||
|
public let endIndexPtr: UInt32
|
||||||
|
public let ipVersion: UInt16
|
||||||
|
public let runtimePtrBytes: UInt16
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ver: UInt16,
|
||||||
|
idxPolicy: UInt16,
|
||||||
|
createAt: UInt32,
|
||||||
|
sPtr: UInt32,
|
||||||
|
ePtr: UInt32,
|
||||||
|
ipVer: UInt16,
|
||||||
|
rtPtrBytes: UInt16
|
||||||
|
) {
|
||||||
|
this.version = ver
|
||||||
|
this.indexPolicy = idxPolicy
|
||||||
|
this.createdAt = createAt
|
||||||
|
this.startIndexPtr = sPtr
|
||||||
|
this.endIndexPtr = ePtr
|
||||||
|
this.ipVersion = ipVer
|
||||||
|
this.runtimePtrBytes = rtPtrBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toString(): String {
|
||||||
|
return "{version: ${this.version}, index_policy: ${this.indexPolicy}, " +
|
||||||
|
"created_at: ${this.createdAt}, start_index_ptr: ${this.startIndexPtr}, " +
|
||||||
|
"end_index_ptr: ${this.endIndexPtr}, ip_version: ${this.ipVersion}, " +
|
||||||
|
"runtime_ptr_bytes: ${this.runtimePtrBytes}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a little-endian UInt16 from byte array at offset
|
||||||
|
func readHeaderUInt16(buf: Array<Byte>, offset: Int64): UInt16 {
|
||||||
|
let b0 = UInt16(buf[offset])
|
||||||
|
let b1 = UInt16(buf[offset + 1]) << 8
|
||||||
|
return b0 | b1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a little-endian UInt32 from byte array at offset
|
||||||
|
func readHeaderUInt32(buf: Array<Byte>, offset: Int64): UInt32 {
|
||||||
|
let b0 = UInt32(buf[offset])
|
||||||
|
let b1 = UInt32(buf[offset + 1]) << 8
|
||||||
|
let b2 = UInt32(buf[offset + 2]) << 16
|
||||||
|
let b3 = UInt32(buf[offset + 3]) << 24
|
||||||
|
return b0 | b1 | b2 | b3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Header from raw byte buffer (first 256 bytes of xdb file)
|
||||||
|
public func newHeaderFromBytes(buf: Array<Byte>): Header {
|
||||||
|
return Header(
|
||||||
|
readHeaderUInt16(buf, 0),
|
||||||
|
readHeaderUInt16(buf, 2),
|
||||||
|
readHeaderUInt32(buf, 4),
|
||||||
|
readHeaderUInt32(buf, 8),
|
||||||
|
readHeaderUInt32(buf, 12),
|
||||||
|
readHeaderUInt16(buf, 16),
|
||||||
|
readHeaderUInt16(buf, 18)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
package ip2region.xdb
|
||||||
|
|
||||||
|
import std.fs.*
|
||||||
|
import std.io.SeekPosition
|
||||||
|
|
||||||
|
// Searcher for ip2region xdb database
|
||||||
|
public class Searcher {
|
||||||
|
let version: Version
|
||||||
|
let handle: File
|
||||||
|
var ioCount: Int64
|
||||||
|
let vectorIndex: Array<Byte>
|
||||||
|
let contentBuff: Array<Byte>
|
||||||
|
let mode: Int64
|
||||||
|
|
||||||
|
public let FileOnlyMode: Int64 = 0
|
||||||
|
public let VectorIndexMode: Int64 = 1
|
||||||
|
public let ContentBuffMode: Int64 = 2
|
||||||
|
|
||||||
|
public init(ver: Version, dbFile: String) {
|
||||||
|
this.version = ver
|
||||||
|
this.handle = File(dbFile, Read)
|
||||||
|
this.vectorIndex = Array<Byte>(0, repeat: 0)
|
||||||
|
this.contentBuff = Array<Byte>(0, repeat: 0)
|
||||||
|
this.mode = FileOnlyMode
|
||||||
|
this.ioCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(ver: Version, dbFile: String, vIndex: Array<Byte>) {
|
||||||
|
this.version = ver
|
||||||
|
this.handle = File(dbFile, Read)
|
||||||
|
this.vectorIndex = vIndex
|
||||||
|
this.contentBuff = Array<Byte>(0, repeat: 0)
|
||||||
|
this.mode = VectorIndexMode
|
||||||
|
this.ioCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(ver: Version, cBuff: Array<Byte>, dbPath: String) {
|
||||||
|
this.version = ver
|
||||||
|
this.handle = File(dbPath, Read)
|
||||||
|
this.vectorIndex = Array<Byte>(0, repeat: 0)
|
||||||
|
this.contentBuff = cBuff
|
||||||
|
this.mode = ContentBuffMode
|
||||||
|
this.ioCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func close() {
|
||||||
|
this.handle.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getIOCount(): Int64 {
|
||||||
|
return this.ioCount
|
||||||
|
}
|
||||||
|
|
||||||
|
public func searchByString(ipStr: String): String {
|
||||||
|
let ipBytes = parseIP(ipStr)
|
||||||
|
return this.search(ipBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func search(ip: Array<Byte>): String {
|
||||||
|
this.ioCount = 0
|
||||||
|
|
||||||
|
let il0 = Int64(ip[0])
|
||||||
|
let il1 = Int64(ip[1])
|
||||||
|
let idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize
|
||||||
|
|
||||||
|
var sPtr: Int64 = 0
|
||||||
|
var ePtr: Int64 = 0
|
||||||
|
|
||||||
|
if (this.mode == VectorIndexMode) {
|
||||||
|
sPtr = readLEUint32(this.vectorIndex, idx)
|
||||||
|
ePtr = readLEUint32(this.vectorIndex, idx + 4)
|
||||||
|
} else if (this.mode == ContentBuffMode) {
|
||||||
|
sPtr = readLEUint32(this.contentBuff, HeaderInfoLength + idx)
|
||||||
|
ePtr = readLEUint32(this.contentBuff, HeaderInfoLength + idx + 4)
|
||||||
|
} else {
|
||||||
|
let buff = Array<Byte>(VectorIndexSize, repeat: 0)
|
||||||
|
this.readFromFile(HeaderInfoLength + idx, buff)
|
||||||
|
sPtr = readLEUint32(buff, 0)
|
||||||
|
ePtr = readLEUint32(buff, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sPtr == 0 || ePtr == 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let segIndexSize = this.version.segmentIndexSize
|
||||||
|
let bytes = this.version.bytes
|
||||||
|
let dBytes = bytes * 2
|
||||||
|
var dataLen: Int64 = 0
|
||||||
|
var dataPtr: Int64 = 0
|
||||||
|
|
||||||
|
var l: Int64 = 0
|
||||||
|
var h: Int64 = (ePtr - sPtr) / segIndexSize
|
||||||
|
let buff = Array<Byte>(segIndexSize, repeat: 0)
|
||||||
|
|
||||||
|
while (l <= h) {
|
||||||
|
let m = (l + h) / 2
|
||||||
|
let p = sPtr + m * segIndexSize
|
||||||
|
|
||||||
|
this.read(p, buff)
|
||||||
|
this.ioCount = this.ioCount + 1
|
||||||
|
|
||||||
|
if (this.compareIP(ip, buff, 0) < 0) {
|
||||||
|
h = m - 1
|
||||||
|
} else if (this.compareIP(ip, buff, bytes) > 0) {
|
||||||
|
l = m + 1
|
||||||
|
} else {
|
||||||
|
dataLen = readLEUint16(buff, dBytes)
|
||||||
|
dataPtr = readLEUint32(buff, dBytes + 2)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLen == 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let regionBuff = Array<Byte>(dataLen, repeat: 0)
|
||||||
|
this.read(dataPtr, regionBuff)
|
||||||
|
return String.fromUtf8(regionBuff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(offset: Int64, buff: Array<Byte>) {
|
||||||
|
if (this.mode == ContentBuffMode) {
|
||||||
|
for (i in 0..buff.size) {
|
||||||
|
buff[i] = this.contentBuff[offset + i]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.readFromFile(offset, buff)
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFromFile(offset: Int64, buff: Array<Byte>) {
|
||||||
|
this.handle.seek(SeekPosition.Begin(offset))
|
||||||
|
this.handle.read(buff)
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareIP(ip: Array<Byte>, entry: Array<Byte>, offset: Int64): Int64 {
|
||||||
|
if (this.version.id == IPv4VersionNo) {
|
||||||
|
return compareIPv4(ip, entry, offset)
|
||||||
|
}
|
||||||
|
return compareIPv6(ip, entry, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package ip2region.xdb
|
||||||
|
|
||||||
|
import std.net.IPAddress
|
||||||
|
|
||||||
|
// --- IP address parsing and formatting
|
||||||
|
|
||||||
|
// Parse IP string to byte array
|
||||||
|
// For IPv4, returns 4 bytes in big-endian order (network byte order)
|
||||||
|
// For IPv6, returns 16 bytes in big-endian order
|
||||||
|
public func parseIP(ip: String): Array<Byte> {
|
||||||
|
let addr = IPAddress.parse(ip)
|
||||||
|
return addr.getAddressBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert IP byte array back to string
|
||||||
|
public func ipToString(ip: Array<Byte>): String {
|
||||||
|
if (ip.size == 4) {
|
||||||
|
return "${Int64(ip[0])}.${Int64(ip[1])}.${Int64(ip[2])}.${Int64(ip[3])}"
|
||||||
|
}
|
||||||
|
// IPv6 simplified format
|
||||||
|
var result = ""
|
||||||
|
for (i in 0..ip.size) {
|
||||||
|
if (i > 0 && i % 2 == 0) {
|
||||||
|
result = result + ":"
|
||||||
|
}
|
||||||
|
result = result + "${Int64(ip[i])}"
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two IP byte arrays.
|
||||||
|
// Returns: -1 if ip1 < ip2, 0 if equal, 1 if ip1 > ip2
|
||||||
|
public func ipCompare(ip1: Array<Byte>, ip2: Array<Byte>): Int64 {
|
||||||
|
var len = ip1.size
|
||||||
|
if (ip2.size < len) {
|
||||||
|
len = ip2.size
|
||||||
|
}
|
||||||
|
for (i in 0..len) {
|
||||||
|
if (ip1[i] < ip2[i]) {
|
||||||
|
return -1
|
||||||
|
} else if (ip1[i] > ip2[i]) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ip1.size < ip2.size) {
|
||||||
|
return -1
|
||||||
|
} else if (ip1.size > ip2.size) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Binary data reading helpers for xdb format
|
||||||
|
|
||||||
|
// Read a little-endian UInt32 from a byte buffer at the given offset
|
||||||
|
public func readLEUint32(buf: Array<Byte>, offset: Int64): Int64 {
|
||||||
|
let b0 = Int64(buf[offset])
|
||||||
|
let b1 = Int64(buf[offset + 1]) << 8
|
||||||
|
let b2 = Int64(buf[offset + 2]) << 16
|
||||||
|
let b3 = Int64(buf[offset + 3]) << 24
|
||||||
|
return b0 | b1 | b2 | b3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a little-endian UInt16 from a byte buffer at the given offset
|
||||||
|
public func readLEUint16(buf: Array<Byte>, offset: Int64): Int64 {
|
||||||
|
let b0 = Int64(buf[offset])
|
||||||
|
let b1 = Int64(buf[offset + 1]) << 8
|
||||||
|
return b0 | b1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare IPv4 address (big-endian) with xdb stored IPv4 (little-endian)
|
||||||
|
// xdb stores IPv4 in little-endian, so we compare ip[i] with xdbEntry[3-i]
|
||||||
|
public func compareIPv4(ip: Array<Byte>, xdbEntry: Array<Byte>, entryOffset: Int64): Int64 {
|
||||||
|
for (i in 0..4) {
|
||||||
|
let ipByte = Int64(ip[i])
|
||||||
|
let xdbByte = Int64(xdbEntry[entryOffset + 3 - i])
|
||||||
|
if (ipByte < xdbByte) {
|
||||||
|
return -1
|
||||||
|
} else if (ipByte > xdbByte) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the vector index from a content buffer
|
||||||
|
// The vector index starts at HeaderInfoLength and has VectorIndexRows * VectorIndexCols * VectorIndexSize bytes
|
||||||
|
public func loadVectorIndex(cBuff: Array<Byte>): Array<Byte> {
|
||||||
|
let start = HeaderInfoLength
|
||||||
|
let len = VectorIndexRows * VectorIndexCols * VectorIndexSize
|
||||||
|
let vi = Array<Byte>(len, repeat: 0)
|
||||||
|
for (i in 0..len) {
|
||||||
|
vi[i] = cBuff[start + i]
|
||||||
|
}
|
||||||
|
return vi
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare IPv6 address (big-endian) with xdb stored IPv6 (big-endian)
|
||||||
|
public func compareIPv6(ip: Array<Byte>, xdbEntry: Array<Byte>, entryOffset: Int64): Int64 {
|
||||||
|
for (i in 0..16) {
|
||||||
|
if (ip[i] < xdbEntry[entryOffset + i]) {
|
||||||
|
return -1
|
||||||
|
} else if (ip[i] > xdbEntry[entryOffset + i]) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package ip2region.xdb
|
||||||
|
|
||||||
|
// IP version definitions for ip2region xdb searcher
|
||||||
|
public class Version {
|
||||||
|
public let id: Int64
|
||||||
|
public let name: String
|
||||||
|
public let bytes: Int64
|
||||||
|
public let segmentIndexSize: Int64
|
||||||
|
|
||||||
|
public init(id: Int64, name: String, bytes: Int64, segmentIndexSize: Int64) {
|
||||||
|
this.id = id
|
||||||
|
this.name = name
|
||||||
|
this.bytes = bytes
|
||||||
|
this.segmentIndexSize = segmentIndexSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toString(): String {
|
||||||
|
return "{id: ${this.id}, name: ${this.name}, bytes: ${this.bytes}, " +
|
||||||
|
"segment_index_size: ${this.segmentIndexSize}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public let IPv4VersionNo: Int64 = 4
|
||||||
|
public let IPv6VersionNo: Int64 = 6
|
||||||
|
|
||||||
|
// Pre-defined IP versions
|
||||||
|
public let IPv4 = Version(4, "IPv4", 4, 14)
|
||||||
|
public let IPv6 = Version(6, "IPv6", 16, 38)
|
||||||
|
|
||||||
|
// Determine IP version from header info
|
||||||
|
public func versionFromHeader(header: Header): Version {
|
||||||
|
// Old structure (2.0) with IPv4 only
|
||||||
|
if (Int64(header.version) == Structure20) {
|
||||||
|
return IPv4
|
||||||
|
}
|
||||||
|
// Structure 3.0+
|
||||||
|
if (Int64(header.ipVersion) == IPv4VersionNo) {
|
||||||
|
return IPv4
|
||||||
|
}
|
||||||
|
return IPv6
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
bin/
|
||||||
|
|
@ -1,11 +1,27 @@
|
||||||
|
|
||||||
all: xdb_search xdb_bench
|
all: bin header search bench make edit
|
||||||
|
|
||||||
xdb_search: xdb_search.cc xdb_search_test.cc
|
FILES=$(wildcard src/*.cc)
|
||||||
g++ -std=c++11 -O2 $^ -o $@
|
|
||||||
|
|
||||||
xdb_bench: xdb_search.cc xdb_bench.cc xdb_bench_test.cc
|
bin:
|
||||||
g++ -std=c++11 -O2 $^ -o $@
|
mkdir -p bin
|
||||||
|
|
||||||
|
header: $(FILES) test/header.cc
|
||||||
|
g++ -std=c++11 -O2 $^ -o bin/$@
|
||||||
|
|
||||||
|
search: $(FILES) test/search.cc
|
||||||
|
g++ -std=c++11 -O2 $^ -o bin/$@
|
||||||
|
|
||||||
|
bench: $(FILES) test/bench.cc
|
||||||
|
g++ -std=c++11 -O2 $^ -o bin/$@
|
||||||
|
|
||||||
|
make: $(FILES) test/make.cc
|
||||||
|
g++ -std=c++11 -O2 $^ -o bin/$@
|
||||||
|
|
||||||
|
edit: $(FILES)
|
||||||
|
g++ -std=c++11 -O2 $^ test/edit_v4.cc -o bin/edit_v4
|
||||||
|
g++ -std=c++11 -O2 $^ test/edit_v6.cc -o bin/edit_v6
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f xdb_search xdb_bench
|
rm -rf bin
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
|
# ip2region C++ query client
|
||||||
|
|
||||||
|
## 0. File Description
|
||||||
|
|
||||||
|
```
|
||||||
|
Makefile --------- Build
|
||||||
|
|
||||||
|
src ------------------ Source directory
|
||||||
|
src/base.* ----------- Constants and utility functions
|
||||||
|
src/ip.* ------------- IP processing implementation
|
||||||
|
src/header.* --------- xdb header parsing implementation
|
||||||
|
src/search.* --------- xdb search implementation
|
||||||
|
src/bench.* ---------- Search benchmarking implementation
|
||||||
|
src/make.* ----------- xdb file generation implementation
|
||||||
|
src/edit.* ----------- Raw data editing implementation
|
||||||
|
|
||||||
|
test ---------------- Test directory
|
||||||
|
test/header.cc ------ Test header
|
||||||
|
test/search.cc ------ Test search
|
||||||
|
test/bench.cc ------- Benchmarking
|
||||||
|
test/make.cc -------- Generate xdb file
|
||||||
|
test/edit_v4.cc ----- Test raw data editing (ipv4)
|
||||||
|
test/edit_v6.cc ----- Test raw data editing (ipv6)
|
||||||
|
|
||||||
|
|
||||||
|
bin --------------- Executable directory (generated via make)
|
||||||
|
bin/header -------- Test header
|
||||||
|
bin/search -------- Test search
|
||||||
|
bin/bench --------- Benchmarking
|
||||||
|
bin/make ---------- Generate xdb file
|
||||||
|
bin/edit_v4 ------- Test raw data editing (ipv4)
|
||||||
|
bin/edit_v6 ------- Test raw data editing (ipv6)
|
||||||
|
|
||||||
|
readme.md --------- readme
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Compilation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ make
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Search
|
||||||
|
|
||||||
|
### 2.1 Example
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "src/search.h"
|
||||||
|
|
||||||
|
// IP Version: xdb::ipv4 xdb::ipv6
|
||||||
|
// Policy: xdb::policy_file xdb::policy_vector xdb::policy_content
|
||||||
|
// No cache Partial cache Full cache
|
||||||
|
int main() {
|
||||||
|
std::string xdb_name = "../../data/ip2region_v6.xdb";
|
||||||
|
int version = xdb::ipv6;
|
||||||
|
int policy = xdb::policy_content;
|
||||||
|
std::string ip = "2001:200:124::";
|
||||||
|
|
||||||
|
xdb::search_t s(xdb_name, version, policy);
|
||||||
|
std::cout << s.search(ip) << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ g++ src/*.cc 1.cc --- Compile
|
||||||
|
// $ ./a.out ------------- Test
|
||||||
|
// Japan|Tokyo|Asagaya-minami|WIDE Project|JP
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Test xdb Header
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./bin/header
|
||||||
|
Test IPv4
|
||||||
|
Version: 3
|
||||||
|
Cache Policy: 1
|
||||||
|
File Generation Time: 2025-09-06 02:24:16
|
||||||
|
Index Start Address: 955933
|
||||||
|
Index End Address: 11042415
|
||||||
|
IP Version: 4
|
||||||
|
Pointer Bytes: 4
|
||||||
|
|
||||||
|
Test IPv6
|
||||||
|
Version: 3
|
||||||
|
Cache Policy: 1
|
||||||
|
File Generation Time: 2025-10-17 04:41:04
|
||||||
|
Index Start Address: 3094259
|
||||||
|
Index End Address: 36258303
|
||||||
|
IP Version: 6
|
||||||
|
Pointer Bytes: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Test Search
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./bin/search
|
||||||
|
Test IPv4 No cache: Success
|
||||||
|
Test IPv4 Partial cache: Success
|
||||||
|
Test IPv4 Full cache: Success
|
||||||
|
Test IPv6 No cache: Success
|
||||||
|
Test IPv6 Partial cache: Success
|
||||||
|
Test IPv6 Full cache: Success
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Benchmarking and Correctness Verification
|
||||||
|
|
||||||
|
```
|
||||||
|
./bin/bench
|
||||||
|
Test IPv4, No cache, total: 3910284, took: 27.60s, cost: 6.59μs/op, io count: 28227147
|
||||||
|
Test IPv4, Partial cache, total: 3910284, took: 21.85s, cost: 5.15μs/op, io count: 24316863
|
||||||
|
Test IPv4, Full cache, total: 3910284, took: 2.26s, cost: 0.25μs/op, io count: 0
|
||||||
|
Test IPv6, No cache, total: 4792520, took: 100.40s, cost: 20.22μs/op, io count: 80758866
|
||||||
|
Test IPv6, Partial cache, total: 4792520, took: 93.06s, cost: 18.71μs/op, io count: 75966346
|
||||||
|
Test IPv6, Full cache, total: 4792520, took: 6.24s, cost: 0.81μs/op, io count: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Generate xdb File
|
||||||
|
|
||||||
|
### 4.1 Generate xdb File
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./bin/make
|
||||||
|
Generate ipv4 xdb file, took: 0.57s
|
||||||
|
Generate ipv6 xdb file, took: 1.24s
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Raw Data Editing
|
||||||
|
|
||||||
|
### 5.1. Instructions for Use
|
||||||
|
|
||||||
|
* New IP attribution files can contain empty lines
|
||||||
|
* New IP attribution files can be out of order; the program will automatically sort them
|
||||||
|
* New IP attribution files can overlap; as long as there is no ambiguity, the program will automatically merge them
|
||||||
|
* The final result will automatically merge adjacent lines with the same attribution
|
||||||
|
* For the following tests, the original file uses the data file provided in the repository, and the new file uses 1.txt in the current directory
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
|
# ip2region C++ 查询客户端
|
||||||
|
|
||||||
|
## 0. 文件说明
|
||||||
|
```
|
||||||
|
Makefile --------- 构建
|
||||||
|
|
||||||
|
src ------------------ 源文件目录
|
||||||
|
src/base.* ----------- 常量及工具函数
|
||||||
|
src/ip.* ------------- 实现 IP 处理
|
||||||
|
src/header.* --------- 实现 xdb 头部解析
|
||||||
|
src/search.* --------- 实现 xdb 查找
|
||||||
|
src/bench.* ---------- 实现 查找 测速
|
||||||
|
src/make.* ----------- 实现 生成 xdb 文件
|
||||||
|
src/edit.* ----------- 实现 原始数据编辑
|
||||||
|
|
||||||
|
test ---------------- 测试目录
|
||||||
|
test/header.cc ------ 测试 头部
|
||||||
|
test/search.cc ------ 测试 查找
|
||||||
|
test/bench.cc ------- 测速
|
||||||
|
test/make.cc -------- 生成 xdb 文件
|
||||||
|
test/edit_v4.cc ----- 测试 原始数据编辑(ipv4)
|
||||||
|
test/edit_v6.cc ----- 测试 原始数据编辑(ipv6)
|
||||||
|
|
||||||
|
|
||||||
|
bin --------------- 可执行文件目录(通过 make 生成)
|
||||||
|
bin/header -------- 测试 头部
|
||||||
|
bin/search -------- 测试 查找
|
||||||
|
bin/bench --------- 测速
|
||||||
|
bin/make ---------- 生成 xdb 文件
|
||||||
|
bin/edit_v4 ------- 测试 原始数据编辑(ipv4)
|
||||||
|
bin/edit_v6 ------- 测试 原始数据编辑(ipv6)
|
||||||
|
|
||||||
|
readme.md --------- readme
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. 编译
|
||||||
|
```
|
||||||
|
$ make
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 查找
|
||||||
|
### 2.1 示例
|
||||||
|
```cpp
|
||||||
|
#include "src/search.h"
|
||||||
|
|
||||||
|
// IP 版本: xdb::ipv4 xdb::ipv6
|
||||||
|
// 策略: xdb::policy_file xdb::policy_vector xdb::policy_content
|
||||||
|
// 不缓存 部分缓存 全部缓存
|
||||||
|
int main() {
|
||||||
|
std::string xdb_name = "../../data/ip2region_v6.xdb";
|
||||||
|
int version = xdb::ipv6;
|
||||||
|
int policy = xdb::policy_content;
|
||||||
|
std::string ip = "2001:200:124::";
|
||||||
|
|
||||||
|
xdb::search_t s(xdb_name, version, policy);
|
||||||
|
std::cout << s.search(ip) << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ g++ src/*.cc 1.cc --- 编译
|
||||||
|
// $ ./a.out ------------- 测试
|
||||||
|
// Japan|Tokyo|Asagaya-minami|WIDE Project|JP
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 测试 xdb 头部
|
||||||
|
```
|
||||||
|
$ ./bin/header
|
||||||
|
测试 IPv4
|
||||||
|
版本号: 3
|
||||||
|
缓存策略: 1
|
||||||
|
文件生成时间: 2025-09-06 02:24:16
|
||||||
|
索引起始地址: 955933
|
||||||
|
索引结束地址: 11042415
|
||||||
|
IP版本: 4
|
||||||
|
指针字节数: 4
|
||||||
|
|
||||||
|
测试 IPv6
|
||||||
|
版本号: 3
|
||||||
|
缓存策略: 1
|
||||||
|
文件生成时间: 2025-10-17 04:41:04
|
||||||
|
索引起始地址: 3094259
|
||||||
|
索引结束地址: 36258303
|
||||||
|
IP版本: 6
|
||||||
|
指针字节数: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 测试查找
|
||||||
|
```
|
||||||
|
$ ./bin/search
|
||||||
|
测试 IPv4 不缓存: 成功
|
||||||
|
测试 IPv4 部分缓存: 成功
|
||||||
|
测试 IPv4 全部缓存: 成功
|
||||||
|
测试 IPv6 不缓存: 成功
|
||||||
|
测试 IPv6 部分缓存: 成功
|
||||||
|
测试 IPv6 全部缓存: 成功
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 测速以及检验正确性
|
||||||
|
```
|
||||||
|
./bin/bench
|
||||||
|
测试 IPv4, 不缓存, total: 3910284, took: 27.60s, cost: 6.59μs/op, io count: 28227147
|
||||||
|
测试 IPv4, 部分缓存, total: 3910284, took: 21.85s, cost: 5.15μs/op, io count: 24316863
|
||||||
|
测试 IPv4, 全部缓存, total: 3910284, took: 2.26s, cost: 0.25μs/op, io count: 0
|
||||||
|
测试 IPv6, 不缓存, total: 4792520, took: 100.40s, cost: 20.22μs/op, io count: 80758866
|
||||||
|
测试 IPv6, 部分缓存, total: 4792520, took: 93.06s, cost: 18.71μs/op, io count: 75966346
|
||||||
|
测试 IPv6, 全部缓存, total: 4792520, took: 6.24s, cost: 0.81μs/op, io count: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 生成 xdb 文件
|
||||||
|
### 4.1 生成 xdb 文件
|
||||||
|
```
|
||||||
|
$ ./bin/make
|
||||||
|
生成 ipv4 的 xdb 文件, took: 0.57s
|
||||||
|
生成 ipv6 的 xdb 文件, took: 1.24s
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 原始数据编辑
|
||||||
|
### 5.1. 使用说明
|
||||||
|
* 新的IP归属地文件可以包含空行
|
||||||
|
* 新的IP归属地文件顺序可以乱序, 程序会自动排序
|
||||||
|
* 新的IP归属地文件顺序可以重叠, 只要无二义性, 程序会自动合并
|
||||||
|
* 最终的结果会将相邻的且归属地相同的行自动合并
|
||||||
|
* 以下测试, 原文件使用仓库自带的数据文件, 新文件使用当前目录下的 1.txt
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
# ip2region xdb C++ 查询客户端实现
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
### 完全基于文件的查询
|
|
||||||
```
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "xdb_search.h"
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
|
||||||
char file_name[] = "../../data/ip2region.xdb";
|
|
||||||
char ip[] = "1.2.3.4";
|
|
||||||
|
|
||||||
xdb_search_t xdb(file_name);
|
|
||||||
xdb.init_file();
|
|
||||||
|
|
||||||
std::cout << xdb.search(ip) << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 缓存 `vector_index` 索引
|
|
||||||
```
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "xdb_search.h"
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
|
||||||
char file_name[] = "../../data/ip2region.xdb";
|
|
||||||
char ip[] = "1.2.3.4";
|
|
||||||
|
|
||||||
xdb_search_t xdb(file_name);
|
|
||||||
xdb.init_vector_index();
|
|
||||||
|
|
||||||
std::cout << xdb.search(ip) << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 缓存整个 `xdb` 数据
|
|
||||||
```
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "xdb_search.h"
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
|
||||||
char file_name[] = "../../data/ip2region.xdb";
|
|
||||||
char ip[] = "1.2.3.4";
|
|
||||||
|
|
||||||
xdb_search_t xdb(file_name);
|
|
||||||
xdb.init_content();
|
|
||||||
|
|
||||||
std::cout << xdb.search(ip) << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试程序编译
|
|
||||||
1. 切换到当前目录
|
|
||||||
2. 编译
|
|
||||||
|
|
||||||
```
|
|
||||||
$ make
|
|
||||||
g++ -std=c++11 -O2 xdb_search.cc xdb_search_test.cc -o xdb_search
|
|
||||||
g++ -std=c++11 -O2 xdb_search.cc xdb_bench.cc xdb_bench_test.cc -o xdb_bench
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试查询
|
|
||||||
### 说明
|
|
||||||
```
|
|
||||||
$ ./xdb_search --help
|
|
||||||
./xdb_search [command options]
|
|
||||||
options:
|
|
||||||
--db string ip2region binary xdb file path
|
|
||||||
--cache-policy string cache policy: file/vector_index/content
|
|
||||||
--help print help
|
|
||||||
```
|
|
||||||
|
|
||||||
### 测试
|
|
||||||
```
|
|
||||||
$ ./xdb_search --db ../../data/ip2region.xdb --cache-policy vector_index
|
|
||||||
cache policy : vector_index
|
|
||||||
ip2region>> 1.2.3.4
|
|
||||||
美国|0|华盛顿|0|谷歌
|
|
||||||
```
|
|
||||||
|
|
||||||
## bench 测试
|
|
||||||
### 说明
|
|
||||||
```
|
|
||||||
$ ./xdb_bench --help
|
|
||||||
./xdb_bench [command options]
|
|
||||||
options:
|
|
||||||
--db string ip2region binary xdb file path
|
|
||||||
--src string source ip text file path
|
|
||||||
--cache-policy string cache policy: file/vector_index/content
|
|
||||||
--help print help
|
|
||||||
```
|
|
||||||
|
|
||||||
### 测试
|
|
||||||
```
|
|
||||||
$ ./xdb_bench --db ../../data/ip2region.xdb --src ../../data/ip.merge.txt --cache-policy content
|
|
||||||
total: 3419220, took: 3.44 s, cost: 0.27 μs/op, io count: 0
|
|
||||||
$ ./xdb_bench --db ../../data/ip2region.xdb --src ../../data/ip.merge.txt --cache-policy vector_index
|
|
||||||
total: 3419220, took: 45.99 s, cost: 12.24 μs/op, io count: 21739300
|
|
||||||
$ ./xdb_bench --db ../../data/ip2region.xdb --src ../../data/ip.merge.txt --cache-policy file
|
|
||||||
total: 3419220, took: 60.39 s, cost: 16.32 μs/op, io count: 25158520
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
|
||||||
|
#include "base.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
int ip_version; // ip 版本
|
||||||
|
int ip_size; // ip 占的字节数
|
||||||
|
int content_size;
|
||||||
|
|
||||||
|
void init_xdb(int version) {
|
||||||
|
ip_version = version;
|
||||||
|
ip_size = version == ipv4 ? 4 : 16;
|
||||||
|
content_size = ip_size * 2 + 2 + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_exit(const string &msg) {
|
||||||
|
std::cout << msg << std::endl;
|
||||||
|
exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void read_bin(int index, char *buf, size_t len, FILE *db) {
|
||||||
|
fseek(db, index, SEEK_SET);
|
||||||
|
if (fread(buf, 1, len, db) != len)
|
||||||
|
log_exit(__func__);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned to_uint(const char *buf) {
|
||||||
|
return ((buf[0]) & 0x000000FF) | ((buf[1] << 8) & 0x0000FF00) |
|
||||||
|
((buf[2] << 16) & 0x00FF0000) | ((buf[3] << 24) & 0xFF000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned to_ushort(const char *buf) {
|
||||||
|
return ((buf[0]) & 0x000000FF) | ((buf[1] << 8) & 0x0000FF00);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned to_int(const char *buf, int n) {
|
||||||
|
return n == 2 ? to_ushort(buf) : to_uint(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_uint(unsigned data, char buf[]) {
|
||||||
|
buf[0] = (data >> 0) & 0xFF;
|
||||||
|
buf[1] = (data >> 8) & 0xFF;
|
||||||
|
buf[2] = (data >> 16) & 0xFF;
|
||||||
|
buf[3] = (data >> 24) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_uint(unsigned data, FILE *dst) {
|
||||||
|
char buf[4];
|
||||||
|
write_uint(data, buf);
|
||||||
|
fwrite(buf, 1, sizeof(buf), dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_ushort(unsigned data, char buf[]) {
|
||||||
|
buf[0] = (data >> 0) & 0xFF;
|
||||||
|
buf[1] = (data >> 8) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_ushort(unsigned data, FILE *dst) {
|
||||||
|
char buf[2];
|
||||||
|
write_ushort(data, buf);
|
||||||
|
fwrite(buf, 1, sizeof(buf), dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_string(const char *buf, unsigned len, FILE *dst) {
|
||||||
|
fwrite(buf, 1, len, dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long long get_time() {
|
||||||
|
struct timeval tv1;
|
||||||
|
gettimeofday(&tv1, NULL);
|
||||||
|
return (unsigned long long)tv1.tv_sec * 1000 * 1000 + tv1.tv_usec;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
#ifndef BASE_H
|
||||||
|
#define BASE_H
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iostream>
|
||||||
|
#include <list>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
|
||||||
|
constexpr int ipv4 = 4;
|
||||||
|
constexpr int ipv6 = 6;
|
||||||
|
|
||||||
|
constexpr int policy_file = 0;
|
||||||
|
constexpr int policy_vector = 1;
|
||||||
|
constexpr int policy_content = 2;
|
||||||
|
|
||||||
|
constexpr int length_header = 256;
|
||||||
|
constexpr int length_vector = 256 * 256 * 8;
|
||||||
|
|
||||||
|
extern int ip_version; // ip 版本
|
||||||
|
extern int ip_size; // ip 占的字节数
|
||||||
|
extern int content_size;
|
||||||
|
|
||||||
|
void init_xdb(int version);
|
||||||
|
|
||||||
|
void log_exit(const string &msg);
|
||||||
|
|
||||||
|
void read_bin(int index, char *buf, size_t len, FILE *db);
|
||||||
|
|
||||||
|
unsigned to_uint(const char *buf);
|
||||||
|
unsigned to_ushort(const char *buf);
|
||||||
|
unsigned to_int(const char *buf, int n);
|
||||||
|
|
||||||
|
void write_uint(unsigned data, char buf[]);
|
||||||
|
void write_uint(unsigned data, FILE *dst);
|
||||||
|
|
||||||
|
void write_ushort(unsigned data, char buf[]);
|
||||||
|
void write_ushort(unsigned data, FILE *dst);
|
||||||
|
|
||||||
|
void write_string(const char *buf, unsigned len, FILE *dst);
|
||||||
|
|
||||||
|
unsigned long long get_time();
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
|
||||||
|
#include "bench.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
bench_t::bench_t(const std::string &file_name, int version, int policy)
|
||||||
|
: search(file_name, version, policy) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void bench_t::test_one(const ip_t &ip, const string region) {
|
||||||
|
if (search.search(ip.to_string()) != region)
|
||||||
|
xdb::log_exit("failed: " + ip.to_string() + " " + region);
|
||||||
|
sum_io_count += search.get_io_count();
|
||||||
|
sum_cost_time += search.get_cost_time();
|
||||||
|
sum_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bench_t::test_line(char *buf) {
|
||||||
|
size_t buf_len = strlen(buf);
|
||||||
|
if (buf_len == 0)
|
||||||
|
return;
|
||||||
|
buf[buf_len - 1] = '\0'; // 去掉换行符
|
||||||
|
|
||||||
|
node_t node(buf);
|
||||||
|
|
||||||
|
// 只测五个
|
||||||
|
for (int i = 0; i < 5 && node.ip1 < node.ip2; ++i) {
|
||||||
|
test_one(node.ip1, node.region);
|
||||||
|
node.ip1 = node.ip1 + 1;
|
||||||
|
}
|
||||||
|
test_one(node.ip2, node.region);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bench_t::test_file(const std::string &file_name) {
|
||||||
|
FILE *f = fopen(file_name.data(), "r");
|
||||||
|
if (f == NULL)
|
||||||
|
xdb::log_exit("can't open " + file_name);
|
||||||
|
char buf[1024];
|
||||||
|
while (fgets(buf, sizeof(buf), f) != NULL)
|
||||||
|
test_line(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bench_t::test(const string &file_name) {
|
||||||
|
sum_io_count = 0;
|
||||||
|
sum_cost_time = 0;
|
||||||
|
sum_count = 0;
|
||||||
|
|
||||||
|
unsigned long long tv1 = xdb::get_time();
|
||||||
|
test_file(file_name);
|
||||||
|
unsigned long long tv2 = xdb::get_time();
|
||||||
|
|
||||||
|
double took = (tv2 - tv1) * 1.0 / 1000 / 1000;
|
||||||
|
double cost = sum_cost_time * 1.0 / sum_count;
|
||||||
|
|
||||||
|
printf(
|
||||||
|
"total: %llu, took: %8.2fs, cost: %6.2fμs/op, io "
|
||||||
|
"count: "
|
||||||
|
"%llu\n",
|
||||||
|
sum_count,
|
||||||
|
took,
|
||||||
|
cost,
|
||||||
|
sum_io_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
#ifndef BENCH_H
|
||||||
|
#define BENCH_H
|
||||||
|
|
||||||
|
#include "search.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
class bench_t {
|
||||||
|
public:
|
||||||
|
bench_t(const string &file_name, int version, int policy);
|
||||||
|
|
||||||
|
void test(const string &file_name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void test_one(const ip_t &ip, const string region);
|
||||||
|
void test_line(char *buf);
|
||||||
|
void test_file(const std::string &file_name);
|
||||||
|
|
||||||
|
search_t search;
|
||||||
|
|
||||||
|
unsigned long long sum_io_count;
|
||||||
|
unsigned long long sum_cost_time;
|
||||||
|
unsigned long long sum_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
|
||||||
|
#include "edit.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
void handle_ip_txt(const string& name, std::list<node_t>& regions) {
|
||||||
|
FILE* f = fopen(name.data(), "r");
|
||||||
|
if (f == NULL)
|
||||||
|
log_exit("can't open " + name);
|
||||||
|
|
||||||
|
char buf[1024];
|
||||||
|
while (fgets(buf, sizeof(buf), f) != NULL) {
|
||||||
|
unsigned int buf_len = strlen(buf);
|
||||||
|
// 去掉多余的空
|
||||||
|
while (buf_len > 0 && isspace(buf[buf_len - 1]))
|
||||||
|
--buf_len;
|
||||||
|
if (buf_len == 0)
|
||||||
|
continue;
|
||||||
|
buf[buf_len] = '\0';
|
||||||
|
regions.push_back(node_t(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_t::handle_new_file(const std::string& file_name) {
|
||||||
|
handle_ip_txt(file_name, new_regions); // 输入
|
||||||
|
new_regions.sort(); // 排序
|
||||||
|
|
||||||
|
// 检验及其去重
|
||||||
|
auto it = new_regions.begin();
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
if (it == new_regions.end())
|
||||||
|
break;
|
||||||
|
auto next = it;
|
||||||
|
++next;
|
||||||
|
if (next == new_regions.end())
|
||||||
|
break;
|
||||||
|
if (it->ip1 > it->ip2)
|
||||||
|
it = new_regions.erase(it); // 非法, 直接跳过
|
||||||
|
else if (it->ip1 == next->ip1 || next->ip1 <= it->ip2) {
|
||||||
|
// 数据重叠
|
||||||
|
if (it->region != next->region)
|
||||||
|
log_exit("数据有二义性: " + it->to_string() + ", " +
|
||||||
|
next->to_string());
|
||||||
|
it->ip2 = std::max(it->ip2, next->ip2);
|
||||||
|
new_regions.erase(next);
|
||||||
|
} else if (it->ip2 + 1 == next->ip1 && it->region == next->region) {
|
||||||
|
// 数据连接
|
||||||
|
it->ip2 = next->ip2;
|
||||||
|
new_regions.erase(next);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_t::handle_old_file(const std::string& file_name) {
|
||||||
|
handle_ip_txt(file_name, old_regions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_t::merge() {
|
||||||
|
auto it1 = old_regions.begin();
|
||||||
|
auto it2 = new_regions.begin();
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
if (it2 == new_regions.end())
|
||||||
|
break;
|
||||||
|
if (it2->ip1 > it2->ip2) {
|
||||||
|
++it2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// it1->ip1 it1->ip2 it2->ip1 it2->ip2
|
||||||
|
while (it1->ip2 < it2->ip1)
|
||||||
|
++it1;
|
||||||
|
if (it1->ip2 <= it2->ip2) {
|
||||||
|
// it1->ip1 it2->ip1 it1->ip2 it2->ip2
|
||||||
|
node_t node;
|
||||||
|
node.ip1 = it2->ip1;
|
||||||
|
node.ip2 = it1->ip2;
|
||||||
|
node.region = it2->region;
|
||||||
|
|
||||||
|
it1->ip2 = node.ip1 - 1;
|
||||||
|
it2->ip1 = node.ip2 + 1;
|
||||||
|
|
||||||
|
++it1;
|
||||||
|
it1 = old_regions.insert(it1, node);
|
||||||
|
++it1;
|
||||||
|
} else {
|
||||||
|
// it1->ip1 it2->ip1 it2->ip2 it1->ip2
|
||||||
|
node_t node;
|
||||||
|
node.ip1 = it2->ip2 + 1;
|
||||||
|
node.ip2 = it1->ip2;
|
||||||
|
node.region = it1->region;
|
||||||
|
|
||||||
|
it1->ip2 = it2->ip1 - 1;
|
||||||
|
|
||||||
|
++it1;
|
||||||
|
it1 = old_regions.insert(it1, *it2);
|
||||||
|
|
||||||
|
++it1;
|
||||||
|
it1 = old_regions.insert(it1, node);
|
||||||
|
|
||||||
|
++it2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_t::write_old_file(const std::string& file_name) {
|
||||||
|
FILE* f = fopen(file_name.data(), "w");
|
||||||
|
if (f == NULL)
|
||||||
|
log_exit("can't open " + file_name);
|
||||||
|
|
||||||
|
auto it = old_regions.begin();
|
||||||
|
|
||||||
|
// 删除非法的数据
|
||||||
|
for (;;) {
|
||||||
|
if (it == old_regions.end())
|
||||||
|
break;
|
||||||
|
if (it->ip1 > it->ip2)
|
||||||
|
it = old_regions.erase(it);
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并数据域相同的相邻数据
|
||||||
|
it = old_regions.begin();
|
||||||
|
for (;;) {
|
||||||
|
if (it == old_regions.end())
|
||||||
|
break;
|
||||||
|
auto next = it;
|
||||||
|
++next;
|
||||||
|
if (next == old_regions.end())
|
||||||
|
break;
|
||||||
|
if (it->region == next->region) {
|
||||||
|
it->ip2 = next->ip2;
|
||||||
|
old_regions.erase(next);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& d : old_regions) {
|
||||||
|
string res =
|
||||||
|
d.ip1.to_string() + "|" + d.ip2.to_string() + "|" + d.region + "\n";
|
||||||
|
fputs(res.data(), f);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_t::edit_t(const string& name_old, const string& name_new, int version) {
|
||||||
|
unsigned long long tv1 = get_time();
|
||||||
|
|
||||||
|
init_xdb(version);
|
||||||
|
|
||||||
|
handle_new_file(name_new);
|
||||||
|
handle_old_file(name_old);
|
||||||
|
merge();
|
||||||
|
write_old_file(name_old);
|
||||||
|
|
||||||
|
unsigned long long tv2 = get_time();
|
||||||
|
|
||||||
|
double took = (tv2 - tv1) * 1.0 / 1000 / 1000;
|
||||||
|
|
||||||
|
printf("took: %.2fs\n", took);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#ifndef EDIT_H
|
||||||
|
#define EDIT_H
|
||||||
|
|
||||||
|
#include "ip.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
class edit_t {
|
||||||
|
public:
|
||||||
|
edit_t(const string& old_name, const string& new_name, int version);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handle_new_file(const string& file_name);
|
||||||
|
void handle_old_file(const string& file_name);
|
||||||
|
void merge();
|
||||||
|
void write_old_file(const string& file_name);
|
||||||
|
|
||||||
|
std::list<node_t> old_regions;
|
||||||
|
std::list<node_t> new_regions;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
header_t::header_t(FILE* db) {
|
||||||
|
read_bin(0, header, sizeof(header), db);
|
||||||
|
}
|
||||||
|
|
||||||
|
header_t::~header_t() {
|
||||||
|
}
|
||||||
|
|
||||||
|
int header_t::version() {
|
||||||
|
return to_int(header, 2); // 版本号(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
int header_t::index_policy() {
|
||||||
|
return to_int(header + 2, 2); // 缓存策略(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
int header_t::create_at() {
|
||||||
|
return to_int(header + 4, 4); // 文件生成时间(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
int header_t::index_start() {
|
||||||
|
return to_int(header + 8, 4); // 索引起始地址(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
int header_t::index_end() {
|
||||||
|
return to_int(header + 12, 4); // 索引结束地址(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
int header_t::ip_version() {
|
||||||
|
return to_int(header + 16, 2); // IP 版本(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
int header_t::ptr() {
|
||||||
|
return to_int(header + 18, 2); // 指针字节数(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
#ifndef HEADER_H
|
||||||
|
#define HEADER_H
|
||||||
|
|
||||||
|
#include "base.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
class header_t {
|
||||||
|
public:
|
||||||
|
header_t(FILE* db);
|
||||||
|
virtual ~header_t();
|
||||||
|
|
||||||
|
int version(); // 版本号
|
||||||
|
int index_policy(); // 缓存策略
|
||||||
|
int create_at(); // 文件生成时间
|
||||||
|
int index_start(); // 索引起始地址
|
||||||
|
int index_end(); // 索引结束地址
|
||||||
|
int ip_version(); // IP 版本
|
||||||
|
int ptr(); // 指针字节数
|
||||||
|
|
||||||
|
protected:
|
||||||
|
char header[length_header];
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
|
||||||
|
#include "ip.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
ip_t::ip_t() {
|
||||||
|
memset(p, '\0', sizeof(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
ip_t::ip_t(const ip_t& rhs, int val) {
|
||||||
|
memcpy(p, rhs.p, ip_size);
|
||||||
|
|
||||||
|
if (val == 0 || val == 255)
|
||||||
|
for (int i = 2; i < ip_size; ++i)
|
||||||
|
p[i] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
ip_t::ip_t(const char* p) {
|
||||||
|
from_xdb(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_t::from_str(const string& str) {
|
||||||
|
int af_inet = ip_version == ipv4 ? AF_INET : AF_INET6;
|
||||||
|
return inet_pton(af_inet, str.data(), p) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ip_t::from_xdb(const char str[16]) {
|
||||||
|
for (int i = 0; i < ip_size; ++i)
|
||||||
|
if (ip_version == ipv6)
|
||||||
|
p[i] = str[i];
|
||||||
|
else
|
||||||
|
p[i] = str[ip_size - 1 - i];
|
||||||
|
}
|
||||||
|
|
||||||
|
ip_t& ip_t::operator=(const ip_t& rhs) {
|
||||||
|
memcpy(p, rhs.p, ip_size);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ip_t::compare(const ip_t& rhs) const {
|
||||||
|
for (int i = 0; i < ip_size; ++i) {
|
||||||
|
if ((unsigned char)p[i] > (unsigned char)rhs.p[i])
|
||||||
|
return 1;
|
||||||
|
if ((unsigned char)p[i] < (unsigned char)rhs.p[i])
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_t::operator<(const ip_t& rhs) const {
|
||||||
|
return compare(rhs) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_t::operator<=(const ip_t& rhs) const {
|
||||||
|
return compare(rhs) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_t::operator>(const ip_t& rhs) const {
|
||||||
|
return compare(rhs) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_t::operator>=(const ip_t& rhs) const {
|
||||||
|
return compare(rhs) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_t::operator==(const ip_t& rhs) const {
|
||||||
|
return compare(rhs) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_t::operator!=(const ip_t& rhs) const {
|
||||||
|
return compare(rhs) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
string ip_t::to_string() const {
|
||||||
|
char buf[INET6_ADDRSTRLEN + 1];
|
||||||
|
int af_inet = ip_version == ipv4 ? AF_INET : AF_INET6;
|
||||||
|
inet_ntop(af_inet, p, buf, sizeof(buf));
|
||||||
|
return string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
string ip_t::to_bit() const {
|
||||||
|
string str;
|
||||||
|
for (int i = 0; i < ip_size; ++i)
|
||||||
|
if (ip_version == ipv6)
|
||||||
|
str.push_back(p[i]);
|
||||||
|
else
|
||||||
|
str.push_back(p[ip_size - 1 - i]);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
ip_t operator+(const ip_t& lhs, int v) {
|
||||||
|
ip_t ip;
|
||||||
|
|
||||||
|
int i = ip_size;
|
||||||
|
while (--i >= 0) {
|
||||||
|
v += lhs.p[i];
|
||||||
|
ip.p[i] = v % 256;
|
||||||
|
v /= 256;
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
ip_t operator-(const ip_t& lhs, int v) {
|
||||||
|
ip_t ip;
|
||||||
|
|
||||||
|
int i = ip_size;
|
||||||
|
v = -v;
|
||||||
|
while (--i >= 0) {
|
||||||
|
v += lhs.p[i];
|
||||||
|
if (v == -1)
|
||||||
|
ip.p[i] = 255;
|
||||||
|
else {
|
||||||
|
ip.p[i] = v;
|
||||||
|
v = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// node_t
|
||||||
|
node_t::node_t() {
|
||||||
|
}
|
||||||
|
|
||||||
|
node_t::node_t(char* buf) {
|
||||||
|
char* pos1 = strchr(buf, '|');
|
||||||
|
|
||||||
|
if (pos1 == NULL)
|
||||||
|
log_exit("invalid data: " + std::string(buf));
|
||||||
|
char* pos2 = strchr(pos1 + 1, '|');
|
||||||
|
if (pos2 == NULL)
|
||||||
|
log_exit("invalid data: " + std::string(buf));
|
||||||
|
*pos1 = '\0';
|
||||||
|
*pos2 = '\0';
|
||||||
|
|
||||||
|
region = pos2 + 1;
|
||||||
|
|
||||||
|
if (!ip1.from_str(buf) || !ip2.from_str(pos1 + 1) || ip2 < ip1 ||
|
||||||
|
region.empty()) {
|
||||||
|
*pos1 = *pos2 = '|';
|
||||||
|
log_exit(string("invalid data: ") + buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool node_t::operator<(const node_t& rhs) const {
|
||||||
|
if (ip1 < rhs.ip1)
|
||||||
|
return true;
|
||||||
|
return ip2 < rhs.ip2;
|
||||||
|
}
|
||||||
|
|
||||||
|
string node_t::to_string() const {
|
||||||
|
return ip1.to_string() + "|" + ip2.to_string() + "|" + region;
|
||||||
|
}
|
||||||
|
|
||||||
|
string node_t::to_bit() const {
|
||||||
|
return ip1.to_bit() + ip2.to_bit();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
#ifndef IP_H
|
||||||
|
#define IP_H
|
||||||
|
|
||||||
|
#include "base.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
struct ip_t {
|
||||||
|
unsigned char p[16];
|
||||||
|
|
||||||
|
ip_t();
|
||||||
|
ip_t(const char* p);
|
||||||
|
// val 为 0 或 255 时, 将 ip 的后几位置为 val
|
||||||
|
ip_t(const ip_t& rhs, int val = -1);
|
||||||
|
|
||||||
|
bool from_str(const string& str);
|
||||||
|
void from_xdb(const char str[16]);
|
||||||
|
|
||||||
|
ip_t& operator=(const ip_t& rhs);
|
||||||
|
|
||||||
|
int compare(const ip_t& rhs) const;
|
||||||
|
bool operator<(const ip_t& rhs) const;
|
||||||
|
bool operator<=(const ip_t& rhs) const;
|
||||||
|
bool operator>(const ip_t& rhs) const;
|
||||||
|
bool operator>=(const ip_t& rhs) const;
|
||||||
|
bool operator==(const ip_t& rhs) const;
|
||||||
|
bool operator!=(const ip_t& rhs) const;
|
||||||
|
|
||||||
|
string to_string() const;
|
||||||
|
string to_bit() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
ip_t operator+(const ip_t& lhs, int v);
|
||||||
|
ip_t operator-(const ip_t& lhs, int v);
|
||||||
|
|
||||||
|
struct node_t {
|
||||||
|
ip_t ip1;
|
||||||
|
ip_t ip2;
|
||||||
|
string region;
|
||||||
|
|
||||||
|
node_t();
|
||||||
|
node_t(char* buf);
|
||||||
|
|
||||||
|
bool operator<(const node_t& rhs) const;
|
||||||
|
|
||||||
|
string to_string() const;
|
||||||
|
string to_bit() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
|
||||||
|
#include "make.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
void make_t::vector_index_push_back(int row, int col, const node_t &node) {
|
||||||
|
vector_index[row][col].push_back(
|
||||||
|
std::make_pair<string, string>(node.to_bit(), string(node.region)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_t::vector_index_push_back(node_t &node) {
|
||||||
|
ip_t ip1 = node.ip1;
|
||||||
|
ip_t ip2 = node.ip2;
|
||||||
|
|
||||||
|
unsigned ip1_1 = ip1.p[0];
|
||||||
|
unsigned ip1_2 = ip1.p[1];
|
||||||
|
unsigned ip2_1 = ip2.p[0];
|
||||||
|
unsigned ip2_2 = ip2.p[1];
|
||||||
|
|
||||||
|
if (ip1_1 == ip2_1 && ip1_2 == ip2_2) {
|
||||||
|
vector_index_push_back(ip1_1, ip1_2, node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.ip1 = ip1;
|
||||||
|
node.ip2 = ip_t(ip1, 255);
|
||||||
|
vector_index_push_back(ip1_1, ip1_2, node);
|
||||||
|
|
||||||
|
node.ip1 = ip_t(ip2, 0);
|
||||||
|
node.ip2 = ip2;
|
||||||
|
vector_index_push_back(ip2_1, ip2_2, node);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
++ip1_2;
|
||||||
|
if (ip1_2 == 256) {
|
||||||
|
++ip1_1;
|
||||||
|
ip1_2 = 0;
|
||||||
|
}
|
||||||
|
if (ip1_1 == ip2_1 && ip1_2 == ip2_2)
|
||||||
|
break;
|
||||||
|
ip1.p[0] = ip1_1;
|
||||||
|
ip1.p[1] = ip1_2;
|
||||||
|
node.ip1 = ip_t(ip1, 0);
|
||||||
|
node.ip2 = ip_t(ip1, 255);
|
||||||
|
vector_index_push_back(ip1_1, ip1_2, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_t::handle_input_help(char *buf) {
|
||||||
|
// 去掉多余的空
|
||||||
|
unsigned int buf_len = strlen(buf);
|
||||||
|
while (buf_len > 0 && isspace(buf[buf_len - 1]))
|
||||||
|
--buf_len;
|
||||||
|
if (buf_len == 0)
|
||||||
|
return;
|
||||||
|
buf[buf_len] = '\0';
|
||||||
|
|
||||||
|
node_t node(buf);
|
||||||
|
|
||||||
|
if (node.ip1 < next_ip) {
|
||||||
|
log_exit("ip 未排序: " + node.ip1.to_string() + ", " +
|
||||||
|
next_ip.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
next_ip = node.ip2 + 1;
|
||||||
|
|
||||||
|
if (region.find(node.region) == region.end()) {
|
||||||
|
region[node.region] = region_index;
|
||||||
|
region_index += node.region.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
vector_index_push_back(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_t::handle_input(const std::string &file_name) {
|
||||||
|
FILE *src = fopen(file_name.data(), "r");
|
||||||
|
if (src == NULL)
|
||||||
|
log_exit("can't open " + file_name);
|
||||||
|
|
||||||
|
char buf[1024];
|
||||||
|
while (fgets(buf, sizeof(buf), src) != NULL)
|
||||||
|
handle_input_help(buf);
|
||||||
|
fclose(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_t::handle_header() {
|
||||||
|
char buf[length_header];
|
||||||
|
memset(buf, 0, length_header);
|
||||||
|
write_ushort(3, buf); // 版本号
|
||||||
|
write_ushort(1, buf + 2); // 缓存策略
|
||||||
|
write_uint(time(NULL), buf + 4); // 时间
|
||||||
|
// 索引
|
||||||
|
unsigned int content_left = length_header + length_vector;
|
||||||
|
for (auto &d : region)
|
||||||
|
content_left += d.first.size();
|
||||||
|
unsigned int content_right = content_left;
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; ++i)
|
||||||
|
for (int j = 0; j < 256; ++j)
|
||||||
|
content_right += vector_index[i][j].size() * content_size;
|
||||||
|
content_right -= content_size;
|
||||||
|
write_uint(content_left, buf + 8);
|
||||||
|
write_uint(content_right, buf + 12);
|
||||||
|
write_ushort(ip_version, buf + 16); // IP
|
||||||
|
write_ushort(4, buf + 18); // 指针数
|
||||||
|
|
||||||
|
write_string(buf, length_header, db);
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_t::handle_vector_index() {
|
||||||
|
unsigned index = length_header + length_vector;
|
||||||
|
for (auto &d : region)
|
||||||
|
index += d.first.size();
|
||||||
|
for (unsigned i = 0; i < 256; ++i)
|
||||||
|
for (unsigned j = 0; j < 256; ++j)
|
||||||
|
if (vector_index[i][j].size() == 0) {
|
||||||
|
write_uint(0, db);
|
||||||
|
write_uint(0, db);
|
||||||
|
} else {
|
||||||
|
write_uint(index, db);
|
||||||
|
index += content_size * vector_index[i][j].size();
|
||||||
|
write_uint(index, db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_t::handle_region() {
|
||||||
|
for (auto &d : region) {
|
||||||
|
fseek(db, d.second, SEEK_SET);
|
||||||
|
write_string(d.first.data(), d.first.size(), db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_t::handle_content() {
|
||||||
|
fseek(db, 0, SEEK_END);
|
||||||
|
for (unsigned i = 0; i < 256; ++i)
|
||||||
|
for (unsigned j = 0; j < 256; ++j)
|
||||||
|
for (auto d : vector_index[i][j]) {
|
||||||
|
write_string(d.first.data(), d.first.size(), db);
|
||||||
|
write_ushort(d.second.size(), db);
|
||||||
|
write_uint(region[d.second], db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_t::make_t(const string &src, const string &dst, int version)
|
||||||
|
: region_index(length_vector + length_header) {
|
||||||
|
unsigned long long tv1 = get_time();
|
||||||
|
|
||||||
|
init_xdb(version);
|
||||||
|
|
||||||
|
handle_input(src);
|
||||||
|
|
||||||
|
db = fopen(dst.data(), "w");
|
||||||
|
if (db == NULL)
|
||||||
|
log_exit("can't open " + dst);
|
||||||
|
|
||||||
|
handle_header();
|
||||||
|
handle_vector_index();
|
||||||
|
handle_region();
|
||||||
|
handle_content();
|
||||||
|
|
||||||
|
fclose(db);
|
||||||
|
|
||||||
|
unsigned long long tv2 = get_time();
|
||||||
|
printf("took: %.2fs\n", (tv2 - tv1) * 1.0 / 1000 / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#ifndef MAKE_H
|
||||||
|
#define MAKE_H
|
||||||
|
|
||||||
|
#include "ip.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
class make_t {
|
||||||
|
public:
|
||||||
|
make_t(const string &src, const string &dst, int version);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void vector_index_push_back(int row, int col, const node_t &node);
|
||||||
|
void vector_index_push_back(node_t &node);
|
||||||
|
void handle_input_help(char buf[]);
|
||||||
|
void handle_input(const std::string &file_name);
|
||||||
|
|
||||||
|
void handle_header();
|
||||||
|
void handle_vector_index();
|
||||||
|
void handle_region();
|
||||||
|
void handle_content();
|
||||||
|
|
||||||
|
FILE *db = NULL;
|
||||||
|
|
||||||
|
std::vector<std::pair<string, string>> vector_index[256][256];
|
||||||
|
|
||||||
|
std::unordered_map<string, unsigned> region;
|
||||||
|
|
||||||
|
unsigned region_index;
|
||||||
|
ip_t next_ip;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
|
||||||
|
#include "search.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
search_t::search_t(const string &file, int version, int p)
|
||||||
|
: db(fopen(file.data(), "r")), header(db), policy(p) {
|
||||||
|
init_xdb(version);
|
||||||
|
|
||||||
|
if (db == NULL)
|
||||||
|
log_exit("can't open " + file);
|
||||||
|
if (header.ip_version() != version)
|
||||||
|
log_exit("ip 版本不匹配");
|
||||||
|
|
||||||
|
if (policy != policy_file) {
|
||||||
|
read_bin(length_header, vector, length_vector, db);
|
||||||
|
if (policy == policy_content) {
|
||||||
|
fseek(db, 0, SEEK_END);
|
||||||
|
int size = ftell(db) - length_vector - length_header;
|
||||||
|
content = (char *)malloc(size);
|
||||||
|
read_bin(length_vector + length_header, content, size, db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search_t::~search_t() {
|
||||||
|
fclose(db);
|
||||||
|
if (policy == policy_content)
|
||||||
|
free(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
int search_t::get_io_count() {
|
||||||
|
return io_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int search_t::get_cost_time() {
|
||||||
|
return cost_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
char const *search_t::get_content_index_help(int index) {
|
||||||
|
if (policy != policy_file)
|
||||||
|
return vector + index;
|
||||||
|
|
||||||
|
++io_count;
|
||||||
|
static char v[8];
|
||||||
|
read_bin(length_header + index, v, sizeof(v), db);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void search_t::get_content_index(const ip_t &ip, int &left, int &right) {
|
||||||
|
int index = ((unsigned char)ip.p[0] * 256 + (unsigned char)ip.p[1]) * 8;
|
||||||
|
|
||||||
|
const char *p = get_content_index_help(index);
|
||||||
|
left = to_uint(p);
|
||||||
|
right = to_uint(p + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
char const *search_t::get_content_help(int index) {
|
||||||
|
if (policy == policy_content)
|
||||||
|
return content + index - length_header - length_vector;
|
||||||
|
++io_count;
|
||||||
|
static char v[16 + 16 + 2 + 4];
|
||||||
|
read_bin(index, v, content_size, db);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
string search_t::get_region(int index, int len) {
|
||||||
|
if (policy == policy_content)
|
||||||
|
return string(content + index - length_header - length_vector, len);
|
||||||
|
++io_count;
|
||||||
|
char *p = (char *)malloc(sizeof(char) * len);
|
||||||
|
read_bin(index, p, len, db);
|
||||||
|
string res(p, len);
|
||||||
|
free(p);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void search_t::get_content(int index,
|
||||||
|
ip_t &ip_left,
|
||||||
|
ip_t &ip_right,
|
||||||
|
int ®ion_len,
|
||||||
|
int ®ion_index) {
|
||||||
|
const char *p = get_content_help(index);
|
||||||
|
|
||||||
|
ip_left.from_xdb(p);
|
||||||
|
ip_right.from_xdb(p + ip_size);
|
||||||
|
|
||||||
|
region_len = to_ushort(p + ip_size * 2);
|
||||||
|
region_index = to_uint(p + ip_size * 2 + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
string search_t::search(const ip_t &ip) {
|
||||||
|
io_count = 0;
|
||||||
|
|
||||||
|
int content_left, content_right;
|
||||||
|
get_content_index(ip, content_left, content_right);
|
||||||
|
|
||||||
|
if (content_left == 0 || content_right == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
ip_t ip_left, ip_right;
|
||||||
|
int region_len;
|
||||||
|
int region_index;
|
||||||
|
|
||||||
|
int left = 0;
|
||||||
|
int right = (content_right - content_left) / content_size;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int mid = left + (right - left) / 2;
|
||||||
|
int mid_index = content_left + mid * content_size;
|
||||||
|
get_content(mid_index, ip_left, ip_right, region_len, region_index);
|
||||||
|
|
||||||
|
// ip ip_left ip_right
|
||||||
|
if (ip < ip_left)
|
||||||
|
right = mid - 1;
|
||||||
|
// ip_left ip_right ip
|
||||||
|
else if (ip_right < ip)
|
||||||
|
left = mid + 1;
|
||||||
|
else
|
||||||
|
return get_region(region_index, region_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string search_t::search(const string &str) {
|
||||||
|
unsigned long long t1 = get_time();
|
||||||
|
|
||||||
|
ip_t ip;
|
||||||
|
if (ip.from_str(str) == false)
|
||||||
|
return "invalid ipv" + std::to_string(ip_version) + ": " + str;
|
||||||
|
string region = search(ip);
|
||||||
|
|
||||||
|
unsigned long long t2 = get_time();
|
||||||
|
cost_time = t2 - t1;
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
#ifndef SEARCH_H
|
||||||
|
#define SEARCH_H
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
#include "ip.h"
|
||||||
|
|
||||||
|
namespace xdb {
|
||||||
|
|
||||||
|
class search_t {
|
||||||
|
protected:
|
||||||
|
FILE *db;
|
||||||
|
|
||||||
|
header_t header;
|
||||||
|
|
||||||
|
int policy;
|
||||||
|
|
||||||
|
int io_count;
|
||||||
|
int cost_time;
|
||||||
|
|
||||||
|
char vector[length_vector];
|
||||||
|
char *content;
|
||||||
|
|
||||||
|
public:
|
||||||
|
search_t(const string &file_name, int version, int policy);
|
||||||
|
virtual ~search_t();
|
||||||
|
|
||||||
|
int get_io_count();
|
||||||
|
int get_cost_time();
|
||||||
|
|
||||||
|
string search(const string &str);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
string search(const ip_t &ip);
|
||||||
|
|
||||||
|
void get_content_index(const ip_t &ip1, int &left, int &right);
|
||||||
|
void get_content(int index,
|
||||||
|
ip_t &left,
|
||||||
|
ip_t &right,
|
||||||
|
int ®ion_len,
|
||||||
|
int ®ion_index);
|
||||||
|
|
||||||
|
char const *get_content_index_help(int index);
|
||||||
|
char const *get_content_help(int index);
|
||||||
|
string get_region(int index, int len);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xdb
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
#include "../src/bench.h"
|
||||||
|
|
||||||
|
std::map<int, std::string> prompt;
|
||||||
|
|
||||||
|
void test_ipv4(int policy) {
|
||||||
|
std::cout << "测试 IPv4, " << prompt[policy];
|
||||||
|
xdb::bench_t("../../data/ip2region_v4.xdb", xdb::ipv4, policy)
|
||||||
|
.test("../../data/ipv4_source.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ipv6(int policy) {
|
||||||
|
std::cout << "测试 IPv6, " << prompt[policy];
|
||||||
|
xdb::bench_t("../../data/ip2region_v6.xdb", xdb::ipv6, policy)
|
||||||
|
.test("../../data/ipv6_source.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
prompt[xdb::policy_file] = " 不缓存, ";
|
||||||
|
prompt[xdb::policy_vector] = "部分缓存, ";
|
||||||
|
prompt[xdb::policy_content] = "全部缓存, ";
|
||||||
|
|
||||||
|
test_ipv4(xdb::policy_file);
|
||||||
|
test_ipv4(xdb::policy_vector);
|
||||||
|
test_ipv4(xdb::policy_content);
|
||||||
|
|
||||||
|
test_ipv6(xdb::policy_file);
|
||||||
|
test_ipv6(xdb::policy_vector);
|
||||||
|
test_ipv6(xdb::policy_content);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
#include "../src/edit.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::string file_name_old = "../../data/ipv4_source.txt";
|
||||||
|
std::string file_name_new = "./1.txt";
|
||||||
|
xdb::edit_t xdb(file_name_old, file_name_new, xdb::ipv4);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
#include "../src/edit.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::string file_name_old = "../../data/ipv6_source.txt";
|
||||||
|
std::string file_name_new = "./1.txt";
|
||||||
|
xdb::edit_t xdb(file_name_old, file_name_new, xdb::ipv6);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
#include "../src/header.h"
|
||||||
|
|
||||||
|
void test(const std::string& prompt, const std::string& file_name) {
|
||||||
|
std::cout << prompt << std::endl;
|
||||||
|
|
||||||
|
xdb::header_t head(fopen(file_name.data(), "r"));
|
||||||
|
|
||||||
|
std::cout << "版本号: " << head.version() << std::endl;
|
||||||
|
std::cout << "缓存策略: " << head.index_policy() << std::endl;
|
||||||
|
|
||||||
|
time_t rawtime = head.create_at();
|
||||||
|
struct tm* info = localtime(&rawtime);
|
||||||
|
char buf[80];
|
||||||
|
strftime(buf, 80, "%Y-%m-%d %H:%M:%S", info);
|
||||||
|
|
||||||
|
std::cout << "文件生成时间: " << buf << std::endl;
|
||||||
|
std::cout << "索引起始地址: " << head.index_start() << std::endl;
|
||||||
|
std::cout << "索引结束地址: " << head.index_end() << std::endl;
|
||||||
|
std::cout << "IP版本: " << head.ip_version() << std::endl;
|
||||||
|
std::cout << "指针字节数: " << head.ptr() << std::endl;
|
||||||
|
|
||||||
|
std::cout << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test("测试 IPv4", "../../data/ip2region_v4.xdb");
|
||||||
|
test("测试 IPv6", "../../data/ip2region_v6.xdb");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
#include "../src/make.h"
|
||||||
|
|
||||||
|
void test(const std::string& prompt,
|
||||||
|
const std::string& filename_xdb,
|
||||||
|
const std::string& filename_src,
|
||||||
|
int version
|
||||||
|
|
||||||
|
) {
|
||||||
|
std::cout << prompt;
|
||||||
|
xdb::make_t(filename_xdb, filename_src, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test("生成 ipv4 的 xdb 文件, ",
|
||||||
|
"../../data/ipv4_source.txt",
|
||||||
|
"./ip2region_v4.xdb",
|
||||||
|
xdb::ipv4);
|
||||||
|
|
||||||
|
test("生成 ipv6 的 xdb 文件, ",
|
||||||
|
"../../data/ipv6_source.txt",
|
||||||
|
"./ip2region_v6.xdb",
|
||||||
|
xdb::ipv6);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
#include "../src/search.h"
|
||||||
|
|
||||||
|
std::map<int, std::string> prompt;
|
||||||
|
|
||||||
|
void test(xdb::search_t& s, const std::string& ip, const std::string& region) {
|
||||||
|
if (s.search(ip) != region)
|
||||||
|
xdb::log_exit("测试失败, ip " + ip + ", region " + region);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ipv4(int policy) {
|
||||||
|
std::cout << "测试 IPv4 " << prompt[policy];
|
||||||
|
|
||||||
|
xdb::search_t s("../../data/ip2region_v4.xdb", xdb::ipv4, policy);
|
||||||
|
test(s, "0.0.0.0", "Reserved|Reserved|Reserved|0|0");
|
||||||
|
test(s, "1.2.3.4", "Australia|Queensland|Brisbane|0|AU");
|
||||||
|
|
||||||
|
std::cout << " 成功" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ipv6(int policy) {
|
||||||
|
std::cout << "测试 IPv6 " << prompt[policy];
|
||||||
|
|
||||||
|
xdb::search_t s("../../data/ip2region_v6.xdb", xdb::ipv6, policy);
|
||||||
|
test(s, "::1", "");
|
||||||
|
test(s, "2001:200:124::", "Japan|Tokyo|Asagaya-minami|WIDE Project|JP");
|
||||||
|
test(s, "2001:200:124::", "Japan|Tokyo|Asagaya-minami|WIDE Project|JP");
|
||||||
|
test(s, "240e:3b7:3273:51d0:cd38:8ae1:e3c0:b708", "中国|广东省|深圳市|电信|CN");
|
||||||
|
|
||||||
|
std::cout << " 成功" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
prompt[xdb::policy_file] = " 不缓存:";
|
||||||
|
prompt[xdb::policy_vector] = "部分缓存:";
|
||||||
|
prompt[xdb::policy_content] = "全部缓存:";
|
||||||
|
|
||||||
|
test_ipv4(xdb::policy_file);
|
||||||
|
test_ipv4(xdb::policy_vector);
|
||||||
|
test_ipv4(xdb::policy_content);
|
||||||
|
|
||||||
|
test_ipv6(xdb::policy_file);
|
||||||
|
test_ipv6(xdb::policy_vector);
|
||||||
|
test_ipv6(xdb::policy_content);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
|
|
||||||
#include "xdb_bench.h"
|
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/time.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
static void log_exit(const std::string &msg) {
|
|
||||||
std::cout << msg << std::endl;
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static unsigned long long get_time() {
|
|
||||||
struct timeval tv1;
|
|
||||||
gettimeofday(&tv1, NULL);
|
|
||||||
return (unsigned long long)tv1.tv_sec * 1000 * 1000 + tv1.tv_usec;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ip2uint(const char *buf, unsigned int &ip) {
|
|
||||||
struct in_addr addr;
|
|
||||||
if (inet_pton(AF_INET, buf, &addr) == 0)
|
|
||||||
return false;
|
|
||||||
// 网络字节序为大端存储, 在此转换为小端存储
|
|
||||||
ip = (((addr.s_addr >> 0) & 0xFF) << 24) |
|
|
||||||
(((addr.s_addr >> 8) & 0xFF) << 16) |
|
|
||||||
(((addr.s_addr >> 16) & 0xFF) << 8) |
|
|
||||||
(((addr.s_addr >> 24) & 0xFF) << 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string uint2ip(unsigned int ip) {
|
|
||||||
char buf[16];
|
|
||||||
snprintf(buf,
|
|
||||||
sizeof(buf),
|
|
||||||
"%d.%d.%d.%d",
|
|
||||||
(ip >> 24) & 0xFF,
|
|
||||||
(ip >> 16) & 0xFF,
|
|
||||||
(ip >> 8) & 0xFF,
|
|
||||||
ip & 0xFF);
|
|
||||||
return std::string(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_bench_t::xdb_bench_t(const std::string &file_name) : xdb_search(file_name) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_bench_t::init_file() {
|
|
||||||
xdb_search.init_file();
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_bench_t::init_vector_index() {
|
|
||||||
xdb_search.init_vector_index();
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_bench_t::init_content() {
|
|
||||||
xdb_search.init_content();
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_bench_t::bench_test_one(unsigned int ip_uint, const char *region) {
|
|
||||||
if (xdb_search.search(uint2ip(ip_uint)) != region)
|
|
||||||
log_exit("failed: " + uint2ip(ip_uint));
|
|
||||||
sum_io_count += xdb_search.get_io_count();
|
|
||||||
sum_cost_time += xdb_search.get_cost_time();
|
|
||||||
sum_count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_bench_t::bench_test_line(char *buf) {
|
|
||||||
size_t buf_len = strlen(buf);
|
|
||||||
if (buf_len == 0)
|
|
||||||
return;
|
|
||||||
buf[buf_len - 1] = '\0'; // 去掉换行符
|
|
||||||
|
|
||||||
char *pos1 = strchr(buf, '|');
|
|
||||||
|
|
||||||
if (pos1 == NULL)
|
|
||||||
log_exit("invalid data: " + std::string(buf));
|
|
||||||
char *pos2 = strchr(pos1 + 1, '|');
|
|
||||||
if (pos2 == NULL)
|
|
||||||
log_exit("invalid data: " + std::string(buf));
|
|
||||||
*pos1 = '\0';
|
|
||||||
*pos2 = '\0';
|
|
||||||
|
|
||||||
unsigned int ip1, ip2;
|
|
||||||
if (!ip2uint(buf, ip1) || !ip2uint(pos1 + 1, ip2) || ip1 > ip2) {
|
|
||||||
*pos1 = *pos2 = '|';
|
|
||||||
log_exit(std::string("invalid data: ") + buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *region = pos2 + 1;
|
|
||||||
|
|
||||||
unsigned int ip_mid = ip1 + (ip2 - ip1) / 2;
|
|
||||||
std::vector<unsigned int> ip_vec;
|
|
||||||
ip_vec.push_back(ip1);
|
|
||||||
ip_vec.push_back(ip1 + (ip_mid - ip1) / 2);
|
|
||||||
ip_vec.push_back(ip_mid);
|
|
||||||
ip_vec.push_back(ip_mid + (ip2 - ip_mid) / 2);
|
|
||||||
ip_vec.push_back(ip2);
|
|
||||||
|
|
||||||
for (auto &d : ip_vec)
|
|
||||||
bench_test_one(d, region);
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_bench_t::bench_test_file(const std::string &file_name) {
|
|
||||||
FILE *f = fopen(file_name.data(), "r");
|
|
||||||
if (f == NULL)
|
|
||||||
log_exit("can't open " + file_name);
|
|
||||||
char buf[1024];
|
|
||||||
while (fgets(buf, sizeof(buf), f) != NULL)
|
|
||||||
bench_test_line(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_bench_t::bench(const std::string &file_name) {
|
|
||||||
sum_io_count = 0;
|
|
||||||
sum_cost_time = 0;
|
|
||||||
sum_count = 0;
|
|
||||||
|
|
||||||
unsigned long long tv1 = get_time();
|
|
||||||
bench_test_file(file_name);
|
|
||||||
unsigned long long tv2 = get_time();
|
|
||||||
|
|
||||||
double took = (tv2 - tv1) * 1.0 / 1000 / 1000;
|
|
||||||
double cost = sum_cost_time * 1.0 / sum_count;
|
|
||||||
|
|
||||||
printf(
|
|
||||||
"total: %llu, took: %.2f s, cost: %.2f μs/op, io "
|
|
||||||
"count: "
|
|
||||||
"%llu\n",
|
|
||||||
sum_count,
|
|
||||||
took,
|
|
||||||
cost,
|
|
||||||
sum_io_count);
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#ifndef XDB_BENCH_H
|
|
||||||
#define XDB_BENCH_H
|
|
||||||
|
|
||||||
#include "xdb_search.h"
|
|
||||||
|
|
||||||
class xdb_bench_t {
|
|
||||||
public:
|
|
||||||
xdb_bench_t(const std::string &file_name);
|
|
||||||
|
|
||||||
void init_file();
|
|
||||||
void init_vector_index();
|
|
||||||
void init_content();
|
|
||||||
|
|
||||||
void bench(const std::string &file_name);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void bench_test_one(unsigned int ip_uint, const char *region);
|
|
||||||
void bench_test_line(char *buf);
|
|
||||||
void bench_test_file(const std::string &file_name);
|
|
||||||
|
|
||||||
xdb_search_t xdb_search;
|
|
||||||
|
|
||||||
unsigned long long sum_io_count;
|
|
||||||
unsigned long long sum_cost_time;
|
|
||||||
unsigned long long sum_count;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
|
|
||||||
#include "xdb_bench.h"
|
|
||||||
|
|
||||||
#include <getopt.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
void print_help(int argc, char* argv[]) {
|
|
||||||
printf("./xdb_bench [command options]\n");
|
|
||||||
printf("options:\n");
|
|
||||||
printf(" --db string ip2region binary xdb file path\n");
|
|
||||||
printf(" --src string source ip text file path\n");
|
|
||||||
printf(
|
|
||||||
" --cache-policy string cache policy: "
|
|
||||||
"file/vector_index/content\n");
|
|
||||||
printf(" --help print help\n");
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
|
||||||
struct option long_options[] = {
|
|
||||||
{"db", required_argument, 0, 'd'},
|
|
||||||
{"cache-policy", required_argument, 0, 't'},
|
|
||||||
{"src", required_argument, 0, 's'},
|
|
||||||
{"help", no_argument, 0, 'h'},
|
|
||||||
{0, 0, 0, 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string db_file_name = "../../data/ip2region.xdb";
|
|
||||||
std::string src_file_name = "../../data/ip.merge.txt";
|
|
||||||
std::string cache_policy = "vector_index";
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
int c = getopt_long(argc, argv, "", long_options, NULL);
|
|
||||||
if (c == -1)
|
|
||||||
break;
|
|
||||||
switch (c) {
|
|
||||||
case 'd':
|
|
||||||
db_file_name = optarg;
|
|
||||||
break;
|
|
||||||
case 'h':
|
|
||||||
print_help(argc, argv);
|
|
||||||
break;
|
|
||||||
case 't':
|
|
||||||
cache_policy = optarg;
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
src_file_name = optarg;
|
|
||||||
break;
|
|
||||||
case '?':
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_bench_t xdb(db_file_name);
|
|
||||||
|
|
||||||
if (cache_policy == "content")
|
|
||||||
xdb.init_content();
|
|
||||||
else if (cache_policy == "vector_index")
|
|
||||||
xdb.init_vector_index();
|
|
||||||
else if (cache_policy == "file")
|
|
||||||
xdb.init_file();
|
|
||||||
else {
|
|
||||||
std::cout << "invalid cache policy: " << cache_policy << std::endl;
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb.bench(src_file_name);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
|
|
||||||
#include "xdb_search.h"
|
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <sys/time.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
static void log_exit(const std::string &msg) {
|
|
||||||
std::cout << msg << std::endl;
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static unsigned long long get_time() {
|
|
||||||
struct timeval tv1;
|
|
||||||
gettimeofday(&tv1, NULL);
|
|
||||||
return (unsigned long long)tv1.tv_sec * 1000 * 1000 + tv1.tv_usec;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void read_bin(int index, char *buf, size_t len, FILE *db) {
|
|
||||||
fseek(db, index, SEEK_SET);
|
|
||||||
if (fread(buf, 1, len, db) != len)
|
|
||||||
log_exit(__func__);
|
|
||||||
}
|
|
||||||
|
|
||||||
static unsigned int read_uint(const char *buf) {
|
|
||||||
return ((buf[0]) & 0x000000FF) | ((buf[1] << 8) & 0x0000FF00) |
|
|
||||||
((buf[2] << 16) & 0x00FF0000) | ((buf[3] << 24) & 0xFF000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
static unsigned short read_ushort(const char *buf) {
|
|
||||||
return ((buf[0]) & 0x000000FF) | ((buf[1] << 8) & 0x0000FF00);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ip2uint(const char *buf, unsigned int &ip) {
|
|
||||||
struct in_addr addr;
|
|
||||||
if (inet_pton(AF_INET, buf, &addr) == 0)
|
|
||||||
return false;
|
|
||||||
// 网络字节序为大端存储, 在此转换为小端存储
|
|
||||||
ip = (((addr.s_addr >> 0) & 0xFF) << 24) |
|
|
||||||
(((addr.s_addr >> 8) & 0xFF) << 16) |
|
|
||||||
(((addr.s_addr >> 16) & 0xFF) << 8) |
|
|
||||||
(((addr.s_addr >> 24) & 0xFF) << 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_search_t::get_content_index(unsigned int ip,
|
|
||||||
unsigned int &left,
|
|
||||||
unsigned int &right) {
|
|
||||||
unsigned int ip_1 = (ip >> 24) & 0xFF;
|
|
||||||
unsigned int ip_2 = (ip >> 16) & 0xFF;
|
|
||||||
unsigned int index = (ip_1 * vector_index_cols + ip_2) * vector_index_size;
|
|
||||||
|
|
||||||
if (content != NULL) {
|
|
||||||
left = read_uint(content + index + header_length);
|
|
||||||
right = read_uint(content + index + header_length + 4);
|
|
||||||
} else if (vector_index != NULL) {
|
|
||||||
left = read_uint(vector_index + index);
|
|
||||||
right = read_uint(vector_index + index + 4);
|
|
||||||
} else {
|
|
||||||
++io_count;
|
|
||||||
char buf[8];
|
|
||||||
read_bin(header_length + index, buf, sizeof(buf), db);
|
|
||||||
left = read_uint(buf);
|
|
||||||
right = read_uint(buf + 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_search_t::get_content(unsigned int index,
|
|
||||||
unsigned int &ip_left,
|
|
||||||
unsigned int &ip_right,
|
|
||||||
unsigned short ®ion_len,
|
|
||||||
unsigned int ®ion_index) {
|
|
||||||
char buf[segment_index_size]; // 4 + 4 + 2 + 4
|
|
||||||
const char *p;
|
|
||||||
|
|
||||||
if (content != NULL) {
|
|
||||||
p = content + index;
|
|
||||||
} else {
|
|
||||||
++io_count;
|
|
||||||
read_bin(index, buf, sizeof(buf), db);
|
|
||||||
p = buf;
|
|
||||||
}
|
|
||||||
ip_left = read_uint(p);
|
|
||||||
ip_right = read_uint(p + 4);
|
|
||||||
region_len = read_ushort(p + 8);
|
|
||||||
region_index = read_uint(p + 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string xdb_search_t::get_region(unsigned int index, unsigned short len) {
|
|
||||||
if (content != NULL) {
|
|
||||||
return std::string(content + index, len);
|
|
||||||
} else {
|
|
||||||
++io_count;
|
|
||||||
char *buf = (char *)malloc(sizeof(char) * len);
|
|
||||||
read_bin(index, buf, len, db);
|
|
||||||
std::string res(buf, len);
|
|
||||||
free(buf);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_search_t::xdb_search_t(const std::string &file_name) {
|
|
||||||
db = fopen(file_name.data(), "r");
|
|
||||||
vector_index = NULL;
|
|
||||||
content = NULL;
|
|
||||||
|
|
||||||
if (db == NULL)
|
|
||||||
log_exit("can't open " + file_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_search_t::init_file() {
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_search_t::init_vector_index() {
|
|
||||||
vector_index = (char *)malloc(vector_index_length);
|
|
||||||
read_bin(header_length, vector_index, vector_index_length, db);
|
|
||||||
}
|
|
||||||
|
|
||||||
void xdb_search_t::init_content() {
|
|
||||||
fseek(db, 0, SEEK_END);
|
|
||||||
unsigned int size = ftell(db);
|
|
||||||
content = (char *)malloc(size);
|
|
||||||
read_bin(0, content, size, db);
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_search_t::~xdb_search_t() {
|
|
||||||
if (db != NULL) {
|
|
||||||
fclose(db);
|
|
||||||
db = NULL;
|
|
||||||
}
|
|
||||||
if (vector_index != NULL) {
|
|
||||||
free(vector_index);
|
|
||||||
vector_index = NULL;
|
|
||||||
}
|
|
||||||
if (content != NULL) {
|
|
||||||
free(content);
|
|
||||||
content = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long long xdb_search_t::get_io_count() {
|
|
||||||
return io_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long long xdb_search_t::get_cost_time() {
|
|
||||||
return cost_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string xdb_search_t::search(const std::string &ip_str) {
|
|
||||||
unsigned long long t1 = get_time();
|
|
||||||
|
|
||||||
unsigned int ip_uint;
|
|
||||||
if (!ip2uint(ip_str.data(), ip_uint))
|
|
||||||
return "invalid ip: " + ip_str;
|
|
||||||
std::string region = search(ip_uint);
|
|
||||||
|
|
||||||
unsigned long long t2 = get_time();
|
|
||||||
cost_time = t2 - t1;
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string xdb_search_t::search(unsigned int ip_uint) {
|
|
||||||
io_count = 0;
|
|
||||||
|
|
||||||
unsigned int content_index_left, content_index_right;
|
|
||||||
get_content_index(ip_uint, content_index_left, content_index_right);
|
|
||||||
|
|
||||||
unsigned int left, right, mid;
|
|
||||||
unsigned int ip_left, ip_right;
|
|
||||||
unsigned short region_len;
|
|
||||||
unsigned int region_index;
|
|
||||||
unsigned int mid_index;
|
|
||||||
|
|
||||||
left = 0;
|
|
||||||
right = (content_index_right - content_index_left) / segment_index_size;
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
mid = left + (right - left) / 2;
|
|
||||||
mid_index = content_index_left + mid * segment_index_size;
|
|
||||||
get_content(mid_index, ip_left, ip_right, region_len, region_index);
|
|
||||||
|
|
||||||
if (ip_left > ip_uint)
|
|
||||||
right = mid - 1;
|
|
||||||
else if (ip_right < ip_uint)
|
|
||||||
left = mid + 1;
|
|
||||||
else
|
|
||||||
return get_region(region_index, region_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
#ifndef XDB_SEARCH_H
|
|
||||||
#define XDB_SEARCH_H
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class xdb_search_t {
|
|
||||||
public:
|
|
||||||
xdb_search_t(const std::string &file_name);
|
|
||||||
~xdb_search_t();
|
|
||||||
|
|
||||||
void init_file();
|
|
||||||
void init_vector_index();
|
|
||||||
void init_content();
|
|
||||||
|
|
||||||
unsigned long long get_io_count();
|
|
||||||
unsigned long long get_cost_time();
|
|
||||||
|
|
||||||
std::string search(const std::string &ip);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void get_content_index(unsigned int ip,
|
|
||||||
unsigned int &left,
|
|
||||||
unsigned int &right);
|
|
||||||
|
|
||||||
void get_content(unsigned int index,
|
|
||||||
unsigned int &ip_left,
|
|
||||||
unsigned int &ip_right,
|
|
||||||
unsigned short ®ion_len,
|
|
||||||
unsigned int ®ion_index);
|
|
||||||
|
|
||||||
std::string get_region(unsigned int index, unsigned short len);
|
|
||||||
|
|
||||||
std::string search(unsigned int ip_uint);
|
|
||||||
|
|
||||||
FILE *db;
|
|
||||||
char *vector_index;
|
|
||||||
char *content;
|
|
||||||
unsigned long long io_count;
|
|
||||||
unsigned long long cost_time;
|
|
||||||
|
|
||||||
static constexpr int header_length = 256;
|
|
||||||
static constexpr int vector_index_rows = 256;
|
|
||||||
static constexpr int vector_index_cols = 256;
|
|
||||||
static constexpr int vector_index_size = 8;
|
|
||||||
static constexpr int vector_index_length =
|
|
||||||
vector_index_rows * vector_index_cols * vector_index_size;
|
|
||||||
static constexpr int segment_index_size = 14;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
|
|
||||||
#include "xdb_search.h"
|
|
||||||
|
|
||||||
#include <getopt.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
void print_help(int argc, char* argv[]) {
|
|
||||||
printf("./xdb_search [command options]\n");
|
|
||||||
printf("options:\n");
|
|
||||||
printf(" --db string ip2region binary xdb file path\n");
|
|
||||||
printf(
|
|
||||||
" --cache-policy string cache policy: "
|
|
||||||
"file/vector_index/content\n");
|
|
||||||
printf(" --help print help\n");
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
|
||||||
struct option long_options[] = {
|
|
||||||
{"db", required_argument, 0, 'd'},
|
|
||||||
{"cache-policy", required_argument, 0, 't'},
|
|
||||||
{"help", no_argument, 0, 'h'},
|
|
||||||
{0, 0, 0, 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string db_file_name = "../../data/ip2region.xdb";
|
|
||||||
std::string cache_policy = "vector_index";
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
int c = getopt_long(argc, argv, "", long_options, NULL);
|
|
||||||
if (c == -1)
|
|
||||||
break;
|
|
||||||
switch (c) {
|
|
||||||
case 'd':
|
|
||||||
db_file_name = optarg;
|
|
||||||
break;
|
|
||||||
case 'h':
|
|
||||||
print_help(argc, argv);
|
|
||||||
break;
|
|
||||||
case 't':
|
|
||||||
cache_policy = optarg;
|
|
||||||
break;
|
|
||||||
case '?':
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xdb_search_t xdb(db_file_name);
|
|
||||||
|
|
||||||
if (cache_policy == "content")
|
|
||||||
xdb.init_content();
|
|
||||||
else if (cache_policy == "vector_index")
|
|
||||||
xdb.init_vector_index();
|
|
||||||
else if (cache_policy == "file")
|
|
||||||
xdb.init_file();
|
|
||||||
else {
|
|
||||||
std::cout << "invalid cache policy: " << cache_policy << std::endl;
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ip;
|
|
||||||
for (;;) {
|
|
||||||
std::cout << "ip2region>> ";
|
|
||||||
std::getline(std::cin, ip);
|
|
||||||
if (ip.empty())
|
|
||||||
continue;
|
|
||||||
if (ip == "exit" || ip == "quit")
|
|
||||||
break;
|
|
||||||
std::cout << xdb.search(ip) << std::endl;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
spelling_exclusion_path = .\exclusion.dic
|
||||||
|
|
||||||
|
[*.csproj]
|
||||||
|
charset = utf-8
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{xml,config,csproj,nuspec,props,resx,targets,yml,tasks,json}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.sh]
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
csharp_style_namespace_declarations = file_scoped:silent
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
file_header_template = Copyright 2025 The Ip2Region Authors. All rights reserved.\nUse of this source code is governed by a Apache2.0-style\nlicense that can be found in the LICENSE file.\n@Author Alan <lzh.shap@gmail.com>\n@Date 2023/07/25\nUpdated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [Unreleased]
|
## [3.0.0] - 2025-11-22
|
||||||
|
- 支持 .NET 10.0
|
||||||
|
- 增加 IPv6 支持
|
||||||
|
- 修复若干 bug
|
||||||
|
|
||||||
## [2.0.1] - 2023-07-30
|
## [2.0.1] - 2023-07-30
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using IP2Region.Net.XDB;
|
||||||
|
|
||||||
|
namespace IP2Region.Net.BenchMark;
|
||||||
|
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class Benchmarks
|
||||||
|
{
|
||||||
|
private static readonly string XdbPathV4 = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region_v4.xdb");
|
||||||
|
private static readonly string XdbPathV6 = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region_v6.xdb");
|
||||||
|
private static readonly Searcher _contentV4Searcher = new(CachePolicy.Content, XdbPathV4);
|
||||||
|
private static readonly Searcher _vectorV4Searcher = new(CachePolicy.VectorIndex, XdbPathV4);
|
||||||
|
private static readonly Searcher _fileV4Searcher = new(CachePolicy.File, XdbPathV4);
|
||||||
|
private static readonly Searcher _contentV6Searcher = new(CachePolicy.Content, XdbPathV6);
|
||||||
|
private static readonly Searcher _vectorV6Searcher = new(CachePolicy.VectorIndex, XdbPathV6);
|
||||||
|
private static readonly Searcher _fileV6Searcher = new(CachePolicy.File, XdbPathV6);
|
||||||
|
|
||||||
|
private readonly string _testIPv4Address = "114.114.114.114";
|
||||||
|
private readonly string _testIPv6Address = "240e:3b7:3272:d8d0:db09:c067:8d59:539e";
|
||||||
|
|
||||||
|
public Benchmarks()
|
||||||
|
{
|
||||||
|
_contentV4Searcher.Search(_testIPv4Address);
|
||||||
|
_vectorV4Searcher.Search(_testIPv4Address);
|
||||||
|
_fileV4Searcher.Search(_testIPv4Address);
|
||||||
|
|
||||||
|
_contentV6Searcher.Search(_testIPv6Address);
|
||||||
|
_vectorV6Searcher.Search(_testIPv6Address);
|
||||||
|
_fileV6Searcher.Search(_testIPv6Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
[BenchmarkCategory("IPv4")]
|
||||||
|
public void ContentIPv4() => _contentV4Searcher.Search(_testIPv4Address);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
[BenchmarkCategory("IPv4")]
|
||||||
|
public void VectorIPv4() => _vectorV4Searcher.Search(_testIPv4Address);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
[BenchmarkCategory("IPv4")]
|
||||||
|
public void FileIPv4() => _fileV4Searcher.Search(_testIPv4Address);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
[BenchmarkCategory("IPv6")]
|
||||||
|
public void ContentIPv6() => _contentV6Searcher.Search(_testIPv6Address);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
[BenchmarkCategory("IPv6")]
|
||||||
|
public void VectorIPv6() => _vectorV6Searcher.Search(_testIPv6Address);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
[BenchmarkCategory("IPv6")]
|
||||||
|
public void FileIPv6() => _fileV6Searcher.Search(_testIPv6Address);
|
||||||
|
}
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.6" />
|
||||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.2" />
|
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.15.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -17,8 +17,12 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="..\..\..\data\ip2region.xdb">
|
<Content Include="..\..\..\data\ip2region_v4.xdb">
|
||||||
<Link>IP2Region/ip2region.xdb</Link>
|
<Link>IP2Region/ip2region_v4.xdb</Link>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="..\..\..\data\ip2region_v6.xdb">
|
||||||
|
<Link>IP2Region/ip2region_v6.xdb</Link>
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,11 @@
|
||||||
using BenchmarkDotNet.Attributes;
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
using BenchmarkDotNet.Running;
|
using BenchmarkDotNet.Running;
|
||||||
using IP2Region.Net.Abstractions;
|
using IP2Region.Net.BenchMark;
|
||||||
using IP2Region.Net.XDB;
|
|
||||||
|
|
||||||
BenchmarkRunner.Run(typeof(Program).Assembly);
|
BenchmarkRunner.Run<Benchmarks>();
|
||||||
|
|
||||||
public class CachePolicyCompare
|
|
||||||
{
|
|
||||||
private static readonly string XdbPath = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region.xdb");
|
|
||||||
private readonly ISearcher _contentSearcher = new Searcher(CachePolicy.Content, XdbPath);
|
|
||||||
private readonly ISearcher _vectorSearcher = new Searcher(CachePolicy.VectorIndex,XdbPath);
|
|
||||||
private readonly ISearcher _fileSearcher = new Searcher(CachePolicy.File,XdbPath);
|
|
||||||
|
|
||||||
private readonly string _testIpAddress = "114.114.114.114";
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
[BenchmarkCategory(nameof(CachePolicy.Content))]
|
|
||||||
public void CachePolicy_Content() => _contentSearcher.Search(_testIpAddress);
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
[BenchmarkCategory(nameof(CachePolicy.VectorIndex))]
|
|
||||||
public void CachePolicy_VectorIndex() => _vectorSearcher.Search(_testIpAddress);
|
|
||||||
|
|
||||||
|
|
||||||
[Benchmark]
|
|
||||||
[BenchmarkCategory(nameof(CachePolicy.File))]
|
|
||||||
public void CachePolicy_File() => _fileSearcher.Search(_testIpAddress);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
|
@ -9,11 +9,20 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="xunit" Version="2.*" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.*">
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -21,12 +30,20 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="..\..\..\data\ip.merge.txt">
|
<Content Include="..\..\..\data\ipv4_source.txt">
|
||||||
<Link>TestData/ip.merge.txt</Link>
|
<Link>TestData/ipv4_source.txt</Link>
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Include="..\..\..\data\ip2region.xdb">
|
<Content Include="..\..\..\data\ip2region_v4.xdb">
|
||||||
<Link>TestData/ip2region.xdb</Link>
|
<Link>TestData/ip2region_v4.xdb</Link>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="..\..\..\data\ipv6_source.txt">
|
||||||
|
<Link>TestData/ipv6_source.txt</Link>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="..\..\..\data\ip2region_v6.xdb">
|
||||||
|
<Link>TestData/ip2region_v6.xdb</Link>
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,163 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
using IP2Region.Net.Abstractions;
|
||||||
using IP2Region.Net.XDB;
|
using IP2Region.Net.XDB;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System.Net;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace IP2Region.Net.Test;
|
namespace IP2Region.Net.Test;
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class SearcherTest
|
public class SearcherTest
|
||||||
{
|
{
|
||||||
private readonly string _xdbPath = Path.Combine(AppContext.BaseDirectory, "TestData", "ip2region.xdb");
|
private readonly string _xdbPathV4 = Path.Combine(AppContext.BaseDirectory, "TestData", "ip2region_v4.xdb");
|
||||||
|
private readonly string _xdbPathV6 = Path.Combine(AppContext.BaseDirectory, "TestData", "ip2region_v6.xdb");
|
||||||
|
|
||||||
public static IEnumerable<string> Ips()
|
[Theory]
|
||||||
{
|
[InlineData("58.251.27.201", "中国|广东省|深圳市|联通|CN", "v4")]
|
||||||
yield return "114.114.114.114";
|
[InlineData("114.114.114.114", "中国|江苏省|南京市|0|CN", "v4")]
|
||||||
yield return "119.29.29.29";
|
[InlineData("119.29.29.29", "中国|北京|北京市|腾讯|CN", "v4")]
|
||||||
yield return "223.5.5.5";
|
[InlineData("223.5.5.5", "中国|浙江省|杭州市|阿里|CN", "v4")]
|
||||||
yield return "180.76.76.76";
|
[InlineData("180.76.76.76", "中国|北京|北京市|百度|CN", "v4")]
|
||||||
yield return "8.8.8.8";
|
[InlineData("8.8.8.8", "United States|California|0|Google LLC|US", "v4")]
|
||||||
}
|
[InlineData("240e:3b7:3272:d8d0:db09:c067:8d59:539e", "中国|广东省|深圳市|电信|CN", "v6")]
|
||||||
|
public void TestSearchCacheContent(string ip, string expected, string version)
|
||||||
[TestCaseSource(nameof(Ips))]
|
|
||||||
[Parallelizable(ParallelScope.All)]
|
|
||||||
public void TestSearchCacheContent(string ip)
|
|
||||||
{
|
{
|
||||||
|
var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6;
|
||||||
var contentSearcher = new Searcher(CachePolicy.Content, _xdbPath);
|
var contentSearcher = new Searcher(CachePolicy.Content, _xdbPath);
|
||||||
var region = contentSearcher.Search(ip);
|
var region = contentSearcher.Search(ip);
|
||||||
Console.WriteLine(region);
|
Assert.Equal(expected, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCaseSource(nameof(Ips))]
|
[Theory]
|
||||||
[Parallelizable(ParallelScope.All)]
|
[InlineData("58.251.27.201", "中国|广东省|深圳市|联通|CN", "v4")]
|
||||||
public void TestSearchCacheVector(string ip)
|
[InlineData("114.114.114.114", "中国|江苏省|南京市|0|CN", "v4")]
|
||||||
|
[InlineData("119.29.29.29", "中国|北京|北京市|腾讯|CN", "v4")]
|
||||||
|
[InlineData("223.5.5.5", "中国|浙江省|杭州市|阿里|CN", "v4")]
|
||||||
|
[InlineData("180.76.76.76", "中国|北京|北京市|百度|CN", "v4")]
|
||||||
|
[InlineData("8.8.8.8", "United States|California|0|Google LLC|US", "v4")]
|
||||||
|
[InlineData("240e:3b7:3272:d8d0:db09:c067:8d59:539e", "中国|广东省|深圳市|电信|CN", "v6")]
|
||||||
|
public void TestSearchCacheVector(string ip, string expected, string version)
|
||||||
{
|
{
|
||||||
|
var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6;
|
||||||
var vectorSearcher = new Searcher(CachePolicy.VectorIndex, _xdbPath);
|
var vectorSearcher = new Searcher(CachePolicy.VectorIndex, _xdbPath);
|
||||||
var region = vectorSearcher.Search(ip);
|
var region = vectorSearcher.Search(ip);
|
||||||
Console.WriteLine(region);
|
Assert.Equal(expected, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCaseSource(nameof(Ips))]
|
[Theory]
|
||||||
[Parallelizable(ParallelScope.All)]
|
[InlineData("58.251.0.0", "中国|广东省|深圳市|联通|CN", "v4")]
|
||||||
public void TestSearchCacheFile(string ip)
|
[InlineData("58.251.255.255", "中国|广东省|深圳市|联通|CN", "v4")]
|
||||||
|
[InlineData("58.251.27.201", "中国|广东省|深圳市|联通|CN", "v4")]
|
||||||
|
[InlineData("114.114.114.114", "中国|江苏省|南京市|0|CN", "v4")]
|
||||||
|
[InlineData("119.29.29.29", "中国|北京|北京市|腾讯|CN", "v4")]
|
||||||
|
[InlineData("223.5.5.5", "中国|浙江省|杭州市|阿里|CN", "v4")]
|
||||||
|
[InlineData("180.76.76.76", "中国|北京|北京市|百度|CN", "v4")]
|
||||||
|
[InlineData("8.8.8.8", "United States|California|0|Google LLC|US", "v4")]
|
||||||
|
[InlineData("240e:3b7:3272:d8d0:db09:c067:8d59:539e", "中国|广东省|深圳市|电信|CN", "v6")]
|
||||||
|
[InlineData("240e:044d:2d00:0000:0000:0000:0000:0000", "中国|云南|楚雄|电信|CN", "v6")]
|
||||||
|
public void TestSearchCacheFile(string ip, string expected, string version)
|
||||||
{
|
{
|
||||||
|
var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6;
|
||||||
var fileSearcher = new Searcher(CachePolicy.File, _xdbPath);
|
var fileSearcher = new Searcher(CachePolicy.File, _xdbPath);
|
||||||
var region = fileSearcher.Search(ip);
|
var region = fileSearcher.Search(ip);
|
||||||
Console.WriteLine(region);
|
Assert.Equal(expected, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(CachePolicy.Content)]
|
[Fact]
|
||||||
[TestCase(CachePolicy.VectorIndex)]
|
public void IoCount_File_Ok()
|
||||||
[TestCase(CachePolicy.File)]
|
|
||||||
public void TestBenchSearch(CachePolicy cachePolicy)
|
|
||||||
{
|
{
|
||||||
Searcher searcher = new Searcher(cachePolicy,_xdbPath);
|
var searcher = new Searcher(CachePolicy.File, _xdbPathV4);
|
||||||
var srcPath = Path.Combine(AppContext.BaseDirectory, "TestData", "ip.merge.txt");
|
searcher.Search("58.251.27.201");
|
||||||
|
Assert.Equal(3, searcher.IoCount);
|
||||||
|
|
||||||
|
searcher.Search("58.251.27.201");
|
||||||
|
Assert.Equal(3, searcher.IoCount);
|
||||||
|
|
||||||
|
searcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IoCount_Vector_Ok()
|
||||||
|
{
|
||||||
|
var searcher = new Searcher(CachePolicy.VectorIndex, _xdbPathV4);
|
||||||
|
searcher.Search("58.251.27.201");
|
||||||
|
Assert.Equal(2, searcher.IoCount);
|
||||||
|
|
||||||
|
searcher.Search("58.251.27.201");
|
||||||
|
Assert.Equal(2, searcher.IoCount);
|
||||||
|
|
||||||
|
searcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IoCount_Content_Ok()
|
||||||
|
{
|
||||||
|
var searcher = new Searcher(CachePolicy.Content, _xdbPathV4);
|
||||||
|
searcher.Search("58.251.27.201");
|
||||||
|
Assert.Equal(0, searcher.IoCount);
|
||||||
|
|
||||||
|
searcher.Search("58.251.27.201");
|
||||||
|
Assert.Equal(0, searcher.IoCount);
|
||||||
|
|
||||||
|
searcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("58.251.255.255", "中国|广东省|深圳市|联通|CN")]
|
||||||
|
public void Search_Ip_Ok(string ipStr, string expected)
|
||||||
|
{
|
||||||
|
var fileSearcher = new Searcher(CachePolicy.File, _xdbPathV4);
|
||||||
|
var ipAddress = IPAddress.Parse(ipStr);
|
||||||
|
var region = fileSearcher.Search(ipAddress);
|
||||||
|
Assert.Equal(expected, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("58.251.255.255", "中国|广东省|深圳市|联通|CN")]
|
||||||
|
public void AddIP2RegionService_Ok(string ipStr, string expected)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddIP2RegionService(_xdbPathV4, CachePolicy.File);
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
var searcher = provider.GetRequiredService<ISearcher>();
|
||||||
|
var region = searcher.Search(ipStr);
|
||||||
|
Assert.Equal(expected, region);
|
||||||
|
|
||||||
|
searcher = provider.GetRequiredKeyedService<ISearcher>("IP2Region.Net");
|
||||||
|
region = searcher.Search(ipStr);
|
||||||
|
Assert.Equal(expected, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(CachePolicy.Content, "v4")]
|
||||||
|
[InlineData(CachePolicy.VectorIndex, "v4")]
|
||||||
|
[InlineData(CachePolicy.File, "v4")]
|
||||||
|
[InlineData(CachePolicy.Content, "v6")]
|
||||||
|
[InlineData(CachePolicy.VectorIndex, "v6")]
|
||||||
|
[InlineData(CachePolicy.File, "v6")]
|
||||||
|
public void TestBenchSearch(CachePolicy cachePolicy, string version)
|
||||||
|
{
|
||||||
|
var _xdbPath = version == "v4" ? _xdbPathV4 : _xdbPathV6;
|
||||||
|
var searcher = new Searcher(cachePolicy, _xdbPath);
|
||||||
|
var srcPath = Path.Combine(AppContext.BaseDirectory, "TestData", $"ip{version}_source.txt");
|
||||||
|
|
||||||
foreach (var line in File.ReadLines(srcPath))
|
foreach (var line in File.ReadLines(srcPath))
|
||||||
{
|
{
|
||||||
var ps = line.Trim().Split("|", 3);
|
var ps = line.Trim().Split("|", 3);
|
||||||
|
var sip = ps[0];
|
||||||
|
var eip = ps[1];
|
||||||
|
|
||||||
if (ps.Length != 3)
|
var s1 = searcher.Search(sip);
|
||||||
{
|
var s2 = searcher.Search(eip);
|
||||||
throw new ArgumentException($"invalid ip segment line {line}", nameof(line));
|
Assert.Equal(s1, ps[2]);
|
||||||
}
|
Assert.Equal(s2, ps[2]);
|
||||||
|
|
||||||
var sip = Util.IpAddressToUInt32(ps[0]);
|
|
||||||
var eip = Util.IpAddressToUInt32(ps[1]);
|
|
||||||
var mip = Util.GetMidIp(sip, eip);
|
|
||||||
|
|
||||||
uint[] temp = { sip, Util.GetMidIp(sip, mip), mip, Util.GetMidIp(mip, eip), eip };
|
|
||||||
|
|
||||||
foreach (var ip in temp)
|
|
||||||
{
|
|
||||||
var region = searcher.Search(ip);
|
|
||||||
|
|
||||||
if (region != ps[2])
|
|
||||||
{
|
|
||||||
throw new Exception($"failed search {ip} with ({region}!={ps[2]})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
global using NUnit.Framework;
|
|
||||||
|
|
@ -1,14 +1,27 @@
|
||||||
using IP2Region.Net.XDB;
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace IP2Region.Net.Test;
|
namespace IP2Region.Net.Test;
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class UtilTest
|
public class UtilTest
|
||||||
{
|
{
|
||||||
[TestCase("114.114.114.114")]
|
[Fact]
|
||||||
public void TestIpAddressToUInt32(string value)
|
public void IpAddressToUInt32_Ok()
|
||||||
{
|
{
|
||||||
var uintIp = XDB.Util.IpAddressToUInt32(value);
|
var uintIp = XDB.Util.IpAddressToUInt32("114.114.114.114");
|
||||||
Console.WriteLine(uintIp);
|
Assert.Equal((uint)1920103026, uintIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMidIp_Ok()
|
||||||
|
{
|
||||||
|
var uintIp = XDB.Util.GetMidIp(1, 10);
|
||||||
|
Assert.Equal((uint)5, uintIp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace IP2Region.Net.Test;
|
||||||
|
|
||||||
|
public class XdbTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task VersionIPV4_Ok()
|
||||||
|
{
|
||||||
|
var db = Path.Combine(AppContext.BaseDirectory, "TestData", $"ip2region_v4.xdb");
|
||||||
|
|
||||||
|
var version = await XDB.Util.GetVersionAsync(db);
|
||||||
|
Assert.Equal(3, version.Ver);
|
||||||
|
Assert.Equal(1, version.CachePolice);
|
||||||
|
//Assert.Equal("2025-09-06 02:24:16", version.CreatedTime.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
//Assert.Equal((uint)955933, version.StartIndex);
|
||||||
|
//Assert.Equal((uint)11042415, version.EndIndex);
|
||||||
|
Assert.Equal(4, version.IPVer);
|
||||||
|
Assert.Equal(4, version.BytesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VersionIPV6_Ok()
|
||||||
|
{
|
||||||
|
var db = Path.Combine(AppContext.BaseDirectory, "TestData", $"ip2region_v6.xdb");
|
||||||
|
var version = await XDB.Util.GetVersionAsync(db);
|
||||||
|
Assert.Equal(3, version.Ver);
|
||||||
|
Assert.Equal(1, version.CachePolice);
|
||||||
|
//Assert.Equal("2025-10-17 04:41:04", version.CreatedTime.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
//Assert.Equal((uint)3094259, version.StartIndex);
|
||||||
|
//Assert.Equal((uint)36258303, version.EndIndex);
|
||||||
|
Assert.Equal(6, version.IPVer);
|
||||||
|
Assert.Equal(4, version.BytesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetVersionAsync_Error()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(async () => await XDB.Util.GetVersionAsync(null!));
|
||||||
|
await Assert.ThrowsAsync<FileNotFoundException>(async () => await XDB.Util.GetVersionAsync(Path.Combine(AppContext.BaseDirectory, "test.xdb")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<Solution>
|
||||||
|
<Folder Name="/Solution Items/">
|
||||||
|
<File Path=".editorconfig" />
|
||||||
|
<File Path="README.md" />
|
||||||
|
</Folder>
|
||||||
|
<Project Path="IP2Region.Net.BenchMark/IP2Region.Net.BenchMark.csproj" />
|
||||||
|
<Project Path="IP2Region.Net.Test/IP2Region.Net.Test.csproj" />
|
||||||
|
<Project Path="IP2Region.Net/IP2Region.Net.csproj" />
|
||||||
|
</Solution>
|
||||||
|
|
@ -1,20 +1,40 @@
|
||||||
// Copyright 2023 The Ip2Region Authors. All rights reserved.
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
// @Author Alan <lzh.shap@gmail.com>
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
// @Date 2023/07/25
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace IP2Region.Net.Abstractions;
|
namespace IP2Region.Net.Abstractions;
|
||||||
|
|
||||||
public interface ISearcher
|
/// <summary>
|
||||||
|
/// IP 转化为地理位置搜索器接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ISearcher : IDisposable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索方法
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipStr">IP 地址字符串 如 192.168.0.1</param>
|
||||||
|
/// <returns></returns>
|
||||||
string? Search(string ipStr);
|
string? Search(string ipStr);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索方法
|
||||||
|
/// </summary>
|
||||||
string? Search(IPAddress ipAddress);
|
string? Search(IPAddress ipAddress);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索方法 仅限 IPv4 使用
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">IPv4 地址字节数组小端读取 uint 数值</param>
|
||||||
|
[Obsolete("已弃用,请改用其他方法;Deprecated; please use Search(string) or Search(IPAddress) method.")]
|
||||||
string? Search(uint ipAddress);
|
string? Search(uint ipAddress);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得 内部 IO 访问次数
|
||||||
|
/// </summary>
|
||||||
int IoCount { get; }
|
int IoCount { get; }
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
using IP2Region.Net.Abstractions;
|
||||||
|
using IP2Region.Net.XDB;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IP2Region 服务扩展类
|
||||||
|
/// </summary>
|
||||||
|
public static class IP2RegionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 IP2RegionService 服务。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services"><see cref="IServiceCollection"/> 集合</param>
|
||||||
|
/// <param name="path">IP2Region 数据库文件的路径。</param>
|
||||||
|
/// <param name="cachePolicy">缓存策略,默认为 <see cref="CachePolicy.Content"/>。</param>
|
||||||
|
public static IServiceCollection AddIP2RegionService(this IServiceCollection services, string path, CachePolicy cachePolicy = CachePolicy.Content)
|
||||||
|
{
|
||||||
|
services.TryAddSingleton<ISearcher>(provider =>
|
||||||
|
{
|
||||||
|
return new Searcher(cachePolicy, path);
|
||||||
|
});
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
services.TryAddKeyedSingleton("IP2Region.Net", (provider, _) =>
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<ISearcher>();
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<id>IP2Region.Net</id>
|
<id>IP2Region.Net</id>
|
||||||
<version>2.0.2</version>
|
<version>3.0.2</version>
|
||||||
<title>IP2Region.Net</title>
|
<title>IP2Region.Net</title>
|
||||||
<authors>Alan Lee</authors>
|
<authors>Alan Lee;Argo Zhang(argo@live.ca)</authors>
|
||||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageProjectUrl>https://github.com/lionsoul2014/ip2region/tree/master/binding/csharp</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/lionsoul2014/ip2region/tree/master/binding/csharp</PackageProjectUrl>
|
||||||
|
|
@ -15,17 +15,48 @@
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net7.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<UserSecretsId>c2f07fe1-a300-4de3-8200-1278ed8cb5b7</UserSecretsId>
|
<UserSecretsId>c2f07fe1-a300-4de3-8200-1278ed8cb5b7</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||||
<PackageReference Include="System.Buffers" Version="4.5.1" />
|
<PackageReference Include="System.Buffers" Version="4.5.1" />
|
||||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\CHANGELOG.md">
|
<None Include="..\CHANGELOG.md">
|
||||||
<Link>CHANGELOG.md</Link>
|
<Link>CHANGELOG.md</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
// Copyright 2023 The Ip2Region Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
// @Author Alan <lzh.shap@gmail.com>
|
|
||||||
// @Date 2023/07/25
|
|
||||||
|
|
||||||
using System.Buffers;
|
|
||||||
|
|
||||||
namespace IP2Region.Net.Internal.Abstractions;
|
|
||||||
|
|
||||||
internal abstract class AbstractCacheStrategy
|
|
||||||
{
|
|
||||||
protected const int HeaderInfoLength = 256;
|
|
||||||
protected const int VectorIndexRows = 256;
|
|
||||||
protected const int VectorIndexCols = 256;
|
|
||||||
protected const int VectorIndexSize = 8;
|
|
||||||
|
|
||||||
protected readonly FileStream XdbFileStream;
|
|
||||||
private const int BufferSize = 4096;
|
|
||||||
|
|
||||||
internal int IoCount { get; private set; }
|
|
||||||
|
|
||||||
protected AbstractCacheStrategy(string xdbPath)
|
|
||||||
{
|
|
||||||
XdbFileStream = new FileStream(xdbPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize,
|
|
||||||
useAsync: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int GetVectorIndexStartPos(uint ip)
|
|
||||||
{
|
|
||||||
var il0 = ip >> 24 & 0xFF;
|
|
||||||
var il1 = ip >> 16 & 0xFF;
|
|
||||||
var idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize;
|
|
||||||
return (int)idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal abstract ReadOnlyMemory<byte> GetVectorIndex(uint ip);
|
|
||||||
|
|
||||||
internal virtual ReadOnlyMemory<byte> GetData(int offset, int length)
|
|
||||||
{
|
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(length);
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
XdbFileStream.Seek(offset, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
int bytesRead;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int bytesToRead = Math.Min(BufferSize, length - totalBytesRead);
|
|
||||||
bytesRead = XdbFileStream.Read(buffer, totalBytesRead, bytesToRead);
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
|
|
||||||
IoCount++;
|
|
||||||
} while (bytesRead > 0 && totalBytesRead < length);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReadOnlyMemory<byte>(buffer, 0, totalBytesRead);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +1,20 @@
|
||||||
// Copyright 2023 The Ip2Region Authors. All rights reserved.
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
// @Author Alan <lzh.shap@gmail.com>
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
// @Date 2023/07/25
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
using IP2Region.Net.Internal.Abstractions;
|
|
||||||
using IP2Region.Net.XDB;
|
using IP2Region.Net.XDB;
|
||||||
|
|
||||||
namespace IP2Region.Net.Internal;
|
namespace IP2Region.Net.Internal;
|
||||||
|
|
||||||
internal class CacheStrategyFactory
|
static class CacheStrategyFactory
|
||||||
{
|
{
|
||||||
private readonly string _xdbPath;
|
public static ICacheStrategy CreateCacheStrategy(CachePolicy cachePolicy, string xdbPath) => cachePolicy switch
|
||||||
|
|
||||||
public CacheStrategyFactory(string xdbPath)
|
|
||||||
{
|
{
|
||||||
_xdbPath = xdbPath;
|
CachePolicy.Content => new ContentCacheStrategy(xdbPath),
|
||||||
}
|
CachePolicy.VectorIndex => new VectorIndexCacheStrategy(xdbPath),
|
||||||
|
_ => new FileCacheStrategy(xdbPath),
|
||||||
public AbstractCacheStrategy CreateCacheStrategy(CachePolicy cachePolicy)
|
|
||||||
{
|
|
||||||
return cachePolicy switch
|
|
||||||
{
|
|
||||||
CachePolicy.Content => new ContentCacheStrategy(_xdbPath),
|
|
||||||
CachePolicy.VectorIndex => new VectorIndexCacheStrategy(_xdbPath),
|
|
||||||
CachePolicy.File => new FileCacheStrategy(_xdbPath),
|
|
||||||
_ => throw new ArgumentException(nameof(cachePolicy))
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
@ -1,32 +1,34 @@
|
||||||
// Copyright 2023 The Ip2Region Authors. All rights reserved.
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
// @Author Alan <lzh.shap@gmail.com>
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
// @Date 2023/07/25
|
// @Date 2023/07/25
|
||||||
|
// Updated by Wong <vcd.hai@outlook.com> at 2025/12/31
|
||||||
using IP2Region.Net.Internal.Abstractions;
|
|
||||||
|
|
||||||
namespace IP2Region.Net.Internal;
|
namespace IP2Region.Net.Internal;
|
||||||
|
|
||||||
internal class ContentCacheStrategy : AbstractCacheStrategy
|
class ContentCacheStrategy(string xdbPath) : ICacheStrategy
|
||||||
{
|
{
|
||||||
private readonly ReadOnlyMemory<byte> _cacheData;
|
// TODO: these constants can be moved to the interface as defaults when using .NET 10
|
||||||
|
private const int HeaderInfoLength = 256;
|
||||||
|
private const int VectorIndexSize = 8;
|
||||||
|
|
||||||
public ContentCacheStrategy(string xdbPath) : base(xdbPath)
|
private readonly ReadOnlyMemory<byte> _cacheData = File.ReadAllBytes(xdbPath);
|
||||||
|
|
||||||
|
public int IoCount => 0;
|
||||||
|
|
||||||
|
public void ResetIoCount()
|
||||||
{
|
{
|
||||||
_cacheData = base.GetData(0, (int)XdbFileStream.Length);
|
// Do nothing
|
||||||
XdbFileStream.Close();
|
|
||||||
XdbFileStream.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override ReadOnlyMemory<byte> GetVectorIndex(uint ip)
|
public ReadOnlyMemory<byte> GetVectorIndex(int offset)
|
||||||
{
|
=> _cacheData.Slice(HeaderInfoLength + offset, VectorIndexSize);
|
||||||
int idx = GetVectorIndexStartPos(ip);
|
|
||||||
return _cacheData.Slice(HeaderInfoLength + idx, VectorIndexSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override ReadOnlyMemory<byte> GetData(int offset, int length)
|
public ReadOnlyMemory<byte> GetData(long offset, int length) => _cacheData.Slice((int)offset, length);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
{
|
{
|
||||||
return _cacheData.Slice(offset, length);
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,22 +1,85 @@
|
||||||
// Copyright 2023 The Ip2Region Authors. All rights reserved.
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
// @Author Alan <lzh.shap@gmail.com>
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
// @Date 2023/07/25
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
using IP2Region.Net.Internal.Abstractions;
|
using System.Buffers;
|
||||||
|
|
||||||
namespace IP2Region.Net.Internal;
|
namespace IP2Region.Net.Internal;
|
||||||
|
|
||||||
internal class FileCacheStrategy : AbstractCacheStrategy
|
class FileCacheStrategy(string xdbPath) : ICacheStrategy
|
||||||
{
|
{
|
||||||
public FileCacheStrategy(string xdbPath) : base(xdbPath)
|
protected const int HeaderInfoLength = 256;
|
||||||
|
protected const int VectorIndexSize = 8;
|
||||||
|
|
||||||
|
protected const int BufferSize = 64 * 1024;
|
||||||
|
|
||||||
|
protected FileStream XdbFileStream = new(xdbPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.RandomAccess);
|
||||||
|
|
||||||
|
public int IoCount { get; set; }
|
||||||
|
|
||||||
|
public void ResetIoCount()
|
||||||
{
|
{
|
||||||
|
IoCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override ReadOnlyMemory<byte> GetVectorIndex(uint ip)
|
public virtual ReadOnlyMemory<byte> GetVectorIndex(int offset) => GetData(HeaderInfoLength + offset, VectorIndexSize);
|
||||||
|
|
||||||
|
public virtual ReadOnlyMemory<byte> GetData(long offset, int length)
|
||||||
{
|
{
|
||||||
var idx = GetVectorIndexStartPos(ip);
|
var buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||||
return GetData(HeaderInfoLength + idx, VectorIndexSize);
|
try
|
||||||
|
{
|
||||||
|
int totalBytesRead = 0;
|
||||||
|
XdbFileStream.Seek(offset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
int bytesRead;
|
||||||
|
while (totalBytesRead < length)
|
||||||
|
{
|
||||||
|
bytesRead = XdbFileStream.Read(buffer, totalBytesRead, length - totalBytesRead);
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBytesRead += bytesRead;
|
||||||
|
IoCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = new byte[totalBytesRead];
|
||||||
|
if (totalBytesRead > 0)
|
||||||
|
{
|
||||||
|
Array.Copy(buffer, 0, ret, 0, totalBytesRead);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放文件句柄
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing"></param>
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
XdbFileStream.Close();
|
||||||
|
XdbFileStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
namespace IP2Region.Net.Internal;
|
||||||
|
|
||||||
|
internal interface ICacheStrategy : IDisposable
|
||||||
|
{
|
||||||
|
int IoCount { get; }
|
||||||
|
|
||||||
|
void ResetIoCount();
|
||||||
|
|
||||||
|
ReadOnlyMemory<byte> GetVectorIndex(int offset);
|
||||||
|
|
||||||
|
ReadOnlyMemory<byte> GetData(long offset, int length);
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,23 @@
|
||||||
// Copyright 2023 The Ip2Region Authors. All rights reserved.
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
// @Author Alan <lzh.shap@gmail.com>
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
// @Date 2023/07/25
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
using IP2Region.Net.Internal.Abstractions;
|
|
||||||
|
|
||||||
namespace IP2Region.Net.Internal;
|
namespace IP2Region.Net.Internal;
|
||||||
|
|
||||||
internal class VectorIndexCacheStrategy : AbstractCacheStrategy
|
class VectorIndexCacheStrategy : FileCacheStrategy
|
||||||
{
|
{
|
||||||
private readonly ReadOnlyMemory<byte> _vectorIndex;
|
private const int VectorIndexRows = 256;
|
||||||
|
private const int VectorIndexCols = 256;
|
||||||
|
|
||||||
|
private readonly ReadOnlyMemory<byte> _vectorCache;
|
||||||
|
|
||||||
public VectorIndexCacheStrategy(string xdbPath) : base(xdbPath)
|
public VectorIndexCacheStrategy(string xdbPath) : base(xdbPath)
|
||||||
{
|
{
|
||||||
var vectorLength = VectorIndexRows * VectorIndexCols * VectorIndexSize;
|
_vectorCache = GetData(HeaderInfoLength, VectorIndexRows * VectorIndexCols * VectorIndexSize);
|
||||||
_vectorIndex = base.GetData(HeaderInfoLength, vectorLength);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override ReadOnlyMemory<byte> GetVectorIndex(uint ip)
|
public override ReadOnlyMemory<byte> GetVectorIndex(int offset) => _vectorCache.Slice(offset, VectorIndexSize);
|
||||||
{
|
|
||||||
var idx = GetVectorIndexStartPos(ip);
|
|
||||||
return _vectorIndex.Slice(idx, VectorIndexSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,27 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
namespace IP2Region.Net.XDB;
|
namespace IP2Region.Net.XDB;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 缓存策略枚举
|
||||||
|
/// </summary>
|
||||||
public enum CachePolicy
|
public enum CachePolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// no cache
|
/// no cache
|
||||||
/// </summary>
|
/// </summary>
|
||||||
File,
|
File,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// cache vector index , reduce the number of IO operations
|
/// cache vector index , reduce the number of IO operations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
VectorIndex,
|
VectorIndex,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// default cache policy , cache whole xdb file
|
/// default cache policy , cache whole xdb file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,167 @@
|
||||||
// Copyright 2023 The Ip2Region Authors. All rights reserved.
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a Apache2.0-style
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
// @Author Alan Lee <lzh.shap@gmail.com>
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
// @Date 2023/07/23
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
using System.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using IP2Region.Net.Abstractions;
|
using IP2Region.Net.Abstractions;
|
||||||
using IP2Region.Net.Internal;
|
using IP2Region.Net.Internal;
|
||||||
using IP2Region.Net.Internal.Abstractions;
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace IP2Region.Net.XDB;
|
namespace IP2Region.Net.XDB;
|
||||||
|
|
||||||
public class Searcher : ISearcher
|
/// <summary>
|
||||||
|
/// <see cref="ISearcher"/> 实现类
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// </remarks>
|
||||||
|
public class Searcher(CachePolicy cachePolicy, string xdbPath) : ISearcher
|
||||||
{
|
{
|
||||||
const int SegmentIndexSize = 14;
|
private readonly ICacheStrategy _cacheStrategy = CacheStrategyFactory.CreateCacheStrategy(cachePolicy, xdbPath);
|
||||||
|
|
||||||
private readonly AbstractCacheStrategy _cacheStrategy;
|
/// <summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// </summary>
|
||||||
public int IoCount => _cacheStrategy.IoCount;
|
public int IoCount => _cacheStrategy.IoCount;
|
||||||
|
|
||||||
public Searcher(CachePolicy cachePolicy, string dbPath)
|
/// <summary>
|
||||||
{
|
/// <inheritdoc/>
|
||||||
var factory = new CacheStrategyFactory(dbPath);
|
/// </summary>
|
||||||
_cacheStrategy = factory.CreateCacheStrategy(cachePolicy);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? Search(string ipStr)
|
public string? Search(string ipStr)
|
||||||
{
|
{
|
||||||
var ip = Util.IpAddressToUInt32(ipStr);
|
var ipAddress = IPAddress.Parse(ipStr);
|
||||||
return Search(ip);
|
return SearchCore(ipAddress.GetAddressBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? Search(IPAddress ipAddress)
|
/// <summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// </summary>
|
||||||
|
public string? Search(IPAddress ipAddress) => SearchCore(ipAddress.GetAddressBytes());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("已弃用,请改用其他方法;Deprecated; please use Search(string) or Search(IPAddress) method.")]
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public string? Search(uint ipAddress)
|
||||||
{
|
{
|
||||||
var ip = Util.IpAddressToUInt32(ipAddress);
|
var bytes = BitConverter.GetBytes(ipAddress);
|
||||||
return Search(ip);
|
Array.Reverse(bytes);
|
||||||
|
return SearchCore(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? Search(uint ip)
|
string? SearchCore(byte[] ipBytes)
|
||||||
{
|
{
|
||||||
var index = _cacheStrategy.GetVectorIndex(ip);
|
// 重置 IO 计数器
|
||||||
uint sPtr = MemoryMarshal.Read<uint>(index.Span);
|
_cacheStrategy.ResetIoCount();
|
||||||
uint ePtr = MemoryMarshal.Read<uint>(index.Span.Slice(4));
|
|
||||||
|
|
||||||
|
// 每个 vector 索引项的字节数
|
||||||
|
var vectorIndexSize = 8;
|
||||||
|
|
||||||
|
// vector 索引的列数
|
||||||
|
var vectorIndexCols = 256;
|
||||||
|
|
||||||
|
// 计算得到 vector 索引项的开始地址。
|
||||||
|
var il0 = ipBytes[0];
|
||||||
|
var il1 = ipBytes[1];
|
||||||
|
var idx = il0 * vectorIndexCols * vectorIndexSize + il1 * vectorIndexSize;
|
||||||
|
|
||||||
|
var vector = _cacheStrategy.GetVectorIndex(idx);
|
||||||
|
var sPtr = BinaryPrimitives.ReadUInt32LittleEndian(vector.Span);
|
||||||
|
var ePtr = BinaryPrimitives.ReadUInt32LittleEndian(vector.Span.Slice(4));
|
||||||
|
|
||||||
|
// @Note: ptr validate, zero ptr means source data missing
|
||||||
|
// so we could just stop here and return an empty string.
|
||||||
|
if (sPtr == 0 || ePtr == 0)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var length = ipBytes.Length;
|
||||||
|
var indexSize = length * 2 + 6;
|
||||||
|
var l = 0;
|
||||||
|
var h = (ePtr - sPtr) / indexSize;
|
||||||
var dataLen = 0;
|
var dataLen = 0;
|
||||||
uint dataPtr = 0;
|
long dataPtr = 0;
|
||||||
uint l = 0;
|
|
||||||
uint h = (ePtr -sPtr) / SegmentIndexSize;
|
|
||||||
|
|
||||||
while (l <= h)
|
while (l <= h)
|
||||||
{
|
{
|
||||||
var mid = Util.GetMidIp(l, h);
|
int m = (int)(l + h) >> 1;
|
||||||
var pos = sPtr + mid * SegmentIndexSize;
|
|
||||||
|
|
||||||
var buffer = _cacheStrategy.GetData((int)pos, SegmentIndexSize);
|
var p = sPtr + m * indexSize;
|
||||||
uint sip = MemoryMarshal.Read<uint>(buffer.Span);
|
var buff = _cacheStrategy.GetData(p, indexSize);
|
||||||
uint eip = MemoryMarshal.Read<uint>(buffer.Span.Slice(4));
|
|
||||||
|
|
||||||
if (ip < sip)
|
var s = buff.Span.Slice(0, length);
|
||||||
|
var e = buff.Span.Slice(length, length);
|
||||||
|
if (ByteCompare(ipBytes, s) < 0)
|
||||||
{
|
{
|
||||||
h = mid - 1;
|
h = m - 1;
|
||||||
}
|
}
|
||||||
else if (ip > eip)
|
else if (ByteCompare(ipBytes, e) > 0)
|
||||||
{
|
{
|
||||||
l = mid + 1;
|
l = m + 1;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dataLen = MemoryMarshal.Read<ushort>(buffer.Span.Slice(8));
|
dataLen = BinaryPrimitives.ReadUInt16LittleEndian(buff.Span.Slice(length * 2, 2));
|
||||||
dataPtr = MemoryMarshal.Read<uint>(buffer.Span.Slice(10));
|
dataPtr = BinaryPrimitives.ReadUInt32LittleEndian(buff.Span.Slice(length * 2 + 2, 4));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataLen == 0)
|
var regionBuff = _cacheStrategy.GetData(dataPtr, dataLen);
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
var regionBuff = _cacheStrategy.GetData((int)dataPtr,dataLen);
|
|
||||||
return Encoding.UTF8.GetString(regionBuff.Span.ToArray());
|
return Encoding.UTF8.GetString(regionBuff.Span.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int ByteCompare(byte[] ip1, ReadOnlySpan<byte> ip2) => ip1.Length == 4 ? IPv4Compare(ip1, ip2) : IPv6Compare(ip1, ip2);
|
||||||
|
|
||||||
|
static int IPv4Compare(byte[] ip1, ReadOnlySpan<byte> ip2)
|
||||||
|
{
|
||||||
|
var ret = 0;
|
||||||
|
for (int i = 0; i < ip1.Length; i++)
|
||||||
|
{
|
||||||
|
var ip2Index = ip1.Length - 1 - i;
|
||||||
|
if (ip1[i] < ip2[ip2Index])
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (ip1[i] > ip2[ip2Index])
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int IPv6Compare(byte[] ip1, ReadOnlySpan<byte> ip2)
|
||||||
|
{
|
||||||
|
var ret = 0;
|
||||||
|
for (int i = 0; i < ip1.Length; i++)
|
||||||
|
{
|
||||||
|
if (ip1[i] < ip2[i])
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (ip1[i] > ip2[i])
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cacheStrategy.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,20 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace IP2Region.Net.XDB;
|
namespace IP2Region.Net.XDB;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工具类
|
||||||
|
/// </summary>
|
||||||
public static class Util
|
public static class Util
|
||||||
{
|
{
|
||||||
public static uint IpAddressToUInt32(string ipAddress)
|
public static uint IpAddressToUInt32(string ipAddress)
|
||||||
|
|
@ -20,4 +32,60 @@ public static class Util
|
||||||
|
|
||||||
public static uint GetMidIp(uint x, uint y)
|
public static uint GetMidIp(uint x, uint y)
|
||||||
=> (x & y) + ((x ^ y) >> 1);
|
=> (x & y) + ((x ^ y) >> 1);
|
||||||
|
|
||||||
|
public static async Task<XdbVersion> GetVersionAsync(string dbPath, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(dbPath))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(dbPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(dbPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("xdb file not found.", dbPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = File.OpenRead(dbPath);
|
||||||
|
return await GetVersionAsync(reader, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<XdbVersion> GetVersionAsync(FileStream reader, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
XdbVersion ret = default;
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(256);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var length = await reader.ReadAsync(buffer, 0, 256, token);
|
||||||
|
if (length == 256)
|
||||||
|
{
|
||||||
|
ret = Parse(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static XdbVersion Parse(ReadOnlySpan<byte> buffer)
|
||||||
|
{
|
||||||
|
var ret = new XdbVersion
|
||||||
|
{
|
||||||
|
Ver = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(0, 2)),
|
||||||
|
CachePolice = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2, 2)),
|
||||||
|
StartIndex = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8, 4)),
|
||||||
|
EndIndex = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(12, 4)),
|
||||||
|
IPVer = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(16, 2)),
|
||||||
|
BytesCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(18, 2))
|
||||||
|
};
|
||||||
|
|
||||||
|
var createdAt = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(4, 4));
|
||||||
|
var dtm = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.FromHours(0));
|
||||||
|
ret.CreatedTime = dtm.AddSeconds(createdAt);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2025 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// @Author Alan <lzh.shap@gmail.com>
|
||||||
|
// @Date 2023/07/25
|
||||||
|
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
|
||||||
|
|
||||||
|
namespace IP2Region.Net.XDB;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// XdbVersion 结构体
|
||||||
|
/// </summary>
|
||||||
|
public struct XdbVersion
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获得/设置 版本号
|
||||||
|
/// </summary>
|
||||||
|
public ushort Ver { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得/设置 缓存策略
|
||||||
|
/// </summary>
|
||||||
|
public ushort CachePolice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得/设置 文件生成时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset CreatedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得/设置 索引起始地址
|
||||||
|
/// </summary>
|
||||||
|
public uint StartIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得/设置 索引结束地址
|
||||||
|
/// </summary>
|
||||||
|
public uint EndIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得/设置 IP版本
|
||||||
|
/// </summary>
|
||||||
|
public ushort IPVer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得/设置 指针字节数
|
||||||
|
/// </summary>
|
||||||
|
public ushort BytesCount { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -24,34 +24,71 @@ ISearcher searcher = new Searcher(CachePolicy , "your xdb file path");
|
||||||
| CachePolicy.Content | Cache the entire `xdb` data. | Yes |
|
| CachePolicy.Content | Cache the entire `xdb` data. | Yes |
|
||||||
| CachePolicy.VectorIndex | Cache `vecotorIndex` to speed up queries and reduce system io pressure by reducing one fixed IO operation. | Yes |
|
| CachePolicy.VectorIndex | Cache `vecotorIndex` to speed up queries and reduce system io pressure by reducing one fixed IO operation. | Yes |
|
||||||
| CachePolicy.File | Completely file-based queries | Yes |
|
| CachePolicy.File | Completely file-based queries | Yes |
|
||||||
|
|
||||||
### XDB File Description
|
### XDB File Description
|
||||||
Generate using [maker](https://github.com/lionsoul2014/ip2region/tree/master/maker/csharp), or [download](https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb) pre-generated xdb files
|
Generate using [maker](https://github.com/lionsoul2014/ip2region/tree/master/maker/csharp), or [download](https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb) pre-generated xdb files
|
||||||
|
|
||||||
## ASP.NET Core Usage
|
## ASP.NET Core Usage
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
services.AddSingleton<ISearcher>(new Searcher(CachePolicy , "your xdb file path"));
|
services.AddIP2RegionService("your xdb file path", cachePolicy: CachePolicy.Content);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
NET6/7
|
||||||
|
```csharp
|
||||||
|
provider.GetRequiredService<ISearcher>()
|
||||||
|
```
|
||||||
|
|
||||||
|
NET8+ support keyed service
|
||||||
|
```csharp
|
||||||
|
provider.GetRequiredKeyedService<ISearcher>("IP2Region.Net");
|
||||||
|
```
|
||||||
|
|
||||||
|
## TargetFrameworks
|
||||||
|
netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
// * Summary *
|
||||||
|
|
||||||
``` ini
|
BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171)
|
||||||
|
13th Gen Intel Core i7-13700 2.10GHz, 1 CPU, 24 logical and 16 physical cores
|
||||||
BenchmarkDotNet=v0.13.2, OS=macOS 13.4.1 (c) (22F770820d) [Darwin 22.5.0]
|
.NET SDK 10.0.100
|
||||||
Apple M1, 1 CPU, 8 logical and 8 physical cores
|
[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3
|
||||||
.NET SDK=7.0.306
|
DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3
|
||||||
[Host] : .NET 6.0.20 (6.0.2023.32017), Arm64 RyuJIT AdvSIMD
|
|
||||||
DefaultJob : .NET 6.0.20 (6.0.2023.32017), Arm64 RyuJIT AdvSIMD
|
|
||||||
|
|
||||||
|
|
||||||
```
|
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|
||||||
| Method | Mean | Error | StdDev |
|
|------------ |-------------:|-----------:|-----------:|-------:|----------:|
|
||||||
|-------------------------|-----------:|---------:|---------:|
|
| ContentIPv4 | 53.70 ns | 0.296 ns | 0.277 ns | 0.0086 | 136 B |
|
||||||
| CachePolicy_Content | 155.7 ns | 0.46 ns | 0.39 ns |
|
| VectorIPv4 | 4,446.04 ns | 18.673 ns | 15.593 ns | 0.0076 | 232 B |
|
||||||
| CachePolicy_File | 2,186.8 ns | 34.27 ns | 32.06 ns |
|
| FileIPv4 | 6,712.40 ns | 15.718 ns | 13.934 ns | 0.0153 | 264 B |
|
||||||
| CachePolicy_VectorIndex | 1,570.3 ns | 27.53 ns | 22.99 ns |
|
| ContentIPv6 | 145.53 ns | 0.331 ns | 0.277 ns | 0.0126 | 200 B |
|
||||||
|
| VectorIPv6 | 7,058.39 ns | 125.505 ns | 117.398 ns | 0.0381 | 712 B |
|
||||||
|
| FileIPv6 | 10,657.97 ns | 53.907 ns | 50.425 ns | 0.0458 | 744 B |
|
||||||
|
|
||||||
|
// * Hints *
|
||||||
|
Outliers
|
||||||
|
Benchmarks.VectorIPv4: Default -> 2 outliers were removed (4.55 us, 4.58 us)
|
||||||
|
Benchmarks.FileIPv4: Default -> 1 outlier was removed (6.79 us)
|
||||||
|
Benchmarks.ContentIPv6: Default -> 2 outliers were removed (148.08 ns, 152.27 ns)
|
||||||
|
|
||||||
|
// * Legends *
|
||||||
|
Mean : Arithmetic mean of all measurements
|
||||||
|
Error : Half of 99.9% confidence interval
|
||||||
|
StdDev : Standard deviation of all measurements
|
||||||
|
Gen0 : GC Generation 0 collects per 1000 operations
|
||||||
|
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
|
||||||
|
1 ns : 1 Nanosecond (0.000000001 sec)
|
||||||
|
|
||||||
|
// * Diagnostic Output - MemoryDiagnoser *
|
||||||
|
|
||||||
|
|
||||||
|
// ***** BenchmarkRunner: End *****
|
||||||
|
Run time: 00:02:06 (126.09 sec), executed benchmarks: 6
|
||||||
|
|
||||||
|
Global total time: 00:02:13 (133.47 sec), executed benchmarks: 6
|
||||||
|
// * Artifacts cleanup *
|
||||||
|
Artifacts cleanup is finished
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
.PHONY: all compile test clean bench-v4 bench-v6
|
||||||
|
|
||||||
|
all: compile
|
||||||
|
|
||||||
|
compile:
|
||||||
|
rebar3 compile
|
||||||
|
|
||||||
|
test:
|
||||||
|
rebar3 eunit
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rebar3 clean
|
||||||
|
|
||||||
|
bench-v4:
|
||||||
|
sh benchmarks/xdb-benchmark.sh ipv4
|
||||||
|
|
||||||
|
bench-v6:
|
||||||
|
sh benchmarks/xdb-benchmark.sh ipv6
|
||||||
|
|
@ -1,129 +1,180 @@
|
||||||
# ip2region xdb erlang 查询客户端
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
### 简介
|
# ip2region Erlang query client
|
||||||
该bingding以erlang语言实现xdb查询客户端,基于Erlang OTP Application,查询逻辑由ip2region_worker工作进程实现,支持配多个工作进程来进行负载均衡。
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
|
This binding implements the xdb query client in `Erlang`, based on the `Erlang OTP Application`. The query logic is implemented by the `ip2region_worker` worker process, supporting multiple worker processes for load balancing.
|
||||||
|
|
||||||
|
### Application Configuration
|
||||||
|
|
||||||
|
The configurable parameters for this application are in `ip2region.app.src`, as follows:
|
||||||
|
|
||||||
### 应用配置
|
|
||||||
该应用可配置的参数在ip2region.app.src中,如下:
|
|
||||||
```erlang
|
```erlang
|
||||||
{env,[
|
{env,[
|
||||||
{poolargs, [
|
{poolargs, [
|
||||||
{size, 1}, %% 工作进程默认数量
|
{size, 1}, %% Default number of worker processes
|
||||||
{max_overflow, 5} %% 工作进程最大数量
|
{max_overflow, 5} %% Maximum number of worker processes
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"} %% Default IPv4 xdb file
|
||||||
]}
|
]}
|
||||||
]}
|
]}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 编译
|
### Dual-stack configuration (IPv4 + IPv6)
|
||||||
|
|
||||||
|
To enable IPv6 queries, add the `ipv6` entry to the `db` list and place both xdb files under `priv/`:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
{env,[
|
||||||
|
{poolargs, [
|
||||||
|
{size, 1},
|
||||||
|
{max_overflow, 5}
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"},
|
||||||
|
{ipv6, "ip2region_v6.xdb"}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `xdb:search/1` interface automatically detects IPv4 and IPv6 inputs and routes them to the correct worker pool.
|
||||||
|
|
||||||
|
### Compile
|
||||||
|
|
||||||
|
```bash
|
||||||
$ rebar3 compile
|
$ rebar3 compile
|
||||||
```
|
```
|
||||||
### 运行
|
|
||||||
将xdb文件放到priv目录下,然后启动erlang节点:
|
|
||||||
```
|
|
||||||
$ rebar3 shell
|
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
Place the xdb file in the `priv` directory, then start the Erlang node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rebar3 shell
|
||||||
```
|
```
|
||||||
在erlang shell中调用xdb:search/1接口查询Ip地址信息, 该接口支持以list格式字符串、binary格式字符串、tuple和整数表示的IP地址,如下:
|
|
||||||
```
|
Call the `xdb:search/1` interface in the Erlang shell to query IP address information. This interface supports IP addresses represented as list strings, binary strings, tuples, and integers:
|
||||||
|
|
||||||
|
```erlang
|
||||||
1> xdb:search("1.0.8.0").
|
1> xdb:search("1.0.8.0").
|
||||||
[20013,22269,124,48,124,24191,19996,30465,124,24191,24030,
|
[20013,22269,124,24191,19996,30465,124,24191,24030,24066,
|
||||||
24066,124,30005,20449]
|
124,20013,22269,30005,20449,124,67,78]
|
||||||
2>
|
2>
|
||||||
3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
|
3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
|
4> io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
4> io:format("~ts~n", [xdb:search({1,0,8,0})]).
|
5> io:format("~ts~n", [xdb:search({1,0,8,0})]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
6> io:format("~ts~n", [xdb:search(16779264)]).
|
6> io:format("~ts~n", [xdb:search(16779264)]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用方法
|
With dual-stack enabled, IPv6 addresses are supported in the same way:
|
||||||
* 在rebar.config中引入依赖
|
|
||||||
|
```erlang
|
||||||
|
1> io:format("~ts~n", [xdb:search("2001:4860:4860::8888")]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
2> io:format("~ts~n", [xdb:search(<<"2001:4860:4860::8888">>)]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
3> io:format("~ts~n", [xdb:search({8193,18528,18528,0,0,0,0,34952})]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
- Add the dependency in `rebar.config`
|
||||||
|
|
||||||
|
```erlang
|
||||||
{deps, [
|
{deps, [
|
||||||
ip2region
|
ip2region
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
```
|
|
||||||
* 启动ip2region Application
|
|
||||||
```
|
|
||||||
......
|
|
||||||
|
|
||||||
application:ensure_started(ip2region),
|
|
||||||
|
|
||||||
......
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* 调用xdb:search/1接口查询IP信息
|
- Start the ip2region Application
|
||||||
```
|
|
||||||
......
|
|
||||||
|
|
||||||
ip2region:search("1.0.8.0"),
|
```erlang
|
||||||
|
{ok, _} = application:ensure_all_started(ip2region).
|
||||||
......
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 单元测试
|
- Call the `xdb:search/1` interface to query IP information
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
xdb:search("1.0.8.0").
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Unit Test
|
||||||
|
|
||||||
|
```bash
|
||||||
$ rebar3 eunit
|
$ rebar3 eunit
|
||||||
===> Verifying dependencies...
|
===> Verifying dependencies...
|
||||||
===> Analyzing applications...
|
===> Analyzing applications...
|
||||||
===> Compiling ip2region
|
===> Compiling ip2region
|
||||||
===> Performing EUnit tests...
|
===> Performing EUnit tests...
|
||||||
=INFO REPORT==== 17-Jan-2023::11:52:59.920155 ===
|
=INFO REPORT==== 28-Jun-2026::04:53:28 ===
|
||||||
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
|
XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
|
||||||
|
|
||||||
....
|
....
|
||||||
Finished in 0.074 seconds
|
Finished in 0.192 seconds
|
||||||
4 tests, 0 failures
|
63 tests, 0 failures
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 基准测试
|
### Benchmark
|
||||||
```
|
|
||||||
|
Both IPv4 and IPv6 benchmarks share the same script. Run it with the desired IP version:
|
||||||
|
|
||||||
|
> `cold` = first pass over the source file: each IP triggers a real search and the
|
||||||
|
> result is written into the ETS cache. `warm` = second pass over the same list,
|
||||||
|
> where every lookup is served directly from the ETS cache.
|
||||||
|
|
||||||
|
```bash
|
||||||
$ cd benchmarks/
|
$ cd benchmarks/
|
||||||
$ sh xdb-benchmark.sh
|
$ sh xdb-benchmark.sh ipv4
|
||||||
===> Verifying dependencies...
|
```
|
||||||
===> Analyzing applications...
|
|
||||||
===> Compiling ip2region
|
For IPv6:
|
||||||
Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
|
|
||||||
|
```bash
|
||||||
Eshell V12.3.2.2 (abort with ^G)
|
$ sh xdb-benchmark.sh ipv6
|
||||||
1> =INFO REPORT==== 17-Jan-2023::11:37:35.631095 ===
|
```
|
||||||
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/default/lib/ip2region/priv/ip2region.xdb
|
|
||||||
|
Or use the Makefile targets from the `binding/erlang` directory:
|
||||||
===> Booted ip2region
|
|
||||||
===> Evaluating: "xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()."
|
```bash
|
||||||
CPU info:
|
$ make bench-v4
|
||||||
model name : AMD EPYC 7K62 48-Core Processor
|
$ make bench-v6
|
||||||
cache size : 512 KB
|
```
|
||||||
cpu MHz : 2595.124
|
|
||||||
bogomips : 5190.24
|
#### IPv4 benchmark example
|
||||||
cores/threads : 2
|
|
||||||
|
```bash
|
||||||
Erlang info:
|
System:
|
||||||
system_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
|
CPU : Apple M4
|
||||||
load test data use 4.835593s
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
start run benchmark tests
|
Loaded : 487169 IPs in 1.335 s
|
||||||
|
|
||||||
search from file:
|
Benchmarks:
|
||||||
ip count:683844,
|
cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op)
|
||||||
total time: 28.201699s,
|
warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op)
|
||||||
search 24248.326315375536 times per second,
|
|
||||||
use 41.23995969841075 micro second per search
|
Done.
|
||||||
|
```
|
||||||
search from cache:
|
|
||||||
ip count:683844,
|
#### IPv6 benchmark example
|
||||||
total time: 0.671801s,
|
|
||||||
search 1017926.4395259906 times per second,
|
```bash
|
||||||
use 0.9823892583688677 micro second per search
|
System:
|
||||||
|
CPU : Apple M4
|
||||||
benchmark test finish
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
|
Loaded : 638953 IPs in 2.949 s
|
||||||
|
|
||||||
|
Benchmarks:
|
||||||
|
cold total= 20.504s count= 638953 qps= 31162.52 avg= 0.032090 ms/op (32.090 us/op)
|
||||||
|
warm total= 0.444s count= 638953 qps= 1437781.56 avg= 0.000696 ms/op ( 0.696 us/op)
|
||||||
|
|
||||||
|
Done.
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
|
# ip2region Erlang 查询客户端
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
该 bingding 以 `Erlang` 语言实现 xdb 查询客户端,基于 `Erlang OTP Application`,查询逻辑由 `ip2region_worker` 工作进程实现,支持配多个工作进程来进行负载均衡。
|
||||||
|
|
||||||
|
### 应用配置
|
||||||
|
|
||||||
|
该应用可配置的参数在 `ip2region.app.src` 中, 如下:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
{env,[
|
||||||
|
{poolargs, [
|
||||||
|
{size, 1}, %% 工作进程默认数量
|
||||||
|
{max_overflow, 5} %% 工作进程最大数量
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"} %% 默认 IPv4 xdb 文件
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 双栈配置(IPv4 + IPv6)
|
||||||
|
|
||||||
|
如需启用 IPv6 查询,在 `db` 列表中加入 `ipv6` 项,并将两个 xdb 文件放到 `priv/` 目录下:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
{env,[
|
||||||
|
{poolargs, [
|
||||||
|
{size, 1},
|
||||||
|
{max_overflow, 5}
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"},
|
||||||
|
{ipv6, "ip2region_v6.xdb"}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
`xdb:search/1` 接口会自动识别 IPv4 与 IPv6 输入,并将其路由到对应的工作进程池。
|
||||||
|
|
||||||
|
### 编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rebar3 compile
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行
|
||||||
|
|
||||||
|
将 xdb 文件放到 `priv` 目录下,然后启动 Erlang 节点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rebar3 shell
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Erlang shell 中调用 `xdb:search/1` 接口查询 IP 地址信息。该接口支持 list 字符串、binary 字符串、tuple 和整数表示的 IP 地址:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
1> xdb:search("1.0.8.0").
|
||||||
|
[20013,22269,124,24191,19996,30465,124,24191,24030,24066,
|
||||||
|
124,20013,22269,30005,20449,124,67,78]
|
||||||
|
2>
|
||||||
|
3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
|
||||||
|
中国|广东省|广州市|中国电信|CN
|
||||||
|
4> io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
|
||||||
|
中国|广东省|广州市|中国电信|CN
|
||||||
|
5> io:format("~ts~n", [xdb:search({1,0,8,0})]).
|
||||||
|
中国|广东省|广州市|中国电信|CN
|
||||||
|
6> io:format("~ts~n", [xdb:search(16779264)]).
|
||||||
|
中国|广东省|广州市|中国电信|CN
|
||||||
|
```
|
||||||
|
|
||||||
|
启用双栈后,IPv6 地址也按同样方式支持:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
1> io:format("~ts~n", [xdb:search("2001:4860:4860::8888")]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
2> io:format("~ts~n", [xdb:search(<<"2001:4860:4860::8888">>)]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
3> io:format("~ts~n", [xdb:search({8193,18528,18528,0,0,0,0,34952})]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
- 在 `rebar.config` 中引入依赖
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
{deps, [
|
||||||
|
ip2region
|
||||||
|
]}.
|
||||||
|
```
|
||||||
|
|
||||||
|
- 启动 ip2region Application
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
{ok, _} = application:ensure_all_started(ip2region).
|
||||||
|
```
|
||||||
|
|
||||||
|
- 调用 `xdb:search/1` 接口查询 IP 信息
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
xdb:search("1.0.8.0").
|
||||||
|
```
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ rebar3 eunit
|
||||||
|
===> Verifying dependencies...
|
||||||
|
===> Analyzing applications...
|
||||||
|
===> Compiling ip2region
|
||||||
|
===> Performing EUnit tests...
|
||||||
|
=INFO REPORT==== 28-Jun-2026::04:53:28 ===
|
||||||
|
XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
|
||||||
|
|
||||||
|
....
|
||||||
|
Finished in 0.192 seconds
|
||||||
|
63 tests, 0 failures
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基准测试
|
||||||
|
|
||||||
|
IPv4 与 IPv6 共用一个脚本,通过参数指定版本:
|
||||||
|
|
||||||
|
> `cold` = 第一次遍历源文件:每个 IP 都会触发真实查询,并把结果写入 ETS 缓存。
|
||||||
|
> `warm` = 第二次遍历同一列表,所有查询都直接命中 ETS 缓存。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd benchmarks/
|
||||||
|
$ sh xdb-benchmark.sh ipv4
|
||||||
|
```
|
||||||
|
|
||||||
|
IPv6:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sh xdb-benchmark.sh ipv6
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以在 `binding/erlang` 目录直接用 Makefile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ make bench-v4
|
||||||
|
$ make bench-v6
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IPv4 基准测试示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
System:
|
||||||
|
CPU : Apple M4
|
||||||
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
|
Loaded : 487169 IPs in 1.335 s
|
||||||
|
|
||||||
|
Benchmarks:
|
||||||
|
cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op)
|
||||||
|
warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op)
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IPv6 基准测试示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
System:
|
||||||
|
CPU : Apple M4
|
||||||
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
|
Loaded : 638953 IPs in 2.949 s
|
||||||
|
|
||||||
|
Benchmarks:
|
||||||
|
cold total= 20.504s count= 638953 qps= 31162.52 avg= 0.032090 ms/op (32.090 us/op)
|
||||||
|
warm total= 0.444s count= 638953 qps= 1437781.56 avg= 0.000696 ms/op ( 0.696 us/op)
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
@ -1,5 +1,37 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
cd ..
|
# Run from binding/erlang so paths and rebar3 artifacts are correct.
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
rebar3 shell --eval="xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()."
|
VERSION="${1:-ipv4}"
|
||||||
|
PROJECT_ROOT="$(cd ../.. && pwd)"
|
||||||
|
|
||||||
|
if [ "$VERSION" = "ipv6" ]; then
|
||||||
|
DB_FILE="${PROJECT_ROOT}/data/ip2region_v6.xdb"
|
||||||
|
DATA_FILE="${PROJECT_ROOT}/data/ipv6_source.txt"
|
||||||
|
DB_CONFIG="[{ipv6, \"${DB_FILE}\"}]"
|
||||||
|
elif [ "$VERSION" = "ipv4" ]; then
|
||||||
|
DATA_FILE="${PROJECT_ROOT}/data/ipv4_source.txt"
|
||||||
|
DB_CONFIG="[{ipv4, \"ip2region.xdb\"}]"
|
||||||
|
else
|
||||||
|
echo "Usage: $0 [ipv4|ipv6]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rebar3 compile
|
||||||
|
|
||||||
|
# shellcheck disable=SC2089
|
||||||
|
EVAL="application:load(ip2region), \
|
||||||
|
application:set_env(ip2region, db, ${DB_CONFIG}), \
|
||||||
|
application:set_env(ip2region, poolargs, [{size,4},{max_overflow,8}]), \
|
||||||
|
{ok,_}=application:ensure_all_started(ip2region), \
|
||||||
|
xdb_benchmark:main(\"${DATA_FILE}\"), \
|
||||||
|
init:stop()."
|
||||||
|
|
||||||
|
# shellcheck disable=SC2090
|
||||||
|
erl -pa _build/default/lib/poolboy/ebin \
|
||||||
|
-pa _build/default/lib/ip2region/ebin \
|
||||||
|
-kernel logger_level error \
|
||||||
|
-noshell \
|
||||||
|
-eval "${EVAL}"
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,39 @@
|
||||||
-define(NONE, none).
|
-define(NONE, none).
|
||||||
-define(APP_NAME, ip2region).
|
-define(APP_NAME, ip2region).
|
||||||
|
|
||||||
-define(XDB_VECTOR_INDEX, ets_xdb_vector_index).
|
|
||||||
-define(XDB_SEGMENT_INDEX, ets_xdb_segement_index).
|
|
||||||
-define(IP2REGION_CACHE, ets_ip2region_cache).
|
|
||||||
|
|
||||||
|
|
||||||
-define(XDB_HEADER_SIZE, 256).
|
-define(XDB_HEADER_SIZE, 256).
|
||||||
-define(XDB_VECTOR_COLS, 256).
|
-define(XDB_VECTOR_COLS, 256).
|
||||||
-define(XDB_VECTOR_INDEX_SIZE, 8).
|
-define(XDB_VECTOR_INDEX_SIZE, 8).
|
||||||
-define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256
|
-define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256
|
||||||
|
|
||||||
-define(XDB_SEGMENT_INDEX_SIZE, 14).
|
%% IP versions as stored in xdb header
|
||||||
|
-define(IP_VERSION_4, 4).
|
||||||
|
-define(IP_VERSION_6, 6).
|
||||||
|
|
||||||
-define(IP2REGION_POOL, ip2region_pool).
|
%% Segment index sizes (bytes): start_ip + end_ip + data_len + data_ptr
|
||||||
|
-define(XDB_SEGMENT_INDEX_SIZE_V4, 14). %% 4 + 4 + 2 + 4
|
||||||
|
-define(XDB_SEGMENT_INDEX_SIZE_V6, 38). %% 16 + 16 + 2 + 4
|
||||||
|
|
||||||
-ifndef(IF).
|
%% Per-version ETS tables
|
||||||
-define(IF(C, T, F), case (C) of true -> (T); false -> (F) end).
|
-define(XDB_VECTOR_INDEX_V4, ets_xdb_vector_index_v4).
|
||||||
-define(IF(C, T), ?IF(C, T, skip)).
|
-define(XDB_VECTOR_INDEX_V6, ets_xdb_vector_index_v6).
|
||||||
-endif.
|
-define(XDB_SEGMENT_INDEX_V4, ets_xdb_segment_index_v4).
|
||||||
|
-define(XDB_SEGMENT_INDEX_V6, ets_xdb_segment_index_v6).
|
||||||
|
-define(IP2REGION_CACHE_V4, ets_ip2region_cache_v4).
|
||||||
|
-define(IP2REGION_CACHE_V6, ets_ip2region_cache_v6).
|
||||||
|
|
||||||
|
%% Per-version pool names.
|
||||||
|
-define(IP2REGION_POOL_V4, ip2region_pool_v4).
|
||||||
|
-define(IP2REGION_POOL_V6, ip2region_pool_v6).
|
||||||
|
|
||||||
|
-record(xdb_header, {
|
||||||
|
version :: non_neg_integer(),
|
||||||
|
index_policy :: non_neg_integer(),
|
||||||
|
created_at :: non_neg_integer(),
|
||||||
|
start_index_ptr :: non_neg_integer(),
|
||||||
|
end_index_ptr :: non_neg_integer(),
|
||||||
|
ip_version :: non_neg_integer(),
|
||||||
|
runtime_ptr_bytes :: non_neg_integer()
|
||||||
|
}).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
{ex_doc, [
|
{ex_doc, [
|
||||||
{extras, ["README.md"]},
|
{extras, ["README.md"]},
|
||||||
{main, "README.md"},
|
{main, "README.md"},
|
||||||
{source_url, "https://github.com/leihua996/ip2region/tree/master/binding/erlang"}
|
{source_url, "https://github.com/lionsoul2014/ip2region/tree/master/binding/erlang"}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{hex, [{doc, ex_doc}]}.
|
{hex, [{doc, ex_doc}]}.
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,20 @@
|
||||||
{mod, {ip2region_app, []}},
|
{mod, {ip2region_app, []}},
|
||||||
{applications,
|
{applications,
|
||||||
[kernel,
|
[kernel,
|
||||||
stdlib
|
stdlib,
|
||||||
|
poolboy
|
||||||
]},
|
]},
|
||||||
{env,[
|
{env,[
|
||||||
{poolargs, [
|
{poolargs, [
|
||||||
{size, 1},
|
{size, 1},
|
||||||
{max_overflow, 5}
|
{max_overflow, 5}
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"}
|
||||||
]}
|
]}
|
||||||
]},
|
]},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
|
|
||||||
{licenses, ["Apache-2.0"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
{links, [{"Github", "https://github.com/leihua996/ip2region/tree/master/binding/erlang"}]}
|
{links, [{"Github", "https://github.com/lionsoul2014/ip2region/tree/master/binding/erlang"}]}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue