https://github.com/kyeongjun-dev/network
시리즈 2에서 server 코드에 server_socket.settimeout(10)을 설정하여 10초동안 client에서 연결하지 않으면, server가 종료된다고 언급했다.
그런데 사실 10초동안 '연결'이 안되면 종료하는 것에 더하여, 연결된 후로 '10초 동안 아무런 동작을 하지 않으면' 조건에서도 종료된다.
정리하자면
1. 소켓의 timeout은 10초동안 client로부터 연결이 없으면, 종료된다.
2. 소켓의 timeout은 연결된 후, 10초동안 아무런 활동이 없으면 종료된다.
이를 실제로 구현하고 테스트해보자.
timeout(여기선 10초)로 설정된 소켓을 생성한 뒤, client로부터 전송된 메시지를 출력한다. - 04/server.py 파일
import socket
timeout_duration = 10
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('127.0.0.1', 30000))
server_socket.listen()
print("클라이언트 접속을 기다립니다...")
# accept()는 블로킹 상태로 무한정 대기
client_socket, addr = server_socket.accept()
print(f"{addr} 에서 접속했습니다. 이제부터 {timeout_duration}초 내에 데이터를 보내야 합니다.")
# 'accept'로 생성된 클라이언트와의 통신 소켓에 타임아웃(timeout_duration) 설정
client_socket.settimeout(timeout_duration)
while True:
try:
# recv()는 데이터가 들어올 때까지 여기서 실행을 멈춤(blocking)
# settimeout() 때문에 timeout_duration초가 지나면 socket.timeout 예외를 발생시킴
data = client_socket.recv(1024)
# 클라이언트가 연결을 정상적으로 종료한 경우
if not data:
print("클라이언트가 연결을 끊었습니다.")
break
print(f"수신 메시지: {data.decode('utf-8')}")
client_socket.sendall("메시지를 잘 받았습니다!".encode('utf-8'))
except socket.timeout:
print(f"{timeout_duration}초 동안 데이터 수신이 없어 연결을 종료합니다.")
break # while 루프 탈출
except ConnectionResetError:
print("클라이언트와의 연결이 비정상적으로 끊어졌습니다.")
break
except Exception as e:
print(f"오류가 발생했습니다: {e}")
break
# 소켓 정리
print(f"{addr} 와의 연결을 닫습니다.")
client_socket.close()
server_socket.close()
연결된 후, timeout_duration에 설정된 초만큼 아무것도 하지 않고 기다린다. 이때 이 값을 04/server.py의 timeout 시간보다 길게 한다(여기선 15초) - 04/client.py 파일이다.
import socket
import time
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
client_socket.connect(('127.0.0.1', 30000))
print("서버에 접속했습니다.")
print("아무 내용이나 입력 후 엔터를 누르면 서버로 메시지를 보냅니다.")
print("10초 이상 아무것도 안 하면 서버에서 연결이 끊깁니다.")
print("종료하려면 'exit'을 입력하세요.")
while True:
wait_start_time = time.time()
# 사용자 입력 대기
message = input("> ")
print(f"{time.time() - wait_start_time}초 이후에 메시지를 전송합니다.")
if message.lower() == 'exit':
break
# 메시지 전송
client_socket.sendall(message.encode('utf-8'))
# 서버로부터 응답 수신
data = client_socket.recv(1024)
print(f"서버 응답: {data.decode('utf-8')}")
except ConnectionRefusedError:
print("서버에 연결할 수 없습니다.")
except (ConnectionResetError, BrokenPipeError):
print("서버에 의해 연결이 끊겼습니다.")
except Exception as e:
print(f"오류가 발생했습니다: {e}")
finally:
print("클라이언트를 종료합니다.")
client_socket.close()
04/server.py를 실행한 뒤, 04/client.py를 실행하고 server의 timeout 이내에 메시지를 전송하면 계속 ping-pong이 되다가 timeout이 넘어서 메시지를 전송하면 server는 종료되고, client도 연결을 종료한다.

위 캡처에서 10.75초 이후에 메시지를 전송했을 때, 빈 문자열이 서버 응답으로 오는데
이는 서버로부터 빈 문자열 b''를 받았기 때문이다. 이를 실제로 wireshark로 패킷 분석해보자. (소스코드 : 레포 05 디렉토리)
05 디렉토리로 이동 후, docker compose를 실행한다.
docker-compose up --build
05/cmd파일을 참고해서 clinet, server 컨테이너에서 tcpdump를 진행한다.
docker exec -it client tcpdump -i any -n 'port 30000' -w /app/captures/client.pcap
docker exec -it server tcpdump -i any -n 'port 30000' -w /app/captures/server.pcap
05/cmd 파일을 참고해서 client, server 스크립트를 실행시킨 후 10초 내에 메시지 전송 및 10초 이후에 메시지 전송을 진행한다.

tcpdump를 Ctrl + C로 종료한다. 그리고 wireshark로 패킷을 확인한다.

분석을 해보면 (server가 172.22.0.2, client가 172.22.0.3)
1. ~0초 : tcp 연결
2. 3.6초 : client가 server에 hello 메시지 전송 및 server가 client에게 응답
3. 8.4초 : client가 server에 world 메시지 전송 및 server가 client에게 응답
4. 18.4초 : server가 client에 FIN, ACK 전송 및 client가 server에 ACK 응답 - tcp 연결 해제
5. 20.3초 : client가 server에 메시지를 전송하지만, 이미 닫힌 연결이라서 server가 client가 RST 전송 - tcp 연결은 해제되었지만, client 코드상에서는 사용자의 input을 그대로 기다림
05정도만 되도 어느정도 ping-pong하는 시나리오를 테스트할 수 있다. 하지만 결국 실제 백엔드 서버에서 어떻게 동작하는지 테스트를 하지 않으면 와닿지(?) 않기 때문에
다음 글에서는 gunicorn + flask 조합으로 백엔드 서버를 구성하고, 패킷 분석도 해보도록 한다.