주제

  • File Transfer

과제1: File Transfer Server & Client (Basic)

요구사항

  • 클라이언트와 서버 코드를 작성하여, 다른 IP 주소끼리 통신하여 파일을 주고받는다.

Client code (asgn1_client.py)

조건

  • 강의자료에 나와있는 실습코드 기준으로 작성했다.
  • 서버에 파일을 입력하여 요청하고, 이를 전달받는다.

코드 설명

  • 서버에 UDP로 전송해야 하므로, 필요한 socket 모듈을 import한다.
  • 실습 코드를 참고하여 작성했다. 넣은 주소가 IPv4 주소 체계이니 이를 사용하도록 socket.AF_INET을, UDP 소켓을 만들어야 하니 socket.SOCK_DGRAM을 같이 인자로 사용한다.
  • while문을 돌려서 사용자가 종료를 원하지 않는 한(ctrl+c) 계속 파일을 요청할 수 있게 했다. 사용자로부터 파일 이름을 입력받아 sendto()함수를 이용해 서버에 UDP방식으로 메시지를 보낸다.
  • 이후 서버로부터 파일 응답을 수신하는데, 파일이 없으면 서버에서 과제의 조건대로 ‘404 Not Found’를 반환하게 했고, 이를 클라이언트가 받을 경우 바이트 형태의 데이터이므로 UTF-8 인코딩하여 문자열로 출력되게 했다.

  • 만약 서버에 존재하는 파일을 입력한 경우, 파일 크기를 첫 응답으로 받는다. 이후의 파일 수신 로직 처리에 필요하지만 사용자 딴에서 이 값을 굳이 궁금해하지는 않을 것 같아, 로그로 출력하진 않았다. 이 크기를 바탕으로 수신을 준비한다.
  • 서버로부터 mtu단위로 데이터를 반복해서 수신한다(이후 설명하겠지만, mtu는 entry block point에서 인자로 전달한다). 파일을 바이너리로 열고 수신한 chunk를 계속 저장하며 remain값을 줄이고, 수신이 끝날 때까지 반복한다(remain≤0)
  • 파일을 전부 수신한 후 사용자가 알 수 있게끔 성공 메시지를 출력한다.
  • try-except문을 통해 사용자가 ctrl+c를 누르는 등 종료 시 KeyboardInterrupt로그를 길게 출력하지 않나 ‘Shutting down’문구를 출력하며 예쁘게 처리되도록 했다.

  • entry point block이다. 지난 주 과제에서 작성한 것과 같은 방식이다.
  • 인자로 address와 port를 입력하니 argparse를 import에서 커맨드라인 인자를 처리하도록 한다.
  • ArgumentParser()는 인자를 등록 및 파싱 가능하게 한다. UDP서버를 띄우기 위해 ip주소와 포트 번호가 필요하니 이를 사용해야한다.
  • 주소를 –address로 입력받고 문자열로 처리, 반드시 입력하게 한다.
  • 포트 번호를 –port로 입력받고 int로 처리, 값 입력 없을 시 실습 코드처럼 기본값으로 3034를 사용하도록 한다.
  • 과제 조건에 FLAGS.mtu가 명시되어있어서, 문자 그대로 써보려고 여기서 인자로 같이 넘기는 방법을 선택했다. default값을 1500으로 둬서, 요청받은 파일을 1500 Bytes씩 잘라서 전송하는 기준을 설정했다.
  • 앞서 받은 인자들을 파싱하여 FLAGS 객체에 저장하고, 이를 main()에 넘겨 실행한다.

코드 전문

```plain text import socket

def main(server_address, server_port, mtu): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
    try:
        filename = input('Filename: ').strip()
        sock.sendto(filename.encode('utf-8'), (server_address, server_port))
        response, server = sock.recvfrom(mtu)
        response = response.decode('utf-8')
        if response == '404 Not Found':
            print(response)
            continue

        size = int(response)
        print(f'Requested {filename} ({size} bytes) from {server_address}:{server_port}')
        
        remain = size
        with open(filename, 'wb') as f:
            while remain > 0:
                chunk = sock.recv(mtu)
                f.write(chunk)
                remain-=len(chunk)
        print(f'File download success')

    except KeyboardInterrupt:
        print(f'Shutting down... {sock}')
        break

if name == ‘main’: import argparse parser = argparse.ArgumentParser() parser.add_argument(‘–address’, type=str, required=True) parser.add_argument(‘–port’, type=int, default=3034) parser.add_argument(‘–mtu’, type=int, default=1500) FLAGS = parser.parse_args()

main(FLAGS.address, FLAGS.port, FLAGS.mtu) ```

Server code (asgn1_server.py)

조건

  • 클라이언트에서 요청받은 파일을 전송해준다. 이때 파일을 보내기 전 파일의 크기부터 전송한다.
  • 서버 시작 시 전송가능한 파일 리스트 및 정보(파일 이름, 경로, 크기)를 저장해두면 좋다고 하니, 이를 서버 측에서 출력되도록 한다.

