소켓 주소
고친 사람 github-actions[bot]
소켓 주소는 통신하는 한쪽 끝의 프로그램 하나를 집어 가리킵니다. 흔히 컴퓨터의 주소와 그 안의 프로그램을 가리키는 번호를 한 쌍으로 묶어 적습니다. 서버는 이 주소로 연결을 기다립니다. 클라이언트는 이 주소를 보고 서버를 찾아갑니다.
쉽고 빠른 이해
소켓 주소는 데이터를 받을 프로그램 하나를 가리키는 주소입니다. 10.0.0.1:8080 처럼 컴퓨터의 주소 뒤에 번호를 붙여 적습니다.
컴퓨터의 주소만으로는 컴퓨터 앞까지만 닿습니다. 한 컴퓨터에서 프로그램 여럿이 동시에 통신합니다. 번호까지 붙여야 데이터를 받을 프로그램이 하나로 정해집니다.
어떻게 도나:
- 서버는 자기 소켓 주소를 정해 운영체제에 등록해 둡니다
- 클라이언트는 그 소켓 주소를 적어 연결을 겁니다
- 운영체제는 도착한 데이터에 적힌 소켓 주소를 보고 넘겨줄 프로그램을 찾습니다
앱 코드는 소켓 주소를 직접 적는 일이 드뭅니다. 이름과 포트를 적으면 라이브러리가 소켓 주소로 바꿔 씁니다. 같은 컴퓨터 안의 프로그램끼리는 파일 경로를 소켓 주소로 쓰기도 합니다.
대가는 한 소켓 주소를 보통 프로그램 하나만 맡는다는 것입니다. 이미 누가 맡은 주소에 다른 서버를 띄우면 실패합니다. 주소의 종류마다 모양이 달라서 코드가 그 차이를 다뤄야 하기도 합니다.
상세
이 절은 소켓 주소를 이루는 두 값에서 시작해 모양이 전혀 다른 소켓 주소까지 차례로 봅니다.
편지 봉투에 건물 주소만 적으면 편지는 건물 앞까지만 갑니다. 호수까지 적어야 집 한 채에 닿습니다. 건물 주소와 호수는 따로 떼면 쓸모가 반쪽입니다. 둘을 같이 적어야 받는 사람이 하나로 정해집니다.
소켓은 프로그램이 네트워크로 데이터를 주고받으려고 운영체제에서 받아 쓰는 통신 창구입니다. 프로그램은 소켓에 쓰고 소켓에서 읽습니다. 소켓 주소는 이 창구에 붙는 주소입니다.
주소와 번호의 한 쌍
IP 주소(Internet Protocol address, 인터넷 프로토콜 주소)는 네트워크에서 컴퓨터 하나를 가리키는 번호입니다. 데이터는 이 번호를 보고 네트워크를 건너 그 컴퓨터까지 갑니다. 편지로 치면 건물 주소입니다.
포트는 한 컴퓨터 안에서 데이터를 받을 프로그램을 가리키는 번호입니다. 0부터 65535까지 씁니다. 웹 서버가 443번을 맡는 식으로 프로그램마다 다른 번호를 씁니다. 편지로 치면 호수입니다.
소켓 주소는 이 둘을 한 쌍으로 묶은 것입니다. 쌍으로 묶는 까닭은 어느 한쪽만으로는 받는 쪽이 정해지지 않기 때문입니다. IP 주소만 있으면 컴퓨터 안의 어느 프로그램인지 모릅니다. 포트만 있으면 어느 컴퓨터의 8080번인지 모릅니다.
flowchart TD
D["데이터 · 10.0.0.1:8080 으로"]
subgraph C1["컴퓨터 · IP 주소 10.0.0.1"]
P1["포트 443 · 웹 서버"]
P2["포트 8080 · 다른 프로그램"]
end
D --> P2
그림의 데이터는 IP 주소로 컴퓨터를 찾습니다. 그 안에서는 포트로 프로그램을 고릅니다.
글로 적을 때는 주소와 번호를 쌍점으로 잇습니다.
| 주소 종류 | 소켓 주소 표기 |
|---|---|
| IPv4(Internet Protocol version 4, 인터넷 프로토콜 4판) | 10.0.0.1:8080 |
| IPv6(Internet Protocol version 6, 인터넷 프로토콜 6판) | [2001:db8::1]:8080 |
IPv6 주소에는 대괄호를 씌웁니다. IPv6 주소는 안에서 이미 쌍점을 쓰기 때문입니다. 괄호가 없으면 마지막 쌍점이 번호 앞의 구분인지 주소의 일부인지 가를 수 없습니다.
연결 하나를 가르는 두 소켓 주소
연결을 거는 순서를 따라가며 양쪽 소켓 주소가 언제 정해지는지 봅니다.
TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 보내기 전에 양쪽이 연결을 먼저 맺는 통신 방식입니다. 연결에는 끝이 둘입니다. 끝마다 소켓이 하나씩 있습니다. 소켓마다 소켓 주소가 하나씩 붙습니다.
서버 쪽 소켓 주소는 서버가 미리 정합니다. 소켓에 주소를 붙이는 이 일을 바인드라고 부릅니다. 서버 주소가 정해져 있어야 클라이언트가 어디로 연결을 걸지 압니다. 같은 소켓 주소에 이미 다른 소켓이 바인드해 있으면 바인드는 보통 실패합니다.
클라이언트 쪽 소켓 주소는 보통 운영체제가 정합니다. 클라이언트가 연결을 거는 함수 connect 를 부르면 운영체제가 남는 포트 하나를 골라 붙여 줍니다. 이렇게 빌려 주는 번호를 임시 포트라고 부릅니다.
아래 그림은 이 순서를 값으로 보입니다. 서버가 들어온 연결을 받아 오는 함수는 accept 입니다.
sequenceDiagram
participant 클라 as 클라이언트 프로그램
participant 운영체제 as 클라이언트 운영체제
participant 서버 as 서버 프로그램
Note over 서버: 10.0.0.1:8080 에 바인드해 둔다
클라->>운영체제: connect · 10.0.0.1:8080 으로
Note over 운영체제: 임시 포트 51413 을 골라<br/>10.0.0.5:51413 을 붙인다
운영체제->>서버: 연결 요청 · 보낸 쪽 10.0.0.5:51413
서버-->>운영체제: 응답 · 받는 쪽 10.0.0.5:51413
Note over 서버: accept 가 10.0.0.5:51413 을 돌려준다
그림의 클라이언트 컴퓨터는 주소가 10.0.0.5 입니다. 클라이언트는 상대의 소켓 주소만 적어 connect 를 불렀습니다. 자기 쪽 주소는 운영체제가 붙였습니다. 서버는 accept 로 연결을 받아 올 때 상대의 소켓 주소를 함께 받습니다. 서버는 이 값으로 누가 접속했는지 압니다.
오가는 데이터 묶음마다 보낸 쪽과 받는 쪽의 소켓 주소가 둘 다 적혀 있습니다. 서버가 응답할 때는 둘을 맞바꿔 적습니다. 그래서 응답이 클라이언트의 51413번 포트로 돌아옵니다.
같은 포트로 연결 여럿을 받는 법
서버 하나는 8080번 하나로 연결을 많이 받습니다. 서버 쪽 소켓 주소가 전부 같은데도 운영체제가 연결을 헷갈리지 않습니다.
운영체제는 연결 하나를 양쪽 소켓 주소 두 개로 가립니다. 주소 둘과 번호 둘, 값 넷의 묶음입니다. 이 묶음을 4-튜플이라고 부릅니다. 서버 쪽 값이 같아도 클라이언트 쪽 값이 다르면 다른 연결입니다.
| 연결 | 클라이언트 쪽 | 서버 쪽 |
|---|---|---|
| 첫째 | 10.0.0.5:51413 |
10.0.0.1:8080 |
| 둘째 | 10.0.0.5:51414 |
10.0.0.1:8080 |
| 셋째 | 10.0.0.7:51413 |
10.0.0.1:8080 |
표의 세 연결은 서버 쪽 값이 모두 같습니다. 첫째와 둘째는 같은 컴퓨터에서 왔지만 포트가 다릅니다. 첫째와 셋째는 포트가 같지만 컴퓨터가 다릅니다. 넷 가운데 하나라도 다르면 운영체제는 다른 연결로 봅니다.
데이터가 도착하면 운영체제는 데이터에 적힌 값 넷을 읽습니다. 넷이 모두 맞는 연결을 찾아 그 연결의 소켓에 데이터를 넘깁니다. 그래서 한 포트에 연결이 많이 붙어도 데이터가 섞이지 않습니다.
flowchart TD
D["도착한 데이터<br/>10.0.0.7:51413 → 10.0.0.1:8080"]
D --> M["운영체제가 값 넷을 맞춰 본다"]
subgraph P["포트 8080 에 붙은 연결"]
S1["첫째 연결의 소켓"]
S2["둘째 연결의 소켓"]
S3["셋째 연결의 소켓"]
end
M --> S3
비워 두는 값
소켓 주소의 두 값 가운데 하나를 「아무거나」로 비워 둘 수 있습니다. 그런 값이 둘 있습니다.
0.0.0.0 은 한 주소가 아니라 이 컴퓨터의 모든 주소를 뜻하는 값입니다. 서버가 0.0.0.0:8080 에 바인드하면 어느 주소로 들어온 연결이든 8080번이면 받습니다. 무엇이든 맞는다는 표시라서 이런 값을 와일드카드 주소라고 부릅니다. IPv6 에서는 :: 가 같은 뜻입니다.
포트 0은 번호를 운영체제에 맡긴다는 뜻입니다. 127.0.0.1:0 에 바인드하면 운영체제가 비어 있는 번호를 골라 붙입니다. 고른 번호는 바인드한 뒤 getsockname 으로 소켓에 물어서 알아냅니다. 이 함수는 소켓의 자기 쪽 소켓 주소를 돌려줍니다.
코드가 소켓 주소를 담는 모양
이 소절은 소켓 주소가 코드 안에서 어떤 값으로 다뤄지는지 봅니다. 언어마다 모양이 다릅니다. 먼저 표로 견줍니다. C 의 모양은 따로 풀어 봅니다.
파이썬은 주소와 번호를 튜플 하나에 담아 넘깁니다. 자바는 클래스 이름부터 소켓 주소입니다. new InetSocketAddress("10.0.0.1", 8080) 으로 만듭니다. 두 언어 모두 안에서는 운영체제의 C 함수를 부릅니다. 그래서 C 의 모양이 나머지의 바탕입니다.
C 에서 소켓을 다루는 함수 묶음을 버클리 소켓이라고 부릅니다. 이 함수들은 소켓 주소를 구조체로 받습니다. 구조체의 첫 칸에는 언제나 주소의 종류가 들어갑니다.
주소의 종류를 주소 패밀리라고 부릅니다. 이 값이 있어야 함수가 구조체의 나머지 칸을 어떻게 읽을지 압니다. 이름은 AF_ 로 시작합니다. AF(address family)는 주소 패밀리를 영어로 줄인 것입니다. IPv4 는 AF_INET 이고 IPv6 는 AF_INET6 입니다.
IPv4 소켓 주소를 담는 구조체가 sockaddr_in 입니다. 칸은 셋입니다.
| 칸 | 담는 값 |
|---|---|
sin_family |
주소 패밀리. 늘 AF_INET |
sin_port |
포트 번호 |
sin_addr |
IP 주소 |
IPv6 소켓 주소는 sockaddr_in6 라는 구조체에 따로 담습니다. 주소가 길어서 칸의 크기가 다릅니다.
bind 나 connect 같은 함수는 IPv4 주소도 IPv6 주소도 받아야 합니다. 그래서 특정 구조체 대신 공통 모양인 struct sockaddr 를 받습니다. 소켓 주소 구조체들은 모두 주소 패밀리를 첫 칸에 둡니다. 공통 모양은 이 첫 칸을 함께 읽으려고 둔 형입니다.
함수는 구조체를 통째로 받지 않습니다. 그 구조체가 놓인 메모리 위치를 받습니다. 이런 위치 값을 포인터라고 부릅니다. 부르는 쪽은 자기 구조체의 포인터를 struct sockaddr * 형으로 바꿔 넘깁니다.
이때 메모리의 바이트는 하나도 바뀌지 않습니다. 그 바이트를 어떤 구조체로 볼지만 바뀝니다. 함수는 첫 칸의 주소 패밀리를 읽고 원래 종류를 알아냅니다.
flowchart TD
subgraph A["sockaddr_in"]
A1["첫 칸 · AF_INET"] --> A2["나머지 칸 · 포트와 IPv4 주소"]
end
subgraph B["sockaddr_in6"]
B1["첫 칸 · AF_INET6"] --> B2["나머지 칸 · IPv6 에 맞춘 칸들"]
end
A --> P["struct sockaddr * 로 형만 바꿔 넘긴다"]
B --> P
P --> R["bind 가 첫 칸의 주소 패밀리를 읽는다"]
R -->|AF_INET| X["sockaddr_in 으로 읽는다"]
R -->|AF_INET6| Y["sockaddr_in6 으로 읽는다"]
아래는 10.0.0.1:8080 을 구조체에 담아 바인드하는 코드입니다. 오른쪽 주석은 그 줄이 메모리에 남기는 바이트입니다.
struct sockaddr_in a = {0};
a.sin_family = AF_INET;
a.sin_port = htons(8080); // 1F 90
inet_pton(AF_INET, "10.0.0.1",
&a.sin_addr); // 0A 00 00 01
bind(fd, (struct sockaddr *)&a,
sizeof a);
fd 는 앞서 만든 소켓을 가리키는 번호입니다. 소켓을 만들 때 운영체제가 돌려준 값입니다. &a 는 구조체 a 의 포인터입니다. 앞에 붙은 (struct sockaddr *) 가 그 포인터를 공통 모양의 형으로 바꿉니다.
첫 줄의 = {0} 은 구조체의 칸을 모두 0으로 채우고 시작한다는 뜻입니다. 마지막 줄의 sizeof a 는 구조체의 바이트 크기입니다. 함수는 포인터만 받아서는 구조체가 얼마나 큰지 모릅니다. 그래서 크기를 따로 받습니다.
8080 을 16진수로 쓰면 0x1F90 입니다. 이 값은 두 바이트입니다. 윗자리를 맡은 1F 가 상위 바이트입니다. 아랫자리인 90 은 하위 바이트입니다. 주석은 상위 바이트 1F 가 앞에 놓인다는 것을 보입니다.
네트워크로 오가는 번호는 상위 바이트부터 적기로 약속했습니다. 이 순서를 네트워크 바이트 순서라고 부릅니다. 보내는 쪽과 받는 쪽이 순서를 맞춰야 같은 번호로 읽습니다.
여러 바이트로 된 값을 어느 쪽 바이트부터 메모리에 놓느냐를 엔디언이라고 부릅니다. 요즘 흔한 컴퓨터의 CPU(Central Processing Unit, 중앙 처리 장치)는 대부분 하위 바이트를 앞에 둡니다. 그래서 번호를 바꾸지 않고 넣으면 바이트가 뒤집힌 채 나갑니다.
block-beta columns 3 space h1["앞 바이트"] h2["뒤 바이트"] a["htons 를 거친 8080"] b["1F"] c["90"] d["바꾸지 않고 넣은 8080"] e["90"] f["1F"]
htons 가 이 뒤집힘을 막습니다. 이름은 host to network short 의 줄임입니다. 이 컴퓨터의 순서로 된 16비트 값을 네트워크 순서로 바꿉니다.
inet_pton 은 "10.0.0.1" 같은 글자를 4바이트 주소로 바꿉니다. 주석의 0A 00 00 01 이 그 결과입니다. 10 이 16진수로 0A 입니다.
호스트 이름에서 소켓 주소로
애플리케이션 코드는 소켓 주소를 직접 적는 일이 드뭅니다. 설정 파일에는 보통 db.example.com:5432 처럼 이름과 포트를 적습니다. 이름이 소켓 주소가 되려면 한 단계를 더 거쳐야 합니다.
db.example.com 같은 컴퓨터의 이름을 호스트 이름이라고 부릅니다. 사람이 외우기 쉽게 붙인 이름이라 소켓은 이 이름을 모릅니다.
그래서 이름을 IP 주소로 바꾸는 일이 먼저 옵니다. DNS(Domain Name System, 도메인 네임 시스템)가 이 일을 맡습니다. 라이브러리는 받은 IP 주소에 포트를 붙여 소켓 주소를 만든 뒤 connect 를 부릅니다.
이름 하나가 IP 주소 여럿으로 풀리기도 합니다. 그러면 소켓 주소도 여럿이 생깁니다. 라이브러리는 대개 첫째에 먼저 붙어 봅니다. 실패하면 다음 것으로 넘어갑니다.
flowchart TD
A["설정 · db.example.com:5432"] --> B["DNS 가 이름을 IP 주소로"]
B --> C["10.0.0.21 · 10.0.0.22"]
C --> D["포트를 붙여 소켓 주소 둘<br/>10.0.0.21:5432 · 10.0.0.22:5432"]
D --> E["첫째에 connect"]
E -->|실패하면| F["둘째에 connect"]
그림은 이름 하나가 IP 주소 둘로 풀린 경우입니다. 「이름으로 접속했는데 어느 서버에 붙었나」를 알려면 연결된 소켓 주소를 봐야 합니다. 상대 쪽 소켓 주소는 getpeername 으로 소켓에 물어 알아냅니다.
IP 를 쓰지 않는 소켓 주소
소켓 주소가 언제나 IP 주소와 포트인 것은 아닙니다. 주소 패밀리가 다르면 주소의 모양도 다릅니다. IP 와 포트의 쌍은 그중 가장 흔한 모양입니다.
유닉스 도메인 소켓은 같은 컴퓨터 안의 프로그램끼리 통신하는 소켓입니다. 네트워크를 건너지 않으니 IP 주소와 포트가 필요 없습니다. 대신 /tmp/app.sock 같은 파일 경로가 소켓 주소입니다. 주소 패밀리는 AF_UNIX 입니다.
부르는 함수는 IP 소켓과 같습니다. bind 에 경로를 담은 구조체를 넘기면 그 경로에 소켓이 생깁니다. 상대는 connect 에 같은 경로를 적어 붙습니다. 주소의 모양만 바뀝니다. 쓰는 순서는 같습니다.
관련 항목
소켓 주소를 이루는 값
IP 주소 · 포트 · IPv4 · IPv6 · 주소 패밀리 · 와일드카드 주소 · 루프백 · 임시 포트 · 잘 알려진 포트
소켓 주소를 넘기고 돌려받는 호출
소켓 · 바인드 · 리슨 · accept · connect · getsockname · getpeername · 버클리 소켓 · 시스템 콜
소켓 주소를 코드에 담는 타입
sockaddr · sockaddr_in · sockaddr_in6 · sockaddr_un · sockaddr_storage · InetSocketAddress
소켓 주소를 바이트로 적는 규칙
네트워크 바이트 순서 · 엔디언 · htons · inet_pton
소켓 주소 여럿으로 연결을 가르는 묶음
4-튜플 · 5-튜플 · TCP · UDP · NAT · 포트 포워딩
소켓 주소를 두고 나는 오류와 그 해법
EADDRINUSE · EADDRNOTAVAIL · TIME_WAIT · SO_REUSEADDR · SO_REUSEPORT · 포트 고갈
소켓 주소와 헷갈리는 다른 주소
다른 이름: socket address · 소켓 어드레스