Análise Técnica e Implementação em C do Protocolo TCP

Fundamentos do Protocolo TCP

O Transmission Control Protocol (TCP) atua como a espinha dorsal da camada de transporte em redes de computadores, garantindo a entrega confiável de fluxos de bytes entre processos em hosts distintos. Diferente de protocolos orientados a datagramas, o TCP estabelece um canal de comunicação dedicado antes de qualquer troca de dados.

Características Arquiteturais

  • Orientado a Conexão: Exige um handshake inicial para sincronização de estados e um processo formal de encerramento.
  • Confiabilidade Extrema: Implementa mecanismos para mitigar perdas, duplicações e reordenações de pacotes na rede subjacente.
  • Comunicação Ponto a Ponto: Opera estritamente entre dois endpoints, não suportando nativmaente multicast ou broadcast.
  • Fluxo de Bytes Contínuo: Não preserva as limitações de mensagens da camada de aplicação; os dados são tratados como um rio contínuo de octetos.
  • Operação Full-Duplex: Permite que ambas as extremidades transmitam e recebam dados simultaneamente através do mesmo canal lógico.

Mecanismos de Garantia de Confiabilidade

Como o protocolo IP subjacente é inerentemente não confiável (best-effort), o TCP introduz uma série de algoritmos para assegurar a integridade e a ordem dos dados:

  • Números de Sequência: Cada byte transmitido recebe um identificador de 32 bits. Isso permite que o receptor remonte o fluxo na ordem correta e descarte duplicatas. O Número de Sequência Inicial (ISN) é gerado aleatoriamente durante o handshake.
  • Confirmações Cumulativas (ACK): O receptor envia ACKs indicando o próximo byte esperado. A ausência de um ACK dentro de um intervalo calculado (RTO) dispara a retransmissão do segmento pelo emissor.
  • Somas de Verificação (Checksum): Um campo de 16 bits calculado sobre o cabeçalho, os dados e um pseudo-cabeçalho IP. Se a validação falhar no destino, o segmento é silencitamente descartado.
  • Controle de Fluxo via Janela Deslizante: O receptor anuncia dinamicamente o espaço disponível em seu buffer de kernel (Receive Window). O emissor ajusta sua taxa de injeção de dados para evitar o transbordamento do buffer do par.

Anatomia do Cabeçalho TCP

O cabeçalho TCP possui um tamanho base de 20 bytes, podendo chegar a 60 bytes com a inclusão de opções. Os campos críticos incluem:

  • Portas (Origem e Destino): Campos de 16 bits que identificam os processos nas extremidades.
  • Offset de Dados: Indica o tamanho do cabeçalho em palavras de 32 bits, essencial para localizar o início do payload.
  • Flags de Controle: Bits que ditam o propósito do segmento:
    • SYN: Inicia a sincronização de sequências.
    • ACK: Valida o campo de número de confirmação.
    • FIN: Sinaliza o fim da transmissão de dados por uma das partes.
    • RST: Aborta a conexão imediatamente devido a erros críticos.
    • PSH: Instrui a pilha de rede a entregar os dados à aplicação imediatamente, sem aguardar o preenchimento do buffer.
    • URG: Indica a presença de dados fora de banda, guiados pelo Ponteiro de Urgência.

Ciclo de Vida e Máquina de Estados

Estabelecimento (Three-Way Handshake)

  1. O cliente envia um segmento SYN com seu ISN e transita para SYN_SENT.
  2. O servidor responde com SYN-ACK, confirmando o ISN do cliente e enviendo seu próprio ISN, entrando em SYN_RCVD.
  3. O cliente finaliza com um ACK para o ISN do servidor. Ambos transitam para ESTABLISHED.

Encerramento (Four-Way Handshake)

Como a comunicação é full-duplex, cada direção deve ser fechada independentemente. A parte que inicia o fechamento envia um FIN e entra em FIN_WAIT_1. Após a troca de ACKs e do FIN da parte passiva, o iniciador entra no estado TIME_WAIT por um período de 2MSL (Maximum Segment Lifetime), garantindo que pacotes retardatários na rede sejam drenados antes que a porta possa ser reutilizada.

API de Soquetes POSIX para TCP

