Как стать автором
Поиск
Написать публикацию
Обновить

Перенос Sypex Geo API из PHP на go

Время на прочтение5 мин
Количество просмотров3.1K

Sypex Geo — периодически обновляемая база данных для определения местоположения по IP-адресу. Распространяется по лицензии BSD, можно использовать в коммерческих продуктах. Подробно про нее в публикациях автора @zapimir: Sypex Geo — быстрое определение города по IP, В Sypex Geo добавлена привязка к API ВКонтакте.

На сайте разработчиков, кроме собственного клиента, есть несколько реализаций API на разных ЯП. На PHP доступны бандл для Symphony 2, расширение для Laravel и Yii.

В рамках хардкорного обучения языку golang я написал к Sypex Geo 2.2 своё api.

На гите лежит версия alfa, брать с осторожностью. Все баги и косяки, конечно, на моей совести обычного PHP-шника, пишу, чтобы умерло через 30 сек (привет, Серёга C#), и в прод без проверки я б пока не тянул.

Как пользоваться

Ниже приведен пример готового http-сервера, который по запросу http://localhost:8080/ip=2.4.30.5 выдаст вот такой JSON-объект с городом, страной и регионом:

{
    "city": {
        "id": 2992166,
        "name_ru": "Монпелье",
        "name_en": "Montpellier",
        "lat": 43.61092,
        "lon": 3.87723,
        "region_seek": 0
    },
    "country": {
        "id": 74,
        "iso": "FR",
        "name_ru": "Франция",
        "name_en": "France"
    },
    "region": {
        "id": 3007670,
        "iso": "FR-K",
        "name_ru": "Лангедок-Руссильон",
        "name_en": "Region Languedoc-Roussillon"
    }
}

Для работы надо скачать с сайта разрабов последнюю версию Sypex Geo City. Закиньте её в каталог с программой (или куда-то ещё, но тогда передайте серверу полный путь с флагом -d 'full/path/SxGeoCity.dat')

Код под спойлером.

Код сервера
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"github.com/barsuk/sxgeo" // сама библиотека, о ней дальше
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"os"
)

func main() {
	var ip string
	var endian bool
	var setEndian int
	var dbPath string
	flag.StringVar(&ip, "ip", "", "ip address to convert")
	flag.IntVar(&setEndian, "se", 0, "set endianness")
	flag.BoolVar(&endian, "e", false, "check endianness of your system")
	flag.StringVar(&dbPath, "d", "./SxGeoCity.dat", "path to SxGeoCity.dat file")
	flag.Parse()

  // можно передать флаг endian и проверить, как скомпилирована ваша система: little/big Endian
	if endian {
		sxgeo.DetectEndian()
		os.Exit(0)
	}

  // можно установить правильный вариант архитектуры. В случае ошибки библиотека должна выдавать чушь, или я что-то забыл..
	if setEndian > 0 {
		sxgeo.SetEndian(sxgeo.BIG)
		fmt.Printf("host binary endian set to %s\n", sxgeo.Endian())
	}

  // для работы надо считать файл SxGeoCity.dat в память.
	if _, err := sxgeo.ReadDBToMemory(dbPath); err != nil {
		log.Fatalf("error: cannot read database file: %v", err)
	}

  // можно и не запускать сервер, а использовать прогу из командной строки
  // я использовал этот вариант для проверки корректности очередной обновлённой базы от ребят из Sypex Geo.
	if len(ip) > 0 {
		city, err := sxgeo.GetCityFull(ip)
		if err != nil {
			fmt.Printf("error: %v", err)
			os.Exit(1)
		}

		enc, err := json.Marshal(city)
		if err != nil {
			fmt.Printf("error: %v", err)
			os.Exit(1)
		}

		fmt.Printf("%s\n", enc)
		os.Exit(0)
	}
	r := gin.New()
	r.GET("/", sxgeoHandler)
	erro := r.Run(fmt.Sprintf(":%d", 8080))
	if erro != nil {
		log.Fatalf("gin felt: %v", erro)
	}
}

// обработчик запроса с простой проверкой
func sxgeoHandler(c *gin.Context) {
	// проверим на длину
  ip := c.Query("ip")
	if len(ip) < 4 {
		c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "give me an IP, please"})
		return
	}
	fmt.Printf("IP: %s\n", ip) // отложим в лог запрос
  
	city, err := sxgeo.GetCityFull(ip) // вызываем библиотечный метод
	if err != nil {
		c.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.IndentedJSON(http.StatusAccepted, city)
}

Ещё один пример готового кода для использования из sxgeo_test.go.

