Learn/'24_Fall_(EE542) Internet&Cloud Computin

(LAB 04) Fast, Reliable File Transfer

QBBong 2025. 1. 9. 06:45
728x90

Lab 04 요약: Fast, Reliable File Transfer

 

Lab 04의 목표는 빠르고 신뢰할 수 있는 파일 전송 프로그램을 설계하고 구현하는 것입니다. 이 과제에서는 네트워크의 대기 시간, 손실률, 대역폭 제한 등의 어려운 조건에서 TCP보다 더 높은 성능을 보이는 파일 전송 유틸리티를 개발합니다. 최종 결과물은 명령줄 인터페이스를 통해 동작하며, 안정적이고 오류 없이 파일을 전송해야 한다. 이 랩은 기존 업무에서 사용했던 프로토콜을 차용했다.


주요 요구 사항

 

1. 프로그램 설계

  • IP 기반 파일 전송 유틸리티 구현.

   전송 파일의 크기는 최소 1GB 이상이어야 함.

   명령줄 인터페이스는 scp와 유사하게 설계.

 

2. 성능 테스트 시나리오

   Case 1:

     RTT 10ms, 손실률 1%, 대역폭 100Mbps.

   Case 2:

     RTT 200ms, 손실률 20%, 대역폭 100Mbps.

   Case 3:

     RTT 200ms, 손실 없음, 대역폭 80Mbps.

     각 시나리오에서 MTU를 1500과 9000으로 설정하여 테스트.

 

3. 네트워크 구성

   VirtualBox를 사용해 서버, 클라이언트, VyOS 라우터로 구성된 네트워크 설정.

   테스트가 완료된 후 AWS EC2 환경에서 동일한 설정으로 프로그램 성능 확인.


주요 구현 단계

 

1. 네트워크 설정

   VyOS 라우터에서 네트워크 지연 및 패킷 손실 설정:

   tc qdisc 명령으로 네트워크 조건 시뮬레이션.

   iperf를 통해 대역폭 및 패킷 손실 확인.

 

2. 파일 전송 유틸리티 구현

   기본 기능:

     파일을 지정된 경로에 저장 및 전송.

   전송 과정:

     송신자 측에서 데이터 전송 시작 시간 기록.

     수신자 측에서 데이터 수신 종료 시간 기록.

   신뢰성 확인:

     원본 파일과 수신된 파일의 MD5 해시값 비교.

 

3. 성능 최적화

   멀티스레딩:

     데이터 전송을 병렬화하여 성능 향상.

   데이터 구조:

     효율적인 버퍼 관리 및 데이터 분할 처리.

   전송 전략:

     손실률 및 대기 시간에 따라 패킷 크기와 재전송 전략 조정.

 

4. 결과 분석

     각 시나리오에서 파일 전송 속도와 정확성을 기록.

     MTU 설정 변경에 따른 성능 차이 분석.

 

(fileRcv.cpp)

#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdexcept>
#include <fcntl.h>
#include <chrono>
#include <errno.h>
#include <sys/select.h>
#include <vector>

#define BUFFER_SIZE 1024
#define HEADER_SIZE 64
#define TR_CODE_SIZE 1
#define RESPONSE_CODE_SIZE 1
#define FILE_NAME_SIZE 50
#define FILE_SIZE_SIZE 12

// Response code definitions
#define RESP_CODE_NORMAL '0'        // Normal transmission
#define RESP_CODE_DUPLICATE '1'     // Resume transmission

#define LOG_LEVEL_BASIC 0           //  0: basic , 1: debug, 2: error
#define LOG_LEVEL_DEBUG 1
#define LOG_LEVEL_ERROR 2
int log_level = 0;                  // Default log level

// Enum to define the protocol type
enum class Protocol {
    TCP,
    UDP,
};

// Function to log messages based on log level
void logMessage(const std::string &message, int level) {
    if (level == LOG_LEVEL_ERROR || log_level == level || log_level == LOG_LEVEL_DEBUG) {
        std::cout << std::endl << "\t" << message << std::endl;
    }
}

// Error handling
void error(const std::string &msg) {
    std::cerr << msg << std::endl;
    exit(1);
}

// Set socket to non-blocking mode
void setNonBlocking(int sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        throw std::runtime_error("Error getting socket flags");
    }
    if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
        throw std::runtime_error("Error setting non-blocking mode");
    }
}

// Function to bind and listen on the server socket
int setupServerSocket(int port, Protocol protocol, struct sockaddr_in &serv_addr) {
    int sockfd;

    if (protocol == Protocol::TCP) {
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
    } else {
        sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    }

    if (sockfd < 0) {
        throw std::runtime_error("Error opening socket");
    }

    // Set SO_REUSEADDR option to allow address reuse
    int opt = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        throw std::runtime_error("Error setting socket options");
    }

    std::memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(port);

    if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
        throw std::runtime_error("Error binding socket");
    }

    if (protocol == Protocol::TCP) {
        if (listen(sockfd, 5) < 0) {
            throw std::runtime_error("Error on listen");
        }
    }

    return sockfd;
}

// Function to detect if the client has disconnected
bool isClientDisconnected(int sockfd, Protocol protocol, struct sockaddr_in* cli_addr = nullptr) {
    char buffer[1];
    socklen_t cli_len = sizeof(*cli_addr);
    int result;

    if (protocol == Protocol::TCP) {
        result = recv(sockfd, buffer, sizeof(buffer), MSG_PEEK | MSG_DONTWAIT);
    } else if (protocol == Protocol::UDP && cli_addr) {
        result = recvfrom(sockfd, buffer, sizeof(buffer), MSG_PEEK | MSG_DONTWAIT, (struct sockaddr*)cli_addr, &cli_len);
    } else {
        return false;
    }

    if (result == 0) {
        logMessage("Client disconnected.", LOG_LEVEL_BASIC);
        return true;
    } else if (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
        return false;
    }

    return false;
}

