안녕하세요, 개발자 여러분! 오늘은 pywebtransport 라이브러리를 사용하여 웹트랜스포트 클라이언트와 서버를 Docker 컨테이너 환경에서 운영할 때 발생하는 연결 타임아웃 문제에 대해 심층적으로 분석하고 해결 방법을 제시합니다.
특히 Docker 환경에서 컨테이너 간 통신 시 `127.0.0.1` (localhost) 주소 사용과 관련된 오해로 인해 발생하는 `pywebtransport.exceptions.ConnectionError: Session ready/closed timeout` 에러는 많은 개발자를 혼란스럽게 할 수 있습니다. 이 글을 통해 문제를 명확히 이해하고 효과적으로 해결하는 방법을 알아보겠습니다.
1. 에러 발생 상황
개발자는 pywebtransport 기반의 간단한 Echo 서버와 클라이언트를 구현하여, 각각 별도의 Docker 컨테이너에서 실행하려 했습니다. Docker Compose를 사용하여 두 컨테이너를 동일한 브리지 네트워크에 연결했지만, 클라이언트 컨테이너가 서버 컨테이너에 연결하는 데 실패하고 일정 시간(30초) 후 연결 타임아웃 에러를 발생시켰습니다.
클라이언트 코드 (`client.py`)
import asyncio
import ssl
from pywebtransport import ClientConfig, WebTransportClient
from pywebtransport.types import EventType
async def main() -> None:
config = ClientConfig(verify_mode=ssl.CERT_NONE)
async with WebTransportClient(config=config) as client:
session = await client.connect(url="https://127.0.0.1:4433/") # 주목!
await session.send_datagram(data=b"Hello, Datagram!")
event = await session.events.wait_for(event_type=EventType.DATAGRAM_RECEIVED)
if isinstance(event.data, dict) and (data := event.data.get("data")):
print(f"Datagram: {data!r}")
stream = await session.create_bidirectional_stream()
await stream.write_all(data=b"Hello, Stream!", end_stream=True)
response = await stream.read_all()
print(f"Stream: {response!r}")
await session.close()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
서버 코드 (`server.py`)
import asyncio
from pywebtransport import Event, ServerApp, ServerConfig, WebTransportSession, WebTransportStream
from pywebtransport.types import EventType
from pywebtransport.utils import generate_self_signed_cert
generate_self_signed_cert(hostname="127.0.0.1")
app = ServerApp(config=ServerConfig(certfile="127.0.0.1.crt", keyfile="127.0.0.1.key"))
@app.route(path="/")
async def echo_handler(session: WebTransportSession) -> None:
async def on_datagram(event: Event) -> None:
if isinstance(event.data, dict) and (data := event.data.get("data")):
await session.send_datagram(data=b"ECHO: " + data)
session.events.on(event_type=EventType.DATAGRAM_RECEIVED, handler=on_datagram)
try:
async for stream in session.incoming_bidirectional_streams():
asyncio.create_task(handle_stream(stream))
finally:
session.events.off(event_type=EventType.DATAGRAM_RECEIVED, handler=on_datagram)
async def handle_stream(stream: WebTransportStream) -> None:
try:
data = await stream.read_all()
await stream.write_all(data=b"ECHO: " + data, end_stream=True)
except Exception:
pass
if __name__ == "__main__":
print("Running app")
app.run(host="127.0.0.1", port=4433) # 주목!
Docker Compose 파일 (`docker-compose.yml`)
version: '3.8'
services:
wt-server:
build: ./server
ports:
- "4433:4433/udp"
container_name: wt-server
networks:
- wt-net
environment:
- PYTHONUNBUFFERED=1
wt-client:
build: ./client
depends_on:
- wt-server
networks:
- wt-net
container_name: wt-client
environment:
- PYTHONUNBUFFERED=1
networks:
wt-net:
driver: bridge
발생한 에러 로그
wt-client | Traceback (most recent call last):
...
wt-client | File "/app/client.py", line 12, in main
wt-client | session = await client.connect(url="https://127.0.0.1:4433/")
wt-client | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
wt-client | File "/usr/local/lib/python3.11/site-packages/pywebtransport/connection/connection.py", line 427, in wait_for_ready_session
wt-client | raise ConnectionError(message=f"Session ready/closed timeout after {timeout}s.") from None
wt-client | pywebtransport.exceptions.ConnectionError: [0x2] Session ready/closed timeout after 30.0s.
wt-client exited with code 1
로그에서 볼 수 있듯이, 클라이언트가 `127.0.0.1:4433`으로 연결을 시도하는 부분에서 타임아웃이 발생했습니다.
2. 명확한 발생 원인
이 문제는 Docker 네트워크 환경에 대한 이해 부족에서 비롯된 두 가지 핵심 원인으로 발생합니다.
2.1. 클라이언트의 잘못된 서버 주소 지정
클라이언트 코드(`client.py`)는 `url=”https://127.0.0.1:4433/”`로 서버에 연결을 시도합니다. 그러나 Docker 컨테이너 내부에서 `127.0.0.1`은 해당 컨테이너 자신을 가리키는 `localhost` 주소입니다. 즉, `wt-client` 컨테이너는 `wt-server` 컨테이너가 아닌 자기 자신에게 연결을 시도하고 있었던 것입니다.
Docker Compose는 `networks` 설정을 통해 컨테이너 간에 브리지 네트워크를 생성하고, 이 네트워크 내에서 각 서비스 이름(예: `wt-server`)을 사용하여 다른 컨테이너에 접근할 수 있도록 DNS 확인 기능을 제공합니다. 따라서 `wt-client` 컨테이너가 `wt-server` 컨테이너에 접근하려면 `127.0.0.1` 대신 서비스 이름인 `wt-server`를 사용해야 합니다.
2.2. 서버의 잘못된 바인딩 주소
서버 코드(`server.py`)는 `app.run(host=”127.0.0.1″, port=4433)`로 실행됩니다. 이는 서버 애플리케이션이 컨테이너 내부의 `localhost` 인터페이스(즉, `127.0.0.1`)에서만 연결을 수신하겠다는 의미입니다. 따라서 `wt-server` 컨테이너 내부에서만 이 서버에 접근할 수 있으며, 동일한 Docker 네트워크에 있는 다른 컨테이너(`wt-client`)는 이 서버에 연결할 수 없습니다.
Docker 컨테이너가 컨테이너 외부(호스트 머신)나 동일 네트워크 내의 다른 컨테이너로부터의 연결을 수락하려면, 서버 애플리케이션은 모든 네트워크 인터페이스를 의미하는 `0.0.0.0` 주소에 바인딩되어야 합니다.
요약하자면, 클라이언트는 잘못된 주소로 연결을 시도했고, 서버는 외부로부터의 연결을 수신할 준비가 되어 있지 않았던 것입니다.
3. 해결 방법 및 코드 예시
이 문제를 해결하기 위해서는 클라이언트와 서버 코드 모두에서 네트워크 관련 설정을 수정해야 합니다.
3.1. 서버 바인딩 주소 수정 (`server.py`)
서버가 모든 네트워크 인터페이스에서 연결을 수신하도록 `host` 주소를 `0.0.0.0`으로 변경합니다.
# server.py
# ... (생략) ...
if __name__ == "__main__":
print("Running app")
app.run(host="0.0.0.0", port=4433) # 127.0.0.1을 0.0.0.0으로 변경
3.2. 클라이언트 연결 URL 수정 (`client.py`)
클라이언트가 서버 컨테이너의 서비스 이름인 `wt-server`를 사용하여 연결하도록 `url`을 변경합니다.
# client.py
# ... (생략) ...
async def main() -> None:
config = ClientConfig(verify_mode=ssl.CERT_NONE)
async with WebTransportClient(config=config) as client:
# 127.0.0.1을 서버 서비스 이름인 wt-server로 변경
session = await client.connect(url="https://wt-server:4433/")
# ... (생략) ...
두 파일을 위와 같이 수정한 후, `docker-compose up –build` 명령으로 컨테이너를 다시 빌드하고 실행하면 클라이언트가 서버에 성공적으로 연결되고 Echo 메시지를 주고받는 것을 확인할 수 있습니다.
참고로, 서버의 `generate_self_signed_cert(hostname=”127.0.0.1″)`는 현재 클라이언트가 `verify_mode=ssl.CERT_NONE`으로 인증서 유효성 검증을 하지 않기 때문에 문제가 되지 않습니다. 하지만 프로덕션 환경에서는 서버의 실제 호스트 이름(이 경우 `wt-server`)과 일치하는 유효한 인증서를 사용하는 것이 보안상 매우 중요합니다.
4. 향후 예방을 위한 팁
- Docker 네트워크의 기본 이해:
- 각 컨테이너 내부에서 `127.0.0.1` 또는 `localhost`는 오직 해당 컨테이너 자신만을 의미합니다.
- Docker Compose 네트워크에서 다른 컨테이너에 접근하려면 해당 서비스의 이름(예: `wt-server`)을 DNS 호스트 이름처럼 사용해야 합니다.
- 컨테이너 외부(호스트 머신)나 다른 컨테이너로부터 연결을 수신하려면, 애플리케이션은 `0.0.0.0` 주소에 바인딩되어야 합니다.
- 환경 변수 활용: 서버 주소와 같은 동적인 값은 하드코딩하는 대신 환경 변수를 통해 관리하는 것이 좋습니다. 예를 들어, 클라이언트 컨테이너에 `SERVER_HOST=wt-server` 환경 변수를 전달하고 클라이언트 코드에서 이를 읽어 사용하는 방식입니다.
- 인증서 관리: `ssl.CERT_NONE`은 개발 및 테스트 시 편리하지만, 실제 서비스에서는 절대 사용해서는 안 됩니다. 보안을 위해 신뢰할 수 있는 CA(인증 기관)에서 발급받은 인증서를 사용하거나, 내부망의 경우 자체 CA를 구축하여 관리해야 합니다. 또한, 인증서의 `Subject Alternative Name (SAN)` 필드에 서버의 도메인 이름(또는 서비스 이름)이 정확히 명시되어야 합니다.
- 로깅 및 디버깅: 네트워크 관련 문제가 발생했을 때는 컨테이너 로그를 자세히 확인하고, 필요한 경우 컨테이너 내부에 접속하여 `ping`, `telnet`, `curl` 등의 도구로 연결 테스트를 수행하는 것이 유용합니다.
이 글이 pywebtransport와 Docker 환경에서 발생하는 연결 문제를 해결하고, 더 나아가 Docker 네트워크의 기본 개념을 이해하는 데 도움이 되기를 바랍니다. 안전하고 효율적인 개발 환경을 구축하시길 응원합니다!
![[에러 해결] CUDA 11.1 설치 불가: PyTorch 프로젝트 환경 설정 버전 불일치 원인과 해결 방법 [에러 해결] CUDA 11.1 설치 불가: PyTorch 프로젝트 환경 설정 버전 불일치 원인과 해결 방법](https://dev-error.com/wp-content/plugins/contextual-related-posts/default.png)