A interação com a pilha de rede em sistemas Unix-like é realizada através de chamadas de sistema padronizadas. Abaixo estão as assinaturas fundamentais reestruturadas para clareza:

/**
 * Cria um endpoint de comunicação (soquete).
 * @param family Família de protocolos (ex: AF_INET para IPv4).
 * @param type Tipo do soquete (SOCK_STREAM para TCP).
 * @param protocol Protocolo específico (0 para seleção automática).
 * @return Descritor de arquivo do soquete ou -1 em caso de falha.
 */
int socket(int family, int type, int protocol);

/**
 * Inicia uma conexão ativa em um soquete de fluxo.
 * @param sock_fd Descritor do soquete criado.
 * @param target_addr Estrutura contendo IP e porta do destino.
 * @param addr_len Tamanho da estrutura de endereço.
 * @return 0 em caso de sucesso, -1 em caso de erro.
 */
int connect(int sock_fd, const struct sockaddr *target_addr, socklen_t addr_len);

/**
 * Associa um soquete a um endereço e porta locais.
 * @param sock_fd Descritor do soquete.
 * @param local_addr Estrutura com o IP e porta de bind.
 * @param addr_len Tamanho da estrutura.
 * @return 0 em sucesso, -1 em erro.
 */
int bind(int sock_fd, const struct sockaddr *local_addr, socklen_t addr_len);

/**
 * Coloca o soquete em modo de escuta passiva.
 * @param sock_fd Descritor do soquete vinculado.
 * @param backlog Tamanho máximo da fila de conexões pendentes.
 * @return 0 em sucesso, -1 em erro.
 */
int listen(int sock_fd, int backlog);

/**
 * Extrai a primeira conexão da fila e cria um novo soquete para comunicação.
 * @param sock_fd Descritor do soquete de escuta.
 * @param peer_addr [Saída] Endereço do cliente que conectou.
 * @param addr_len [Entrada/Saída] Tamanho da estrutura de endereço.
 * @return Novo descritor de arquivo para a sessão ou -1 em erro.
 */
int accept(int sock_fd, struct sockaddr *peer_addr, socklen_t *addr_len);

/**
 * Transmite dados através de uma conexão estabelecida.
 * @param sock_fd Descritor da sessão ativa.
 * @param buffer Ponteiro para os dados a serem enviados.
 * @param length Quantidade de bytes a transmitir.
 * @param flags Modificadores de comportamento (ex: MSG_OOB).
 * @return Número de bytes efetivamente enviados ou -1 em erro.
 */
ssize_t send(int sock_fd, const void *buffer, size_t length, int flags);

/**
 * Lê dados do buffer de recebimento do soquete.
 * @param sock_fd Descritor da sessão ativa.
 * @param buffer Destino para os dados lidos.
 * @param length Capacidade máxima do buffer de destino.
 * @param flags Modificadores de leitura.
 * @return Bytes lidos, 0 se a conexão foi encerrada graciosamente, ou -1 em erro.
 */
ssize_t recv(int sock_fd, void *buffer, size_t length, int flags);

Gerenciamento de Buffers de Kernel

O kernel do sistema operacional mantém buffers circulares para envio (SO_SNDBUF) e recebimento (SO_RCVBUF). O tamanho desses buffers impacta diretamente a janela TCP e, consequentemente, a vazão (throughput) da conexão. A manipulação é feita via setsockopt e getsockopt. É crucial notar que o kernel do Linux frequentemente dobra o valor solicitado pelo usuário para acomodar metadados internos de gerenciamento.

Processamento de Dados Urgentes (Out-of-Band)

O TCP permite a transmissão de dados "fora de banda" (OOB) para sinalizar eventos críticos que devem bypassar a fila de dados normais. Na implementação BSD (padrão no Linux), apenas o último byte do segmento marcado com a flag URG é tratado como dado urgente. A notificação de chegada é feita via sinal SIGURG.

Servidor com Tratamento de OOB via Sigaction

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <pthread.h>

#define LISTEN_PORT 9090
#define BUFFER_CAPACITY 1024

int active_socket;

