Stop and Wait ARQ를 이용한 이미지 다운로드
주제
- Stop and Wait ARQ
과제: Download password image using Stop and Wait ARQ
요구사항
*Option 1) 이미지 다운로드 후 비밀번호 해독
- 비밀번호 수신 및 해독 과정 보고서
- 완벽한 Stop-and-Wait ARQ 클라이언트 구현이 아니어도 인정함 (단, 스크린샷 등으로 과정 자세히 설명)
코드 설명
- 권장하는 과제 구현 순서에 따라 12주차 DCFTv1에 seq, ack 기능을 덧붙여가며 먼저 로컬에서 서버와 클라이언트 작동을 테스트했고, 이후 완성된 클라이언트로 GCP 서버에서 이미지를 다운로드했다.
서버-클라이언트 공통 구현 부분
[ calculate_checksum ]

체크섬(checksum)은 데이터 전송 중에 오류를 감지하기 위해 사용되는 기술이다. 수신자(클라이언트)가 데이터 무결성을 체크하기 위해 사용된다.
서버는 모든 패킷에 checksum을 붙여 보낼 것이고, 클라이언트는 수신한 패킷의 checksum이 맞지 않으면 Ack를 보내지 않고 버려야 한다. 따라서 클라이언트에 checksum을 검증하는 기능을 구현해야 한다.
UDP 프로토콜에서 체크섬은 데이터를 2바이트(16비트) 단위로 처리한다. 이때 데이터가 홀수 바이트라면, 마지막 바이트가 완전한 16비트를 형성하지 못하여 체크섬을 계산하지 못한다. 원본 데이터에 손상을 가하지 않으면서 이를 해결하기 위해 0을 추가하여 길이를 짝수로 만들었다. 이때 바이트, big-endian 형식임에 주의해야 한다.
2바이트마다 1의 보수를 계산해 합산해야한다. 이를 int로 바꿔 계속 더한다. 체크섬은 크기를 유지해야 하니, 더하다 넘친 부분은 다시 앞으로 더해준다.
2바이트(16비트)를 정수로 표현하기 위해 원래는 2^16 식으로 65536을 만들었는데 실행이 너무 느려져서 하드코딩했다. 이 코드로 코딩테스트를 볼 건 아니니, 이렇게 해도 괜찮을 것 같다.
최종적으로 송신자는 데이터 전체를 다 더하여 결과가 0xFFFF(2^16 -1)가 되는 코드를 보내야 하므로, 65535에서 checksum을 뺀 것을 최종 체크섬으로 결정한다.
[ entry point block ]

- entry point block이다. 지난 주 과제에서 작성한 것과 같은 방식이나 포트 default를 3039로 설정하여 포트 인자 입력을 하지 않더라도 이후 진행할 GCP서버와의 통신에서도 수정 없이 작동되도록 하였다.
- 인자로 address와 port를 입력하니 argparse를 import에서 커맨드라인 인자를 처리하도록 한다.
- ArgumentParser()는 인자를 등록 및 파싱 가능하게 한다. UDP서버를 띄우기 위해 ip주소와 포트 번호가 필요하니 이를 사용해야한다.
- 주소를 –address로 입력받고 문자열로 처리, 반드시 입력하게 한다.
- 포트 번호를 –port로 입력받고 int로 처리, 값 입력 없을 시 기본값으로 3039를 사용하도록 한다.
- (지난 주) 과제 조건에 FLAGS.mtu가 명시되어있어서, 문자 그대로 써보려고 여기서 인자로 같이 넘기는 방법을 선택했다. default값을 1500으로 둬서, 요청받은 파일을 1500 Bytes씩 잘라서 전송하는 기준을 설정했다. 내가 구현한 코드에서는 서버에서 mtu 인자가 필요 없지만, 클라이언트 코드와의 일관성을 위해 유지했다.
- 서버가 클라이언트에게 제공할 파일들이 있는 폴더의 경로를 명시하여 인자로 넣는다. 클라이언트 코드는 이 부분이 빠졌고 나머지는 동일하다.
- 앞서 받은 인자들을 파싱하여 FLAGS 객체에 저장하고, 이를 main()에 넘겨 실행한다.
서버(server.py)
- 12주차 과제에서 Checksum, Stop-and-Wait ARQ 적용한 부분 제외 거의 동일하다.
- INFO
/ DOWNLOAD 프로토콜을 구현했다. 저번 주 과제에서는 이에 대한 언급은 있었으나 과제의 구체적인 프로토콜 사용 조건이 없었으므로, 사용자가 filename 문자열로 보내면 알아서 서버에서 INFO/DOWNLOAD 나누어 처리하도록 했는데 이번에는 더 명확하게 양측 코드에서 프로토콜을 찾을 수 있다.
[ 전역변수와 checksum 함수 ]

