사전 SO_REUSEADDR
개념

SO_REUSEADDR

gabury1고친 사람 github-actions[bot]

SO_REUSEADDR 는 방금 끈 서버가 같은 포트를 곧바로 다시 쓰게 해 주는 소켓 설정입니다. 재시작한 서버는 포트가 아직 쓰인다는 오류를 받곤 합니다. 끝난 연결의 기록이 포트를 한동안 붙들고 있기 때문입니다. 서버 프로그램은 대개 포트를 잡기 전에 이 설정을 켭니다.

쉽고 빠른 이해

무슨 일을 하나 — 끝난 연결의 기록이 남은 포트를 서버가 다시 잡게 해 줍니다. 8080번에서 돌던 서버를 내리고 바로 올려도 Address already in use 오류 없이 뜹니다.

왜 필요한가 — 연결을 끊은 뒤에도 운영체제는 그 연결의 기록을 한동안 남겨 둡니다. 기본 규칙으로는 이 기록이 남은 포트를 아무도 새로 못 잡습니다. 배포할 때마다 서버가 그동안 못 뜨면 곤란합니다.

어떻게 도나

  1. 서버가 통신 창구를 만듭니다
  2. 포트를 잡기 전에 이 설정을 켭니다
  3. 운영체제는 남은 것이 끝난 연결의 기록뿐이면 포트를 내줍니다

대가 — 리눅스에서 달라지는 것은 재시작 때의 포트 잡기입니다. 지금 연결을 받는 서버의 포트는 여전히 못 잡습니다. 윈도우에서는 같은 이름의 설정이 남이 쓰는 포트까지 잡게 해 줍니다. 그러면 연결이 어느 서버로 갈지 정해지지 않습니다. 그래서 리눅스 서버는 이 설정을 켜 둡니다. 윈도우 서버는 대신 SO_EXCLUSIVEADDRUSE 라는 설정으로 포트를 지킵니다.

상세

회의실을 다 쓰고 나오면 문에 「정리 중」 팻말이 한동안 걸립니다. 기본 규칙은 팻말이 떼질 때까지 아무도 못 들어가는 것입니다. 다음 회의 주인이 「팻말만 걸려 있으면 들어가겠다」고 미리 말해 두면 바로 들어갈 수 있습니다.

SO_REUSEADDR 는 서버가 운영체제에 미리 해 두는 이 말입니다. 이 절은 먼저 재시작한 서버가 무엇에 막히는지 봅니다. 이어서 이 설정을 켜는 코드와 운영체제가 포트를 내주는 기준을 봅니다. 끝은 운영체제마다 달라지는 동작과 이 설정을 켜는 소켓입니다.

소켓은 프로그램이 네트워크로 데이터를 주고받으려고 운영체제에서 받아 쓰는 통신 창구입니다. 서버는 소켓으로 연결을 받고, 클라이언트는 소켓으로 연결을 겁니다.

소켓의 동작을 바꾸는 설정값을 소켓 옵션이라고 합니다. SO_REUSEADDR 는 그중 하나입니다. 기본값은 꺼짐입니다.

포트는 한 컴퓨터 안에서 데이터를 받을 프로그램을 가리키는 번호입니다. 한 컴퓨터에서 여러 서버가 함께 돌아도 포트 번호가 달라서 데이터가 제 프로그램을 찾아갑니다. 이 문서의 예시 서버는 8080 번을 씁니다.

IP 주소(Internet Protocol address, 인터넷 프로토콜 주소)는 네트워크에서 컴퓨터를 가리키는 번호입니다. 소켓은 IP 주소와 포트를 한 쌍으로 붙여 씁니다. 이 한 쌍을 소켓의 주소라고 부릅니다.

이름 앞의 SO(socket option)는 소켓 옵션을 뜻합니다. 뒤의 REUSEADDR(reuse address)는 주소를 다시 쓴다는 뜻입니다. 여기서 주소는 방금 본 IP 주소와 포트의 쌍입니다.

재시작한 서버가 포트를 못 잡는 까닭

서버 프로그램은 연결을 받기 전에 자기 소켓에 주소를 붙입니다. 이 일을 바인드라고 부릅니다. 운영체제는 바인드할 때 그 포트를 이미 누가 쓰는지 확인합니다. 쓰고 있으면 바인드를 거절합니다.

거절할 때 돌아오는 오류 코드가 EADDRINUSE 입니다. 「주소가 이미 쓰이고 있다」는 뜻입니다. 찍히는 메시지는 Address already in use 입니다. 서버를 두 번 띄우면 둘째 서버가 이 오류를 받습니다.

서버를 한 번만 띄워도 이 오류가 날 때가 있습니다. 서버를 끄고 곧바로 다시 띄운 경우입니다. 그 포트를 쓰는 프로그램이 하나도 없는데 바인드가 실패합니다.