void handle_urgent_data(int signum) {
    char urgent_byte;
    ssize_t bytes_read = recv(active_socket, &urgent_byte, 1, MSG_OOB);
    if (bytes_read > 0) {
        printf("\n[Dados Urgentes Recebidos]: %c\n", urgent_byte);
        fflush(stdout);
    }
}

void* process_standard_stream(void* arg) {
    char stream_buffer[BUFFER_CAPACITY];
    while (1) {
        ssize_t received = recv(active_socket, stream_buffer, BUFFER_CAPACITY - 1, 0);
        if (received <= 0) {
            if (received == 0) printf("[Conexão encerrada pelo cliente]\n");
            else if (errno != EINTR) perror("Erro na leitura");
            break;
        }
        stream_buffer[received] = '\0';
        printf("[Dados Normais]: %s\n", stream_buffer);
        if (strncmp(stream_buffer, "exit", 4) == 0) break;
    }
    return NULL;
}

int main() {
    int listening_socket;
    struct sockaddr_in server_config, client_config;
    socklen_t client_len = sizeof(client_config);
    pthread_t worker_thread;
    struct sigaction sa;

    listening_socket = socket(AF_INET, SOCK_STREAM, 0);
    int reuse = 1;
    setsockopt(listening_socket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    memset(&server_config, 0, sizeof(server_config));
    server_config.sin_family = AF_INET;
    server_config.sin_addr.s_addr = INADDR_ANY;
    server_config.sin_port = htons(LISTEN_PORT);

    bind(listening_socket, (struct sockaddr*)&server_config, sizeof(server_config));
    listen(listening_socket, 5);
    printf("Aguardando conexões na porta %d...\n", LISTEN_PORT);

    active_socket = accept(listening_socket, (struct sockaddr*)&client_config, &client_len);
    
    // Configurar sigaction para SIGURG (mais robusto que signal())
    sa.sa_handler = handle_urgent_data;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGURG, &sa, NULL);
    
    fcntl(active_socket, F_SETOWN, getpid());

    pthread_create(&worker_thread, NULL, process_standard_stream, NULL);
    pthread_join(worker_thread, NULL);

    close(active_socket);
    close(listening_socket);
    return 0;
}

Cliente com Envio de OOB

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define TARGET_PORT 9090
#define INPUT_LIMIT 1024

int main() {
    int client_sock;
    struct sockaddr_in target_server;
    char user_input[INPUT_LIMIT];

    client_sock = socket(AF_INET, SOCK_STREAM, 0);
    
    memset(&target_server, 0, sizeof(target_server));
    target_server.sin_family = AF_INET;
    target_server.sin_port = htons(TARGET_PORT);
    inet_pton(AF_INET, "127.0.0.1", &target_server.sin_addr);

    if (connect(client_sock, (struct sockaddr*)&target_server, sizeof(target_server)) < 0) {
        perror("Falha ao conectar");
        exit(1);
    }

    printf("Conectado. Comandos:\n");
    printf("  'urg:<char>' para enviar dado urgente.\n");
    printf("  'exit' para finalizar.\n");

    while (1) {
        printf("> ");
        if (!fgets(user_input, INPUT_LIMIT, stdin)) break;
        
        user_input[strcspn(user_input, "\n")] = 0;

        if (strncmp(user_input, "urg:", 4) == 0 && strlen(user_input) == 5) {
            char oob_char = user_input[4];
            send(client_sock, &oob_char, 1, MSG_OOB);
            printf("Enviado dado urgente: %c\n", oob_char);
        } else if (strcmp(user_input, "exit") == 0) {
            send(client_sock, user_input, strlen(user_input), 0);
            break;
        } else {
            send(client_sock, user_input, strlen(user_input), 0);
        }
    }

    close(client_sock);
    return 0;
}

Implementação de Timeouts em Operações de I/O

Em ambientes de produção, operações de rede bloqueantes indefinidas são inaceitáveis. O uso de SO_RCVTIMEO e SO_SNDTIMEO através de setsockopt permite definir limites máximos de espera para chamadas como recv e send. Quando o tempo expira, a chamada retorna -1 e errno é configurado para EAGAIN ou EWOULDBLOCK.

Servidor Echo Multithread

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define SERVICE_PORT 9091
#define MAX_PAYLOAD 1024

