Sockets: comunicação em rede com C

1. Introdução aos Sockets em C

Sockets são abstrações de software que permitem a comunicação entre processos, seja em uma mesma máquina ou através de uma rede. Em C, a API de sockets (definida em <sys/socket.h>) é a base para construir aplicações de rede no modelo cliente-servidor. Nesse modelo, um processo servidor aguarda passivamente por conexões, enquanto clientes iniciam a comunicação ativamente.

A API de sockets em sistemas Unix/Linux segue o padrão Berkeley, oferecendo funções como socket(), bind(), listen(), accept(), connect(), send(), recv() e close(). A compreensão dessas funções é essencial para desenvolver aplicações de rede robustas e eficientes.

2. Endereçamento e Estruturas de Dados

Para comunicação em rede IPv4, utilizamos a estrutura sockaddr_in:

struct sockaddr_in {
    sa_family_t    sin_family;   // AF_INET
    in_port_t      sin_port;     // Porta em network byte order
    struct in_addr sin_addr;     // Endereço IPv4
    char           sin_zero[8];  // Preenchimento
};

struct in_addr {
    uint32_t s_addr;             // Endereço em network byte order
};

A conversão entre endereços legíveis e binários é feita com inet_pton() (presentation to network) e inet_ntop() (network to presentation):

#include <arpa/inet.h>

struct sockaddr_in addr;
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);

char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));

A ordem de bytes em rede (big-endian) é obrigatória para campos como porta e endereço. As funções de conversão são:

uint16_t htons(uint16_t hostshort);   // host to network short
uint32_t htonl(uint32_t hostlong);    // host to network long
uint16_t ntohs(uint16_t netshort);    // network to host short
uint32_t ntohl(uint32_t netlong);     // network to host long

3. Criação e Configuração do Socket

A função socket() cria um endpoint de comunicação:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // TCP
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);   // UDP

Parâmetros:
- AF_INET: domínio IPv4
- SOCK_STREAM: socket TCP (orientado a conexão)
- SOCK_DGRAM: socket UDP (datagramas)
- Último parâmetro: 0 para protocolo padrão

Para reutilizar endereços após encerramento abrupto, usamos setsockopt():

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

O bind associa o socket a um endereço e porta:

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;  // Todas as interfaces
serv_addr.sin_port = htons(8080);

bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

4. Servidor TCP: Ciclo de Vida Completo

Após criar e configurar o socket, o servidor TCP segue estas etapas:

  1. listen(): Coloca o socket em modo passivo
  2. accept(): Aceita conexões de clientes
  3. send()/recv(): Troca dados com o cliente

Exemplo prático: servidor eco que lê dados e os reenvia ao cliente

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

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // Cria socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Configura reuso de endereço
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // Configura endereço
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // Bind
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // Listen
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Servidor aguardando conexões na porta %d...\n", PORT);

    // Accept e loop de eco
    if ((client_fd = accept(server_fd, (struct sockaddr *)&address, 
                            (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    printf("Cliente conectado!\n");

    int valread;
    while ((valread = read(client_fd, buffer, BUFFER_SIZE)) > 0) {
        printf("Recebido: %s", buffer);
        send(client_fd, buffer, valread, 0);
        memset(buffer, 0, BUFFER_SIZE);
    }

    close(client_fd);
    close(server_fd);
    return 0;
}

5. Cliente TCP: Conectando e Trocando Dados

O cliente TCP segue estas etapas:

  1. Cria socket com socket()
  2. Conecta ao servidor com connect()
  3. Troca dados com send() e recv()

Exemplo prático: cliente que envia mensagens e aguarda resposta

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

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *message = "Olá do cliente!";
    char buffer[BUFFER_SIZE] = {0};

    // Cria socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation error");
        return -1;
    }

    // Configura endereço do servidor
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // Converte IP
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        perror("invalid address");
        return -1;
    }

    // Conecta
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connection failed");
        return -1;
    }

    // Envia mensagem
    send(sock, message, strlen(message), 0);
    printf("Mensagem enviada\n");

    // Recebe resposta
    int valread = read(sock, buffer, BUFFER_SIZE);
    printf("Resposta do servidor: %s\n", buffer);

    close(sock);
    return 0;
}

6. Sockets UDP: Comunicação sem Conexão

Diferente do TCP, UDP não estabelece conexão prévia. Cada envio é um datagrama independente, com possibilidade de perda ou reordenação. As funções principais são sendto() e recvfrom().

Servidor UDP simples:

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

#define PORT 9090
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    char buffer[BUFFER_SIZE];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    int len = sizeof(cliaddr);
    int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, 
                     (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0';
    printf("Recebido do cliente: %s\n", buffer);

    sendto(sockfd, "Recebido!", 9, 0, 
           (struct sockaddr *)&cliaddr, len);

    close(sockfd);
    return 0;
}

Cliente UDP:

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

#define PORT 9090
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in servaddr;
    char buffer[BUFFER_SIZE];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

    sendto(sockfd, "Olá servidor!", 13, 0, 
           (struct sockaddr *)&servaddr, sizeof(servaddr));

    int len = sizeof(servaddr);
    int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, 
                     (struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("Resposta do servidor: %s\n", buffer);

    close(sockfd);
    return 0;
}

7. Tratamento de Erros e Boas Práticas

Todas as funções de socket retornam valores que devem ser verificados:

if (socket(...) < 0) {
    perror("socket");  // Imprime erro no stderr
    exit(EXIT_FAILURE);
}

// Alternativa com strerror
if (bind(...) < 0) {
    fprintf(stderr, "Erro no bind: %s\n", strerror(errno));
    exit(EXIT_FAILURE);
}

O fechamento correto de sockets é fundamental:

close(sockfd);  // Fecha o socket e libera recursos

Para evitar o sinal SIGPIPE ao escrever em socket fechado, configure:

signal(SIGPIPE, SIG_IGN);  // Ignora SIGPIPE
// Ou use MSG_NOSIGNAL no send:
send(sockfd, data, len, MSG_NOSIGNAL);

Para implementar timeouts, use select():

struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;

fd_set fds;
FD_ZERO(&fds);
FD_SET(sockfd, &fds);

int ret = select(sockfd + 1, &fds, NULL, NULL, &tv);
if (ret == 0) {
    printf("Timeout!\n");
} else if (ret < 0) {
    perror("select");
}

Boas práticas adicionais incluem:
- Sempre inicializar estruturas com memset() antes de usar
- Verificar erros em cada chamada de sistema
- Fechar sockets em caso de erro para evitar vazamentos
- Usar SO_REUSEADDR em servidores para reinicialização rápida
- Preferir poll() sobre select() para maior escalabilidade

Referências