파이썬 websockets: 서버·클라이언트의 연결 수명주기

파이썬 websockets: 서버·클라이언트의 연결 수명주기

파이썬에서 WebSocket 서버와 클라이언트를 만들 때 쓰는 대표 라이브러리가 websockets예요. 정확성, 단순함, 견고성, 성능에 초점을 맞춘 라이브러리고, 여러 네트워크 I/O 패러다임을 지원합니다. 기본 구현은 파이썬 내장 비동기 I/O 라이브러리인 asyncio 위에서 동작해서, 많은 클라이언트 연결을 다루는 서버에 잘 맞아요. 이 글에서는 websockets의 에코 서버·클라이언트를 보며 핸드셰이크와 연결 수명주기가 어떻게 숨겨져 있는지 살펴볼게요.

출처: websockets - documentation

본문

에코 서버 (asyncio)

serve()로 서버를 열고, 각 연결마다 echo 코루틴이 실행돼요. async for message in websocket로 메시지를 하나씩 받아 그대로 다시 보냅니다.

#!/usr/bin/env python
"""Echo server using the asyncio API."""
import asyncio
from websockets.asyncio.server import serve

async def echo(websocket):
    async for message in websocket:
        await websocket.send(message)

async def main():
    server = await serve(echo, "localhost", 8765)
    await server.serve_forever()

if __name__ == "__main__":
    asyncio.run(main())

에코 클라이언트 (asyncio)

클라이언트는 connect()로 연결하고, async with 블록 안에서 send()recv()로 메시지를 주고받아요. 이 예제는 연결을 열자마자 "Hello world!"를 보내고 응답을 받아 출력합니다.

#!/usr/bin/env python
"""Client using the asyncio API."""
import asyncio
from websockets.asyncio.client import connect

async def hello():
    async with connect("ws://localhost:8765") as websocket:
        await websocket.send("Hello world!")
        message = await websocket.recv()
        print(message)

if __name__ == "__main__":
    asyncio.run(hello())

연결 수명주기를 라이브러리가 처리

여기서 중요한 점은, 오프닝·클로징 핸드셰이크와 ping·pong 같은 WebSocket 명세 동작을 직접 신경 쓸 필요가 없다는 거예요. websockets가 내부적으로 처리해 주기 때문에 우리는 애플리케이션 로직에만 집중하면 됩니다.

websockets에는 대화형 클라이언트도 있어서 서버를 직접 붙어 테스트해 볼 수 있어요.

$ websockets ws://localhost:8765/
Connected to ws://localhost:8765/.
> Hello world!
> < Hello world!
> Connection closed: 1000 (OK).

스레딩(threading) 구현

asyncio에 익숙하지 않다면 스레딩 구현도 좋은 선택이에요. 클라이언트 용도로 특히 편하고, 클라이언트 연결이 그리 많지 않은 서버에서도 쓸 수 있어요.

#!/usr/bin/env python
"""Client using the threading API."""
from websockets.sync.client import connect

def hello():
    with connect("ws://localhost:8765") as websocket:
        websocket.send("Hello world!")
        message = websocket.recv()
        print(message)

if __name__ == "__main__":
    hello()

스레딩 서버는 with serve(...) 블록 안에서 serve_forever()로 계속 살아있게 합니다.

#!/usr/bin/env python
"""Echo server using the threading API."""
from websockets.sync.server import serve

def echo(websocket):
    for message in websocket:
        websocket.send(message)

def main():
    with serve(echo, "localhost", 8765) as server:
        server.serve_forever()

if __name__ == "__main__":
    main()

버전 참고

asyncio 구현은 websockets.asyncio 모듈로 재작성됐어요(13.0 버전에서 도입). 역사적 구현인 websockets.legacy는 안정적이지만 14.0에서 deprecated 됐고 2030년까지 제거 예정이므로, 가능하면 새 구현으로 올리는 걸 권장합니다.

더 알아보기