void* handle_client_session(void* socket_ptr) {
    int session_fd = *(int*)socket_ptr;
    free(socket_ptr);
    char payload[MAX_PAYLOAD];
    
    pthread_detach(pthread_self());

    while (1) {
        ssize_t bytes_in = recv(session_fd, payload, MAX_PAYLOAD - 1, 0);
        if (bytes_in <= 0) {
            if (bytes_in == 0) printf("Sessão %d finalizada.\n", session_fd);
            break;
        }
        payload[bytes_in] = '\0';
        
        send(session_fd, payload, bytes_in, 0);
        
        if (strstr(payload, "terminate") != NULL) break;
    }
    close(session_fd);
    return NULL;
}

int main() {
    int main_socket;
    struct sockaddr_in host_addr, peer_addr;
    socklen_t peer_len = sizeof(peer_addr);
    pthread_t thread_id;

    main_socket = socket(AF_INET, SOCK_STREAM, 0);
    int opt_flag = 1;
    setsockopt(main_socket, SOL_SOCKET, SO_REUSEADDR, &opt_flag, sizeof(opt_flag));

    memset(&host_addr, 0, sizeof(host_addr));
    host_addr.sin_family = AF_INET;
    host_addr.sin_addr.s_addr = INADDR_ANY;
    host_addr.sin_port = htons(SERVICE_PORT);

    bind(main_socket, (struct sockaddr*)&host_addr, sizeof(host_addr));
    listen(main_socket, 10);
    printf("Servidor Echo ativo na porta %d\n", SERVICE_PORT);

    while (1) {
        int* new_fd = malloc(sizeof(int));
        *new_fd = accept(main_socket, (struct sockaddr*)&peer_addr, &peer_len);
        if (*new_fd < 0) {
            free(new_fd);
            continue;
        }
        pthread_create(&thread_id, NULL, handle_client_session, new_fd);
    }
    
    close(main_socket);
    return 0;
}

Cliente com Controle de Timeout de Leitura

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <errno.h>

#define REMOTE_HOST "127.0.0.1"
#define REMOTE_PORT 9091
#define MSG_BUFFER 1024
#define TIMEOUT_SECONDS 5

int configure_receive_timeout(int sock, int seconds) {
    struct timeval tv;
    tv.tv_sec = seconds;
    tv.tv_usec = 0;
    return setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
}

int main() {
    int net_socket;
    struct sockaddr_in remote_addr;
    char out_msg[MSG_BUFFER], in_msg[MSG_BUFFER];

    net_socket = socket(AF_INET, SOCK_STREAM, 0);
    
    if (configure_receive_timeout(net_socket, TIMEOUT_SECONDS) < 0) {
        perror("Erro ao configurar timeout");
        exit(1);
    }

    memset(&remote_addr, 0, sizeof(remote_addr));
    remote_addr.sin_family = AF_INET;
    remote_addr.sin_port = htons(REMOTE_PORT);
    inet_pton(AF_INET, REMOTE_HOST, &remote_addr.sin_addr);

    if (connect(net_socket, (struct sockaddr*)&remote_addr, sizeof(remote_addr)) < 0) {
        perror("Conexão recusada");
        exit(1);
    }

    printf("Conectado. Digite 'terminate' para sair.\n");

    while (1) {
        printf("Mensagem: ");
        if (!fgets(out_msg, MSG_BUFFER, stdin)) break;
        out_msg[strcspn(out_msg, "\n")] = 0;

        send(net_socket, out_msg, strlen(out_msg), 0);

        if (strcmp(out_msg, "terminate") == 0) break;

        ssize_t read_bytes = recv(net_socket, in_msg, MSG_BUFFER - 1, 0);
        if (read_bytes < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                printf("Timeout: Nenhuma resposta em %d segundos.\n", TIMEOUT_SECONDS);
            } else {
                perror("Erro de leitura");
            }
            break;
        } else if (read_bytes == 0) {
            printf("Servidor desconectou.\n");
            break;
        }
        
        in_msg[read_bytes] = '\0';
        printf("Resposta: %s\n", in_msg);
    }

    close(net_socket);
    return 0;
}

Tags: TCP C POSIX Sockets Network Protocols Socket Programming

Publicado em 8-7 11:50