원인은 방금 끝난 연결이 남긴 기록입니다. 서버의 연결은 대개 TCP(Transmission Control Protocol, 전송 제어 프로토콜)로 맺습니다. TCP 는 데이터가 빠짐없이 순서대로 닿게 챙기는 통신 방식입니다. 다 쓴 연결은 끊습니다.

TCP 연결을 먼저 끊은 쪽은 끊은 뒤에도 그 연결의 기록을 한동안 남겨 둡니다. 늦게 도착한 옛 데이터가 새 연결에 섞이지 않게 하려는 것입니다. 기록을 남겨 두고 기다리는 이 상태를 TIME_WAIT라고 부릅니다.

서버를 끌 때는 서버가 열린 연결을 먼저 끊습니다. 그래서 막 끈 서버의 컴퓨터에는 서버 포트를 쓰던 TIME_WAIT 기록이 여럿 남습니다. 바인드의 기본 규칙은 이 기록도 「쓰는 중」으로 칩니다. 기록이 사라질 때까지 새 서버는 그 포트를 못 잡습니다.

SO_REUSEADDR 를 켜면 이 규칙이 느슨해집니다. 남은 것이 끝난 연결의 기록뿐이면 운영체제가 바인드를 허락합니다. TIME_WAIT 기록은 지워지지 않고 제 시간까지 남습니다. 운영체제가 바인드를 판정할 때 그 기록을 거절할 이유로 치지 않습니다.

옵션을 켜는 코드

옵션은 setsockopt 라는 시스템 콜로 켭니다. 시스템 콜은 프로그램이 운영체제에 직접 부탁하는 호출입니다. 언어마다 있는 소켓 함수가 이 호출을 감쌉니다. 아래는 파이썬으로 적은 서버의 앞부분입니다.

Python
import socket
from socket import SOL_SOCKET, SO_REUSEADDR

s = socket.socket()
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s.bind(("0.0.0.0", 8080))
s.listen()

setsockopt 에는 값 셋을 넘깁니다. 둘째가 옵션 이름입니다. 셋째 1 은 켠다는 뜻입니다. 0 을 넘기면 끕니다.

첫째 SOL_SOCKET(socket level, 소켓 계층)은 옵션이 어느 계층의 것인지 가리킵니다. 이 값은 TCP 같은 프로토콜 하나의 옵션이 아니라 모든 소켓에 공통인 옵션이라는 표시입니다.

bind 에 넘긴 0.0.0.0 은 이 컴퓨터가 가진 모든 IP 주소를 한꺼번에 가리키는 값입니다. 이런 값을 와일드카드 주소라고 합니다. 이 값으로 바인드한 서버는 어느 주소로 온 연결이든 8080 번에서 받습니다.

listen 은 이 소켓으로 연결을 받겠다고 운영체제에 알리는 호출입니다. 이 일을 리슨이라고 합니다. 리슨하고 있는 소켓은 줄여서 리슨 소켓이라 합니다.

켜는 순서가 중요합니다. 운영체제는 바인드를 받는 그때 이 옵션을 보고 허락할지 정합니다. 그래서 setsockopt 는 bind 보다 먼저 불러야 합니다. 바인드한 뒤에 켜면 이미 끝난 판정은 바뀌지 않습니다.

자바에서는 ServerSocket 의 setReuseAddress(true) 가 같은 일을 합니다. 이 메서드도 바인드보다 먼저 불러야 합니다.

운영체제가 포트를 내주는 기준

옵션을 켰다고 무엇이든 허락하지는 않습니다. 리눅스는 그 포트를 먼저 쓰던 옛 소켓이 어떤 소켓인지를 보고 가립니다. 아래 그림은 리눅스가 바인드를 판정하는 차례입니다.

flowchart TD
    A["새 소켓이 8080 번에 bind"] --> B{"8080 번을 쓰는 옛 소켓이 있나"}
    B -->|없다| OK["바인드 성공"]
    B -->|있다| C{"옛 소켓 중에 리슨 소켓이 있나"}
    C -->|있다| NG["바인드 실패 · EADDRINUSE"]
    C -->|없다| D{"새 소켓과 옛 소켓이 모두 SO_REUSEADDR 를 켰나"}
    D -->|켰다| OK
    D -->|안 켰다| NG

그림의 옛 소켓은 새 소켓보다 먼저 8080 번을 잡은 소켓입니다. 막 끈 서버가 남긴 TIME_WAIT 기록도 여기에 듭니다. 리슨 소켓이 살아 있으면 옵션을 보기도 전에 거절합니다. 남은 것이 TIME_WAIT 기록이면 맨 아래 갈림까지 내려옵니다.

맨 아래 갈림은 새 소켓과 옛 소켓을 다 봅니다. 옛 서버가 옵션 없이 떴다면 새 서버만 켜서는 소용이 없습니다. 서버 프로그램은 대개 이 옵션을 늘 켜 둡니다. 그래서 이 조건이 드러날 일은 드뭅니다.

FreeBSD 같은 다른 운영체제는 새로 잡는 쪽만 켜면 됩니다.