// Function to receive the header and parse the information
void receiveHeader(int sockfd, std::string &filename, long &filesize, Protocol protocol, struct sockaddr_in *cli_addr = nullptr) {
    char header[HEADER_SIZE] = {0};
    socklen_t cli_len = sizeof(*cli_addr);

    while (true) {
        if (protocol == Protocol::TCP) {
            int bytesRead = recv(sockfd, header, HEADER_SIZE, 0);
            if (bytesRead < 0) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    continue;  // No data yet, retry
                } else {
                    throw std::runtime_error("Error receiving header");
                }
            }
        } else if (protocol == Protocol::UDP && cli_addr) {
            int bytesRead = recvfrom(sockfd, header, HEADER_SIZE, 0, (struct sockaddr *)cli_addr, &cli_len);
            if (bytesRead < 0) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    continue;
                } else {
                    throw std::runtime_error("Error receiving header via UDP");
                }
            }
        }
        break;
    }

    filename = std::string(header + 2, FILE_NAME_SIZE);
    filename.erase(filename.find_last_not_of(' ') + 1);
    filesize = std::stoll(std::string(header + 52, FILE_SIZE_SIZE));

    logMessage("(1) Recv_HEADER: [" + std::string(header, HEADER_SIZE) + "]", LOG_LEVEL_BASIC);
    logMessage("[DEBUG] Received header: File name: " + filename + ", File size: " + std::to_string(filesize), LOG_LEVEL_DEBUG);
}

// Function to send a response back to the sender
void sendResponse(int sockfd, char responseCode, const std::string &filename, long receivedSize, Protocol protocol, struct sockaddr_in *cli_addr = nullptr) {
    char response[HEADER_SIZE] = {0};
    response[0] = 'H';
    response[1] = responseCode;

    std::string formattedFilename = filename.substr(0, FILE_NAME_SIZE);
    formattedFilename.resize(FILE_NAME_SIZE, ' ');

    std::string receivedSizeStr = std::to_string(receivedSize);
    if (receivedSizeStr.size() < FILE_SIZE_SIZE) {
        receivedSizeStr = std::string(FILE_SIZE_SIZE - receivedSizeStr.size(), '0') + receivedSizeStr;
    }

    std::memcpy(&response[2], formattedFilename.c_str(), FILE_NAME_SIZE);
    std::memcpy(&response[52], receivedSizeStr.c_str(), FILE_SIZE_SIZE);

    if (protocol == Protocol::TCP) {
        if (send(sockfd, response, HEADER_SIZE, 0) < 0) {
            throw std::runtime_error("Error sending response via TCP");
        }
    } else if (protocol == Protocol::UDP && cli_addr) {
        socklen_t cli_len = sizeof(*cli_addr);
        if (sendto(sockfd, response, HEADER_SIZE, 0, (struct sockaddr *)cli_addr, cli_len) < 0) {
            throw std::runtime_error("Error sending response via UDP");
        }
    }

    logMessage("(2) Resp_HEADER: [" + std::string(response, HEADER_SIZE) + "]", LOG_LEVEL_BASIC);
}

// Receive file data with size control (non-blocking, using select)
long receiveFileData(int sockfd, const std::string &filename, long filesize, long resumePosition, Protocol protocol, struct sockaddr_in *cli_addr = nullptr) {
    logMessage("(3) Recv_DATA: ", LOG_LEVEL_BASIC);

    std::ofstream file;
    if (resumePosition > 0) {
        logMessage("Resuming from byte: " + std::to_string(resumePosition), LOG_LEVEL_DEBUG);
        file.open("./rcv/" + filename, std::ios::binary | std::ios::out | std::ios::in);
        file.seekp(resumePosition);
    } else {
        file.open("./rcv/" + filename, std::ios::binary | std::ios::out);
    }

    if (!file.is_open()) {
        throw std::runtime_error("Error opening file for writing: " + filename);
    }

    char buffer[BUFFER_SIZE];
    long bytesReceived = resumePosition;
    socklen_t cli_len = sizeof(*cli_addr);
    fd_set readfds;
    struct timeval timeout;
    int previousProgress = 0;

    while (bytesReceived < filesize) {
        FD_ZERO(&readfds);
        FD_SET(sockfd, &readfds);

        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        int activity = select(sockfd + 1, &readfds, NULL, NULL, &timeout);

        if (activity < 0) {
            throw std::runtime_error("Error in select");
        } else if (activity == 0) {
            logMessage("[ERROR] Timeout while waiting for data", LOG_LEVEL_ERROR);
            continue;
        }

        int bytesRead = 0;
        if (protocol == Protocol::TCP) {
            bytesRead = recv(sockfd, buffer, std::min(BUFFER_SIZE, static_cast<int>(filesize - bytesReceived)), 0);
        } else if (protocol == Protocol::UDP && cli_addr) {
            bytesRead = recvfrom(sockfd, buffer, std::min(BUFFER_SIZE, static_cast<int>(filesize - bytesReceived)), 0, (struct sockaddr *)cli_addr, &cli_len);
        }

        if (bytesRead < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                continue;  // No data available yet, retry
            } else {
                throw std::runtime_error("Error receiving file data");
            }
        } else if (bytesRead == 0) {
            logMessage("[ERROR] Connection closed by client", LOG_LEVEL_ERROR);
            break;  // Client closed connection
        }

        // Write the data to the file
        file.write(buffer, bytesRead);
        bytesReceived += bytesRead;

        // Calculate progress percentage
        double progress = ((double)bytesReceived / filesize) * 100;

        // Only print logs for every 10% progress
        if (static_cast<int>(progress) / 10 > previousProgress / 10) {
            previousProgress = static_cast<int>(progress);  // Update progress checkpoint
            std::cout << "\t\t Recv: " << std::to_string(bytesReceived) << "/" 
                      << std::to_string(filesize) << " bytes (" << std::to_string(static_cast<int>(progress)) << "%)" << std::endl;
        }
    }

    file.close();
    logMessage("\t File reception completed: " + filename, LOG_LEVEL_BASIC);

    return bytesReceived;  // Return the total bytes received
}