Функция из теста TestGetCityFull
func TestGetCityFull(t *testing.T) {
	SetEndian(LITTLE)
	path := os.Getenv("HOME") + "/downloads/SxGeoCity.dat"
	_, err := ReadDBToMemory(path)
	if err != nil {
		t.Fatalf("%s %v", path, err)
	}

	// более-менее валидные адреса можно взять с https://www.4it.me/getlistip?cityid=5138
	tcs := []string{
		"188.255.70.88",
		"1.8.8.8",
		"4.8.8.8",
		"8.9.8.8",
		"192.8.8.8",
		"128.8.8.8",

		"132.95.44.0",

		"31.174.87.24",
		"31.174.87.2",
		"31.174.87.224",
		"198.16.66.100",
		"31.173.87.247",
		"91.193.178.99",
		"178.140.236.47",
		"5.22.153.4",
		"37.49.192.5",
		"2.60.57.9",
		"37.112.130.8",
		"95.107.16.10",
		"24.141.149.0",
	}

	for _, ip := range tcs {
		fmt.Printf("%s\n", ip)
		c, err := GetCityFull(ip)
		if err != nil {
			t.Fatalf("%v", err)
		}
		enc, err := json.Marshal(c)
		if err != nil {
			t.Fatalf("%v", err)
		}

		fmt.Printf("%s\n", enc)
		//os.Exit(0)
	}
}

Язык go кроссплатформенный, но в Windows я не проверял — к сожалению, у меня её нет. Если кто попробует, пишите в комментариях.

Вдруг кто-нибудь совсем не знает го, но забрёл на геоопределение:

Инструкция для Ubuntu

  1. Установите го по инструкции: https://golang.org/doc/install

  2. Создайте каталог ~/go/src/sxgeo и файл main.go в нём.

  3. В файл main.go скопируйте код сервака выше.

  4. Из ~/go/src/sxgeo запускайте go run main.go -d 'path/to/SxGeo.dat'

  5. Пользуйтесь: http://localhost:8080/ip={IPv4 строкой типа 8.8.8.8}

Немного об устройстве sxgeo

Модуль sxgeo работает с файлом в формате SxGeo v2.2. Разработчики очень подробно специфицировали формат, за что им большое человеческое спасибо.

Формат базы данных предполагает зависимость от Byte Order системы: LittleEndian или BigEndian. Поэтому первое, что делаем — устанавливаем или определяем его, иначе получим чушь на выходе распаковки.

Определитель этого параметра системы в sxgeo использует пакет unsafe и намекает на осторожность. Ещё большее опасение должен вызвать источник этого метода, Stackoverflow. Пока проблем не было, но вдруг что. Во избежание, переменная hbo (Host Byte Order) сделана глобальной, и порядок байтов можно определить другим, своим и безопасным способом.

Следующий этап — распаковка БД в память. Родной php-клиент предоставляет возможность или считать всю базу в память, или распаковывать постепенно. В моих условиях памяти было достаточно, а свободного времени мало, поэтому всё в память. Так и быстрее работать будет.

За распаковку отвечает ReadDBToMemory. Функция делает то же, что и конструктор класса в родном клиенте — считывает SxGeo.dat и разбивает бинарную запись в структуры языка: нескольких слайсов байт с городами, регионами, собственно IP, плюс метаданные.

Всё, что упаковано в базу для IP, выдаёт метод модуля — GetCityFull. Внутри него две функции — Seek(ip), определяющая необходимое смещение в БД, и parseFullCity, которая прочитает набор байт после смещения и превратит их в человекочитаемую структуру.

Функция Seek — перевод get_num($ip) из php-клиента. Она отсеет мультикасты и loopback, проверит IP на IPv4-шность и проверит, что IP попадает в диапазон из метаданных базы. Потом вызовет searchDb — этот монстрик и найдёт точное смещение нужной последовательности байтов.

Функция parseFullCity прочитает байты и распарсит их в один из двух наборов: либо страну и пустые регион с городом (мне там почему-то попадалась только одна такая страна), либо полный нормальный комплект. Самая ответственная работа лежит на функции unpack — она из прочтённого слайса байтов в цикле вычленит всё, что предполагается в метаинформации. Тут-то и пригодится правильно определённый byte order вашей системы.

Что дальше

Скорее всего, в этом году библиотека дойдёт до какого-нибудь прода, но уже под другим именем. На гите всё останется в том виде, как есть сейчас. Единственное, что может добавиться — погоняю по скорости с клиентом на PHP.

Сравнивая работу с программами на PHP и на go, отмечу, что в go мне удобнее и понятнее работается с бинарными данными. В PHP оно всё какое-то неродное, что ли.

Цель, которую себе ставил, достигнута — определённый барьер сложности на go взят. Надеюсь, кому-то этот код тоже пригодится.

Теги:
Хабы:
Всего голосов 5: ↑3 и ↓2+1
Комментарии1

Публикации

Ближайшие события