TCP server e client do zero

1. Conceitos Fundamentais de Sockets TCP

O modelo cliente-servidor é a base da comunicação em rede. No protocolo TCP/IP, o servidor oferece um serviço (como ecoar mensagens) e o cliente consome esse serviço. O TCP garante que os dados cheguem na ordem correta e sem perdas, estabelecendo uma conexão confiável entre as partes.

Cada processo na rede é identificado por um endereço IP (identifica o host) e uma porta (identifica o processo específico naquele host). O socket é a interface de comunicação que permite a um programa enviar e receber dados através da rede. Em C, um socket é representado por um descritor de arquivo (file descriptor), similar a um arquivo aberto.

2. Criando um Socket e Configurando o Endereço

A função socket() cria um novo socket:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • AF_INET: domínio IPv4
  • SOCK_STREAM: socket orientado a conexão (TCP)
  • 0: protocolo padrão (TCP para SOCK_STREAM)

Para configurar o endereço, usamos a estrutura sockaddr_in:

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);              // porta em network byte order
addr.sin_addr.s_addr = INADDR_ANY;        // aceita conexões de qualquer interface

htons() converte o número da porta de host byte order para network byte order (big-endian). inet_pton() converte string de IP para formato binário:

inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

3. Implementando o Lado Servidor

O servidor segue quatro passos principais: criar socket, bind, listen e accept.

bind() - associa o socket a um endereço local:

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

listen() - coloca o socket em modo de escuta, definindo o tamanho da fila de conexões pendentes:

listen(sockfd, 5);  // máximo de 5 conexões na fila

accept() - bloqueia até que um cliente se conecte, retornando um novo socket para comunicação:

struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);

4. Implementando o Lado Cliente

O cliente cria um socket e se conecta ao servidor com connect():

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

Para enviar e receber dados:

char buffer[1024];
send(sockfd, "Hello", 5, 0);              // envia dados
recv(sockfd, buffer, sizeof(buffer), 0);   // recebe dados

O fechamento é feito com close():

close(sockfd);

5. Comunicação Bidirecional e Loop de Mensagens

Tanto servidor quanto cliente podem usar loops para comunicação contínua. O servidor ecoa dados:

char buffer[1024];
int n;
while ((n = recv(client_fd, buffer, sizeof(buffer), 0)) > 0) {
    send(client_fd, buffer, n, 0);  // ecoa os dados recebidos
}

O cliente envia mensagens do terminal:

char buffer[1024];
while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
    send(sockfd, buffer, strlen(buffer), 0);
    n = recv(sockfd, buffer, sizeof(buffer), 0);
    buffer[n] = '\0';
    printf("Eco: %s", buffer);
}

É importante tratar envios parciais: send() pode não enviar todos os bytes de uma vez. Um loop garante o envio completo:

int send_all(int fd, char *data, int len) {
    int total = 0;
    while (total < len) {
        int n = send(fd, data + total, len - total, 0);
        if (n == -1) return -1;
        total += n;
    }
    return total;
}

6. Tratamento de Erros e Boas Práticas

Toda chamada de sistema deve ter seu retorno verificado:

if (bind(sockfd, ...) == -1) {
    perror("bind failed");
    exit(1);
}

Recursos devem ser liberados: feche sockets com close() ao final. Para evitar vazamento de descritores em caso de erro, use goto cleanup ou funções de limpeza.

O sinal SIGPIPE ocorre ao tentar escrever em um socket já fechado pelo outro lado. Ignore-o ou trate-o:

signal(SIGPIPE, SIG_IGN);  // ignora SIGPIPE

Timeouts podem ser configurados com setsockopt():

struct timeval tv = {5, 0};  // 5 segundos
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

7. Exemplo Completo: Servidor Eco e Cliente Interativo

Servidor (server.c):

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

int main() {
    int server_fd, client_fd;
    struct sockaddr_in addr;
    char buffer[1024];

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) { perror("socket"); exit(1); }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("bind"); close(server_fd); exit(1);
    }

    if (listen(server_fd, 5) == -1) {
        perror("listen"); close(server_fd); exit(1);
    }

    printf("Servidor aguardando conexões na porta 8080...\n");

    client_fd = accept(server_fd, NULL, NULL);
    if (client_fd == -1) { perror("accept"); close(server_fd); exit(1); }

    printf("Cliente conectado!\n");

    int n;
    while ((n = recv(client_fd, buffer, sizeof(buffer), 0)) > 0) {
        send(client_fd, buffer, n, 0);
    }

    printf("Cliente desconectado.\n");
    close(client_fd);
    close(server_fd);
    return 0;
}

Cliente (client.c):

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

int main() {
    int sockfd;
    struct sockaddr_in addr;
    char buffer[1024];

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket"); exit(1); }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

    if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("connect"); close(sockfd); exit(1);
    }

    printf("Conectado ao servidor. Digite mensagens (Ctrl+D para sair):\n");

    while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        send(sockfd, buffer, strlen(buffer), 0);
        int n = recv(sockfd, buffer, sizeof(buffer), 0);
        if (n <= 0) break;
        buffer[n] = '\0';
        printf("Eco: %s", buffer);
    }

    close(sockfd);
    return 0;
}

Compilação e teste:

gcc -o server server.c
gcc -o client client.c
./server &        # inicia servidor em background
./client          # inicia cliente

Digite mensagens no cliente e veja o eco retornado pelo servidor. Pressione Ctrl+D para encerrar.

Referências