Microsoft MVP성태의 닷넷 이야기
.NET Framework: 931. C# - IP 주소에 따른 국가별 위치 확인 [링크 복사], [링크+제목 복사],
조회: 12032
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)

C# - IP 주소에 따른 국가별 위치 확인

재미있는 글을 하나 읽었습니다. ^^

HiveQL Tuning Case #2 - 적은 데이터는 많게 만들기 (IP별 국가 매핑)
; https://sparkdia.tistory.com/63

(개인적으로 HiveQL은 만져본 적이 없으므로 그냥 단순하게 RDBMS의 관점에서 저 글을 읽었다고 가정하겠습니다.)

범위로 정해진 데이터에 대해 사용자가 비교 코드를 정의할 수 없어, (어찌 보면 ^^ 눈물겨운) 튜닝 작업을 어떤 식으로 했는가에 대해 잘 설명해 주고 있습니다.

그런데 위의 처리를 HiveQL이 아닌, 응용 프로그램 레벨에서 처리하면 의외로 간단하게 해결이 됩니다. 왜냐하면, SQL 쿼리의 경우에는 Equals 연산이 안되므로 BETWEEN과 JOIN 처리를 해야 하지만 코드 레벨로 내려오면 Equals 처리가 가능하기 때문입니다. 어떻게 하는지 한번 ^^ 구현해 볼까요?




우선, 위의 글에 보면 국가별 IP 할당 내역을 다음의 글에 공개하고 있다고 알려주는데요,

국가별 IP 주소 대역 현황
; https://krnic.or.kr/jsp/infoboard/stats/ipCurrent.jsp

CSV 파일로 내려받을 수 있으므로, 이렇게 로딩을 처리할 수 있습니다.

public class IPv4Range
{
    public DateTime Created;
    public string NationCode;
    public IPAddress Started;
    public IPAddress Ended;
    public string Prefix;
    public DateTime Allocated;

    public override string ToString()
    {
        return $"{NationCode}({Started}~{Ended})";
    }
}

public class IPv4Tables
{
    public static IPv4Range[] _ranges;

    static IPv4Tables()
    {
        _ranges = ReadTables();
    }

    private static IPv4Range[] ReadTables()
    {
        List<IPv4Range> list = new List<IPv4Range>();

        foreach (string line in File.ReadAllLines("ipv4.csv").Skip(1))
        {
            string[] items = line.Split(',');

            IPv4Range range = new IPv4Range();
            range.Created = DateTime.ParseExact(items[0], "yyyyMMdd", CultureInfo.InvariantCulture);
            range.NationCode = items[1];
            range.Started = IPAddress.Parse(items[2]);
            range.Ended = IPAddress.Parse(items[3]);
            range.Prefix = items[4];
            range.Allocated = DateTime.ParseExact(items[5], "yyyyMMdd", CultureInfo.InvariantCulture);

            list.Add(range);
        }

        return list.ToArray();
    }
}

물론, 정렬도 시켜놓아야 하므로 IPv4Range 클래스에 IComparer 인터페이스를 구현하고,

public class IPv4Range : IComparable<IPv4Range>
{
    // ...[생략]...

    public int CompareTo(IPv4Range other)
    {
        uint otherAddr = other.Started.ToUInt32();
        uint thisAddr = Started.ToUInt32();

        if (otherAddr == thisAddr)
        {
            return 0;
        }

        if (otherAddr > thisAddr)
        {
            return -1;
        }

        return 1;
    }
}

public static class IPAddressHelper
{
    public static uint ToUInt32(this IPAddress addr)
    {
        return BitConverter.ToUInt32(addr.GetAddressBytes().Reverse().ToArray(), 0);
    }
}

Array.Sort를 호출해 줍니다.

public class IPv4Tables
{
    public static IPv4Range[] _ranges;

    static IPv4Tables()
    {
        _ranges = ReadTables();
        Array.Sort(_ranges);
    }

    // ...[생략]...
}




데이터가 준비되었으니, 아무 IP나 하나 생성해서 단순 검색으로 구현해 테스트할 수 있습니다.

{
    Random rand = new Random((int)new DateTime(2020, 8, 10).Ticks);

    rand.NextBytes(buffer);
    IPAddress ipAddr = new IPAddress(buffer);

    var found = table.Search(ipAddr);

    if (found == null)
    {
        Console.WriteLine("NOT FOUND");
    }
    else
    {
        Console.WriteLine(found.NationCode);
    }
}