// Function to receive the tail and send a tail response
void receiveAndRespondToTail(int sockfd, const std::string &filename, long expectedFilesize, long receivedFilesize, Protocol protocol, struct sockaddr_in *cli_addr = nullptr) {
    char tail[HEADER_SIZE] = {0};
    socklen_t cli_len = sizeof(*cli_addr);

    fd_set readfds;
    struct timeval timeout;

    while (true) {
        FD_ZERO(&readfds);
        FD_SET(sockfd, &readfds);

        timeout.tv_sec = 5;  // Set timeout to 5 seconds
        timeout.tv_usec = 0;

        int activity = select(sockfd + 1, &readfds, NULL, NULL, &timeout);

        if (activity < 0) {
            throw std::runtime_error("Error in select");
        } else if (activity == 0) {
            logMessage("Timeout while waiting for tail", LOG_LEVEL_BASIC);
            continue;
        }

        if (protocol == Protocol::TCP) {
            int bytesRead = recv(sockfd, tail, HEADER_SIZE, 0);
            if (bytesRead < 0) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    continue;  // No data yet, retry
                } else {
                    throw std::runtime_error("Error receiving tail");
                }
            }
        } else if (protocol == Protocol::UDP && cli_addr) {
            int bytesRead = recvfrom(sockfd, tail, HEADER_SIZE, 0, (struct sockaddr *)cli_addr, &cli_len);
            if (bytesRead < 0) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    continue;  // No data yet, retry
                } else {
                    throw std::runtime_error("Error receiving tail via UDP");
                }
            }
        }
        break;
    }

    logMessage("(4) Recv_TAIL: [" + std::string(tail, HEADER_SIZE) + "]", LOG_LEVEL_BASIC);

    long sentFilesize = std::stoll(std::string(tail + 52, FILE_SIZE_SIZE));  // Extract file size from tail

    logMessage("[DEBUG] Received tail: Sent file size: " + std::to_string(sentFilesize), LOG_LEVEL_DEBUG);

    if (sentFilesize != receivedFilesize) {
        logMessage("[ERROR] Warning: Tail file size mismatch. Expected: " + std::to_string(expectedFilesize) + ", Received: " + std::to_string(sentFilesize), LOG_LEVEL_ERROR);
    }

    char response[HEADER_SIZE] = {0};
    response[0] = 'T';

    if (sentFilesize == receivedFilesize) {
        response[1] = RESP_CODE_NORMAL;
        logMessage("[DEBUG] File sizes match, sending confirmation response.", LOG_LEVEL_DEBUG);
    } else {
        response[1] = RESP_CODE_DUPLICATE;
        logMessage("\t File sizes do not match, requesting retransmission.", LOG_LEVEL_BASIC);
    }

    std::string formattedFilename = filename.substr(0, FILE_NAME_SIZE);
    formattedFilename.resize(FILE_NAME_SIZE, ' ');
    std::memcpy(&response[2], formattedFilename.c_str(), FILE_NAME_SIZE);

    std::string receivedSizeStr = std::to_string(receivedFilesize);
    if (receivedSizeStr.size() < FILE_SIZE_SIZE) {
        receivedSizeStr = std::string(FILE_SIZE_SIZE - receivedSizeStr.size(), '0') + receivedSizeStr;
    }
    std::memcpy(&response[52], receivedSizeStr.c_str(), FILE_SIZE_SIZE);

    if (protocol == Protocol::TCP) {
        if (send(sockfd, response, HEADER_SIZE, 0) < 0) {
            throw std::runtime_error("Error sending tail response via TCP");
        }
    } else if (protocol == Protocol::UDP && cli_addr) {
        if (sendto(sockfd, response, HEADER_SIZE, 0, (struct sockaddr *)cli_addr, cli_len) < 0) {
            throw std::runtime_error("Error sending tail response via UDP");
        }
    }

    logMessage("(5) Resp_TAIL: [" + std::string(response, HEADER_SIZE) + "]", LOG_LEVEL_BASIC);
}

double calculateMbps(long bytesTransferred, double elapsedTime) {
    return (bytesTransferred * 8) / (elapsedTime * 1000000);  // Mbps 단위로 변환
}