- HEADER, TIMEOUT을 설정한다. UDP Packet 위에 첫 2 Bytes 공간을 Seq / Ack number 로 사용하고 이후 2 Bytes 는 Checksum 으로 활용하므로 총 4바이트를 차지한다. 이에 HEADER를 4로 설정하고, TIMEOUT은 ARQ 구현을 위해 설정했는데 적당히 위 변수값과 맞추어 4(초)로 설정했다. 강의자료에 따라 서버의 최대 재전송 횟수 5를 나타내는 MAX_RETRY변수와 DCFTv2에서의 안정적인 전송을 위한 데이터 최대 전송 크기인 DATA_MAX_SIZE도 지정해준다.
- checksum 함수는 상단에 설명했으므로 생략.
[ 파일 처리 ]

- get_file_list는 폴더 내 모든 파일의 경로 및 크기를 딕셔너리 형태로 반환하는 함수이다. scan_dir 함수를 써서 재귀 방식으로 폴더를 탐색한다. 파일을 발견하면 os.path.getsize()로 파일 크기를 구하고, 상대경로인 rel_path를 키로 해서 절대 경로인 full_path와 크기를 file_info 딕셔너리에 저장한다.
- 운영체제마다 디렉토리 구분자가 달라서 os.path.join()을 사용해서 경로를 처리하고, 폴더인지 파일인지는 os.path.isdir()로 구분한다.
- 이 함수는 이후 main에서 서버가 실행된 후에 실행된다. 사용자에게 요청한 파일에 대한 정보를 알려준다.
[ main - INFO 요청 처리 ]

