В данной статье я хотел бы поделиться опытом написания небольшого сервера, предназначенного для определения страны, из которой был сделан запрос. Знаю, для этой задачи существует много решений, например, web сервисы, собственный php скрипт,… В моем случае это не подходило. Проблема в том, что я хотел создать решение, требующее минимум ресурсов(т.к. работать данный сервер должен был на платке raspberry pi, у меня дома).

Зачем это нужно? Признаюсь, была идея «оптимизации» показа рекламы в андроид приложении(в России и близлежащих странах использовать wapstart, в других странах admob). В программе на устройстве пользователя необходимо узнать, в какой стране запущено приложение и из этого делать дальнейшие выводы. Можно, конечно, использовать «встроенные» средства определения местоположения, но на мой взгляд, такое разрешение в манифесте отпугнет многих пользователей. Тем более, что данные сведения не выходят за рамки самой программы (никакого логирования на сервере и прочего сбора данных). На самом деле актуальность данной задачки намного шире(например, автовыбор языка в приложении, языка в выскакивающих сообщениях(например, с просьбой оценить приложение), и многое другое).

Одним словом — задача есть. Давайте теперь придумаем собственное решение, которое при этом будет работать достаточно быстро на слабом железе. PHP скрипты, базы данных отпадают сразу. Лично я использовал бинарные таблицы. О том, как и из чего мы будем их получать, поговорим несколько позже. Пока же разберемся с протоколом сервера.

В качестве протокола транспортного уровня предлагаю использовать UDP. Это будет быстрее, чем TCP, а надежность доставки для нас, в данном случае будет на втором месте. Почему? Потому что полученный результат мы будем записывать на устройстве, чтобы больше не обращаться к серверу. А сам результат мы можем получить и при втором-третьем запуске, это уже не так критично(напомню, это утверждение касается лишь моей задачи). Вообщем, в приоритете — скорость работы.

Протокол прикладного уровня будет наш собственный. Давайте же его разработаем. Итак, все, что нам нужно в клиентской части — узнать страну, из которой мы посылаем запрос. На сервере же нас интересует лишь IP адрес отправителя запроса. То есть в качестве запроса можно посылать вообще пустой UDP пакет. Можно, конечно, ввести «магическую константу», чтобы исключить вероятность ответов на запросы других служб, если вдруг те будут обращаться на порт, слушаемый нашим сервером, но это вы можете сделать и без меня(при необходимости). А что же будет посылаться в качестве ответа? Предлагаю для начала более подробно разобраться с алгоритмом определения страны по адресу.

Итак, наш сервер принял пакет с некого адреса. Что нужно сделать, чтобы узнать страну, с которой был отправлен пакет? Для этого нам понадобятся таблицы. Таблицы в текстовом виде для любой(или почти любой) страны вы можете скачать здесь www.find-ip-address.org/ip-country/. Предположим, что при запуске мы подгрузили N таблиц, относящихся к разным странам. Делаем перебор: берем IP адрес из полученного запроса, накладываем на него маску, указанную для нулевого адреса текущей записи в таблице и сравниваем с соответствующем нулевым адресом этой записи. Как только адрес с наложенной маской совпадет с нулевым адресом, имеющим эту маску, мы будем знать, что IP адрес относится к этой стране. Так что же будем возвращать? Мне хотелось разработать универсальное решение, и не привязываться к конкретным странам. Поэтому я решил, что самый лучший вариант — возвращать просто номер таблицы, в которой было найдено соответствие. Пути к таблицам сервер принимает при запуске. Это дает возможность адаптировать данный сервер к любому набору стран, написав скрипт, запускающий сам сервер, и в качестве ключей передающий список таблиц в определенном порядке. Что может быть проще?

То есть сервер будет возвращать просто номер, от нуля до N. Так как стран, насколько мне известно, меньше 255(а на практике таблиц понадобится и того меньше), предлагаю возвращать один байт(unsigned char), в котором и будет наш номер. На стороне сервера данный номер будет индексом таблицы в массиве таблиц. На клиенте мы будем заранее знать, какому значению что соответствует.

Определимся с ошибками. Адрес может быть не найден ни в одной из таблиц, или же ответ может не вернуться (это будем обрабатывать уже в клиенте). Предлагаю возвращать 0xff, если IP адрес не относится ни к одной из имеющихся таблиц. Значение же 0xfe припасем для случая, если ответ от сервера не вернется.

Что-ж, об алгоритме в общем мы поговорили. Как искать и что возвращать разобрались. Давайте же перейдем к реализации. Для начала скачаем табличку для какой-нибудь страны и посмотрим на ее содержимое. На сайте(ссылка выше и под статьей) выбираем страну, output-CIDR, prefix-none. Качаем и открываем блокнотом. Видим адреса и префиксы в следующем виде:
xx.xx.xx.xx/x
Отлично, это именно то, что нам нужно. Теперь подумаем, в каком виде мы будем это хранить. Выше я говорил, что в моей реализации ключевым фактором была скорость. Определенно, текстовый вид для нас не подойдет. IP адрес и маску для каждой записи будем хранить в виде структуры следующего вида:

typedef struct _ADDRS{
    unsigned long ip;
    unsigned long mask;
}ADDRS;


Некоторые могут поспорить и сказать, что маску можно было хранить в виде префикса, и вместо 8 байт на запись хватило бы пяти. Но это повлечет необходимость лишних действий во время перебора. Те для максимального быстродействия маску лучше иметь в «полном» виде.

Вообщем, каждая таблица будет представлять собой массив структур. Начнем мы с генератора таблиц нужного нам формата. На входе будем принимать два ключа — путь к таблице в текстовом виде и путь к выходному бинарнику. Готовый код основной функции преобразования:

int generator(char* buff, unsigned long* result, int len)
{
    int i;
    char buff2[64];
    buff2[0]=0;
    int pos=0;
    char str[2];
    str[1]=0;
    for(i=0;i < len;i++)
    {
        // пробелы пропускаем
        if(buff[i] == ' ')
            continue;

        // конец строки — маска
        if(buff[i] == '\n')
        {
            int a=atoi(buff2);
            int j=0;
            unsigned long mask=0;
            for(;j < a;j++)
                mask |= (1 << (31-j));
            result[pos]=invert_bytes(mask);
            pos++;
            buff2[0]=0;
            continue;
        }

        // разделитель — значит IP адрес
        if(buff[i] == '/')
        {
            result[pos]=inet_addr(buff2);
            pos++;
            buff2[0]=0;
            continue;
        }

        str[0]=buff[i];
        strcpy(buff2+strlen(buff2),str);
    }

    result[pos]=0;pos++;
    result[pos]=0;pos++;

    return pos*4;
}


Данная функция принимает: указатель на буфер с таблицей в текстовом виде, указатель на unsigned long массив, который есть результат(здесь нам не обязательно обращаться к нашей структуре, будем просто последовательно писать ip-маска-ip-маска-...). Последние восемь байт будут нулями(так сервер будет определять конец таблицы). Результат будем писать в long массив, адрес которого принимаем, а возвращать — общее кол-во байт в сгенерированной таблице. Вы можете несколько оптимизировать данную функцию, например, используя memcpy, а не strcpy для копирования символа в конец буфера(str в данном случае — хранилище для одного символа, который должен иметь вид строки, те первый байт символ и ноль на конце) и запоминать текущее положение. Но это уже на ваше усмотрение, при написании форматера(в отличие от сервера) первым по важности критерием была простота, а не скорость. Вообщем, строго не судите, перед вами черновой вариант. Теперь посмотрим на функцию main:

int main(int argc, char** argv)
{
    FILE* file;
    file=fopen(argv[1],"rb");

    fseek(file, 0, SEEK_END);
    int size=ftell(file);
    fseek(file, 0, SEEK_SET);

    char* buff=malloc(size);

    fread(buff,1,size,file);
    fclose(file);

    unsigned long* result=malloc(size);
    size=generator(buff,result,size);

    do{
        file=fopen(argv[2],"wb");
        if(!file)
            creat(argv[2],511);
    }while(!file);

    fwrite(result,1,size,file);
    fclose(file);

    free(buff);
    free(result);
}


Здесь все банально, читаем первый файл, прогоняем через форматер, результат пишем во второй файл(который предварительно создаем, в случае необходимости). С исходным кодом вспомогательных функций(вроде invert_bytes()) вы можете ознакомиться, скачав архив с исходниками, или же придумать собственную реализацию.

Теперь займемся сервером. Функция main:

int main(int argc, char** argv)
{
    int i=0;
    tables=malloc(0x1000);

    // выгружаю в память таблицы(пути принимаются ключами)
    for(i=1;i < argc;i++)
    {
        FILE* file;
        file=fopen(argv[i],"rb");

        fseek(file, 0, SEEK_END);
        unsigned int size=ftell(file);
        fseek(file, 0, SEEK_SET);

        tables[i-1]=malloc(size);
        tables_count++;

        fread(tables[i-1],size,1,file);
        fclose(file);
    }

    unsigned int magic;

    int sock=socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in  local_addr;
    local_addr.sin_family=AF_INET;
    local_addr.sin_addr.s_addr=INADDR_ANY;
    local_addr.sin_port=htons(2000);

    bind(sock, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));

    while(1)
    {
        struct sockaddr_in  client_addr;
        int client_addr_size = sizeof(struct sockaddr);
        int rc=recvfrom(sock, &magic, 4, 0, (struct sockaddr*) &client_addr, &client_addr_size);
        if(rc < 0)
             continue;

        //if(magic != 0x1a2b3c4d)
          //   continue;

        unsigned char country=get_country(client_addr.sin_addr.s_addr);

        // возвращаю номер страны (1 байт)
        sendto(sock, &country, 1, 0, (struct sockaddr*) &client_addr, sizeof(struct sockaddr));
    }

    // освобождаю кучи
    for(i=0;i < tables_count;i++)
        free(tables[i]);

    free(tables);

    close(sock);
}


