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:
taptap
2026-06-24 21:49:50 +02:00
parent 3c98ca5803
commit 948542394e
9 changed files with 576 additions and 41 deletions
+110 -18
View File
@@ -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");