한 포트에 서버 둘

리눅스에서는 이 옵션으로 서버 둘을 한 포트에 띄울 수 없습니다. 리슨 소켓이 있으면 거절하기 때문입니다. 아래 코드는 두 소켓이 모두 옵션을 켜고 같은 포트에 바인드합니다.

Python
a = socket.socket()
a.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
a.bind(("0.0.0.0", 8080))
a.listen()

b = socket.socket()
b.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
b.bind(("0.0.0.0", 8080))  # EADDRINUSE

a 가 리슨하고 있으므로 b 의 바인드는 거절됩니다. 파이썬에서는 이 거절이 OSError 예외로 올라옵니다.

여러 소켓이 한 포트에서 함께 연결을 받게 하는 옵션은 따로 있습니다. 이름은 SO_REUSEPORT 입니다. 들어오는 연결을 여러 프로세스가 나눠 받을 때 씁니다.

윈도우의 SO_REUSEADDR

같은 이름의 옵션이 윈도우에서는 다르게 돕니다. 윈도우의 SO_REUSEADDR 는 다른 소켓이 쓰고 있는 포트를 강제로 잡게 해 줍니다. 앞 소절의 코드가 윈도우에서는 성공합니다.

그 뒤로는 그 포트로 들어온 연결이 어느 소켓에 갈지 정해지지 않습니다. 이 옵션을 켜는 데 관리자 권한도 필요 없습니다. 그래서 악의적인 프로그램이 남의 서버 포트를 가로채는 데 쓸 수 있습니다.

윈도우에는 반대 방향의 옵션이 있습니다. SO_EXCLUSIVEADDRUSE 는 이름 그대로 주소를 혼자 쓰겠다는 옵션입니다. 이 옵션을 켠 소켓의 포트는 다른 소켓이 SO_REUSEADDR 를 켜도 못 잡습니다. 윈도우에서 서버 포트를 지키려면 이 옵션을 켭니다.

이 옵션을 켜는 소켓

리눅스에서 도는 TCP 서버는 리슨 소켓에 이 옵션을 켭니다. 클라이언트와 UDP 소켓은 사정이 다릅니다.

연결을 거는 클라이언트는 이 옵션과 상관없을 때가 많습니다. 클라이언트는 보통 바인드를 부르지 않습니다. 연결을 걸면 운영체제가 남는 포트 하나를 골라 붙여 주기 때문입니다. 이렇게 빌려 주는 번호를 임시 포트라고 부릅니다.

UDP(User Datagram Protocol, 사용자 데이터그램 프로토콜)는 연결을 맺지 않고 데이터 묶음을 하나씩 보내는 방식입니다. UDP 소켓은 리슨을 하지 않습니다. 그래서 리눅스에서는 모든 소켓이 이 옵션을 켰다면 여러 소켓이 같은 포트를 함께 잡을 수 있습니다.

이 쓰임은 멀티캐스트에서 나옵니다. 멀티캐스트는 데이터 하나를 여러 받는 쪽에 한꺼번에 보내는 방식입니다. 한 컴퓨터의 여러 프로그램이 같은 멀티캐스트 포트를 함께 받으려면 모두 이 옵션을 켜야 합니다.

윈도우에서 도는 서버에서는 앞 소절의 SO_EXCLUSIVEADDRUSE 가 이 옵션을 대신합니다. SO_REUSEADDR 를 켜면 남이 쓰는 포트까지 잡게 되기 때문입니다.

관련 항목

SO_REUSEADDR 를 켜고 읽는 호출

setsockopt · getsockopt · 소켓 옵션 · SOL_SOCKET · 시스템 콜 · 버클리 소켓

SO_REUSEADDR 가 판정을 바꾸는 소켓 호출

소켓 · 바인드 · 리슨 · accept · connect · close

SO_REUSEADDR 가 풀어 주는 재시작 오류와 그 원인

EADDRINUSE · TIME_WAIT · 4방향 핸드셰이크 · 2MSL · TCP 상태

SO_REUSEADDR 와 헷갈리는 포트 재사용 설정

SO_REUSEPORT · SO_EXCLUSIVEADDRUSE · tcp_tw_reuse · SO_LINGER · ip_local_port_range

SO_REUSEADDR 가 붙는 주소를 이루는 값

IP 주소 · 포트 · 소켓 주소 · 와일드카드 주소 · 임시 포트 · 4-튜플

SO_REUSEADDR 가 쓰이는 전송 프로토콜

TCP · UDP · 멀티캐스트 · 전송 계층

SO_REUSEADDR 의 동작이 갈리는 운영체제

Linux · Windows · FreeBSD · macOS · POSIX

SO_REUSEADDR 가 막거나 여는 포트 문제

포트 충돌 · 포트 하이재킹 · 포트 고갈 · 서비스 거부 공격

다른 이름: so_reuseaddr · SO_REUSEADDR 옵션 · reuseaddr · 주소 재사용 옵션