클라이언트로 말 걸기
앞 절에서 에코 서버는 accept()에서 멈춰 기다리고 있습니다. 이제 클라이언트를 만들어서 서버 문을 두드려 봅니다.
클라이언트 코드
# 새 파일: echo_client.pyimport socketHOST = "127.0.0.1"PORT = 50007with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client: client.connect((HOST, PORT)) client.sendall(b"Hello, network!") reply = client.recv(1024) print(f"서버가 보낸 응답: {reply.decode()}")
코드 해설
클라이언트도 서버와 같은 방식으로 소켓을 만듭니다. 차이는 bind와 listen이 없다는 점입니다. 클라이언트는 특정 포트에 자리를 잡을 필요가 없습니다. 운영체제가 적당한 임시 포트(ephemeral port)를 자동으로 배정해 줍니다.
client.connect((HOST, PORT))가 서버에 TCP 연결을 시도합니다. 이 순간 3-way 핸드셰이크가 일어납니다. 서버의 accept()가 이 연결을 받아 conn을 돌려줍니다. connect가 반환되면 연결이 완료된 것입니다.
client.sendall(b"Hello, network!")는 바이트 리터럴을 보냅니다. Python에서 b"..."는 바이트열입니다. 문자열인 "..."와 다릅니다. 소켓은 항상 바이트를 주고받습니다. 문자열을 보내려면 .encode()로 변환해야 합니다.
서버는 이 바이트를 받아 그대로 돌려보냅니다. reply = client.recv(1024)로 응답을 받고, .decode()로 문자열로 변환합니다. decode()의 기본 인코딩은 UTF-8입니다.
실습 — 터미널 두 개로 테스트
터미널 두 개를 나란히 열어 실행해 봅니다. 순서가 중요합니다. 서버를 먼저 실행해야 합니다.
터미널 1에서 서버를 시작합니다.
# 파일: 터미널 1python echo_server.py
서버가 127.0.0.1:50007에서 대기 중입니다.
터미널 2에서 클라이언트를 실행합니다.
# 파일: 터미널 2python echo_client.py
서버가 보낸 응답: Hello, network!
터미널 1에도 새 메시지가 나타납니다.
('127.0.0.1', 54321)에서 연결되었습니다.
54321 자리에는 운영체제가 배정한 임시 포트 번호가 나타납니다. 실행할 때마다 다를 수 있습니다. 클라이언트는 50007번으로 서버에 접속했지만, 자신의 포트는 운영체제가 알아서 고른 것입니다.
클라이언트가 없으면 서버는 어떻게 될까
서버는 accept()에서 계속 블로킹됩니다. 클라이언트가 연결하지 않으면 아무 일도 일어나지 않습니다. 이 단순한 서버는 클라이언트 하나를 처리하고 나면 프로세스가 종료됩니다. 여러 클라이언트를 동시에 처리하려면 스레드나 비동기 I/O가 필요합니다. 하지만 그것은 이 교재의 범위를 벗어납니다. 여기서는 소켓의 기본 흐름을 이해하는 것이 목적입니다.
다음 절에서는 서버를 종료하고 바로 재시작했을 때 만나게 되는 익숙한 오류와 그 해결책을 다룹니다.