// Main server function
int main(int argc, char* argv[]) {
    int sockfd, newsockfd, port;
    Protocol protocol = Protocol::TCP;  // Default is TCP
    struct sockaddr_in cli_addr, serv_addr;
    socklen_t clilen = sizeof(cli_addr);

    // Parse arguments
    if (argc < 3) {
        std::cerr << "Usage: ./fileRcv -p port [-proto tcp/udp] [-l loglevel]" << std::endl;
        return 1;
    }

    for (int i = 1; i < argc; ++i) {
        if (std::strcmp(argv[i], "-p") == 0) {
            port = std::stoi(argv[++i]);
        } else if (std::strcmp(argv[i], "-proto") == 0) {
            std::string proto = argv[++i];
            if (proto == "tcp") {
                protocol = Protocol::TCP;
            } else if (proto == "udp") {
                protocol = Protocol::UDP;
            } else {
                throw std::runtime_error("Unsupported protocol: " + proto);
            }
        } else if (std::strcmp(argv[i], "-l") == 0) {
            std::string level = argv[++i];
            if (level == "debug") {
                log_level = LOG_LEVEL_DEBUG;
            } else{
                log_level = LOG_LEVEL_BASIC;
            }
        }
    }

    // Create rcv directory if it doesn't exist
    int result = system("mkdir -p ./rcv");
    if (result == 0) {
        logMessage("\t[Directory]: './rcv' created or already exists.", LOG_LEVEL_BASIC);
    } else {
        logMessage("\t[ERROR] Error creating directory './rcv'.", LOG_LEVEL_ERROR);
    }

    // Set up server socket
    sockfd = setupServerSocket(port, protocol, serv_addr);
    logMessage("\t [SERVER] Server is listening on port " + std::to_string(port) + " using protocol " + (protocol == Protocol::TCP ? "TCP" : "UDP"), LOG_LEVEL_BASIC);

    if (protocol == Protocol::TCP) {
        while (true) {
            newsockfd = accept(sockfd, (struct sockaddr*)&cli_addr, &clilen);
            if (newsockfd < 0) {
                throw std::runtime_error("Error on accept");
            }

            logMessage("\t[CONNECTION] Accepted connection from client", LOG_LEVEL_BASIC);

            try {
                std::string filename;
                long filesize;

                while (true) {
                    if (isClientDisconnected(newsockfd, protocol)) {
                        logMessage("\t[CONNECTION] Client Disconnected...", LOG_LEVEL_BASIC);
                        close(newsockfd);
                        break;  // Stop current session and wait for a new connection
                    }

                    receiveHeader(newsockfd, filename, filesize, protocol);

                    long resumePosition = 0;

                    if (access(("./rcv/" + filename).c_str(), F_OK) != -1) {
                        std::ifstream existingFile("./rcv/" + filename, std::ios::binary | std::ios::ate);
                        long existingSize = existingFile.tellg();
                        existingFile.close();

                        if (existingSize == filesize) {
                            logMessage("\t[File]: " + filename + " already exists with the same size. Skipping reception.", LOG_LEVEL_BASIC);
                            resumePosition = existingSize;
                            sendResponse(newsockfd, RESP_CODE_DUPLICATE, filename, resumePosition, protocol);
                            close(newsockfd);
                            break;
                        } else {
                            resumePosition = existingSize;
                            sendResponse(newsockfd, RESP_CODE_DUPLICATE, filename, resumePosition, protocol);
                        }
                    } else {
                        sendResponse(newsockfd, RESP_CODE_NORMAL, filename, 0, protocol);
                    }

                    // Record Recv start time
                    time_t start_time = time(nullptr);
                    auto start_time_point = std::chrono::high_resolution_clock::now(); // Millisecond precision
                    logMessage("\t [TIME] Recv started at: " + std::string(ctime(&start_time)), LOG_LEVEL_BASIC);

                    long receivedSize = receiveFileData(newsockfd, filename, filesize, resumePosition, protocol);

                    // Receive and respond to the tail, using the actual received file size
                    receiveAndRespondToTail(newsockfd, filename, filesize, receivedSize, protocol);

                    // Record transmission end time and calculate total transmission time
                    time_t end_time = time(nullptr);
                    auto end_time_point = std::chrono::high_resolution_clock::now(); // Millisecond precision

                    std::chrono::duration<double> reception_time = end_time_point - start_time_point;
                    double elapsed_seconds = reception_time.count();
                    double speedMbps = calculateMbps(receivedSize, elapsed_seconds);

                    logMessage("\t [TIME] Recv ended time: " + std::string(ctime(&end_time)), LOG_LEVEL_BASIC);
                    logMessage("\t [TIME] Total Recv time: " + std::to_string(elapsed_seconds) + " seconds", LOG_LEVEL_BASIC);
                    logMessage("\t [TIME] Recv speed: " + std::to_string(speedMbps) + " Mbps", LOG_LEVEL_BASIC);

                    close(newsockfd);  // Close the connection to this client
                    break;  // Exit the loop to wait for a new connection
                }
            } catch (const std::exception& e) {
                logMessage(std::string("Error: ") + e.what(), LOG_LEVEL_ERROR);
                close(newsockfd);  // Ensure the connection is closed even on failure
            }
        }
    } else if (protocol == Protocol::UDP) {
        // UDP connection handling
        while (true) {
            try {
                std::string filename;
                long filesize;
                long resumePosition = 0;

                // Receive and process the header
                receiveHeader(sockfd, filename, filesize, protocol, &cli_addr);

                if (isClientDisconnected(sockfd, protocol, &cli_addr)) {
                    logMessage("UDP client disconnected.", LOG_LEVEL_BASIC);
                    continue;  // Handle the next connection or transmission
                }

                // Check if the file already exists and has the same size
                if (access(("./rcv/" + filename).c_str(), F_OK) != -1) {
                    std::ifstream existingFile("./rcv/" + filename, std::ios::binary | std::ios::ate);
                    long existingSize = existingFile.tellg();
                    existingFile.close();

                    if (existingSize == filesize) {
                        logMessage("\t[File]: " + filename + " already exists with the same size. Skipping reception.", LOG_LEVEL_BASIC);
                        resumePosition = existingSize;
                        sendResponse(sockfd, RESP_CODE_DUPLICATE, filename, resumePosition, protocol, &cli_addr);
                        continue;  // Skip to the next connection or data reception
                    } else {
                        resumePosition = existingSize;
                        sendResponse(sockfd, RESP_CODE_DUPLICATE, filename, resumePosition, protocol, &cli_addr);
                    }
                } else {
                    sendResponse(sockfd, RESP_CODE_NORMAL, filename, 0, protocol, &cli_addr);
                }

                // Record Recv start time
                time_t start_time = time(nullptr);
                auto start_time_point = std::chrono::high_resolution_clock::now(); // Millisecond precision
                logMessage("\t [TIME] Recv started at: " + std::string(ctime(&start_time)), LOG_LEVEL_BASIC);

                long receivedSize = receiveFileData(sockfd, filename, filesize, resumePosition, protocol, &cli_addr);

                // Receive and respond to tail, using the actual received file size
                receiveAndRespondToTail(sockfd, filename, filesize, receivedSize, protocol, &cli_addr);

                // Record transmission end time and calculate total transmission time
                time_t end_time = time(nullptr);
                auto end_time_point = std::chrono::high_resolution_clock::now(); // Millisecond precision

                std::chrono::duration<double> reception_time = end_time_point - start_time_point;
                double elapsed_seconds = reception_time.count();
                double speedMbps = calculateMbps(receivedSize, elapsed_seconds);

                logMessage("\t [TIME] Recv ended time: " + std::string(ctime(&end_time)), LOG_LEVEL_BASIC);
                logMessage("\t [TIME] Total Recv time: " + std::to_string(elapsed_seconds) + " seconds", LOG_LEVEL_BASIC);
                logMessage("\t [TIME] Recv speed: " + std::to_string(speedMbps) + " Mbps", LOG_LEVEL_BASIC);

            } catch (const std::exception& e) {
                logMessage(std::string("Error: ") + e.what(), LOG_LEVEL_ERROR);
            }
        }
    }

    close(sockfd);
    return 0;
}

 

