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. 고성능 네트워크 프로토콜 설계:
• 병렬 처리와 사용자 정의 전송 전략으로 효율성 극대화.
'Learn > '24_Fall_(EE542) Internet&Cloud Computin' 카테고리의 다른 글
(LAB 06) Hadoop and Spark (0) | 2025.01.09 |
---|---|
(LAB 05) Fast, Reliable File Transfer with Custom TCP/IP (0) | 2025.01.09 |
(LAB 03) Network Measurement (0) | 2025.01.09 |
(LAB 02) AWS Bring UP and Queuing (0) | 2025.01.09 |
(LAB 01) Network with VyOS, and simple Socket Program (0) | 2025.01.09 |