Подготавливаем массив таблиц, читая файлы, пути которых передаются ключами. Затем пускаем цикл, в котором принимаем запрос, вызываем наш переборщик таблиц, который в результате вернет номер таблицы или 0xff, и возвращаем клиенту полученное значение. Слушать будем 2000 порт(на самом деле, можно использовать любой другой). Опять таки, это черновой вариант. Рекомендую не запускать бесконечный цикл, а ловить сигнал(скажем, SIGINT) и по нему завершать цикл. Но это я оставляю на ваше усмотрение. Для релизной версии это практически необходимо(для корректного завершения), для тестирования же сойдет и черновой вариант. Теперь «переборщик»:

unsigned char get_country(unsigned long ip)
{
    int i=0;
    ADDRS* addrs;

    for(i=0;i < tables_count;i++)
    {
        addrs=tables[i];
        while(1)
        {
            if(addrs->mask == 0 || addrs->ip == 0)
                break;
            if((ip & addrs->mask) == addrs->ip)
                return i;
        
            addrs++;
        }
    }

    return 0xff;
}


Думаю, в особых комментариях данная функция не нуждается. Запускаем цикл, кол-во проходов которого будет равно количеству таблиц, в каждом проходе берем одну таблицу и проверяем ее. Описание структуры ADDRS я уже приводил, потому не будем задерживаться.

Итак, форматер мы написали. Сервер тоже. Теперь разберемся с клиентом. Здесь все просто(даже прокомментировать нечего):

int main(int argc, char** argv)
{
    int sock=socket(AF_INET, SOCK_DGRAM, 0);

    struct timeval tv;
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,(char*)&tv,sizeof(tv));

    struct sockaddr_in  serv_addr;
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(2000);

    unsigned long magic=0x1a2b3c4d;
    unsigned char country;

    sendto(sock, &magic, 4, 0, (struct sockaddr*) &serv_addr, sizeof(struct sockaddr));
    recvfrom(sock, &country, 1, 0, (struct sockaddr*) &serv_addr, sizeof(struct sockaddr));

    printf("%d\n",country);

    close(sock);
}


При реализации полноценного клиента, входящего в состав приложения, стоит так же сохранять результат, чтобы избежать обращений к серверу в дальнейшем. Напомню, что в моем случае:
во-первых сервер находится у меня дома
во-вторых сервер довольно немощный(raspberry pi, таки не стоечный сервер)

Это и есть мое объяснение тому факту, что все исходники приведены под Linux. Думаю, желающим собрать это под Windows не составит труда отредактировать сорцы под себя.

Давайте-ка потестируем. Для начала напишем небольшой скрипт. Назовем его, скажем, startserv.sh или как вам будет удобно. Пишем:

ПУТЬ_К_СЕРВЕРУ ПУТЬ_К_ТАБЛИЦЕ_1 ПУТЬ_К_ТАБЛИЦЕ_2

Сами таблички лично я храню в папке tables, лежащей в одной папке с бинарником сервера. Теперь обратимся к нашему серверу с клиента. (!)Обратите внимание, внутри одной сети это работать не будет. Как вариант, можете сгенерить собственные таблички из, скажем, таких записей:
192.168.1.0/24
ну и тд, вообщем, сделать так, чтобы одна из таблиц содержала нулевой адрес и маску вашей сети, а остальные — «левые» адреса. Итак, запустим наш клиент:
./client АДРЕС_СЕРВЕРА
при условии, что вы находитесь в папке с клиентом. Если все хорошо, на выходе мы получим номер, вернувшийся от сервера. Похоже, задача выполнена. Наш сервер работает и возвращает нужный номер. Осталось нагенерить табличек и можно внедрять клиента в приложения. К слову, больше дюжины сгенерированных готовых табличек(и их текстовых «родителей») вы найдете в архиве с исходниками. Так же в архиве вы найдете «стресстестовый» клиент, забрасывающий сервер большим кол-вом запросов, считающим вернувшиеся ответы и время на них затраченное. Поэксперементировать с этим, при желании, вы можете самостоятельно, чтобы оценить производительность нашего решения. На этом я с вами прощаюсь.

Ссылка на архив с исходниками: rghost.ru/6RWJjt4xD
Cайт с табличками: www.find-ip-address.org/ip-country/

Комментарии (19)