(fileRcv.cpp)

#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdexcept>
#include <ctime>
#include <vector>
#include <chrono>

#define BUFFER_SIZE 1024
#define HEADER_SIZE 64
#define TR_CODE_SIZE 1
#define RESPONSE_CODE_SIZE 1
#define FILE_NAME_SIZE 50
#define FILE_SIZE_SIZE 12

// Response code definitions
#define RESP_CODE_NORMAL '0'        // Normal transmission
#define RESP_CODE_DUPLICATE '1'     // Resume transmission

//std::string log_level = "basic";  // Default log level
#define LOG_LEVEL_BASIC 0     //  0: basic , 1: debug, 2: error
#define LOG_LEVEL_DEBUG 1
#define LOG_LEVEL_ERROR 2
int log_level=0;

// Enum to define the protocol type
enum class Protocol {
    TCP,
    UDP,
};

// Function to log messages based on log level
void logMessage(const std::string &message, int level) {
    if (level == 0 || level == 2 || (level == 1 && log_level == 1) ) {
        std::cout << std::endl << "\t" << message << std::endl;
    }
}

void error(const std::string &msg) {
    std::cerr << msg << std::endl;
    exit(1);
}

// Extract file name from file path and trim spaces
void extractFilename(const std::string &filepath, std::string &filename) {
    size_t lastSlash = filepath.find_last_of('/');
    if (lastSlash != std::string::npos) {
        filename = filepath.substr(lastSlash + 1);
    } else {
        filename = filepath;
    }
    filename.erase(filename.find_last_not_of(' ') + 1);  // Remove trailing spaces

    logMessage("[DEBUG] FilePath: " + filepath + ", FileName: " + filename, LOG_LEVEL_DEBUG );
}

// Function to create a socket and connect to the server (for TCP) or set up for UDP
int createAndConnectSocket(const std::string &server_ip, int port, Protocol protocol, struct sockaddr_in &serv_addr) {
    int sockfd;

    if (protocol == Protocol::TCP) {
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
    } else {
        sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    }

    if (sockfd < 0) {
        throw std::runtime_error("Error opening socket");
    }

    std::memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(port);

    if (inet_pton(AF_INET, server_ip.c_str(), &serv_addr.sin_addr) <= 0) {
        close(sockfd);
        throw std::runtime_error("Invalid address/ Address not supported");
    }

    if (protocol == Protocol::TCP) {
        if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
            close(sockfd);
            throw std::runtime_error("Connection Failed");
        }
    }

    return sockfd;
}

// Send header (modifying filename for correct transmission)
void sendHeader(int sockfd, const std::string &filename, long filesize, Protocol protocol, const struct sockaddr_in *serv_addr = nullptr) {
    std::string header(HEADER_SIZE, ' '); // Initialize 64-byte header with spaces
    header[0] = 'H'; // Set TR_CODE (transmission header)
    header[1] = RESP_CODE_NORMAL; // Set response code to normal (default '0')

    // Format file name to be exactly 50 characters, fill remaining with spaces
    std::string formattedFilename = filename.substr(0, FILE_NAME_SIZE);  // Truncate if too long
    formattedFilename.resize(FILE_NAME_SIZE, ' ');  // Fill remaining space with ' '

    // Format file size as exactly 12 digits, fill leading with '0'
    std::string fileSizeStr = std::to_string(filesize);
    if (fileSizeStr.size() < FILE_SIZE_SIZE) {
        fileSizeStr = std::string(FILE_SIZE_SIZE - fileSizeStr.size(), '0') + fileSizeStr; // Fill leading with '0'
    }

    // HEADER: TR_CODE + RESPONSE_CODE + file_name (50) + file_size (12)
    std::memcpy(&header[2], formattedFilename.c_str(), FILE_NAME_SIZE); // Copy formatted file name
    std::memcpy(&header[52], fileSizeStr.c_str(), FILE_SIZE_SIZE);      // Copy formatted file size

    if (protocol == Protocol::TCP) {
        if (write(sockfd, header.c_str(), HEADER_SIZE) < 0) {
            throw std::runtime_error("Error sending header");
        }
    } else if (protocol == Protocol::UDP && serv_addr) {
        if (sendto(sockfd, header.c_str(), HEADER_SIZE, 0, (struct sockaddr *)serv_addr, sizeof(*serv_addr)) < 0) {
            throw std::runtime_error("Error sending header via UDP");
        }
    }
    logMessage("(1) Send_HEADER: [" + std::string(header) + "]", LOG_LEVEL_BASIC);
}

