feat: Implement reliable UDP transfer with retransmission
- Add seq_num and flags to udp_packet_header_t for reliability - Add MAGIC_UDP_ACK packet type and udp_ack_t struct for ACKs - Add UDP constants: WINDOW_SIZE=16, MAX_RETRIES=5, RETRANSMIT_MS=100 - Server: Send cumulative + selective ACKs during UDP transfer - Client: Implement sliding window with retransmission - Client: Add send_file_udp() and send_file_udp_single() functions - Fix server make_path call to not create filename as directory - UDP mode uses single-file protocol (not batch) for reliability Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
+110
-18
@@ -129,6 +129,14 @@ int setup_udp_session(int tcp_fd, int *udp_fd_out, struct sockaddr_in *client_ud
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Track ACK state for sending cumulative + selective ACKs
|
||||
typedef struct {
|
||||
uint32_t last_ack_block; // Last contiguous block we've ACKed
|
||||
uint32_t last_ack_bitmap[4]; // Bitmap of selectively received blocks
|
||||
uint32_t ack_count; // Packets since last ACK
|
||||
uint64_t last_ack_time_ms; // Timestamp of last ACK send
|
||||
} ack_tracker_t;
|
||||
|
||||
int receive_file_udp_loop(int tcp_fd, int udp_fd, uint64_t session_id, active_file_t *active) {
|
||||
struct pollfd fds[2];
|
||||
fds[0].fd = tcp_fd;
|
||||
@@ -142,6 +150,12 @@ int receive_file_udp_loop(int tcp_fd, int udp_fd, uint64_t session_id, active_fi
|
||||
tv.tv_usec = 0;
|
||||
setsockopt(udp_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
// Initialize ACK tracker
|
||||
ack_tracker_t ack;
|
||||
memset(&ack, 0, sizeof(ack));
|
||||
ack.last_ack_block = (uint32_t)-1; // Invalid, force first ACK
|
||||
ack.ack_count = 0;
|
||||
|
||||
while (1) {
|
||||
int poll_ret = poll(fds, 2, -1);
|
||||
if (poll_ret < 0) {
|
||||
@@ -162,6 +176,7 @@ int receive_file_udp_loop(int tcp_fd, int udp_fd, uint64_t session_id, active_fi
|
||||
if (n > 0) {
|
||||
if (n >= (ssize_t)sizeof(udp_packet_header_t)) {
|
||||
udp_packet_header_t *hdr = (udp_packet_header_t *)pkt;
|
||||
|
||||
if (hdr->magic == MAGIC_UDP_DATA && hdr->session_id == session_id && hdr->file_id == active->file_id) {
|
||||
uint32_t bid = hdr->block_id;
|
||||
if (bid < active->total_blocks) {
|
||||
@@ -171,6 +186,60 @@ int receive_file_udp_loop(int tcp_fd, int udp_fd, uint64_t session_id, active_fi
|
||||
perror("pwrite file block failed");
|
||||
} else {
|
||||
active->received_blocks[bid] = 1;
|
||||
|
||||
// Update ACK tracker
|
||||
ack.ack_count++;
|
||||
|
||||
// Build selective ACK bitmap
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ack.last_ack_bitmap[i] = 0;
|
||||
}
|
||||
for (uint32_t i = 0; i < active->total_blocks && i < 128; i++) {
|
||||
if (active->received_blocks[i]) {
|
||||
ack.last_ack_bitmap[i / 32] |= (1U << (i % 32));
|
||||
}
|
||||
}
|
||||
|
||||
// Find highest contiguous block
|
||||
uint32_t highest_contiguous = 0;
|
||||
while (highest_contiguous < active->total_blocks && active->received_blocks[highest_contiguous]) {
|
||||
highest_contiguous++;
|
||||
}
|
||||
ack.last_ack_block = highest_contiguous - 1;
|
||||
|
||||
// Check if all blocks received first
|
||||
int all_received = 1;
|
||||
for (uint32_t i = 0; i < active->total_blocks; i++) {
|
||||
if (!active->received_blocks[i]) {
|
||||
all_received = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Send ACK periodically (every UDP_ACK_INTERVAL packets or if window advancing)
|
||||
if (ack.ack_count >= UDP_ACK_INTERVAL || bid == ack.last_ack_block) {
|
||||
udp_ack_t ack_pkt;
|
||||
ack_pkt.magic = MAGIC_UDP_ACK;
|
||||
ack_pkt.session_id = session_id;
|
||||
ack_pkt.file_id = active->file_id;
|
||||
ack_pkt.ack_block_id = ack.last_ack_block;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ack_pkt.ack_bitmap[i] = ack.last_ack_bitmap[i];
|
||||
}
|
||||
|
||||
if (writen(tcp_fd, &ack_pkt, sizeof(ack_pkt)) != sizeof(ack_pkt)) {
|
||||
perror("Failed to send UDP ACK");
|
||||
return -1;
|
||||
}
|
||||
ack.ack_count = 0;
|
||||
}
|
||||
|
||||
if (all_received) {
|
||||
// All blocks received - just break, don't send final ACK
|
||||
// The client already knows from cumulative ACKs that all blocks are received
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +335,16 @@ static void *client_thread(void *arg) {
|
||||
void handle_client(int sock_fd, const char *dest_dir) {
|
||||
printf("Client connected. Starting transfer session...\n");
|
||||
|
||||
// Read mode (first 4 bytes from client)
|
||||
uint32_t mode = MODE_SYNC; // Default to sync mode
|
||||
ssize_t n = readn(sock_fd, &mode, sizeof(mode));
|
||||
if (n != sizeof(mode)) {
|
||||
fprintf(stderr, "Failed to read mode from client\n");
|
||||
return;
|
||||
}
|
||||
int use_raw = (mode == MODE_RAW);
|
||||
printf("Mode: %s\n", use_raw ? "RAW" : "SYNC");
|
||||
|
||||
int use_udp = 0;
|
||||
int udp_fd = -1;
|
||||
struct sockaddr_in client_udp_addr;
|
||||
@@ -275,7 +354,7 @@ void handle_client(int sock_fd, const char *dest_dir) {
|
||||
while (1) {
|
||||
uint32_t magic = 0;
|
||||
// Peek or read magic
|
||||
ssize_t n = readn(sock_fd, &magic, sizeof(magic));
|
||||
n = readn(sock_fd, &magic, sizeof(magic));
|
||||
if (n <= 0) {
|
||||
if (n < 0) {
|
||||
perror("read magic failed");
|
||||
@@ -286,12 +365,14 @@ void handle_client(int sock_fd, const char *dest_dir) {
|
||||
}
|
||||
|
||||
if (magic == MAGIC_UDP_REQ) {
|
||||
|
||||
if (setup_udp_session(sock_fd, &udp_fd, &client_udp_addr, &udp_session_id) < 0) {
|
||||
fprintf(stderr, "Failed to initialize UDP session. Falling back to TCP.\n");
|
||||
uint32_t resp_fail = RESP_ERROR;
|
||||
writen(sock_fd, &resp_fail, sizeof(resp_fail));
|
||||
} else {
|
||||
use_udp = 1;
|
||||
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -346,17 +427,20 @@ void handle_client(int sock_fd, const char *dest_dir) {
|
||||
char target_path[2048];
|
||||
snprintf(target_path, sizeof(target_path), "%s/%s", dest_dir, filename);
|
||||
|
||||
// Check if file already exists with same size and checksum (skip if so)
|
||||
struct stat existing_st;
|
||||
if (stat(target_path, &existing_st) == 0 &&
|
||||
(uint64_t)existing_st.st_size == meta.file_size &&
|
||||
S_ISREG(existing_st.st_mode)) {
|
||||
// File exists with same size - check checksum
|
||||
uint64_t existing_checksum = fast_hash_file(target_path, meta.file_size);
|
||||
if (existing_checksum == meta.checksum) {
|
||||
printf("Skip (checksum match): %s\n", target_path);
|
||||
free(filename);
|
||||
continue;
|
||||
// In SYNC mode: check if file already exists with same size and checksum (skip if so)
|
||||
// In RAW mode: always send/receive the file
|
||||
if (!use_raw) {
|
||||
struct stat existing_st;
|
||||
if (stat(target_path, &existing_st) == 0 &&
|
||||
(uint64_t)existing_st.st_size == meta.file_size &&
|
||||
S_ISREG(existing_st.st_mode)) {
|
||||
// File exists with same size - check checksum
|
||||
uint64_t existing_checksum = fast_hash_file(target_path, meta.file_size);
|
||||
if (existing_checksum == meta.checksum) {
|
||||
printf("Skip (checksum match): %s\n", target_path);
|
||||
free(filename);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +512,7 @@ void handle_client(int sock_fd, const char *dest_dir) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// We received a file metadata packet (read rest of it)
|
||||
uint32_t name_len = 0;
|
||||
uint64_t file_size = 0;
|
||||
@@ -455,17 +540,24 @@ void handle_client(int sock_fd, const char *dest_dir) {
|
||||
char target_path[2048];
|
||||
snprintf(target_path, sizeof(target_path), "%s/%s", dest_dir, filename);
|
||||
|
||||
// Ensure directories exist
|
||||
if (make_path(target_path, 0755) < 0) {
|
||||
perror("Failed to create parent directory path");
|
||||
file_response_t resp_err = {RESP_ERROR, 0, {0, 0}};
|
||||
writen(sock_fd, &resp_err, sizeof(resp_err));
|
||||
continue;
|
||||
// Ensure parent directories exist
|
||||
char *slash = strrchr(target_path, '/');
|
||||
if (slash) {
|
||||
*slash = '\0';
|
||||
if (make_path(target_path, 0755) < 0) {
|
||||
perror("Failed to create parent directory path");
|
||||
*slash = '/';
|
||||
file_response_t resp_err = {RESP_ERROR, 0, {0, 0}};
|
||||
writen(sock_fd, &resp_err, sizeof(resp_err));
|
||||
continue;
|
||||
}
|
||||
*slash = '/';
|
||||
}
|
||||
|
||||
uint32_t file_id = ++file_id_counter;
|
||||
|
||||
if (use_udp) {
|
||||
|
||||
file_response_t resp_udp = {RESP_USE_UDP, file_id, {0, 0}};
|
||||
if (writen(sock_fd, &resp_udp, sizeof(resp_udp)) != sizeof(resp_udp)) {
|
||||
perror("Failed to send file response header");
|
||||
|
||||
Reference in New Issue
Block a user