public class IPv4Range : IComparable<IPv4Range>
{
    // ...[생략]...

    public bool IsInRange(IPAddress target)
    {
        uint started = Started.ToUInt32();
        uint ended = Ended.ToUInt32();

        uint current = target.ToUInt32();

        if (started <= current && current <= ended)
        {
            return true;
        }

        return false;
    }
}

public class IPv4Tables
{
    // ...[생략]...

    public IPv4Range Search(IPAddress item)
    {
        foreach (IPv4Range range in _ranges)
        {
            if (range.IsInRange(item) == true)
            {
                return range;
            }
        }
    
        return null;
    }
}

물론, 21만 개의 범위가 있어 검색이 매우 느립니다. 위치에 따라 다르겠지만 전체 검색을 했을 때 제 컴퓨터에서 14ms가 정도가 나왔습니다. 테스트를 위해 무작위 4바이트 IP 주소 데이터를 대충, 1,000개의 데이터만 만들어 검색해도,

{
    List<IPAddress> randomAddresses = CreateTestAddresses(1); // 닷넷 (및 VM 계열 언어) 코드의 성능 측정 시 주의할 점
    CheckAddress(table, randomAddresses);
}

{
    List<IPAddress> randomAddresses = CreateTestAddresses(1_000);
    CheckAddress(table, randomAddresses);
}

private static void CheckAddress(IPv4Tables table, List<IPAddress> randomAddresses)
{
    List<IPAddress> publics = new List<IPAddress>();

    Stopwatch st = new Stopwatch();
    st.Start();

    foreach (IPAddress addr in randomAddresses)
    {
        var found = table.Search(addr); // 순차 검색
    }

    st.Stop();
    Console.WriteLine($"{st.ElapsedMilliseconds}");
}

private static List<IPAddress> CreateTestAddresses(int count, int seed = 0)
{
    if (seed == 0)
    {
        seed = Environment.TickCount;
    }

    Random rand = new Random(seed);

    byte[] buffer = new byte[4];
    List<IPAddress> addrs = new List<IPAddress>();

    for (int i = 0; i < count; i++)
    {
        rand.NextBytes(buffer);
        IPAddress ipAddr = new IPAddress(buffer);

        addrs.Add(ipAddr);
    }

    return addrs;
}

5,143ms가 걸립니다. 당연히 이 정도 성능으로는 수 천만 개의 데이터라면 사용할 수 없을 정도로 느립니다.




이렇게 느린 성능을 이진 검색으로 바꾸면 20만 개 정도의 국가 분류는 log2(2000000) == 17.6096... 정도니까 사설 IP를 검색한다고 해도 18번 이내의 비교만으로 판정 지을 수 있습니다. 데이터베이스라면, 특정 필드에 인덱스를 걸어놨을 경우 해당 조건을 WHERE에 주면 인덱스 검색이 되는 것이므로 HiveQL에서 JOIN과 BETWEEN으로 했던 연산을 '=' 조건으로 구현한 것과 같으므로 당연히 성능이 극적으로 향상됩니다.

실제로 구현해 볼까요? ^^ 이를 위해 필요한 것은, BinarySearch 메서드와 적절한 Comparer만 있으면 됩니다.

public class IPv4Tables
{
    // ...[생략]...

    public IPv4Range BinarySearch(IPAddress item)
    {
        IPAddressComparer comparer = new IPAddressComparer();
        int n = Array.BinarySearch(_ranges, item, comparer);
        if (n < 0)
        {
            return null;
        }

        return _ranges[n];
    }

    public class IPAddressComparer : System.Collections.IComparer
    {
        public int Compare(object x, object y)
        {
            IPv4Range range = x as IPv4Range;
            IPAddress addr = y as IPAddress;

            uint started = range.Started.ToUInt32();
            uint ended = range.Ended.ToUInt32();
            uint target = addr.ToUInt32();

            if (started <= target && target <= ended)
            {
                return 0;
            }

            if (started > target)
            {
                return 1;
            }

            return -1;
        }
    }
}

이렇게 간단한 코드만으로도, 이제 1,000개는 너무 우습고 1,000,000 개의 IP 주소를 검색해도 1.3초 내에 끝납니다. 천만 개 정도 되면 13초 정도가 되겠지만 살짝 Parallel.For로 바꿔주는 트릭을 쓰면,