코드 설명

  • 필요한 모듈을 import한다. 서버 시작 시 전송 가능한 파일 리스트 및 정보를 저장해두면 좋다고 하여, 운영체제의 파일, 폴더, 경로, 권한을 파이선 코드로 사용할 수 있게 해주는 os모듈을 import했다.
  • get_file_list는 폴더 내 모든 파일의 경로 및 크기를 딕셔너리 형태로 반환하는 함수이다. scan_dir 함수를 써서 재귀 방식으로 폴더를 탐색한다. 파일을 발견하면 os.path.getsize()로 파일 크기를 구하고, 상대경로인 rel_path를 키로 해서 절대 경로인 full_path와 크기를 file_info 딕셔너리에 저장한다.
  • 운영체제마다 디렉토리 구분자가 달라서 os.path.join()을 사용해서 경로를 처리하고, 폴더인지 파일인지는 os.path.isdir()로 구분한다.
  • 이 함수는 이후 main에서 서버가 실행된 후에 실행된다.

  • main함수이다.
  • 넣은 주소가 IPv4 주소 체계이니 이를 사용하도록 socket.AF_INET을, UDP 소켓을 만들어야 하니 socket.SOCK_DGRAM을 같이 인자로 사용한다. 지정된 IP와 포트에 바인딩하여 클라이언트 요청을 받을 준비를 한다.
  • 앞서 설명한 get_file_list()함수를 사용했고, 이는 상대경로 기준 파일명과 파일 크기를 출력해서 어떤 파일들이 서버에 있는지 보여준다.
  • While문으로 무한루프를 돌며 지속적으로 클라이언트의 요청을 수신하고 처리할 수 있게한다. 클라이언트로부터 최대 100바이트를 수신할 수 있게 했는데, 이 값은 나의 테스트 환경 기준 파일명이 100바이트까지 길어질 일이 없어서 넉넉하게 임의로 잡았다.
  • 받은 데이터는 바이트형 파일이름이고, UTF-8로 인코딩하고 공백을 제거한다. client는 응답을 보낼 대상이며 (IP, port) 튜플로 구성했다.
  • 요청한 파일이 없는 경우 과제 조건대로 “404 Not Found”를 client로 전송하게 했다. continue를 통해 서버는 이 요청을 무시하고 다음 요청을 기다린다.

  • 파일 전체를 읽어 메모리에 로드해둔다.
  • 클라이언트에서 수신 루프를 구현했다. 어떤 크기의 파일을 다운로드 받아야하는지 미리 알 수 있게, 파일을 보내기 전에 파일 크기부터 인코딩하여 전송되도록 했다.
  • sent는 클라이언트에게 전송된 바이트 수를 의도했다. 파일 데이터를 mtu(1500 bytes)단위로 잘라서 클라이언트에 전송한다(이때 클라이언트는 이 크기만큼 recvfrom()을 반복호출하여 저장한다). sent를 계속 업데이트하고, 모든 데이터를 보낼 때까지(sent ≥ fsize) 반복한다.

  • entry point block이다. 지난 주 과제에서 작성한 것과 같은 방식이다.
  • 인자로 address와 port를 입력하니 argparse를 import에서 커맨드라인 인자를 처리하도록 한다.
  • ArgumentParser()는 인자를 등록 및 파싱 가능하게 한다. UDP서버를 띄우기 위해 ip주소와 포트 번호가 필요하니 이를 사용해야한다.
  • 주소를 –address로 입력받고 문자열로 처리, 반드시 입력하게 한다.
  • 포트 번호를 –port로 입력받고 int로 처리, 값 입력 없을 시 실습 코드처럼 기본값으로 3034를 사용하도록 한다.
  • 과제 조건에 FLAGS.mtu가 명시되어있어서, 문자 그대로 써보려고 여기서 인자로 같이 넘기는 방법을 선택했다. default값을 1500으로 둬서, 요청받은 파일을 1500 Bytes씩 잘라서 전송하는 기준을 설정했다.
  • 서버가 클라이언트에게 제공할 파일들이 있는 폴더의 경로를 명시하여 인자로 넣는다.
  • 앞서 받은 인자들을 파싱하여 FLAGS 객체에 저장하고, 이를 main()에 넘겨 실행한다.

코드 전문

