iBetter Books
수정

주소와 포트

전화를 걸려면 번호를 알아야 합니다. 소켓도 마찬가지입니다. 데이터를 어디로 보낼지 가리키는 좌표가 필요한데, 그 좌표는 두 부분으로 이루어집니다. 어느 컴퓨터인지를 가리키는 IP 주소와, 그 컴퓨터의 어느 프로그램인지를 가리키는 포트입니다. 이 장에서 이 두 좌표를 살펴보고, 컴퓨터마다 숫자를 다르게 저장하는 바이트 오더라는 함정을 짚겠습니다.

IP 주소와 포트

IP 주소는 네트워크 위에서 컴퓨터를 가리키는 번호입니다. 127.0.0.1처럼 점으로 구분된 네 개의 숫자가 익숙한 IPv4 주소입니다. 그중 127.0.0.1은 특별히 루프백 주소라 부르는데, "나 자신"을 가리킵니다. 이 주소로 보낸 데이터는 네트워크 카드를 거치지 않고 곧장 같은 컴퓨터 안으로 돌아옵니다. 그래서 서버와 클라이언트를 한 컴퓨터에서 함께 시험할 때 더없이 편리합니다. 이 책의 예제도 대부분 루프백 주소를 씁니다.

그런데 한 컴퓨터에서는 여러 프로그램이 동시에 네트워크를 씁니다. 웹 브라우저도, 메신저도, 우리가 만든 서버도 모두 같은 IP 주소를 공유합니다. 그렇다면 도착한 데이터가 그중 어느 프로그램의 것인지 어떻게 구별할까요. 바로 포트가 그 역할을 합니다. IP 주소가 건물 주소라면, 포트는 그 건물 안의 호실 번호입니다.

포트 번호의 범위

포트 번호는 0부터 65535까지입니다. 왜 하필 65535일까요. 포트 번호를 16비트, 즉 2바이트로 표현하기 때문입니다. 2의 16제곱이 65536이므로, 0부터 시작하면 가장 큰 값이 65535가 됩니다. 컴퓨터가 다루는 숫자의 크기는 늘 이렇게 비트 수에서 나옵니다.

이 범위는 관례에 따라 세 구역으로 나뉩니다.

0번부터 1023번까지는 잘 알려진 포트입니다. HTTP의 80번, HTTPS의 443번, SSH의 22번처럼 표준 서비스에 예약되어 있습니다. 이 구역의 포트를 쓰려면 보통 관리자 권한이 필요합니다. 1024번부터 49151번까지는 등록된 포트로, 특정 응용 프로그램들이 관례적으로 사용합니다. 49152번부터 65535번까지는 동적 포트로, 어디에도 묶이지 않은 자유 구역입니다.

그래서 우리가 연습용 서버를 만들 때는 권한 문제도 충돌 위험도 적은 높은 번호를 골라 씁니다. 이 책에서는 주로 9000번 같은 한가한 번호를 쓰겠습니다.

바이트 오더라는 함정

이제 조금 까다로운 이야기를 해야 합니다. 처음 소켓을 배우는 사람을 자주 넘어뜨리는 함정인데, 미리 알아 두면 피할 수 있습니다.

여러 바이트로 이루어진 숫자를 메모리에 저장하는 순서가 컴퓨터마다 다릅니다. 큰 자릿수를 앞에 두는 방식을 빅 엔디안, 작은 자릿수를 앞에 두는 방식을 리틀 엔디안이라 부릅니다. 예를 들어 우리가 흔히 쓰는 인텔 계열 컴퓨터는 리틀 엔디안입니다. 같은 포트 번호 9000도 컴퓨터에 따라 메모리에 다른 순서로 놓여 있을 수 있다는 뜻입니다.

문제는 네트워크입니다. 서로 다른 컴퓨터가 데이터를 주고받는데 각자 자기 방식대로 숫자를 늘어놓으면 값이 뒤바뀌어 버립니다. 그래서 네트워크에는 약속된 한 가지 순서가 있습니다. 바로 빅 엔디안이며, 이를 네트워크 바이트 오더라 부릅니다. 포트 번호처럼 여러 바이트로 된 숫자를 네트워크로 보낼 때는 반드시 이 순서로 변환해야 합니다.

이 변환을 해 주는 함수가 정해져 있습니다. C에서는 htonshtonl이 호스트의 숫자를 네트워크 순서로 바꾸고, ntohsntohl이 그 반대로 되돌립니다. 이름은 "host to network short", "network to host long" 같은 식의 줄임말입니다. 끝의 s는 2바이트, l은 4바이트를 뜻합니다.

다행히 Python에서는 이 변환을 대부분 알아서 처리해 줍니다. 포트 번호를 그냥 정수로 넘기면 socket 모듈이 내부에서 변환합니다. 그래도 무슨 일이 벌어지는지는 직접 봐 두는 편이 좋습니다. Python의 socket 모듈에도 같은 변환 함수가 있습니다.

# 파일: byteorder.pyimport socketport = 9000# 호스트 순서의 9000을 네트워크 순서(빅 엔디안)로 변환network_order = socket.htons(port)print(f"원래 값: {port}")print(f"네트워크 순서로 변환: {network_order}")print(f"다시 호스트 순서로: {socket.ntohs(network_order)}")

리틀 엔디안 컴퓨터에서 이 코드를 실행하면 다음과 같은 결과가 나옵니다.

원래 값: 9000
네트워크 순서로 변환: 10275
다시 호스트 순서로: 9000

9000이 변환되자 10275라는 엉뚱한 값이 되었습니다. 두 바이트의 순서가 뒤집혔기 때문입니다. 이렇게 변환된 값을 다시 ntohs로 되돌리면 원래의 9000으로 돌아옵니다. C로 소켓을 짤 때 이 변환을 빼먹으면 연결이 엉뚱한 포트로 가거나 아예 실패합니다. C 예제에 들어가면 htons가 빠짐없이 등장하는 이유가 바로 이것입니다.

좌표를 이해했으니, 이제 진짜로 두 프로그램을 연결해 첫 한 마디를 주고받아 볼 차례입니다.