개발/개발관련

[개발관련] JAVA_ IPv4 String to Int 변환 (2의 보수, 비트 연산)

mabb 2024. 6. 22. 21:05
반응형

들어가며

네트워크 스캔 기능이 필요하여, DB에 저장된 특정 네트워크("192.168.0.0/24"), 또는 IP 범위("192.168.0.34/24~192.168.0.100/24") 내의 Host IP 문자열 리스트를 반환하는 간단한 메서드를 만들어 보았습니다. 만들면서 비트 연산자, 2의 보수 등도 복습하게 되어 함께 정리합니다.

 

목표

1) 특정 네트워크 내의 모든 Host IP의 List<String> 을 반환하는 메서드 만들기
2) IP 범위 내의 모든 Host IP의 List<String> 을 반환하는 메서드 만들기

 

IPv4 주소와 Java의 int

IPv4주소는 32bit로 표현합니다. 따라서 2³²개인 총 4,294,967,296 개의 IPv4 주소가 존재합니다.  이는 Java의 4byte 정수형인 int로 표현할 수 있는 값의 개수와 동일합니다. IPv4주소는 사람이 보기 편하게 8bit의 옥텟 단위를 컴마로 구분하고, 각 옥텟을 십진수로 나타내어 "192.168.0.123" 처럼 표기합니다. 이러한 IPv4표기법은 Java에서 문자열로 다룹니다. 하지만 저는 네트워크 스캔을 위하여 IPv4를 정수로 변환하여 다루고자 하였습니다.

예를 들어, Google DNS 주소인 "8.8.8.8" 같은 IP주소를
32bit binary로 표현하면 다음과 같습니다.
00001000000010000000100000001000
이 binary를 이용하여 Google DNS 주소를 int로 표현해 보겠습니다.

google DNS 는 int로 67,371,036

google DNS는 int로 67,371,036입니다. IPv4 주소는 4byte인 int로 표현할 수 있는 것입니다.

 

넷마스크와 CIDR

IPv4는 네트워크부와 호스트부로 구분이 됩니다.
가령 "192.168.0.100"이라는 IPv4 주소가 있다고 할 때 IP주소만으로는 이 IP가 어느 네트워크에 속하는지 알 수가 없습니다. 그래서 필요한 것이 네트워크부를 구분하는 추가 정보입니다. 예전에는 A클래스, B클래스, C클래스  등으로 네트워크부를 클래스로 구분하였습니다. 왼쪽부터 3개 옥텟을 네트워크부로 사용하는 C클래스의 경우 "192.168.0.100" 의 네트워크부는 "192.168.0.x"이 됩니다. 왼쪽부터 2개 옥텟을 네트워크부로 사용하는 B클래스의 경우에는 "192.168.x.x"이 네트워크부가 됩니다.
(호스트부의  모든 비트열이 0인 것은 네트워크 주소를 의미합니다. 여기서는 구분을 위해 x로 표기하였습니다. 호스트부의 모든 비트열이 1인 경우는 브로드캐스트 주소를 의미합니다.)

NIC의 IPv4네트워크 설정을 할 때 보통 다음과 같이 설정합니다. 이 중 서브넷 마스크 부분이 네트워크부를 설정하는 부분입니다. 예시에서는 255.255.255.0으로 왼쪽부터 3개의 옥텟을 네트워크부로 사용하므로 C클래스 네트워크에 해당합니다.

Windows NIC IPv4 설정

255.255.255.0은 binary로 다음과 같습니다.
11111111111111111111111100000000
왼쪽부터 24개의 비트가 네트워크부입니다. 요즘은 이를 간단하게 "192.168.0.123/24" 처럼 표기하는 CIDR(Classless Inter-Domain Routing) 표기법을 사용합니다. 이는 상위 비트 몇 개까지가 네트워크부인 지를 IP주소와 함께 직관적으로 표현하는 표기법입니다.

 

2의 보수와 부호 있는 정수(Signed)

Java의 표준 라이브러리인 InetAddress를 사용하여 문자열 형태의 IPv4를 byte[]로 변환하여 다루었습니다. 그 이유는 파싱도 해주고 입력 IP주소 문자열이 IP주소 포맷에 맞는지 검증도 해주기 때문입니다.

bytes 출력

InetAddress가 반환해 주는 bytes배열을 출력해 보면 아래와 같이 음수가 나옵니다. 여기서 Java는 음수를 2의 보수로 표현한다는 기본기를 상기하면 당황하지 않을 수 있습니다. 보수에 대해 간단하게 정리하자면, 반대되는 색깔을 '보색'이라고 하는 것처럼 반대되는 숫자를 '보수'라고 합니다. N의 보수라고 표현하는데 제가 이해한 바로는 현재 수에서 더했을 때 N이 되게 하는 수가 현재 수의 N의 보수입니다. 십진수로 예를 들어보겠습니다.

숫자 3의 9의 보수는 6입니다.
숫자 5의 9의 보수는 4입니다.
숫자 3의 10의 보수는 7입니다.
숫자 5의 10의 보수는 4입니다.

십진수에서 '9'라는 숫자는 한 자리의 십진수 숫자에서 최대 숫자입니다. 십진수에서 '10'이라는 숫자는 두 자리로 자리올림된 최소 숫자입니다. 여기서 음수를 표현하는 데 사용할 수 있는 것은 '10의 보수'입니다.  보통 다음과 같이 현재 수와 현재 수의 음수를 더하면 0이 됩니다.

3 + (-3) = 0

그렇다면 현재 수에 다른 수를 더했을 때 결과가 0이 된다면 다른 수를 현재 수의 음수라고 간주할 수 있을 것입니다.  0과 1만을 다루고 마이너스부호(-)가 없는 컴퓨터에게는 이런 방식으로 음수를 표현하는 방식이 필요한 것입니다. 3과 3의 10의 보수인 7을 더하면 10이 되지만, 숫자의 자릿수가 한 자리이고 자리올림 되는 숫자인 1이 버려진다면 결과는 다음과 같습니다.