```plain text import socket import os

def get_file_list(directory): file_info = {}

def scan_dir(current_dir, relative_path=''):
    for name in os.listdir(current_dir):
        full_path = os.path.join(current_dir, name)
        rel_path = os.path.join(relative_path, name)
        if os.path.isdir(full_path):
            scan_dir(full_path, rel_path)
        else:
            size = os.path.getsize(full_path)
            file_info[rel_path] = {'path': full_path, 'size': size}

scan_dir(directory)
return file_info

def main(server_address, server_port, mtu, shared_dir): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((server_address, server_port)) print(f’Listening on {server_address}:{server_port}’)

file_list = get_file_list(shared_dir)
print("Available files:")
for name, info in file_list.items():
    print(f"- {name} ({info['size']} bytes)")


while True:
    data, client = sock.recvfrom(100)
    fname = data.decode('utf-8').strip()
    print(f"{client} requested {fname}")

    try:
        f = open(fname, 'rb')
    except:
        sock.sendto("404 Not Found".encode('utf-8'), client)
        print(f"File {fname} not found")
        continue

    file_data = f.read()
    f.close()

    fsize = len(file_data)
    sock.sendto(str(fsize).encode('utf-8'), client)

    sent = 0
    while sent < fsize:
        chunk = file_data[sent:sent + mtu]
        sock.sendto(chunk, client)
        sent += len(chunk)

    print(f"Sent {fname} to {client}")

if name == ‘main’: import argparse parser = argparse.ArgumentParser() parser.add_argument(‘–address’, type=str, required=True) parser.add_argument(‘–port’, type=int, default=3034) parser.add_argument(‘–mtu’, type=int, default=1500) FLAGS = parser.parse_args()

shared_dir ='/home/syc'
main(FLAGS.address, FLAGS.port, FLAGS.mtu, shared_dir)

```

테스트

Client는 Host PC를 쓰고 VM을 서버로 쓸 것이므로, 일단 Host PC에서 작업한 서버 코드와 요청할 파일을 미리 VM으로 보냈다.

의도한 대로 동작하며, 모든 파일이 잘 다운로드되었다.

  • transferfile1.txt를 요청하여 다운로드해보고, 없는 파일을 요청하여 404가 뜨게 한 결과이다. Wireshark로 패킷을 캡쳐한 결과, 서로 다른 IP 주소 간 UDP 통신이 진행되고 있음을 알 수 있다.

과제2: File transfer server / client with packet corruption

요구사항

  • UDP 파일 송수신간 Packet Corruption 발생시키기
  • VM 에서 서버 실행
  • corruption 으로 다운로드 완료된 파일이 손상됨을 보일 것 (패킷 캡쳐, 이미지)

코드

  • 과제1에서 사용한 asgn1_server.py와 asgn1_client.py를 그대로 사용했다.

테스트

  • 받아야 할 원본 파일(png)이다.

1. 기본 테스트

  • 10퍼센트의 손실률을 주고 테스트해봤다.
  • 서버는 전송을 완료했지만 클라이언트측에서는 완료 로그가 나오지 않는다. 패킷 손상으로 일부 데이터가 누락되어, 처음 받은 파일 크기만큼 데이터를 전송받지 못해 recv()에서 멈춘다.

2. 손실률 변경

  • 기존 설정을 삭제하고 손실률을 5퍼센트로 낮춰보았다. 또한 클라이언트 코드의 수신 루프 내부에 remain 값을 보여주는 로그를 넣었다.
  • 103975바이트 중 7500을 못받아서 파일 다운로드가 완료되지 못하고 있다. (7500/103975)*100 = 약 7%이므로 추가 손실이 생긴 것 같다. 직전 테스트보다 손상률을 줄이니 이미지가 조금 더 보인다.

3. 추가 테스트

  • corruption을 그대로 두고 100ms delay를 줬다
  • 이미지가 더 상했다. 이미지의 경우 데이터가 순서대로 해석되어야 제대로 복원 가능한데, 지연 시간으로 인해 더 어긋나서 이후 데이터까지 영향을 받아 전체적으로 깨진 것 같다.
  • corrupt를 20%까지 늘리니 이미지가 조금도 보이지 않았다.
  • 몇 번 더 해봤는데 손실률을 같게 해도 매번 이미지가 조금씩 다르게 나온다.

4. 극단적인 손실률

  • 손실률을 90퍼센트까지 높이니 서버는 실행되는데 잘 돌아가던 클라이언트 코드에서 갑자기 문제가 생겼다. 서버에서 전송된 응답(response)이 원래는 텍스트였으나, 손상으로 의미 없는 바이트값이 된 것 같다.

5. 패킷 캡쳐

*corrupt 90%

  • Fragmented IP packets이 엄청 보인다. Payload에 바이너리 및 깨진 ASCII 문자들이 존재한다.
  • 정상 패킷은 바이트 단위로 일정한 구조를 유지하는데 z, E, V 같은 문자(16진수가 아님)가 갑자기 나타나 16진수 패턴이 깨진다.

  • [BAD UDP LENGTH 1508 > IP PAYLOAD LENGTH] 메시지가 보인다. UDP헤더상 길이가 1508로 설정된 패킷인데 실제 payload는 1500이라, 프로토콜 간 정보가 일치하지 않는다. 헤더가 손상되어 UDP 길이가 실제보다 커졌다.
  • Payload가 빨간색으로 강조되어있다. 또한 왼쪽 Hex 값의 흐름을 보면 특정 구조가 없이 이상한 바이트값으로 채워져 있다. 확실히 손상되었다.

*정상 패킷

  • UDP 패킷들 모두 Len=1500으로 같고, BAD UDP LENGTH 등의 오류 메시지가 없고 깨진 데이터가 없다.

Leave a comment