- entry block에서 인자를 전달받는다.
- 넣은 주소가 IPv4 주소 체계이니 이를 사용하도록 socket.AF_INET을, UDP 소켓을 만들어야 하니 socket.SOCK_DGRAM을 같이 인자로 사용한다. 지정된 IP와 포트에 바인딩하여 클라이언트 요청을 받을 준비를 한다.
- 앞서 설명한 get_file_list()함수를 사용했고, 이는 상대경로 기준 파일명과 파일 크기를 출력해서 어떤 파일들이 서버에 있는지 보여준다.
- While문으로 무한루프를 돌며 지속적으로 클라이언트의 요청을 수신하고 처리할 수 있게한다. 클라이언트로부터 최대 100바이트를 수신할 수 있게 했는데, 이 값은 테스트/과제 환경 기준 파일명이 100바이트까지 길어질 일이 없어서 넉넉하게 임의로 잡았다. 받을 데이터는 바이트형 파일이름이고, UTF-8로 인코딩하고 공백을 제거한다.
- 클라이언트로부터 받은 메시지가 ‘INFO ‘로 시작하면 프로토콜에 의해 이후 파일명이 온다. 파일명을 분리해 저장한다.
- 파일이 존재하면 그 크기를 문자열로 변환 후 바이트로 인코딩, 존재하지 않을 시 404 메시지로 응답하고 해당하는 패킷을 만들어 보낸다. 그리고 continue를 통해 루프의 처음으로 이동하여 다른 파일을 입력받을 수 있게 했다.
- 여기부터 Stop and Wait 방식이 구현되었다. ACK는 0과 1 순서 반복으로 보내게 되므로, 현재 seq를 기억할 변수가 필요하여 만들었다. 체크섬을 계산하여 헤더를 만든다(struct의 pack 사용, >:big endian, HH: 2바이트 정수 2개 → 각각 seq 번호(2Bytes)와 checksum(2Bytes)으로 구성되어있음).
- 시간 초과 조건을 판별하기 위해 settimeout을 상단에 정의해둔 TIMEOUT값으로 설정한다. 이후 while 루프로 재전송 횟수를 5회로 제한한다. 변수로 카운트하여 최대 전송 횟수에 다다르면 break로 루프의 처음으로 이동하여 다른 파일을 입력받을 수 있게 했다. while문 밖 if문에서 프로그램을 종료 가능하다(#5회 재전송 실패 시 종료).
- while루프 내부에서 헤더와 응답을 붙여 클라이언트로 전체 패킷을 전송한다. 필요에 따라 반복 전송하기 위함이다. 클라이언트로부터 2바이트 ACK가 수신되면 big endian 2바이트 정수로 디코딩하고, 예상한 ACK가 오면 루프를 종료한다 (seq=0이면 예상 ACK는 1)
- INFO 요청에 대한 처리가 끝나면 timeout을 None으로 지정하여 INFO에 해당하는 while문을 빠져나와 클라이언트의 DOWNLOAD 요청을 대기한다.
[ main - DOWNLOAD 요청 처리 ]

- 에러가 없는 한 클라이언트 요청은 INFO 아니면 DOWNLOAD이므로 if-elif로 처리했다.
- 전체 패킷의 최대 크기는 1500이다. 이를 넘어가면 안되므로 4바이트 헤더 + 파일 데이터(chunk) 구조 유지 및 안정적 전송을 위해 사전에 정의한 DATA_MAX_SIZE만큼의 chunk만 데이터를 읽어온다.
- 체크섬을 계산하여 헤더를 만들고 최대 5회 재전송 구현 while문을 만든다.
- 클라이언트에게 패킷을 보내면 패킷마다 2바이트짜리 클라이언트의 ACK를 기다려 INFO 처리때와 마찬가지로 ACK 값 검증을 한다 (현재 seq=0이면 예상 ACK는 1). 맞다면 break를 통해 다음 chunk로 넘어가며 파일을 전송한다. 만약 클라이언트가 ACK 안 보내는 것 같다면 timeout이 발생하여 알아서 재전송을 하게 된다.
- INFO 처리와 다른 점은, is_last 처리와 seq값의 갱신이다.
- 전송이 끝나면(while 탈출) 파일 객체와 소켓을 닫고 종료한다.
[ entry point block ]

- 상단에 설명하였으므로 생략
클라이언트 (client.py)

- server.py 코드에서 설명하였으므로 생략
[ main - INFO 요청 처리 ]

- 넣은 주소가 IPv4 주소 체계이니 이를 사용하도록 socket.AF_INET을, UDP 소켓을 만들어야 하니 socket.SOCK_DGRAM을 같이 인자로 사용한다.
- while문을 돌려서 사용자가 종료를 원하지 않는 한(ctrl+c) 계속 파일을 요청할 수 있게 했다. 사용자로부터 파일 이름을 입력받아 sendto()함수를 이용해 서버에 UDP방식으로 메시지를 보낸다.
- 이때 INFO 파일명으로 먼저 요청한다.
- 응답 패킷을 수신하고 패킷 첫 2바이트의 시퀀스 번호와 다음 2바이트의 체크섬을 추출, 나머지 부분은 데이터로 처리한다.
- 계산한 체크섬과 맞지 않거나 예상한 시퀀스와 맞지 않으면 무시(버림)하고 다음 패킷을 기다린다. 만약 패킷을 제대로 수신했다면 ACK을 보내고 응답을 디코딩하여 출력한다. 404가 오는 경우 그냥 다시 파일명을 입력하도록 continue 처리한다.
[ main - DOWNLOAD 요청 처리 ]

- 응답으로 받은 response에서 파일 크기를 추출하여 사용자에게 로그로 알리고 서버에 DOWNLOAD 파일명으로 파일 전송을 요청한다.
- 서버가 보낸 데이터 패킷을 순서대로 수신, 저장한다. expected는 다음에 올 것으로 기대되는 시퀀스 번호, remain은 수신 완료 여부를 확인하기 위해 남은 파일 바이트 수이다.
- 패킷마다 체크섬을 확인하고 sequence를 확인한다 (’>H’: Big endian 2bytes).
- 모든 데이터 수신 후 연결을 종료하고자 한다. 안정적인 종료를 위해, 클라이언트는 마지막 ACK를 보낸 뒤 Timer(TIMEOUT)의 2배 시간 동안 대기한다.
- 이후 소켓을 닫고 연결을 종료한다.
[ entry point block ]

- 상단에 설명하였으므로 생략
로컬 작동 테스트

VM으로 서버 코드와 다운로드할 파일 보내고 서버 띄우기

404 테스트 위해 filename을 1로 지정 → continue로 다시 파일명 입력으로 돌아옴
실제 받고자 하는 파일(ditto.png) 요청

INFO 요청 패킷

DOWNLOAD 요청 패킷

서버는 Len=603 마지막 패킷 전송 후 클라이언트의 마지막 ACK 패킷 받고 종료

클라이언트는 마지막 ACK 보낸 후 Timer의 2배 시간 동안 대기 후 종료

정상 수신 확인 (파일 크기 일치)



임의의 ACK 연속 패킷의 끝부분을 참고했을 때 ACK sequence 동작(0→1→0→…) 확인 가능
비밀번호 (GCP 서버 접속)
과정(패킷 캡쳐)

INFO 파일명

DOWNLOAD 파일명

마지막 패킷 length < 1502
마지막 ACK 발신, 클라이언트 대기 후 종료


터미널
결과

나는 7P4VF4OU라는 문자열이 적힌 이미지를 수신했다. (202302627.jpg)
Leave a comment