3+7=0
*10이지만 한 자리 십진수이므로 자리 올림되는 1이 버려졌습니다.

이것이 -부호를 사용하지 않고 음수를 표현하는 방법입니다. 컴퓨터가 실제로 사용하는 숫자는 2진수입니다. 2진수에서는 '1의 보수''2의 보수' 개념이 있습니다. 이진수에서 '1'이라는 숫자는 더했을 때 자리올림이 발생하지 않는 최대 숫자입니다. 이진수에서 '2'라는 숫자는  자리올림 된 숫자를 의미합니다. 이진수에서 '2의 보수는' 자리올림이 발생하게 하는 최소 숫자를 의미합니다.

한편 이진수에서 1의 보수와 2의 보수를 쉽게 계산하는 방법은 다음과 같습니다. byte 기준으로 현재 수와 1의 보수를 더하면 자리올림이 발생하지 않는 최댓값인 11111111이 되어야 합니다. 1의 보수는 단순하게 현재 수의 0과 1을 바꿔주면 됩니다(NOT연산, ~). 이렇게 구한 1의 보수에 1을 더해주면 현재 수와 더했을 때 자리올림이 발생하게 되므로 현재 수의 2의 보수가 됩니다. 

첫 번째 옥텟인 192의 경우 binary로 다음과 같습니다.
11000000
해당 binary의 2의 보수를 구해보면 01000000이 됩니다. 십진수로 64인 것입니다. Java의 byte자료형은 8bit 사이즈에 최상위비트를 음수 구분  플래그로 사용하는 부호 있는(Signed) 정수 자료형입니다. 최상위 비트가 1인 것은 음수, 최상위 비트가 0인 것은 양수로 간주하기로 약속되어 있기 때문에 11000000(192)은 01000000(64)의 2의 보수로 간주하여 64의 음수인 -64로 인식하는 것입니다. 보수의 개념을 복습할 겸 정리해 보았습니다. 숫자 192가 byte 자료형에서 -64로 출력되는 것은 최상위 비트를 부호로 인식하기 때문에 발생한 것일 뿐 실제  binary는 정확합니다.

 

IPv4문자열을 int로 변환하기 위한 비트 연산(<<, |)

result의 bit를 모두 0으로 두고 octet 길이 단위로 시프트 하고 마스킹하여 OR연산자(|)로 합쳐주었습니다. 여기서 헷갈릴 수 있는 부분은 먼저 수행되는 bytes[i] << ((3 - i) * OCTET_LENGTH) 부분입니다. 3-i가 3이라고 했을 때 byte[i] 를 왼쪽으로 24자리만큼 시프트 합니다. 8bit 길이인 byte를 시프트 했으니까 길이를 넘어서는 부분은 버려졌을 것으로 생각할 수 있습니다. 하지만 Java에서 정수 연산 시 기본적으로 int를 사용하기 때문에 byte[i]는 32bit 길이인 int로 casting 되었습니다. OCTET_MASK[i]와 AND연산(&)으로 마스킹 처리를 해주었고 OCTET_MASK는 IPv4기준으로 길이가 4인 배열이므로 혹시나 IPv6 문자열을 입력한 경우 ArrayIndexOutofBoundsException이 발생하게 하여 IPv6는 지원하지 않는 점을 예외로 명시하였습니다.

 

네트워크 부를 구하기 위한 비트 연산(&)

CIDR로 32개 비트 중에서 상위 몇 개 비트가 네트워크부인 지를 알 수 있습니다. 이를 통해 NetMask를 만들고 IP주소와 NetMask를 AND연산하면 해당 IP의 네트워크부를 구할 수 있습니다.
아래와 같이 CIDR와 시프트 연산자로 NetMask를 만들 수 있습니다.

1
2
3
    private static int netMask(int cidr) {
        return 0xFF_FF_FF_FF << (IPV4_LENTH - cidr);
    }
cs

참고로 부호 있는 정수에서 모든 binary가 1인 경우는 -1로 출력이 됩니다. 즉 0xFF_FF_FF_FF는 -1로 기재할 수 있지만 소스 코드의 가독성을 위해 0xFF_FF_FF_FF로 기재하였습니다.

 

결과

IPv4주소 문자열과 CIDR를 통해 해당 네트워크의 모든 호스트 IP를 얻을 수 있게 되었습니다. 또한 2개의 IPv4주소 문자열과 CIDR를 통해 두 IPv4주소 사이의 모든 호스트 IP를 얻을 수 있게 되었습니다.  해당 정적 유틸 클래스를 프로젝트에 추가하여

1. 특정 네트워크 내의 모든 Host IP의 List을 반환하는 메서드 만들기

java test 출력
java test 출력 결과

2.IP 범위 내의 모든 Host IP의 List 을 반환하는 메서드 만들기

java test 출력
java test 출력 결과

3. 예외처리 - IPv6 미지원

java test 출력
java test 출력 결과

4. 예외처리 - 다른 네트워크의 IP 입력

java test 출력
java test 출력 결과

5. 예외처리 - 잘못된 IPv4 문자열 입력

java test 출력
java test 출력 결과

 

 

전체 코드는 깃허브를 통해 확인하실 수 있습니다.

 

myjava/src/main/java/modules/ipscan/Inet4NetworkParser.java at master · mbk1991/myjava

Contribute to mbk1991/myjava development by creating an account on GitHub.

github.com

 

*읽어주셔서 감사합니다. 잘못된 내용이나 궁금하신 점 댓글 부탁드립니다.

반응형