Este artículo describe una implementación técnica para transferir archivos desde un sistema Linux a un sistema Windows utilizando el protocolo UDP. UDP es un protocolo sin conexión y de baja latencia, ideal para aplicaciones que priorizan la velocidad sobre la confiabilidad absoluta, como transmisión en tiempo real donde se tolera cierta pérdida de datos. El sistema presentado incluye un servidor UDP en el lado Linux y un cliente en el lado Windows, con soporte para división de archivos en bloques, reensamblaje de datos y verificación de integridad.
Fundamentos de UDP y diseño del sistema de trensferencia
UDP difiere de TCP en que no establece una conexión dedicada, lo que reduce la sobrecarga y permite una transmisión más rápida. En aplicaciones como videoconferencia o juegos en línea, la pérdida de algunos paquetes es preferible a los retrasos causados por retransmisiones. Sin embargo, para transferencias de archivos, se pueden implementar mecanismos a nivel de aplicación para manejar errores y garantizar la integridad de los datos.
| Característica | UDP | TCP |
|---|---|---|
| Conexión | Sin conexión | Orientado a conexión |
| Fiabilidad | No garantiza orden ni entrega | Entrega garantizada y ordenada |
| Límites de mensaje | Preserva datagramas individuales | Flujo de bytes continuo |
| Sobrecarga | Baja (cabecera de 8 bytes) | Alta (cabecera mínima de 20 bytes) |
Servidor UDP en Linux mediante sockets
En Linux, la programación de redes se basa en la API de sockets. Primero se crea un socket de datagrama para UDP, se vincula a un puerto específico y luego se maneja la recepción y envío de datos.
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int descriptor_sock;
descriptor_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (descriptor_sock < 0) {
perror("Fallo al crear el socket");
exit(EXIT_FAILURE);
}
printf("Socket UDP creado con descriptor: %d\n", descriptor_sock);
return 0;
}
El siguiente paso es vincular el socket a una dirección local. Se utiliza INADDR_ANY para escuchar en todas las interfaces de red y se convierte el puerto al orden de bytes de red.
#include <netinet/in.h>
#define PUERTO 9999
struct sockaddr_in dir_servidor;
memset(&dir_servidor, 0, sizeof(dir_servidor));
dir_servidor.sin_family = AF_INET;
dir_servidor.sin_addr.s_addr = INADDR_ANY;
dir_servidor.sin_port = htons(PUERTO);
if (bind(descriptor_sock, (struct sockaddr *)&dir_servidor, sizeof(dir_servidor)) < 0) {
perror("Error en bind");
close(descriptor_sock);
exit(1);
}
Para recibir datos, se emplea recvfrom, que también captura la dirección del emisor. Luego, se puede responder con sendto.
char buffer[1024];
struct sockaddr_in dir_cliente;
socklen_t tam_cliente = sizeof(dir_cliente);
ssize_t bytes_recibidos = recvfrom(descriptor_sock, buffer, sizeof(buffer), 0,
(struct sockaddr*)&dir_cliente, &tam_cliente);
if (bytes_recibidos > 0) {
buffer[bytes_recibidos] = '\0';
printf("Recibido de %s:%d: %s\n", inet_ntoa(dir_cliente.sin_addr),
ntohs(dir_cliente.sin_port), buffer);
}
const char *respuesta = "ACK";
sendto(descriptor_sock, respuesta, strlen(respuesta), 0,
(struct sockaddr*)&dir_cliente, tam_cliente);
Cliente en Windows con Winsock
Windows utiliza la biblioteca Winsock, que requiere inicialización explícita. Se debe llamar a WSAStartup antes de usar cualquier función de socket y finalizar con WSACleanup.
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
WSADATA datos_wsa;
int resultado = WSAStartup(MAKEWORD(2, 2), &datos_wsa);
if (resultado != 0) {
printf("Fallo en WSAStartup: %d\n", resultado);
return -1;
}
En Windows, los sockets son de tipo SOCKET y se cierran con closesocket. Para compatibilidad entre plataformas, se pueden definir macros.
#ifdef _WIN32
#define CERRAR_SOCKET(s) closesocket(s)
#else
#define CERRAR_SOCKET(s) close(s)
#endif
Además, se puede usar connect en un socket UDP para restringir la comunicación a una dirección específica, lo que facilita el envío con send en lugar de sendto.
División de archivos en bloques
Para transmitir archivos grandes, se dividen en bloques que respeten el MTU de la red (típicamente 1500 bytes). Después de restar las cabeceras IP y UDP, el tamaño máximo de carga útil es de 1472 bytes.
#define TAM_MAXIMO_MTU 1500
#define TAM_CAB_IP 20
#define TAM_CAB_UDP 8
#define TAM_CARGA_UTIL (TAM_MAXIMO_MTU - TAM_CAB_IP - TAM_CAB_UDP)
Cada bloque incluye una cabecera personalizada con metadatos como número de secuencia, total de bloques y un CRC para verificación. Se evita la alineación de estructuras con #pragma pack para portabilidad.
#pragma pack(push, 1)
typedef struct {
uint32_t firma;
uint32_t seq_actual;
uint32_t total_seqs;
uint32_t tam_bloque;
uint32_t crc_bloque;
} cabecera_bloque;
#pragma pack(pop)
Al construir un paquete, se calcula el CRC de la carga útil y se adjunta en la cabecera.
void preparar_paquete(char* paquete, const char* datos, size_t tam_datos,
uint32_t seq, uint32_t total) {
cabecera_bloque* cab = (cabecera_bloque*)paquete;
cab->firma = 0x46545001; // Identificador único
cab->seq_actual = htonl(seq);
cab->total_seqs = htonl(total);
cab->tam_bloque = htonl(tam_datos);
cab->crc_bloque = calcular_crc32((uint8_t*)datos, tam_datos);
memcpy(paquete + sizeof(cabecera_bloque), datos, tam_datos);
}
Manejo de paquetes fuera de orden
Los paquetes pueden llegar desordenados o duplicados. Se implementa un búfer de recepción con un tamaño fijo y un puntero que indica la próxima secuencia esperada.
#define TAM_VENTANA 1024
typedef struct {
int recibido;
char datos[TAM_CARGA_UTIL];
size_t tam;
} entrada_cache;
entrada_cache cache[TAM_VENTANA];
uint32_t seq_esperada = 0;
void procesar_bloque(uint32_t seq, const char* datos, size_t tam) {
if (seq == seq_esperada) {
escribir_en_archivo(datos, tam);
seq_esperada++;
// Verificar si hay bloques consecutivos en caché
while (cache[seq_esperada % TAM_VENTANA].recibido) {
escribir_en_archivo(cache[seq_esperada % TAM_VENTANA].datos,
cache[seq_esperada % TAM_VENTANA].tam);
cache[seq_esperada % TAM_VENTANA].recibido = 0;
seq_esperada++;
}
} else if (seq > seq_esperada && seq < seq_esperada + TAM_VENTANA) {
size_t indice = seq % TAM_VENTANA;
cache[indice].recibido = 1;
memcpy(cache[indice].datos, datos, tam);
cache[indice].tam = tam;
}
}
Verificación de integridad con CRC-32
Para detectar corrupción de datos, se usa CRC-32 en cada bloque y una verificación final con MD5 del archivo completo. La tabla de CRC se precalcula para acelerar el cálculo.
static uint32_t tabla_crc[256];
void inicializar_tabla_crc() {
for (int i = 0; i < 256; i++) {
uint32_t crc = i;
for (int j = 0; j < 8; j++) {
crc = (crc & 1) ? (crc >> 1) ^ 0xEDB88320 : crc >> 1;
}
tabla_crc[i] = crc;
}
}
uint32_t calcular_crc32(const uint8_t *datos, size_t len) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < len; ++i) {
crc = (crc >> 8) ^ tabla_crc[(crc ^ datos[i]) & 0xFF];
}
return crc ^ 0xFFFFFFFF;
}
Al finalizar la transferencia, el emisor envía un paquete EOF con el resumen MD5 del archivo. El recepter compara su cálculo local y confirma el éxito o solicita una retransmisión.