private static void CheckAddress(IPv4Tables table, List<IPAddress> randomAddresses)
{
    List<IPAddress> publics = new List<IPAddress>();

    Stopwatch st = new Stopwatch();
    st.Start();

    Parallel.ForEach(randomAddresses, (item) =>
    {
        var found = table.BinarySearch(item);
    });

    st.Stop();
    Console.WriteLine($"{st.ElapsedMilliseconds}");
}

4코어 4스레드 i5 머신인 제 컴퓨터에서 2,809ms 만에 처리가 끝납니다. 이 정도라면 수 억 건 정도의 데이터 처리도 메모리만 받쳐주면 그냥 간단하게 PC에서 처리해도 될 것입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




제가 해당 업무의 환경을 잘 모르겠지만 "HiveQL Tuning Case #2 - 적은 데이터는 많게 만들기 (IP 별 국가 매핑)" 글은 너무 Hadoop 관점에서만 처리하려고 노력한 것이 아닌가 하는 아쉬움이 남습니다. 어느 정도는 응용 프로그램의 능력도 빌려 적절하게 섞어서 처리했다면 더 좋았을 듯싶습니다. 사실, 단순히 국가 코드 매핑을 위해 (튜닝을 했다고 해도) Hadoop의 리소스를 8분 동안씩이나 점유한다는 것은 리소스 낭비가 아닌가... 생각됩니다. ^^

관점을 넓혀서, 애당초 접속 로그를 남기는 응용 프로그램 레벨에서 국가별 IP 주소 대역을 미리 구해 주던가, 응용 프로그램의 그런 소스 코드 수정이 어렵다면 배치 응용 프로그램 같은 것을 만들어 접속 로그 테이블에 단순히 nullable 필드 하나만 추가해 두고 주기적으로 null 값인 레코드만 선택해 국가 코드를 업데이트하는 것도 좋았을 것입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 10/25/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2020-09-07 05시08분
[dotnetpower] 깔끔한 정리!!!
[guest]
2020-12-23 10시44분
[지나가던사람] 실제로 테스트해보면 정확한 값을 찾지 못하는것 같습니다.
Compare 부분에 문제가 있어보이는데, range.Started.ToUInt32(); 이런식으로 캐스팅한 값으로 비교하는게 올바른 방법인가요?
[guest]
2020-12-23 11시56분
"정확한 값을 찾지 못하는 것"같은 그 IP를 알려주세요. (왜 uint로 변환하는 게 올바른 방법이 아니라고 보는 건가요?)
정성태
2020-12-28 02시30분
[지나가던사람] 늦어져서 죄송합니다! 아이피 주소는 "103.60.16.16"으로 검색했구요. 올려주신 코드의 BinarySearch() 함수를 이용해서 서치했습니다.
서치 결과가 이상해서 브레이크 포인트를 잡고 확인해 보니, 정상적이지 않은 상황에서 return을 했습니다.
(uint로 변환해서 비교하는게 맞냐고 여쭤봤던건 제가 잘 몰라서 여쭤봤습니다!)
[guest]
2020-12-28 04시30분
이상하군요, 동일한 소스 코드로 저는 정상적으로 값이 나오는데요.

IPv4Tables table = new IPv4Tables();
Console.WriteLine(table.BinarySearch(IPAddress.Parse("103.60.16.16")).NationCode); /* 출력 == VN */

제가 재현할 수 있는 좀 더 구체적인 방법이 있을까요?

(그리고, 어차피 IPv4의 경우 4바이트 주소 체계이기 때문에 uint 값 변환이 잘 됩니다.)
정성태
2020-12-28 05시23분
[지나가던사람]
문제가 됐던 부분은 ToUInt32()였는데요.
unsafe를 허락하기 싫어서 만들어 두신 ToUInt32() 버전 중 시프트 시키는 버전을 사용했는데, byte 오더가 맞지 않아서 발생하는 문제였습니다.
(uint)(buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0]); -> return (uint)(buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]);
엄한 곳에서 문제를 찾으려 했네요 ^^;