// Receive header response
std::string receiveHeaderResponse(int sockfd, Protocol protocol, const struct sockaddr_in *serv_addr = nullptr) {
    char response[HEADER_SIZE] = {0};
    socklen_t serv_len = sizeof(*serv_addr);

    if (protocol == Protocol::TCP) {
        if (read(sockfd, response, HEADER_SIZE) < 0) {
            throw std::runtime_error("Error receiving header response");
        }
    } else if (protocol == Protocol::UDP && serv_addr) {
        if (recvfrom(sockfd, response, HEADER_SIZE, 0, (struct sockaddr *)serv_addr, &serv_len) < 0) {
            throw std::runtime_error("Error receiving header response via UDP");
        }
    }
    
    logMessage("(2) Resp_HEADER: [" + std::string(response, HEADER_SIZE) + "]", LOG_LEVEL_BASIC);

    return std::string(response);
}

// Send trailer
void sendTail(int sockfd, const std::string &filename, long filesize, Protocol protocol, const struct sockaddr_in *serv_addr = nullptr) {
    std::string tail(HEADER_SIZE, ' '); // Initialize 64-byte trailer
    tail[0] = 'T'; // Set TR_CODE (transmission trailer)
    tail[1] = RESP_CODE_NORMAL; // Set response code to normal (default '0')

    std::string fileSizeStr = std::string(FILE_SIZE_SIZE - std::to_string(filesize).length(), '0') + std::to_string(filesize);

    // T + file_name + file_size
    std::memcpy(&tail[2], filename.c_str(), filename.size()); // Copy file name
    std::memcpy(&tail[52], fileSizeStr.c_str(), fileSizeStr.size()); // Copy file size

    if (protocol == Protocol::TCP) {
        if (write(sockfd, tail.c_str(), HEADER_SIZE) < 0) {
            throw std::runtime_error("Error sending tail");
        }
    } else if (protocol == Protocol::UDP && serv_addr) {
        if (sendto(sockfd, tail.c_str(), HEADER_SIZE, 0, (struct sockaddr *)serv_addr, sizeof(*serv_addr)) < 0) {
            throw std::runtime_error("Error sending tail via UDP");
        }
    }

    logMessage("(4) Send_TAIL: ["+ std::string(tail) + "]", LOG_LEVEL_BASIC);

}

// Transmit file data (for both TCP and UDP)
void transmitFileData(int sockfd, std::ifstream &file, std::vector<char> &buffer, long resumePosition, Protocol protocol, const struct sockaddr_in *serv_addr = nullptr, long totalFileSize = 0) {

    logMessage("(3) Send_DATA: ", LOG_LEVEL_BASIC);

    if (resumePosition > 0) {
        file.seekg(resumePosition, std::ios::beg); // Move to the position to resume transmission
    }

    long bytesSent = resumePosition; // Bytes already sent
    int previousProgress = 0; // Variable to store the last printed percentage
    while (!file.eof()) {
        file.read(buffer.data(), BUFFER_SIZE);
        int bytesRead = file.gcount();
        if (bytesRead > 0) {
            if (protocol == Protocol::TCP) {
                if (write(sockfd, buffer.data(), bytesRead) < 0) {
                    throw std::runtime_error("Error sending file data");
                }
            } else if (protocol == Protocol::UDP && serv_addr) {
                if (sendto(sockfd, buffer.data(), bytesRead, 0, (struct sockaddr *)serv_addr, sizeof(*serv_addr)) < 0) {
                    throw std::runtime_error("Error sending file data via UDP");
                }
            }
            bytesSent += bytesRead; // Update the accumulated sent bytes
            
            // Calculate the progress based on the total file size
            double progress = ((double)bytesSent / totalFileSize) * 100;

            // Only print logs for every 10% progress
            if (static_cast<int>(progress) / 10 > previousProgress / 10) {
                previousProgress = static_cast<int>(progress); // Update current progress
                std::cout << "\t\t Sent: " << bytesSent << "/" << totalFileSize << " bytes (" << static_cast<int>(progress) << "%)" << std::endl;
            }
        }
    }
}

// Function to receive the trailer response and compare file sizes
void processTailResponse(int sockfd, long filesize, Protocol protocol, const struct sockaddr_in *serv_addr = nullptr) {
    char response[HEADER_SIZE] = {0};
    socklen_t serv_len = sizeof(*serv_addr);

    // Receive trailer response
    if (protocol == Protocol::TCP) {
        if (read(sockfd, response, HEADER_SIZE) < 0) {
            throw std::runtime_error("Error receiving trailer response");
        }
    } else if (protocol == Protocol::UDP && serv_addr) {
        if (recvfrom(sockfd, response, HEADER_SIZE, 0, (struct sockaddr *)serv_addr, &serv_len) < 0) {
            throw std::runtime_error("Error receiving trailer response via UDP");
        }
    }

    logMessage("(5) Resp_TAIL: [" + std::string(response, HEADER_SIZE) + "]", LOG_LEVEL_BASIC);

    // Parse the received file size from the response
    long receivedSize = std::stol(std::string(response + 52, FILE_SIZE_SIZE));  // Extract file size from response

    logMessage("[DEBUG] Received tail response with file size: " + std::to_string(receivedSize), LOG_LEVEL_DEBUG);

    // Compare the received size with the actual file size
    if (receivedSize == filesize) {
        logMessage("\t File successfully transmitted and verified.", LOG_LEVEL_BASIC);
    } else if (receivedSize < filesize) {
        // If the received size is smaller, resume transmission
        logMessage("\t Receiver's file size is smaller. Resume transmission from byte " + std::to_string(receivedSize), LOG_LEVEL_BASIC);
        std::ifstream file("file_to_resume", std::ios::binary);
        if (!file.is_open()) {
            throw std::runtime_error("Error opening file for resuming transmission.");
        }
        file.seekg(receivedSize, std::ios::beg);
        std::vector<char> buffer(BUFFER_SIZE);
        transmitFileData(sockfd, file, buffer, receivedSize, protocol, serv_addr, filesize-receivedSize);
        file.close();
        // After resuming, send the trailer again
        sendTail(sockfd, "file_to_resume", filesize, protocol, serv_addr);
    } else {
        // If the received size is larger than expected, throw an error
        logMessage("[ERROR]: Received file size is larger than sent file size.", LOG_LEVEL_ERROR);
        throw std::runtime_error("Transmission error: Receiver's file size is larger than expected.");
    }
}