확인하는데 귀한 시간 써주셔서 감사합니다.
[guest]
2020-12-29 09시27분
제보해 주신 덕분에, 현재 해당 주석 코드를 수정했습니다. ^^
정성태
2021-03-12 09시22분
How to map IPs to country for free with .NET and IP2Location
; https://blog.elmah.io/how-to-map-ips-to-country-for-free-with-net-and-ip2location/

Indexes on computed Columns : Marking a CLR function as Deterministic
; http://sqlsolace.blogspot.com/2010/04/indexes-on-computed-columns-marking-clr.html
정성태

... 46  47  48  49  50  51  52  53  54  55  56  57  58  [59]  60  ...
NoWriterDateCnt.TitleFile(s)
12165정성태3/2/202011049.NET Framework: 900. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 네 번째 이야기(Monitor.Enter 후킹)파일 다운로드1
12164정성태2/29/202011900오류 유형: 598. Surface Pro 6 - Windows Hello Face Software Device가 인식이 안 되는 문제
12163정성태2/27/202010363.NET Framework: 899. 익명 함수를 가리키는 delegate 필드에 대한 직렬화 문제
12162정성태2/26/202013214디버깅 기술: 166. C#에서 만든 COM 객체를 C/C++로 P/Invoke Interop 시 메모리 누수(Memory Leak) 발생 [6]파일 다운로드2
12161정성태2/26/20209818오류 유형: 597. manifest - The value "x64" of attribute "processorArchitecture" in element "assemblyIdentity" is invalid.
12160정성태2/26/202010514개발 환경 구성: 469. Reg-free COM 개체 사용을 위한 manifest 파일 생성 도구 - COMRegFreeManifest
12159정성태2/26/20208633오류 유형: 596. Visual Studio - The project needs to include ATL support
12158정성태2/25/202010509디버깅 기술: 165. C# - Marshal.GetIUnknownForObject/GetIDispatchForObject 사용 시 메모리 누수(Memory Leak) 발생파일 다운로드1
12157정성태2/25/202010477디버깅 기술: 164. C# - Marshal.GetNativeVariantForObject 사용 시 메모리 누수(Memory Leak) 발생 및 해결 방법파일 다운로드1
12156정성태2/25/20209778오류 유형: 595. LINK : warning LNK4098: defaultlib 'nafxcw.lib' conflicts with use of other libs; use /NODEFAULTLIB:library
12155정성태2/25/20209011오류 유형: 594. Warning NU1701 - This package may not be fully compatible with your project
12154정성태2/25/20208815오류 유형: 593. warning LNK4070: /OUT:... directive in .EXP differs from output filename
12153정성태2/23/202011571.NET Framework: 898. Trampoline을 이용한 후킹의 한계파일 다운로드1
12152정성태2/23/202011311.NET Framework: 897. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)파일 다운로드1
12151정성태2/22/202011786.NET Framework: 896. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)파일 다운로드1
12150정성태2/21/202011671.NET Framework: 895. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 [1]파일 다운로드1
12149정성태2/20/202011443.NET Framework: 894. eBEST C# XingAPI 래퍼 - 연속 조회 처리 방법 [1]
12148정성태2/19/202012684디버깅 기술: 163. x64 환경에서 구현하는 다양한 Trampoline 기법 [1]
12147정성태2/19/202011334디버깅 기술: 162. x86/x64의 기계어 코드 최대 길이
12146정성태2/18/202011532.NET Framework: 893. eBEST C# XingAPI 래퍼 - 로그인 처리파일 다운로드1
12145정성태2/18/202010753.NET Framework: 892. eBEST C# XingAPI 래퍼 - Sqlite 지원 추가파일 다운로드1
12144정성태2/13/202010790.NET Framework: 891. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기파일 다운로드1
12143정성태2/13/20208827.NET Framework: 890. 상황별 GetFunctionPointer 반환값 정리 - x64파일 다운로드1
12142정성태2/12/202010645.NET Framework: 889. C# 코드로 접근하는 MethodDesc, MethodTable파일 다운로드1
12141정성태2/10/202010216.NET Framework: 888. C# - ASP.NET Core 웹 응용 프로그램의 출력 가로채기 [2]파일 다운로드1
12140정성태2/10/202010084.NET Framework: 887. C# - ASP.NET 웹 응용 프로그램의 출력 가로채기파일 다운로드1
... 46  47  48  49  50  51  52  53  54  55  56  57  58  [59]  60  ...