double calculateMbps(long bytesTransferred, double elapsedTime) {
    return (bytesTransferred * 8) / (elapsedTime * 1000000);  // Mbps 단위로 변환
}

int main(int argc, char *argv[]) {

    int sockfd, port;
    std::vector<char> buffer(BUFFER_SIZE);
    std::string server_ip;
    std::string file_path;
    std::string filename;
    //std::string log_level = "basic";  // Default log level
    Protocol protocol = Protocol::TCP; // Default protocol
    struct sockaddr_in serv_addr;

    try {
        // Parse arguments
        if (argc < 7) {
                        throw std::runtime_error("Usage: ./fileSnd -i server_ip -p port -f file [-l loglevel] [-proto tcp/udp]");
        }

        for (int i = 1; i < argc; ++i) {
            if (std::strcmp(argv[i], "-i") == 0) {
                server_ip = argv[++i];
            } else if (std::strcmp(argv[i], "-p") == 0) {
                port = std::stoi(argv[++i]);
            } else if (std::strcmp(argv[i], "-f") == 0) {
                file_path = argv[++i];
            } else if (std::strcmp(argv[i], "-l") == 0) {
                std::string log = argv[++i];
                if (log == "debug"){
                    log_level = LOG_LEVEL_DEBUG;
                } else {
                    log_level = LOG_LEVEL_BASIC;
                }
            } else if (std::strcmp(argv[i], "-proto") == 0) {
                std::string proto = argv[++i];
                if (proto == "tcp") {
                    protocol = Protocol::TCP;
                } else if (proto == "udp") {
                    protocol = Protocol::UDP;
                } else {
                    throw std::runtime_error("Unsupported protocol: " + proto);
                }
            }
        }

        // Extract file name
        extractFilename(file_path, filename);

        // Create and connect socket based on the selected protocol
        sockfd = createAndConnectSocket(server_ip, port, protocol, serv_addr);

        // Open file
        std::ifstream file(file_path, std::ios::binary);
        if (!file.is_open()) {
            throw std::runtime_error("Error opening file: " + file_path);
        }

        // Calculate file size
        file.seekg(0, std::ios::end);
        long filesize = file.tellg();
        file.seekg(0, std::ios::beg);

        // Send header
        sendHeader(sockfd, filename, filesize, protocol, (protocol == Protocol::UDP) ? &serv_addr : nullptr);

        // Receive header response
        std::string response = receiveHeaderResponse(sockfd, protocol, (protocol == Protocol::UDP) ? &serv_addr : nullptr);

        // Parse the response and determine if resumption or new transmission is needed
        long resumePosition = 0;
        bool newFileTransmission = (response[1] == RESP_CODE_NORMAL);

        // Extract the size of the file that has already been received
        long receivedFileSize = std::stol(response.substr(52, FILE_SIZE_SIZE));

        if (!newFileTransmission) {
            if (receivedFileSize >= filesize) {
                logMessage("\t File already fully transmitted. No further action required.", LOG_LEVEL_BASIC);
                // Close the file and socket, and exit the program as the file is already fully transmitted
                file.close();
                close(sockfd);
                return 0;
            } else {
                // File is partially received, resume transmission
                resumePosition = receivedFileSize;
                logMessage("\t Resuming transmission from byte " + std::to_string(resumePosition), LOG_LEVEL_BASIC);
            }
        }

        // Record transmission start time
        time_t start_time = time(nullptr);
        auto start_time_point = std::chrono::high_resolution_clock::now(); // Millisecond precision
        logMessage("\t [TIME] Transmission started at: " + std::string(ctime(&start_time)), LOG_LEVEL_BASIC);

        // Transmit file data
        transmitFileData(sockfd, file, buffer, resumePosition, protocol, (protocol == Protocol::UDP) ? &serv_addr : nullptr, filesize-receivedFileSize);

        // Send trailer
        sendTail(sockfd, filename, filesize, protocol, (protocol == Protocol::UDP) ? &serv_addr : nullptr);

        // Process the tail response and handle retransmission if necessary
        processTailResponse(sockfd, filesize, protocol, (protocol == Protocol::UDP) ? &serv_addr : nullptr);

        // Record transmission end time and calculate total transmission time
        time_t end_time = time(nullptr);
        auto end_time_point = std::chrono::high_resolution_clock::now(); // Millisecond precision

        std::chrono::duration<double> transmission_time = end_time_point - start_time_point;
        double elapsed_seconds = transmission_time.count();
        double speedMbps = calculateMbps(filesize, elapsed_seconds);

        logMessage("\t [TIME] Transmission ended at: " + std::string(ctime(&end_time)), LOG_LEVEL_BASIC);
        logMessage("\t [TIME] Total transmission time: " + std::to_string(elapsed_seconds) + " seconds", LOG_LEVEL_BASIC);
        logMessage("\t [TIME] Transmission speed: " + std::to_string(speedMbps) + " Mbps", LOG_LEVEL_BASIC);

        // Close file after transmission
        file.close();


        close(sockfd);

    } catch (const std::exception &e) {
        error(e.what());
    }

    return 0;
}

 

(makefile)

# Compiler
CXX = g++

# Compiler flags
CXXFLAGS = -Wall -std=c++11

# Target binaries
TARGETS = fileSnd fileRcv

# Source files
SOURCES_SND = fileSnd.cpp
SOURCES_RCV = fileRcv.cpp

# Object files
OBJECTS_SND = $(SOURCES_SND:.cpp=.o)
OBJECTS_RCV = $(SOURCES_RCV:.cpp=.o)

# Default target: build all
all: $(TARGETS)

# Build fileSnd
fileSnd: $(OBJECTS_SND)
	$(CXX) $(CXXFLAGS) -o fileSnd $(OBJECTS_SND)

# Build fileRcv
fileRcv: $(OBJECTS_RCV)
	$(CXX) $(CXXFLAGS) -o fileRcv $(OBJECTS_RCV)

# Clean object files and binaries
clean:
	rm -f $(OBJECTS_SND) $(OBJECTS_RCV) $(TARGETS)

# Rebuild the project
rebuild: clean all

 

(README)

# File Transfer Program (TCP/UDP)

This program provides a file transfer utility using TCP or UDP. It includes a sender program (`fileSnd`) and a receiver program (`fileRcv`). The programs support both TCP and UDP protocols, and allow for resumable file transfers in case of interruptions.

## 1. Compilation

To compile the programs, you need to have `g++` installed. Use the provided `Makefile` to build both `fileSnd` and `fileRcv`.

### Steps to Compile:

1. Open a terminal and navigate to the directory containing the source files (`fileSnd.cpp`, `fileRcv.cpp`) and `Makefile`.
   
2. Run the following command to compile both programs:
   ```
   make
   ```

   This will generate two executable files: `fileSnd` and `fileRcv`.

3. To clean up the object files and executables, you can run:
   ```
   make clean
   ```

## 2. Usage Instructions

The file transfer program consists of two parts: the sender (`fileSnd`) and the receiver (`fileRcv`). You need to run the receiver first before sending files with the sender.

### 2.1 Running the Receiver (`fileRcv`)

Start the receiver on the machine where you want to receive the file. You can specify the protocol (TCP or UDP) and the port number to listen on.

#### Command:
```
./fileRcv -p <port> [-proto tcp/udp] [-l <loglevel>]
```

#### Parameters:
- `-p <port>`: Port number to listen on (required).
- `-proto tcp/udp`: Protocol to use, either `tcp` or `udp` (default is `tcp`).
- `-l <loglevel>`: Logging level. Options are `basic` (default) or `debug`.

#### Example:
```
./fileRcv -p 8080 -proto tcp -l debug
```

This will start the receiver using TCP on port 8080 with debug logging enabled.

### 2.2 Running the Sender (`fileSnd`)

Once the receiver is running, you can send a file from the sender machine. You need to specify the receiver's IP address, port, the file to send, and the protocol.

#### Command:
```
./fileSnd -i <receiver_ip> -p <port> -f <file_to_send> [-proto tcp/udp] [-l <loglevel>]
```

#### Parameters:
- `-i <receiver_ip>`: IP address of the machine running the receiver (required).
- `-p <port>`: Port number on which the receiver is listening (required).
- `-f <file_to_send>`: Path to the file that will be sent (required).
- `-proto tcp/udp`: Protocol to use, either `tcp` or `udp` (default is `tcp`).
- `-l <loglevel>`: Logging level. Options are `basic` (default) or `debug`.

#### Example:
```
./fileSnd -i 192.168.1.10 -p 8080 -f ./data/file.txt -proto udp -l basic
```

This will send `file.txt` to the receiver at IP address `192.168.1.10` using UDP on port 8080 with basic logging enabled.

## 3. Program Workflow

1. **Receiver**: Start the `fileRcv` program on the machine where the file should be saved. Ensure the correct protocol and port are specified.
   
2. **Sender**: Start the `fileSnd` program on the machine sending the file. Provide the receiver's IP address, port number, and the file path.

3. **Transmission**: The sender will establish a connection (if using TCP) or begin sending packets (if using UDP). If the file transfer is interrupted, it will resume from the point of failure when retried.

4. **Logging**: Use the `-l` option to adjust the amount of logging. The `debug` level provides detailed information about each step of the transfer.

## 4. Troubleshooting

- Ensure that the port specified is open and available on both the sender and receiver machines.
- If using UDP, be aware that it is a connectionless protocol and packet loss may occur. The program handles file resumption if interrupted.
- Check the logs to debug any issues. If using `basic` logging, switch to `debug` for more details.

## 5. Clean Up

To remove the compiled binaries and object files, you can run:
```
make clean
```

This will delete the `fileSnd`, `fileRcv` executables and any temporary build files.

## 6. License

This project is released under the MIT License.

과제 제출

1. 소스 코드와 실행 가능한 파일.

2. 설계 문서와 결과 보고서:

     파일 전송 유틸리티의 개념, 알고리즘, 데이터 구조.

     각 테스트 시나리오에서의 성능 결과.

     개선 사항 및 최적화 전략.

3. 데모 비디오:

     프로그램 동작 과정과 결과를 유튜브에 업로드.


학습 목표

1. 네트워크 조건에서의 TCP 한계 이해:

     높은 손실률과 지연 시간에서 TCP의 성능 제한을 경험.

2. 네트워크 시뮬레이션 도구 활용:

     tc, iperf와 같은 네트워크 도구를 활용한 성능 분석.

3. 고성능 네트워크 프로토콜 설계:

     병렬 처리와 사용자 정의 전송 전략으로 효율성 극대화.

728x90
반응형